package main import ( "encoding/json" "errors" "log" "net/http" "strings" ) // newServer wires the store to the HTTP API. If token is non-empty, every route // requires "Authorization: Bearer " EXCEPT the always-public ones: // /healthz and the view shell (/ and /ui). The dashboard's data still comes from // the token-gated endpoints — its JS sends the token — so the public shell leaks // no data on its own. // // Routes: // // GET / redirect to /ui (public) // GET /ui read-only dashboard (HTML shell; data via fetch) (public) // GET /healthz liveness (public) // GET /runs list all runs (timing/tokens), oldest first // GET /export flat report×finding×grade rows (the dashboard feed) // GET /scoreboard points-free per-model rollup // POST /runs upsert one run (model review of a PR; timing/tokens) // POST /reports record a batch of findings + this model's reports // POST /findings/{id}/grade record a triage grade (is_real, severity, …) func newServer(store *Store, token string) http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) }) mux.HandleFunc("POST /runs", func(w http.ResponseWriter, r *http.Request) { var run Run if !decode(w, r, &run) { return } if err := store.AddRun(run); err != nil { writeErr(w, http.StatusBadRequest, err) return } writeJSON(w, http.StatusOK, map[string]string{"run_id": run.RunID}) }) mux.HandleFunc("POST /reports", func(w http.ResponseWriter, r *http.Request) { var reps []ReportIn if !decode(w, r, &reps) { return } ids, err := store.AddReports(reps) if err != nil { writeErr(w, http.StatusBadRequest, err) return } writeJSON(w, http.StatusOK, map[string]any{"finding_ids": ids}) }) mux.HandleFunc("POST /findings/{id}/grade", func(w http.ResponseWriter, r *http.Request) { var g Grade if !decode(w, r, &g) { return } g.FindingID = r.PathValue("id") if err := store.AddGrade(g); err != nil { writeErr(w, http.StatusBadRequest, err) return } writeJSON(w, http.StatusOK, map[string]string{"finding_id": g.FindingID}) }) mux.HandleFunc("GET /export", func(w http.ResponseWriter, _ *http.Request) { rows, err := store.Export() if err != nil { writeErr(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, rows) }) mux.HandleFunc("GET /scoreboard", func(w http.ResponseWriter, _ *http.Request) { stats, err := store.Scoreboard() if err != nil { writeErr(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, stats) }) mux.HandleFunc("GET /runs", func(w http.ResponseWriter, _ *http.Request) { runs, err := store.ListRuns() if err != nil { writeErr(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, runs) }) // View-only dashboard. The shell is public (no data); it fetches the // token-gated endpoints from the browser with the token the user supplies. mux.HandleFunc("GET /ui", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(uiHTML)) }) mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/ui", http.StatusFound) }) return auth(token, mux) } // publicPaths are reachable without the bearer token even when one is set: the // liveness probe and the dashboard SHELL (which carries no data — its JS fetches // the gated endpoints with the token). func isPublicPath(p string) bool { return p == "/healthz" || p == "/" || p == "/ui" || strings.HasPrefix(p, "/ui/") } // auth gates every non-public route behind a bearer token, when one is set. func auth(token string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if token != "" && !isPublicPath(r.URL.Path) { got := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if strings.TrimSpace(got) != token { writeErr(w, http.StatusUnauthorized, errors.New("missing or invalid bearer token")) return } } next.ServeHTTP(w, r) }) } // decode reads a JSON body into v, writing a 400 and returning false on failure. func decode(w http.ResponseWriter, r *http.Request, v any) bool { if err := json.NewDecoder(r.Body).Decode(v); err != nil { writeErr(w, http.StatusBadRequest, errors.New("invalid JSON body: "+err.Error())) return false } return true } func writeJSON(w http.ResponseWriter, code int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) if err := json.NewEncoder(w).Encode(v); err != nil { log.Printf("gadfly-reports: write response: %v", err) } } func writeErr(w http.ResponseWriter, code int, err error) { writeJSON(w, code, map[string]string{"error": err.Error()}) }