ddcf42a3ce
SQLite-backed HTTP store for Gadfly review findings, per-review run timings, and human/Claude grades, with a points-free per-model scoreboard. Pure fact store: it computes no points or rankings (the dashboard maps severity->points client-side and retunes without re-scoring). Findings are content-addressed by location so cross-model reports collapse for consensus; one grade per finding, latest wins. Pure-Go SQLite (CGO-free) + Docker image CI + tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
122 lines
3.5 KiB
Go
122 lines
3.5 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
|
||
// except /healthz requires "Authorization: Bearer <token>".
|
||
//
|
||
// 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()})
|
||
}
|