package main import ( "embed" "flag" "io/fs" "log" "net/http" "os" "path/filepath" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/will/k8s-agent-dashboard/internal/api" "github.com/will/k8s-agent-dashboard/internal/claude" "github.com/will/k8s-agent-dashboard/internal/store" ) //go:embed all:web var webFS embed.FS func defaultClaudeDir() string { home, err := os.UserHomeDir() if err != nil { return "/home/will/.claude" // fallback; best-effort } return filepath.Join(home, ".claude") } func main() { port := flag.String("port", "8080", "Server port") dataDir := flag.String("data", "/data", "Data directory for state") claudeDir := flag.String("claude", defaultClaudeDir(), "Claude Code directory") flag.Parse() // Initialize store s, err := store.New(*dataDir) if err != nil { log.Fatalf("Failed to initialize store: %v", err) } // Initialize Claude loader claudeLoader := claude.NewLoader(*claudeDir) // Initialize event hub hub := claude.NewEventHub(1000) // Create router r := chi.NewRouter() // Middleware r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Compress(5)) r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Content-Type"}, ExposedHeaders: []string{"Link"}, AllowCredentials: false, MaxAge: 300, })) // API routes r.Route("/api", func(r chi.Router) { r.Get("/health", api.HealthCheck) r.Route("/claude", func(r chi.Router) { r.Get("/health", api.GetClaudeHealth(claudeLoader)) r.Get("/stats", api.GetClaudeStats(claudeLoader)) r.Get("/summary", api.GetClaudeSummary(claudeLoader)) r.Get("/inventory", api.GetClaudeInventory(claudeLoader)) r.Get("/debug/files", api.GetClaudeDebugFiles(claudeLoader)) r.Get("/live/backlog", api.GetClaudeLiveBacklog(claudeLoader)) r.Get("/stream", api.GetClaudeStream(hub)) }) r.Get("/status", api.GetClusterStatus(s)) r.Get("/pending", api.GetPendingActions(s)) r.Post("/pending/{id}/approve", api.ApproveAction(s)) r.Post("/pending/{id}/reject", api.RejectAction(s)) r.Get("/history", api.GetActionHistory(s)) r.Get("/workflows", api.GetWorkflows(s)) r.Post("/workflows/{name}/run", api.RunWorkflow(s)) r.Post("/workflows/{name}/complete", api.CompleteWorkflow(s)) // Webhook endpoints r.Post("/webhooks/alertmanager", api.AlertmanagerWebhook(s)) }) // Static files webContent, err := fs.Sub(webFS, "web") if err != nil { log.Fatalf("Failed to get web content: %v", err) } fileServer := http.FileServer(http.FS(webContent)) r.Handle("/*", fileServer) // Start server addr := ":" + *port if envPort := os.Getenv("PORT"); envPort != "" { addr = ":" + envPort } log.Printf("Starting server on %s", addr) log.Printf("Data directory: %s", *dataDir) log.Printf("Claude directory: %s", *claudeDir) stop := make(chan struct{}) go claude.TailHistoryFile(stop, hub, filepath.Join(*claudeDir, "history.jsonl")) if err := http.ListenAndServe(addr, r); err != nil { log.Fatalf("Server failed: %v", err) } }