Skip to content

Commit f2bd4dd

Browse files
committed
feat(api): Add setup wizard and infrastructure bootstrap
Introduce setup API for the dashboard-based setup wizard flow. Bootstrap nginx reverse proxy on first boot with welcome page. Add system counts to /stats for accurate sidebar navigation. Signed-off-by: nfebe <fenn25.fn@gmail.com>
1 parent 672ec21 commit f2bd4dd

6 files changed

Lines changed: 1167 additions & 8 deletions

File tree

internal/api/server.go

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"fmt"
88
"log"
99
"net/http"
10+
"net/http/httputil"
11+
"net/url"
1012
"os"
1113
"os/exec"
1214
"path/filepath"
@@ -70,6 +72,8 @@ type Server struct {
7072
auditMiddleware *audit.Middleware
7173
powerDNSManager *dns.PowerDNSManager
7274
clusterManager *cluster.Manager
75+
dashboardServer *http.Server
76+
cachedIP string
7377
}
7478

7579
func New(cfg *config.Config, configPath string) *Server {
@@ -232,6 +236,9 @@ func New(cfg *config.Config, configPath string) *Server {
232236

233237
s.setupRoutes()
234238

239+
// Start dashboard server on port 8080
240+
go s.StartDashboard()
241+
235242
return s
236243
}
237244

@@ -246,6 +253,24 @@ func (s *Server) setupRoutes() {
246253
// WebSocket endpoint handles its own auth via first-message
247254
api.GET("/containers/:id/exec", s.containerExec)
248255

256+
// Setup endpoints (public, permissive CORS, gated by setup state)
257+
setup := api.Group("/setup")
258+
setup.Use(s.setupCORS())
259+
{
260+
setup.GET("/status", s.getSetupStatus)
261+
262+
guarded := setup.Group("")
263+
guarded.GET("/info", s.getSetupInfo)
264+
guarded.Use(s.setupGuard())
265+
{
266+
guarded.POST("/validate", s.validateSystem)
267+
guarded.GET("/verify-dns", s.verifyDNS)
268+
guarded.POST("/settings", s.configureSettings)
269+
guarded.POST("/authentication", s.configureAuthentication)
270+
guarded.POST("/complete", s.completeSetup)
271+
}
272+
}
273+
249274
protected := api.Group("")
250275
protected.Use(s.authMiddleware.RequireAuth())
251276
if s.auditMiddleware != nil {
@@ -564,10 +589,58 @@ func (s *Server) Start() error {
564589
return s.server.ListenAndServe()
565590
}
566591

592+
func (s *Server) StartDashboard() {
593+
installDir := os.Getenv("INSTALL_DIR")
594+
if installDir == "" {
595+
installDir = "/opt/flatrun"
596+
}
597+
distDir := filepath.Join(installDir, "ui", "dist")
598+
599+
apiTarget, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", s.config.API.Port))
600+
proxy := httputil.NewSingleHostReverseProxy(apiTarget)
601+
602+
mux := http.NewServeMux()
603+
604+
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
605+
proxy.ServeHTTP(w, r)
606+
})
607+
608+
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
609+
filePath := filepath.Join(distDir, filepath.Clean(r.URL.Path))
610+
611+
if info, err := os.Stat(filePath); err == nil && !info.IsDir() {
612+
http.ServeFile(w, r, filePath)
613+
return
614+
}
615+
616+
indexPath := filepath.Join(distDir, "index.html")
617+
if _, err := os.Stat(indexPath); err != nil {
618+
http.Error(w, "Dashboard UI not installed", http.StatusNotFound)
619+
return
620+
}
621+
http.ServeFile(w, r, indexPath)
622+
})
623+
624+
s.dashboardServer = &http.Server{
625+
Addr: ":8080",
626+
Handler: mux,
627+
}
628+
629+
log.Printf("Dashboard server starting on :8080 (serving %s)", distDir)
630+
if err := s.dashboardServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
631+
log.Printf("Dashboard server error: %v", err)
632+
}
633+
}
634+
567635
func (s *Server) Stop() error {
568636
if s.clusterManager != nil {
569637
s.clusterManager.Stop()
570638
}
639+
if s.dashboardServer != nil {
640+
dCtx, dCancel := context.WithTimeout(context.Background(), 5*time.Second)
641+
defer dCancel()
642+
_ = s.dashboardServer.Shutdown(dCtx)
643+
}
571644
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
572645
defer cancel()
573646
return s.server.Shutdown(ctx)
@@ -3769,14 +3842,41 @@ func (s *Server) getSystemStats(c *gin.Context) {
37693842

37703843
systemStats, _ := system.GetSystemStats()
37713844

3845+
var systemPortCount int
3846+
if ports, err := s.networksManager.ListPorts(); err == nil {
3847+
systemPortCount = len(ports)
3848+
}
3849+
3850+
var systemServiceCount int
3851+
if services, err := s.servicesManager.ListServices(); err == nil {
3852+
systemServiceCount = len(services)
3853+
}
3854+
3855+
var infraCount int
3856+
if services, err := s.infraManager.ListServices(); err == nil {
3857+
infraCount = len(services)
3858+
}
3859+
3860+
var certCount int
3861+
if certs, err := s.proxyOrchestrator.ListCertificates(); err == nil {
3862+
certCount = len(certs)
3863+
}
3864+
3865+
appCount := len(s.pluginRegistry.List())
3866+
37723867
c.JSON(http.StatusOK, gin.H{
3773-
"deployments": depStats,
3774-
"containers": containerStats,
3775-
"images": imageStats,
3776-
"volumes": volumeStats,
3777-
"networks": gin.H{"total": networkCount},
3778-
"ports": gin.H{"total": portCount},
3779-
"system": systemStats,
3868+
"deployments": depStats,
3869+
"containers": containerStats,
3870+
"images": imageStats,
3871+
"volumes": volumeStats,
3872+
"networks": gin.H{"total": networkCount},
3873+
"ports": gin.H{"total": portCount},
3874+
"system": systemStats,
3875+
"system_ports": gin.H{"total": systemPortCount},
3876+
"services": gin.H{"total": systemServiceCount},
3877+
"infrastructure": gin.H{"total": infraCount},
3878+
"certificates": gin.H{"total": certCount},
3879+
"apps": gin.H{"total": appCount},
37803880
})
37813881
}
37823882

0 commit comments

Comments
 (0)