// M13a W5: admin HTTP routes for archiverd. One route is // registered when BA_AUTHD_JWT_SECRET is set: // // POST /v1/admin/archiver/run — trigger an immediate archiver // pass (instead of waiting for the next ticker fire). Destructive: // takes the Postgres advisory lock and runs a full drain. Any // authenticated user can fire it; the existing runLock // guarantees no two pass can overlap. // // When BA_AUTHD_JWT_SECRET is unset, no admin routes are // registered. The /health and /metrics endpoints are owned by // the shared metricsHandler and continue to work as before. package main import ( "encoding/json" "log/slog" "net/http" "git3.techno-world.net/lrosales/broad-announce/internal/authd" ) // wireAdminRoutes wires the admin routes onto mux. The triggerCh // is a buffered channel of size 1; a non-blocking send coalesces // concurrent trigger requests (a run that's already in progress // will complete, and any queued trigger is dropped — the next // ticker will fire normally). // // When the JWT gate is disabled (env not set), the function logs // a warning and returns without registering anything — backward // compat for non-M13 deploys. func wireAdminRoutes(mux *http.ServeMux, triggerCh chan<- struct{}, logger *slog.Logger) { if !authd.EnvEnabled() { logger.Warn("admin routes are DISABLED (set BA_AUTHD_JWT_SECRET to enable)") return } ad, err := authd.NewFromEnv() if err != nil { logger.Error("BA_AUTHD_JWT_SECRET set but authd init failed; admin routes DISABLED", "err", err) return } logger.Info("admin routes enabled with JWT gate") // Any authenticated user can fire an archiver run; it's not // state-destroying (the archiver is idempotent thanks to the // Postgres advisory lock + the SELECT/DELETE contract). mux.Handle("POST /v1/admin/archiver/run", ad.RequireAuth(http.HandlerFunc(handleRunNow(triggerCh, logger)))) } // handleRunNow signals the runLoop to fire a pass. The send is // non-blocking: if a trigger is already pending (a previous // request fired and the loop hasn't picked it up yet), we report // it back to the caller instead of queueing duplicates. func handleRunNow(triggerCh chan<- struct{}, logger *slog.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { actor := "" if c := authd.ClaimsFromContext(r.Context()); c != nil { actor = c.UserID } select { case triggerCh <- struct{}{}: logger.Info("admin archiver run triggered", "actor", actor) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "triggered": true, }) default: // Trigger already pending or run in progress. // Coalesce — return 202 Accepted with a hint. logger.Info("admin archiver run coalesced (already pending)", "actor", actor) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) _ = json.NewEncoder(w).Encode(map[string]any{ "triggered": false, "reason": "already_pending", }) } } }