Files
claude-code/dashboard/internal/api/handlers.go
OpenCode Test 5646508adb 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>
2025-12-26 11:34:36 -08:00

158 lines
3.9 KiB
Go

package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/will/k8s-agent-dashboard/internal/store"
)
// JSON helper
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{"error": message})
}
// HealthCheck returns API health status
func HealthCheck(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, map[string]string{
"status": "ok",
"service": "k8s-agent-dashboard",
})
}
// GetClusterStatus returns current cluster status
func GetClusterStatus(s *store.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
status := s.GetClusterStatus()
respondJSON(w, http.StatusOK, status)
}
}
// GetPendingActions returns all pending actions
func GetPendingActions(s *store.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
actions := s.GetPendingActions()
respondJSON(w, http.StatusOK, map[string]interface{}{
"count": len(actions),
"actions": actions,
})
}
}
// ApproveAction approves a pending action
func ApproveAction(s *store.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" {
respondError(w, http.StatusBadRequest, "missing action id")
return
}
var body struct {
Reason string `json:"reason"`
}
json.NewDecoder(r.Body).Decode(&body)
action, err := s.ApproveAction(id, body.Reason)
if err != nil {
respondError(w, http.StatusNotFound, err.Error())
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"status": "approved",
"action": action,
"message": "Action approved and ready for execution",
})
}
}
// RejectAction rejects a pending action
func RejectAction(s *store.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" {
respondError(w, http.StatusBadRequest, "missing action id")
return
}
var body struct {
Reason string `json:"reason"`
}
json.NewDecoder(r.Body).Decode(&body)
if body.Reason == "" {
body.Reason = "Rejected by user"
}
action, err := s.RejectAction(id, body.Reason)
if err != nil {
respondError(w, http.StatusNotFound, err.Error())
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"status": "rejected",
"action": action,
"message": "Action rejected",
})
}
}
// GetActionHistory returns action history
func GetActionHistory(s *store.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
limitStr := r.URL.Query().Get("limit")
limit := 50
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil {
limit = l
}
}
history := s.GetActionHistory(limit)
respondJSON(w, http.StatusOK, map[string]interface{}{
"count": len(history),
"history": history,
})
}
}
// GetWorkflows returns defined workflows
func GetWorkflows(s *store.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
workflows := s.GetWorkflows()
respondJSON(w, http.StatusOK, map[string]interface{}{
"count": len(workflows),
"workflows": workflows,
})
}
}
// RunWorkflow triggers a workflow execution
func RunWorkflow(s *store.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
if name == "" {
respondError(w, http.StatusBadRequest, "missing workflow name")
return
}
// In Phase 2, we just acknowledge the request
// Phase 3 will implement actual execution via Claude Code
respondJSON(w, http.StatusAccepted, map[string]interface{}{
"status": "queued",
"workflow": name,
"message": "Workflow queued for execution. Use Claude Code CLI to run workflows.",
})
}
}