The module was declared as github.com/zzg/fathom, which is not where this repo lives and never has been. Nothing is published at that path, so `go get github.com/zzg/fathom` resolves to a 404 while the import paths in every file assert otherwise. git.z.gd is a real module path rather than a cosmetic correction: Gitea serves the go-import meta tag for it, <meta name="go-import" content="git.z.gd/zzg/fathom git https://git.z.gd/zzg/fathom.git"> and the repo answers unauthenticated over HTTPS, so `go get git.z.gd/zzg/fathom` resolves where the old path could not. Mechanical: the module line plus 46 import paths across 23 files. There is no go.sum to churn, since fathom has no third-party dependencies. go build, go vet, gofmt and the full test suite are clean under the new path, and the built binary still runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
117 lines
3.4 KiB
Go
117 lines
3.4 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"git.z.gd/zzg/fathom/internal/insights"
|
|
)
|
|
|
|
// The insights API.
|
|
//
|
|
// The analyzer runs on its own schedule in the background; these handlers only
|
|
// read what it last produced. A request never triggers analysis, so opening the
|
|
// tab cannot make the server do work proportional to the store.
|
|
|
|
func (s *Server) handleInsights(w http.ResponseWriter, r *http.Request) {
|
|
if s.insights == nil {
|
|
writeJSON(w, map[string]any{"ready": false, "disabled": true})
|
|
return
|
|
}
|
|
res, ready := s.insights.Latest()
|
|
if !ready {
|
|
// Distinguish "nothing found" from "not looked yet", or an empty tab on
|
|
// first load reads as a claim that there is nothing to find.
|
|
writeJSON(w, map[string]any{"ready": false})
|
|
return
|
|
}
|
|
writeJSON(w, map[string]any{"ready": true, "result": res})
|
|
}
|
|
|
|
func (s *Server) handleDismissInsight(w http.ResponseWriter, r *http.Request) {
|
|
if s.insights == nil {
|
|
httpError(w, http.StatusNotFound, "insights are not enabled")
|
|
return
|
|
}
|
|
var req struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil || req.ID == "" {
|
|
httpError(w, http.StatusBadRequest, "no insight id given")
|
|
return
|
|
}
|
|
s.insights.Dismiss(req.ID)
|
|
res, _ := s.insights.Latest()
|
|
writeJSON(w, map[string]any{"ready": true, "result": res})
|
|
}
|
|
|
|
func (s *Server) handleRestoreInsights(w http.ResponseWriter, r *http.Request) {
|
|
if s.insights == nil {
|
|
httpError(w, http.StatusNotFound, "insights are not enabled")
|
|
return
|
|
}
|
|
s.insights.Restore()
|
|
res, _ := s.insights.Latest()
|
|
writeJSON(w, map[string]any{"ready": true, "result": res})
|
|
}
|
|
|
|
func (s *Server) handleLocalToggle(w http.ResponseWriter, r *http.Request) {
|
|
if s.insights == nil {
|
|
httpError(w, http.StatusNotFound, "insights are not enabled")
|
|
return
|
|
}
|
|
var req struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil {
|
|
httpError(w, http.StatusBadRequest, "expected {\"enabled\": true|false}")
|
|
return
|
|
}
|
|
s.insights.SetLocalEnabled(req.Enabled)
|
|
res, _ := s.insights.Latest()
|
|
writeJSON(w, map[string]any{"ready": true, "result": res})
|
|
}
|
|
|
|
// handleSurface returns findings not yet reported and marks them as reported,
|
|
// in one call.
|
|
//
|
|
// It is a POST because it has a side effect, and one call rather than two
|
|
// because two sessions can end at the same moment: a read-then-write would have
|
|
// both report the same finding.
|
|
func (s *Server) handleSurface(w http.ResponseWriter, r *http.Request) {
|
|
if s.insights == nil {
|
|
writeJSON(w, map[string]any{"suppressed": "insights are not enabled"})
|
|
return
|
|
}
|
|
var req struct {
|
|
MinSeverity string `json:"min_severity"`
|
|
MinCostUSD float64 `json:"min_cost_usd"`
|
|
CooldownSecs int `json:"cooldown_secs"`
|
|
}
|
|
// A body is optional; the defaults are the point.
|
|
json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req)
|
|
|
|
opts := insights.DefaultSurfaceOptions()
|
|
if req.MinSeverity != "" {
|
|
opts.MinSeverity = insights.Severity(req.MinSeverity)
|
|
}
|
|
if req.MinCostUSD > 0 {
|
|
opts.MinCostUSD = req.MinCostUSD
|
|
}
|
|
if req.CooldownSecs > 0 {
|
|
opts.Cooldown = time.Duration(req.CooldownSecs) * time.Second
|
|
}
|
|
writeJSON(w, s.insights.SurfaceNew(opts))
|
|
}
|
|
|
|
func (s *Server) handleResetSurfaced(w http.ResponseWriter, r *http.Request) {
|
|
if s.insights == nil {
|
|
httpError(w, http.StatusNotFound, "insights are not enabled")
|
|
return
|
|
}
|
|
s.insights.ResetSurfaced()
|
|
writeJSON(w, map[string]any{"ok": true})
|
|
}
|