Files
gadfly-reports/server.go
T
steve ddcf42a3ce
Build & push image / build-and-push (push) Successful in 1m13s
CI / test (push) Successful in 10m39s
feat: gadfly-reports — findings store + scoreboard daemon
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>
2026-06-26 23:55:24 -04:00

122 lines
3.5 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
// 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()})
}