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:
OpenCode Test
2025-12-26 11:34:36 -08:00
parent a80f714fc2
commit 5646508adb
18 changed files with 1712 additions and 0 deletions

View 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)
}
}