Show egress/ingress levels in the registry; freeze checks at the verdict

Registry: the "last result" column now also shows, per level (egress,
ingress), how many of the recorded checks of the latest cycle succeeded, split
by check family (tcp-22 and tcp-443 are both "tcp"). One grouped query per
chunk of addresses; new fields last_cycle_id, egress, ingress in
GET /admin/registry; the dashboard renders them under the verdict.

Verdict integrity (migration 0010):
- the prober is handed an address once per site and attempt, not on every
  poll, so results are no longer overwritten by later probe rounds;
- UpsertCheckIfOpen refuses writes once the address is aggregating or has its
  verdict, or for an older attempt; senders get {"ok":true,"ignored":N} and a
  result_dropped event is recorded;
- the checking window counts from checking_started_at, not from assigned_at;
- checks.recorded_at (server clock) and checks.after_verdict (flag for rows
  written after the verdict in existing data);
- the verdict rule is a pure function (computeVerdict) and the aggregated
  event carries the egress/ingress check counts.

Rebuilt bin/control-api and bin/admin-dashboard to match. Plans and summaries
are in docs/changes; README, API, USAGE, DASHBOARD and DIAGRAMS are updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-03 17:59:52 +03:00
1 parent db73409e8f
commit 864208238f
34 files changed
+1570 -72

No files matched your search

+16
View File
@@ -422,6 +422,22 @@ td.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; colo
.pill-cancel { background: var(--cancel-soft); color: var(--cancel); }
.pill-occupied { background: var(--occupied-soft); color: var(--occupied); }
/* ---------- registry: egress / ingress levels ---------- */
.levels { margin-top: 6px; display: grid; gap: 4px; }
.level { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; font-size: 12px; }
.level-name { min-width: 52px; color: var(--neutral); font-weight: 600; }
.level-types { display: inline-flex; flex-wrap: wrap; gap: 4px; }
.stat { font-weight: 700; white-space: nowrap; }
.stat-ok { color: var(--success); }
.stat-part { color: var(--warning); }
.stat-fail { color: var(--danger); }
.stat-none { color: var(--neutral); }
.chip { padding: 1px 6px; border-radius: var(--radius-xs); font-size: 11px; white-space: nowrap; }
.chip-ok { background: var(--success-soft); color: var(--success); }
.chip-part { background: var(--warning-soft); color: var(--warning); }
.chip-fail { background: var(--danger-soft); color: var(--danger); }
.chip-none { background: var(--neutral-soft); color: var(--neutral); }
/* ---------- alerts ---------- */
.alert {
display: flex; gap: 10px; align-items: flex-start;