35ebc53561
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>
154 lines
4.9 KiB
Go
154 lines
4.9 KiB
Go
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()})
|
||
}
|