package api import ( "net/http" "os" "path/filepath" "strings" "crypto-miner-server/internal/builder" "crypto-miner-server/internal/db" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" ) func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler, builderHandler *builder.Handler, blueprintHandler *BlueprintHandler, aiHandler *AIHandler, fleetHandler *FleetHandler, webRoot string, publicURLOverride func() string) http.Handler { r := chi.NewRouter() // Middleware r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, AllowCredentials: true, })) // REST API r.Route("/api/v1", func(r chi.Router) { h := NewHandler(database) r.Get("/health", h.HealthCheck) r.Get("/server/info", func(w http.ResponseWriter, r *http.Request) { override := "" if publicURLOverride != nil { override = publicURLOverride() } GetServerInfo(w, r, override) }) // Dashboard r.Get("/dashboard/stats", h.GetDashboardStats) // Agents r.Get("/agents", h.ListAgents) r.Get("/agents/{id}", h.GetAgent) r.Get("/agents/{id}/stats", h.GetAgentStats) if fleetHandler != nil { r.Post("/agents/{id}/command", fleetHandler.PostAgentCommand) r.Get("/agents/{id}/log", fleetHandler.GetAgentLog) } // Fleet ops if fleetHandler != nil { r.Get("/alerts", fleetHandler.GetAlerts) r.Get("/pools/status", fleetHandler.GetPoolStatus) r.Get("/ai/activity", fleetHandler.GetAIActivity) r.Get("/earnings/estimate", fleetHandler.GetEarningsEstimate) } // Shares r.Get("/shares", h.GetRecentShares) // Builds r.Get("/builds", h.ListBuilds) r.Get("/builds/{id}/download", builderHandler.DownloadBuild) r.Get("/builds/{id}/uninstall", builderHandler.DownloadUninstall) // Config r.Get("/config", configHandler.ServeHTTP) r.Put("/config", configHandler.ServeHTTP) // Builder r.Post("/builder/build", builderHandler.ServeHTTP) // Blueprints (config presets) r.Get("/blueprints", blueprintHandler.ServeHTTP) r.Post("/blueprints", blueprintHandler.ServeHTTP) r.Delete("/blueprints", blueprintHandler.ServeHTTP) r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint) // AI Autonomy (Ollama) r.Post("/agent/decide", aiHandler.HandleDecide) r.Post("/agent/report", aiHandler.HandleReport) r.Post("/agent/heartbeat", aiHandler.HandleHeartbeat) }) // WebSocket r.Get("/ws/agent", wsHub.HandleAgentWS) r.Get("/ws/dashboard", wsHub.HandleDashboardWS) // Serve frontend SPA if webRoot != "" { // Check if webroot directory exists if info, err := os.Stat(webRoot); err == nil && info.IsDir() { // Create a file server for the webroot fileServer := http.FileServer(http.Dir(webRoot)) // SPA fallback: serve index.html for all non-API, non-WebSocket routes r.Get("/*", func(w http.ResponseWriter, r *http.Request) { // Clean the path path := strings.TrimPrefix(r.URL.Path, "/") fullPath := filepath.Join(webRoot, path) // Check if the file exists if _, err := os.Stat(fullPath); err == nil { fileServer.ServeHTTP(w, r) return } // SPA fallback - serve index.html http.ServeFile(w, r, filepath.Join(webRoot, "index.html")) }) } else { // Fallback if webroot doesn't exist r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") w.Write([]byte(`Crypto Miner

Crypto Miner Control Server

Server is running. Build the frontend with cd server/web && npm install && npm run build

API: /api/v1/health

`)) }) } } else { r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") w.Write([]byte(`Crypto Miner

Crypto Miner Control Server

Server is running. No frontend configured.

API: /api/v1/health

`)) }) } return r }