Add the Analytics section: check runs, analytics API and page

Runs (migration 0011): a run groups the cycles of one launch. It opens when an
address enters an idle queue, takes everything submitted or re-checked while it
is open and is finalized when all its addresses are done; a re-check after that
opens a new run, so results of different runs never mix. check_runs,
run_results (one result per address and run, with the verdict and the expected
and stored check counts), subnets, run_id on ip_queue and checks. Existing data
is split into runs at pauses of more than an hour; ingress checks get the
validator that held the address (also at write time from now on).

Analytics (internal/analytics): figures computed from the stored checks of the
latest cycle of each address in the run, as facts next to the verdict: summary,
reasons of partial, data quality, subnets, targets and the subnet x target
matrix by check type, ingress by site, error classes, validators, and the
address lists behind the indicators and error classes. API: analytics runs,
report, lists (JSON or CSV), subnet list; run and subnet filters for the
registry.

Dashboard: /analytics matching the approved mockup (run selector, indicators
with address lists and CSV, error-class dialogs, drill-down to the registry),
subnet list on /settings. Sidebar: the control-api link state, theme toggle and
logout moved to the top, the three dots next to the logo removed, sections
grouped.

Rebuilt bin/control-api and bin/admin-dashboard to match. Plan, summary and the
updated README, API, USAGE, DASHBOARD and ADMIN_CLEANUP docs are in docs/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-03 18:36:03 +03:00
1 parent 864208238f
commit b7669c9e41
44 files changed
+4123 -62

No files matched your search

+15 -14
View File
@@ -180,11 +180,9 @@ main > h2.section-title:first-child { margin-top: 0; }
padding: 18px 12px;
display: flex;
flex-direction: column;
gap: 20px;
gap: 14px;
}
.brand { display: flex; align-items: center; gap: 9px; padding: 0 6px 12px; border-bottom: 1px solid var(--border-soft); }
.brand-chrome { display: flex; gap: 4px; }
.brand-chrome i { width: 6px; height: 6px; border-radius: 50%; background: var(--border); }
.brand-mark {
width: 26px; height: 26px;
border-radius: var(--radius-xs);
@@ -218,8 +216,10 @@ nav.nav-groups { display: flex; flex-direction: column; gap: 1px; }
.nav-link.active svg { opacity: 1; }
.nav-link:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sidebar-foot {
margin-top: auto;
/* Session block at the top of the sidebar: link state to control-api, theme
toggle and, with login enabled, who is signed in and the logout button. */
.sidebar-session {
display: grid; gap: 8px;
padding: 9px 10px;
border-radius: var(--radius-sm);
background: var(--surface-alt);
@@ -227,9 +227,16 @@ nav.nav-groups { display: flex; flex-direction: column; gap: 1px; }
font-family: var(--font-display);
font-size: 11px;
color: var(--text-muted);
display: flex; align-items: center; justify-content: space-between; gap: 7px;
}
.sidebar-foot-status { display: flex; align-items: center; gap: 7px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.session-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; margin: 0; }
.session-api { display: flex; align-items: center; gap: 7px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.session-user { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-group-title {
font-family: var(--font-display); font-weight: 700; font-size: 10px;
letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint);
padding: 12px 9px 4px;
}
nav.nav-groups > .nav-group-title:first-child { padding-top: 0; }
.pulse-dot {
width: 6px; height: 6px; border-radius: 1px;
background: var(--success);
@@ -238,6 +245,7 @@ nav.nav-groups { display: flex; flex-direction: column; gap: 1px; }
animation: pulse 2.2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .pulse-dot { animation: none; } }
.pulse-dot.down { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-soft); animation: none; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
.theme-toggle {
@@ -568,13 +576,6 @@ textarea.autosize { overflow-y: hidden; resize: none; }
.login-card .panel-body { display: flex; flex-direction: column; gap: 14px; }
/* .field is flex: 1 1 220px (for rows); in this column the basis would become a 220px height. */
.login-card .field { flex: 0 0 auto; }
.sidebar-user {
margin-top: auto; margin-bottom: 8px;
display: flex; align-items: center; justify-content: space-between; gap: 8px;
font-family: var(--font-display); font-size: 11px; color: var(--text-muted);
}
.sidebar-user-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-user + .sidebar-foot { margin-top: 0; }
/* ---------- scan progress, progress bars, pager, bulk selection ---------- */
progress {