// Package dashboard implements the admin dashboard's HTTP surface: a // server-rendered (html/template + htmx + Alpine.js) web UI giving full // coverage of control-api's /api/v1/admin/* API. It never talks to // internal/db directly and has no state of its own — every page and // fragment is computed fresh, on each request, from control-api's API via // the client in client.go. package dashboard import ( "html/template" "log/slog" "net/http" "time" ) type Config struct { ControlAPIBaseURL string ControlAPITimeout time.Duration LastCompletedCount int OverviewPollIntervalS int // ControlAPIToken is the admin bearer token sent to control-api. Empty = // no Authorization header. ControlAPIToken string // Username/Password are the single dashboard administrator's login. If // either is empty, login is DISABLED (every page is open). Username string Password string // SessionSecret is the HMAC key for session cookies; empty = random per // process start (sessions are lost on restart). SessionSecret string // SessionTTL is the session lifetime (default 8h). SessionTTL time.Duration } type Server struct { CA *client Cfg Config tmpl *template.Template Log *slog.Logger auth *authState } func New(cfg Config, log *slog.Logger) (*Server, error) { tmpl, err := parseTemplates() if err != nil { return nil, err } ca := newClient(cfg.ControlAPIBaseURL, cfg.ControlAPITimeout) ca.token = cfg.ControlAPIToken return &Server{ CA: ca, Cfg: cfg, tmpl: tmpl, Log: log, auth: newAuthState(cfg, log), }, nil } func (s *Server) Handler() http.Handler { mux := http.NewServeMux() s.routes(mux) // Never log cookies, Authorization or form bodies here. return loggingMiddleware(s.Log, s.authMiddleware(mux)) } func loggingMiddleware(log *slog.Logger, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) log.Debug("request", "method", r.Method, "path", r.URL.Path) }) }