Add the Analytics section: check runs, analytics API and page
Runs (migration 0011): a run groups the cycles of one launch. It opens when an address enters an idle queue, takes everything submitted or re-checked while it is open and is finalized when all its addresses are done; a re-check after that opens a new run, so results of different runs never mix. check_runs, run_results (one result per address and run, with the verdict and the expected and stored check counts), subnets, run_id on ip_queue and checks. Existing data is split into runs at pauses of more than an hour; ingress checks get the validator that held the address (also at write time from now on). Analytics (internal/analytics): figures computed from the stored checks of the latest cycle of each address in the run, as facts next to the verdict: summary, reasons of partial, data quality, subnets, targets and the subnet x target matrix by check type, ingress by site, error classes, validators, and the address lists behind the indicators and error classes. API: analytics runs, report, lists (JSON or CSV), subnet list; run and subnet filters for the registry. Dashboard: /analytics matching the approved mockup (run selector, indicators with address lists and CSV, error-class dialogs, drill-down to the registry), subnet list on /settings. Sidebar: the control-api link state, theme toggle and logout moved to the top, the three dots next to the logo removed, sections grouped. Rebuilt bin/control-api and bin/admin-dashboard to match. Plan, summary and the updated README, API, USAGE, DASHBOARD and ADMIN_CLEANUP docs are in docs/. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
864208238f
commit
b7669c9e41
44 files changed
+4123
-62
No files matched your search
@@ -37,6 +37,14 @@ type fakeControlAPI struct {
|
||||
inboundICMP bool
|
||||
|
||||
historyRetentionCycles int
|
||||
|
||||
// Analytics: the run selector, the report JSON per run, the lists per
|
||||
// "run/kind[/class]" and the subnet list of /settings.
|
||||
runs []analyticsRun
|
||||
reports map[int64]string
|
||||
lists map[string]string
|
||||
subnets subnetList
|
||||
analyticsReqs []string
|
||||
// scanFreeAddresses is what POST /ips/scan "discovers" — tests set it
|
||||
// directly rather than this fake reimplementing OpenStack floating-IP
|
||||
// filtering (already covered by internal/orchestrator's own tests).
|
||||
@@ -469,6 +477,76 @@ func (f *fakeControlAPI) handler() http.Handler {
|
||||
writeJSON(w, http.StatusOK, registryHistoryResponse{Registry: item, Checks: f.registryChecks[addr]})
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /api/v1/admin/config/subnets", func(w http.ResponseWriter, r *http.Request) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
out := f.subnets
|
||||
if out.Subnets == nil {
|
||||
out.Subnets = []subnetEntry{}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
})
|
||||
mux.HandleFunc("PUT /api/v1/admin/config/subnets", func(w http.ResponseWriter, r *http.Request) {
|
||||
var in subnetList
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeAPIErr(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
for _, s := range in.Subnets {
|
||||
if !strings.Contains(s.CIDR, "/") {
|
||||
writeAPIErr(w, http.StatusBadRequest, "subnet "+s.CIDR+": not a CIDR")
|
||||
return
|
||||
}
|
||||
}
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.subnets = in
|
||||
writeJSON(w, http.StatusOK, in)
|
||||
})
|
||||
mux.HandleFunc("GET /api/v1/admin/analytics/runs", func(w http.ResponseWriter, r *http.Request) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
out := f.runs
|
||||
if out == nil {
|
||||
out = []analyticsRun{}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
})
|
||||
mux.HandleFunc("GET /api/v1/admin/analytics/runs/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
f.analyticsReqs = append(f.analyticsReqs, r.URL.RequestURI())
|
||||
rep, ok := f.reports[id]
|
||||
if !ok {
|
||||
writeAPIErr(w, http.StatusNotFound, "run not found")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(rep))
|
||||
})
|
||||
mux.HandleFunc("GET /api/v1/admin/analytics/runs/{id}/lists/{kind}", func(w http.ResponseWriter, r *http.Request) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.analyticsReqs = append(f.analyticsReqs, r.URL.RequestURI())
|
||||
key := r.PathValue("id") + "/" + r.PathValue("kind")
|
||||
if c := r.URL.Query().Get("class"); c != "" {
|
||||
key += "/" + c
|
||||
}
|
||||
l, ok := f.lists[key]
|
||||
if !ok {
|
||||
writeAPIErr(w, http.StatusNotFound, "unknown list")
|
||||
return
|
||||
}
|
||||
if r.URL.Query().Get("format") == "csv" {
|
||||
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+r.PathValue("kind")+`_run`+r.PathValue("id")+`.csv"`)
|
||||
_, _ = w.Write([]byte("Адрес\r\n1.2.3.4\r\n"))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(l))
|
||||
})
|
||||
mux.HandleFunc("GET /api/v1/admin/config/inbound-checks", func(w http.ResponseWriter, r *http.Request) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
Reference in new issue
Block a user