Analytics: make the pass, partial and fail indicators clickable

The three verdict cards on /analytics now open the same dialog as the https/ssh
cards, with the addresses of the run that got this verdict (CSV and copy
included). New list kinds verdict_pass, verdict_partial and verdict_fail in
GET /admin/analytics/runs/{id}/lists/{kind}: address, subnet, validator,
egress and ingress "ok of all", checks stored of expected; partial adds the
reason, the same names as the "Why partial" block. Cancelled addresses are not
listed; the row count equals summary.pass/partial/fail. The fail card stays
inert at zero. addr.incomplete() is shared by the list and the report.

Docs, plan and summary in docs/changes/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-04 10:00:39 +03:00
1 parent e95b5eb7d5
commit 2f038f8362
15 files changed
+294 -14

No files matched your search

+97
View File
@@ -227,3 +227,100 @@ func TestListsAndShortValidator(t *testing.T) {
}
}
}
func TestVerdictLists(t *testing.T) {
f := &fixture{}
f.addr(1, "10.0.0.10", db.ResultPass, 4) // late failed ssh: pass by verdict, failed by facts
f.addr(2, "10.0.0.2", db.ResultPass, 4)
f.addr(3, "10.0.0.9", db.ResultPartial, 4) // egress failed, one check missing
f.addr(4, "10.0.0.3", db.ResultPartial, -1) // expected number unknown
f.addr(5, "10.0.0.5", db.ResultFail, 1) // no egress checks at all
f.addr(6, "10.0.0.4", db.ResultFail, 2)
f.addr(7, "10.0.0.1", db.ResultCancelled, 4)
for _, reg := range []int64{1, 2} {
f.check(reg, eg, "https", "https://a.test", true, "vkiplab-v1", "", false)
f.check(reg, eg, "https", "https://b.test", true, "vkiplab-v1", "", false)
f.check(reg, s1, "icmp", "ip", true, "vkiplab-v1", "", false)
}
f.check(1, s1, "ssh", "ip", false, "vkiplab-v1", "dial tcp: i/o timeout", true)
f.check(2, s1, "ssh", "ip", true, "vkiplab-v1", "", false)
f.check(3, eg, "https", "https://a.test", false, "vkiplab-v12", "", false)
f.check(3, eg, "https", "https://b.test", false, "vkiplab-v12", "", false)
f.check(3, s1, "icmp", "ip", true, "vkiplab-v12", "", false)
f.check(4, eg, "https", "https://a.test", true, "vkiplab-v3", "", false)
f.check(4, s1, "ssh", "ip", false, "vkiplab-v3", "dial tcp: i/o timeout", false)
f.check(5, s1, "icmp", "ip", false, "vkiplab-v5", "", false)
f.check(6, eg, "https", "https://a.test", false, "vkiplab-v5", "", false)
f.check(6, s1, "icmp", "ip", false, "vkiplab-v5", "", false)
f.check(7, eg, "https", "https://a.test", true, "vkiplab-v1", "", false)
an := f.compute(t, nil)
// Every list has as many rows as its number on the card; the cancelled address is in none.
s := an.Report.Summary
rows := map[string][][]string{}
for kind, want := range map[string]int{ListVerdictPass: s.Pass, ListVerdictPartial: s.Partial, ListVerdictFail: s.Fail} {
l, err := an.List(kind, "")
if err != nil {
t.Fatal(err)
}
if l.Kind != kind || len(l.Rows) != want || want != 2 {
t.Fatalf("%s: %d rows, summary %d", kind, len(l.Rows), want)
}
for _, r := range l.Rows {
if r[0] == "10.0.0.1" || len(r) != len(l.Columns) {
t.Errorf("%s: bad row %v for columns %v", kind, r, l.Columns)
}
}
rows[kind] = l.Rows
}
// Numeric address order, not text order.
if p := rows[ListVerdictPass]; p[0][0] != "10.0.0.2" || p[1][0] != "10.0.0.10" {
t.Errorf("pass order: %v", p)
}
l, _ := an.List(ListVerdictPass, "")
if want := []string{"Адрес", "Подсеть", "Валидатор", "Egress", "Ingress", "Проверок в цикле"}; !reflect.DeepEqual(l.Columns, want) {
t.Errorf("pass columns: %v", l.Columns)
}
// Levels count successful of all; the failed late ssh shows in ingress.
if want := []string{"10.0.0.2", "10.0.0.0/24", "v1", "2 из 2", "2 из 2", "4 из 4"}; !reflect.DeepEqual(rows[ListVerdictPass][0], want) {
t.Errorf("pass row: %v", rows[ListVerdictPass][0])
}
if r := rows[ListVerdictPass][1]; r[3] != "2 из 2" || r[4] != "1 из 2" || r[5] != "4 из 4" {
t.Errorf("pass with a late failure: %v", r)
}
// Partial has the reason of the "why partial" block; a short set shows stored of expected,
// an unknown expectation only the stored number.
l, _ = an.List(ListVerdictPartial, "")
if len(l.Columns) != 7 || l.Columns[6] != "Причина" {
t.Errorf("partial columns: %v", l.Columns)
}
if want := []string{"10.0.0.3", "10.0.0.0/24", "v3", "1 из 1", "0 из 1", "2", "Только ingress"}; !reflect.DeepEqual(l.Rows[0], want) {
t.Errorf("partial unknown expected: %v", l.Rows[0])
}
if want := []string{"10.0.0.9", "10.0.0.0/24", "v12", "0 из 2", "1 из 1", "3 из 4", "Egress и неполный набор"}; !reflect.DeepEqual(l.Rows[1], want) {
t.Errorf("partial incomplete: %v", l.Rows[1])
}
byReason := map[string]int{}
for _, r := range l.Rows {
byReason[r[6]]++
}
for _, x := range an.Report.Reasons {
if byReason[x.Name] != x.Count {
t.Errorf("reason %q: %d rows, %d in the report", x.Name, byReason[x.Name], x.Count)
}
}
// A level without checks shows a dash, so does a missing validator.
l, _ = an.List(ListVerdictFail, "")
if len(l.Columns) != 6 {
t.Errorf("fail columns: %v", l.Columns)
}
if want := []string{"10.0.0.4", "10.0.0.0/24", "v5", "0 из 1", "0 из 1", "2 из 2"}; !reflect.DeepEqual(l.Rows[0], want) {
t.Errorf("fail row: %v", l.Rows[0])
}
if want := []string{"10.0.0.5", "10.0.0.0/24", "—", "—", "0 из 1", "1 из 1"}; !reflect.DeepEqual(l.Rows[1], want) {
t.Errorf("fail row without egress: %v", l.Rows[1])
}
}