// Package admin serves the operator's view of crossbar: the health table and the routes as JSON, // mounted at /_crossbar/ on the same listener as the proxy. The shape of /_crossbar/hosts is // fixed so operators can read why a request went where it went. package admin import ( "encoding/json" "net/http" "time" "git.wntrmute.dev/kyle/crossbar/internal/config" "git.wntrmute.dev/kyle/crossbar/internal/health" ) // Hosts is what the admin handler needs from the health table. type Hosts interface { All() map[string]health.Status } type HostView struct { Healthy bool `json:"healthy"` Loaded []string `json:"loaded"` // never null: an empty slice when nothing is loaded LastOK string `json:"last_ok"` // time.RFC3339 in UTC, or "" if never LastErr string `json:"last_err"` } type RouteView struct { Hosts []string `json:"hosts"` DefaultModel string `json:"default_model"` } // Handler serves GET /_crossbar/hosts and GET /_crossbar/routes. Any other method on those paths is // a 405 with an Allow: GET header; anything else under the handler is a 404. func Handler(cfg *config.Config, h Hosts) http.Handler { mux := http.NewServeMux() mux.HandleFunc("/_crossbar/hosts", hostsHandler(h)) mux.HandleFunc("/_crossbar/routes", routesHandler(cfg)) mux.HandleFunc("/", notFound) return mux } func hostsHandler(h Hosts) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { wrongMethod(w) return } views := make(map[string]HostView, len(h.All())) for name, s := range h.All() { views[name] = hostView(s) } writeJSON(w, http.StatusOK, views) } } func routesHandler(cfg *config.Config) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { wrongMethod(w) return } views := make(map[string]RouteView, len(cfg.Routes)) for name, route := range cfg.Routes { hosts := make([]string, len(route.Hosts)) copy(hosts, route.Hosts) views[name] = RouteView{Hosts: hosts, DefaultModel: route.DefaultModel} } writeJSON(w, http.StatusOK, views) } } func hostView(s health.Status) HostView { loaded := s.Loaded if loaded == nil { loaded = []string{} } lastOK := "" if !s.LastOK.IsZero() { lastOK = s.LastOK.UTC().Format(time.RFC3339) } return HostView{ Healthy: s.Healthy, Loaded: loaded, LastOK: lastOK, LastErr: s.LastErr, } } func wrongMethod(w http.ResponseWriter) { w.Header().Set("Allow", "GET") writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"}) } func notFound(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) }