Registry and Analytics: run, subnet, direction and protocol filters, successes-by-target chart

Registry (/registry):
- filters by run (slice by the address's cycle in that run), subnet
  (drop-down of configured subnets), direction (egress/ingress) and
  protocol (icmp, tcp, ssh, https, tls); status in scope is computed over
  the narrowed checks
- chart "successful checks per target (egress) / site (ingress)" when both
  direction and protocol are chosen; a row opens the list of addresses
  (dialog, CSV)
- API: direction/protocol parameters and run in GET /admin/registry,
  GET /admin/registry/breakdown and /breakdown/list
- subnet filter passes ids as one JSON parameter (SQLite variable limit)

Analytics (/analytics):
- subnet filter recomputes the whole page over the addresses of the run
  inside the subnet; only their checks are read; cache per run and subnet
- direction and protocol focus the page; with both set the registry chart
  is shown
- subnet parameter in GET /admin/analytics/runs/{id} and lists (JSON, CSV)

Docs: plans and summaries in docs/changes, README, API, USAGE.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-06 14:23:48 +03:00
1 parent 068c10ea1c
commit ded196ec8d
40 files changed
+2545 -188

No files matched your search

+30
View File
@@ -60,6 +60,13 @@ type fakeControlAPI struct {
scanFreeAddresses []string
registry map[string]registryItem
registryChecks map[string][]check
// The chart of /registry: the breakdown served (breakdownStatus != 0 makes
// it fail with that status), the table behind a row and the raw queries
// of both endpoints.
breakdown registryBreakdown
breakdownStatus int
breakdownList string
breakdownReqs []string
// Scan job state machine (see the scan handlers): POST starts a job that
// stays "running" for scanRunPolls GET polls (0 = finishes at once), then
@@ -483,6 +490,29 @@ func (f *fakeControlAPI) handler() http.Handler {
}
writeJSON(w, http.StatusOK, registryPage{Items: page, Total: len(matched), Limit: limit, Offset: offset})
})
mux.HandleFunc("GET /api/v1/admin/registry/breakdown", func(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()
f.breakdownReqs = append(f.breakdownReqs, r.URL.RequestURI())
if f.breakdownStatus != 0 {
writeAPIErr(w, f.breakdownStatus, "breakdown unavailable")
return
}
writeJSON(w, http.StatusOK, f.breakdown)
})
mux.HandleFunc("GET /api/v1/admin/registry/breakdown/list", func(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()
f.breakdownReqs = append(f.breakdownReqs, r.URL.RequestURI())
if r.URL.Query().Get("format") == "csv" {
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="registry_ingress_tls_rxmsk.csv"`)
_, _ = w.Write([]byte("Адрес\r\n1.2.3.4\r\n"))
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(f.breakdownList))
})
mux.HandleFunc("GET /api/v1/admin/registry/{ip}", func(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()