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

+43
View File
@@ -16,6 +16,10 @@ const (
ListIngressSSHAny = "ingress_ssh_any"
ListIngressSSHAll = "ingress_ssh_all"
ListError = "error"
// Addresses by the verdict the system gave them at aggregation.
ListVerdictPass = "verdict_" + db.ResultPass
ListVerdictPartial = "verdict_" + db.ResultPartial
ListVerdictFail = "verdict_" + db.ResultFail
)
// List is a table of addresses behind one indicator or one error class.
@@ -73,6 +77,22 @@ func (an *Analysis) List(kind, class string) (*List, error) {
l.Rows = append(l.Rows, []string{a.res.IPAddress, a.subnet, strings.Join(sites, ", "), strings.Join(errs, ", ")})
}
}
case ListVerdictPass, ListVerdictPartial, ListVerdictFail:
verdict := strings.TrimPrefix(kind, "verdict_")
l.Columns = []string{"Адрес", "Подсеть", "Валидатор", "Egress", "Ingress", "Проверок в цикле"}
if kind == ListVerdictPartial {
l.Columns = append(l.Columns, "Причина")
}
for _, a := range an.sorted() {
if a.res.Verdict != verdict {
continue
}
row := []string{a.res.IPAddress, a.subnet, orDash(ShortValidator(a.https.validator)), okOf(a.egress), okOf(a.ingress), a.checksCell()}
if kind == ListVerdictPartial {
row = append(row, reasonName(a.egress.ok < a.egress.n, a.ingress.ok < a.ingress.n, a.incomplete()))
}
l.Rows = append(l.Rows, row)
}
case ListError:
if class == "" {
return nil, ErrUnknownList("error without class")
@@ -98,6 +118,29 @@ func (an *Analysis) List(kind, class string) (*List, error) {
return l, nil
}
// checksCell is "stored из expected", or just stored when expected is unknown.
func (a *addr) checksCell() string {
if a.res.ExpectedChecks < 0 {
return fmt.Sprint(a.stored)
}
return fmt.Sprintf("%d из %d", a.stored, a.res.ExpectedChecks)
}
// okOf is "successful из all" of one level, "—" when it has no checks.
func okOf(t typeStat) string {
if t.n == 0 {
return "—"
}
return fmt.Sprintf("%d из %d", t.ok, t.n)
}
func orDash(s string) string {
if s == "" {
return "—"
}
return s
}
// sorted returns the non-cancelled addresses in numeric address order.
func (an *Analysis) sorted() []*addr {
out := make([]*addr, 0, len(an.addrs))