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

+58
View File
@@ -1,6 +1,7 @@
package analytics
import (
"net/netip"
"reflect"
"testing"
"time"
@@ -30,12 +31,18 @@ func (f *fixture) check(reg int64, source, typ, target string, ok bool, validato
}
func (f *fixture) compute(t *testing.T, subnets []db.Subnet) *Analysis {
t.Helper()
return f.computeIn(t, subnets, netip.Prefix{})
}
func (f *fixture) computeIn(t *testing.T, subnets []db.Subnet, subnet netip.Prefix) *Analysis {
t.Helper()
end := t0.Add(10 * time.Minute)
an, err := Compute(Input{
Run: db.CheckRun{ID: 7, Kind: db.RunManual, State: db.RunFinalized, StartedAt: t0, FinalizedAt: &end},
Results: f.results,
Subnets: subnets,
Subnet: subnet,
SiteNames: map[int]string{1: "rxmsk", 2: "rxyc"},
Each: func(fn func(db.RunCheck)) error {
for _, c := range f.checks {
@@ -153,6 +160,57 @@ func TestComputeCountsFactsPerAddress(t *testing.T) {
}
}
// TestComputeNarrowedToSubnet: with Input.Subnet every block and every list is
// built from the addresses inside it (nested subnets included) and nothing else.
func TestComputeNarrowedToSubnet(t *testing.T) {
f := &fixture{}
f.addr(1, "10.0.0.1", db.ResultPass, 3)
f.addr(2, "10.0.0.2", db.ResultPartial, 3)
f.addr(3, "10.0.1.1", db.ResultPartial, 3)
f.addr(4, "10.0.1.200", db.ResultCancelled, 3)
for reg := int64(1); reg <= 4; reg++ {
f.check(reg, eg, "https", "https://a.test", reg != 2, "vkiplab-v1", "", false)
f.check(reg, s1, "icmp", "ip", true, "vkiplab-v1", "", false)
f.check(reg, s1, "ssh", "ip", reg != 3, "vkiplab-v1", "dial tcp: i/o timeout", false)
}
subnets := []db.Subnet{{CIDR: "10.0.0.0/24"}, {CIDR: "10.0.1.0/24"}}
all := f.compute(t, subnets).Report
if all.Scope != nil || all.Summary.Addresses != 3 || len(all.Subnets) != 2 {
t.Fatalf("without a subnet the whole run is reported: %+v %+v", all.Scope, all.Summary)
}
// 10.0.0.0/23 holds both configured subnets: the same numbers as the whole run.
if wide := f.computeIn(t, subnets, netip.MustParsePrefix("10.0.0.0/23")).Report; wide.Summary != all.Summary || len(wide.Subnets) != 2 {
t.Errorf("a wider subnet changes nothing: %+v", wide.Summary)
}
an := f.computeIn(t, subnets, netip.MustParsePrefix("10.0.1.0/24"))
r := an.Report
if r.Scope == nil || r.Scope.Subnet != "10.0.1.0/24" || r.Scope.RunAddresses != 3 {
t.Errorf("scope = %+v", r.Scope)
}
if r.Summary.Addresses != 1 || r.Summary.Partial != 1 || r.Summary.Pass != 0 || r.Summary.Cancelled != 1 || r.Summary.IngressSSHAny != 1 || r.Summary.EgressHTTPSAny != 0 {
t.Errorf("summary = %+v", r.Summary)
}
if len(r.Subnets) != 1 || r.Subnets[0].CIDR != "10.0.1.0/24" || r.Subnets[0].Addresses != 1 {
t.Errorf("subnets = %+v", r.Subnets)
}
if !reflect.DeepEqual(r.Targets.Failed["https"], []int{0}) || len(r.Sites.Rows) != 1 || r.Sites.Rows[0].Stats[1] != (SiteStat{Total: 1, OK: 0}) {
t.Errorf("targets %+v, sites %+v", r.Targets, r.Sites)
}
// the lists are cut from the same subset, so they agree with the tiles
for kind, want := range map[string]int{ListVerdictPartial: r.Summary.Partial, ListVerdictPass: 0, ListIngressSSHAny: r.Summary.IngressSSHAny, ListEgressHTTPSAny: 0} {
l, err := an.List(kind, "")
if err != nil {
t.Fatal(err)
}
if len(l.Rows) != want {
t.Errorf("%s: %d rows, want %d", kind, len(l.Rows), want)
}
}
}
func TestSubnetMatching(t *testing.T) {
in := []db.Subnet{{CIDR: "10.0.0.0/8"}, {CIDR: "10.1.0.0/16"}}
m := newSubnetMatcher(in)