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>
This commit is contained in:
@@ -9,16 +9,22 @@ import (
|
||||
)
|
||||
|
||||
// newServer wires the store to the HTTP API. If token is non-empty, every route
|
||||
// except /healthz requires "Authorization: Bearer <token>".
|
||||
// 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 /healthz liveness
|
||||
// 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, …)
|
||||
// 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()
|
||||
|
||||
@@ -82,13 +88,39 @@ func newServer(store *Store, token string) http.Handler {
|
||||
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)
|
||||
}
|
||||
|
||||
// auth gates everything but /healthz behind a bearer token, when one is set.
|
||||
// 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 != "" && r.URL.Path != "/healthz" {
|
||||
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"))
|
||||
|
||||
Reference in New Issue
Block a user