feat: Implement Phase 2 dashboard for K8s agent system
Lightweight Go-based dashboard for Raspberry Pi cluster: Backend: - chi router with REST API - Embedded static file serving - JSON file-based state storage - Health checks and CORS support Frontend: - Responsive dark theme UI - Status view with nodes, alerts, ArgoCD apps - Pending actions with approve/reject - Action history and audit trail - Workflow listing and manual triggers Deployment: - Multi-stage Dockerfile (small Alpine image) - Kubernetes manifests with Pi 3 tolerations - Resource limits: 32-64Mi memory, 10-100m CPU - ArgoCD application manifest - Kustomize configuration API endpoints: - GET /api/status - Cluster status - GET/POST /api/pending - Action management - GET /api/history - Action audit trail - GET/POST /api/workflows - Workflow management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
81
dashboard/cmd/server/main.go
Normal file
81
dashboard/cmd/server/main.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"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/store"
|
||||
)
|
||||
|
||||
//go:embed all:web
|
||||
var webFS embed.FS
|
||||
|
||||
func main() {
|
||||
port := flag.String("port", "8080", "Server port")
|
||||
dataDir := flag.String("data", "/data", "Data directory for state")
|
||||
flag.Parse()
|
||||
|
||||
// Initialize store
|
||||
s, err := store.New(*dataDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize store: %v", err)
|
||||
}
|
||||
|
||||
// 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.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))
|
||||
})
|
||||
|
||||
// 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)
|
||||
|
||||
if err := http.ListenAndServe(addr, r); err != nil {
|
||||
log.Fatalf("Server failed: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user