// Package api exposes the HTTP control-plane used by the MCP gateway // and the orchestrator dashboard. package api import ( "encoding/json" "fmt" "net/http" "forge.secuaas.ovh/olivier/claude-failover/internal/delegation" "forge.secuaas.ovh/olivier/claude-failover/internal/state" ) const version = "0.2.0" // DelegationProvider is the slice of delegation.Manager used by the HTTP // server. Kept as an interface so tests don't have to spin up a real // secutools client. type DelegationProvider interface { Active() []delegation.ActiveJob CountersSnapshot() delegation.Snapshot } // Server is a minimal HTTP server exposing /health, /status, // /watchdog/status and /api/delegated/status. type Server struct { addr string state *state.State delegation DelegationProvider } // New creates a Server listening on addr. func New(addr string, s *state.State) *Server { return &Server{addr: addr, state: s} } // WithDelegation enables /api/delegated/* endpoints. Pass nil (or skip // the call) to keep them disabled — those paths return 404. func (s *Server) WithDelegation(d DelegationProvider) *Server { s.delegation = d return s } // Start registers routes and begins serving. Blocks until the listener fails. func (s *Server) Start() error { mux := http.NewServeMux() mux.HandleFunc("/health", s.handleHealth) mux.HandleFunc("/status", s.handleStatus) mux.HandleFunc("/watchdog/status", s.handleWatchdogStatus) mux.HandleFunc("/api/delegated/status", s.handleDelegatedStatus) return http.ListenAndServe(s.addr, mux) } func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"status":"ok","version":%q}`, version) } func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write(s.state.JSON()) } // handleWatchdogStatus returns operational counters consumed by the // orchestrator dashboard. Includes delegation metrics when wired. func (s *Server) handleWatchdogStatus(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") out := map[string]any{ "version": version, } if s.delegation != nil { out["delegation"] = s.delegation.CountersSnapshot() } _ = json.NewEncoder(w).Encode(out) } // handleDelegatedStatus returns the list of in-flight delegated jobs. func (s *Server) handleDelegatedStatus(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if s.delegation == nil { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"error":"delegation disabled"}`)) return } out := map[string]any{ "active": s.delegation.Active(), "counters": s.delegation.CountersSnapshot(), } _ = json.NewEncoder(w).Encode(out) }