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 // except /healthz requires "Authorization: Bearer ". // // Routes: // // GET /healthz liveness // 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, …) // GET /export flat report×finding×grade rows (the dashboard feed) // GET /scoreboard points-free per-model rollup 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) }) return auth(token, mux) } // auth gates everything but /healthz 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 != "" && r.URL.Path != "/healthz" { 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()}) }