Files
gadfly-reports/server.go
T
steve 35ebc53561
Build & push image / build-and-push (push) Successful in 26s
CI / test (push) Successful in 10m24s
feat: built-in read-only dashboard at /ui + GET /runs
Serves a self-contained vanilla-JS dashboard (embedded via go:embed): a per-model performance table — runs, minutes, findings, confirmed/false-positive/ungraded, points, points-per-minute, points-per-run, by-severity — with drill-down filters (date range, repo, provider, model, lens, grade/severity), free-text search, and a click-to-scope findings detail table.

Scoring stays client-side: the page has an editable points curve and computes points + value-per-minute in the browser, so the store remains point-free. Adds GET /runs (lists all runs, incl. zero-finding ones) so minutes/runs are filterable. The /ui shell is public (carries no data); data endpoints stay token-gated and the JS sends the token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:22:39 -04:00

154 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <token>" 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()})
}