Files
cloud-ip-validator/internal/dashboard/static/analytics.css
T
ayurishchevandClaude Sonnet 5.5 068c10ea1c Analytics: compare two finished runs
New page /analytics/compare and API GET /admin/analytics/compare (+ /lists/{group}):
the administrator picks an old (A) and a new (B) run; the report shows the new
addresses (only in B), the ones that left (only in A) and the common ones whose
membership in the seven indicators (pass, partial, fail, egress https any/all,
ingress ssh any/all) differs, with a "what changed" summary per address; the
dynamics of each indicator (delta = new - left + entered - exited) and a verdict
transition matrix. Every number opens a list with CSV. Cancelled addresses are not
part of a run. The list dialog moved to a shared analytics-dialog.js and template;
/analytics got a "compare with another run" button.

Docs, plan and summary in docs/changes/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 10:26:37 +03:00

129 lines
12 KiB
CSS

/* Analytics page. Everything is prefixed an-; the dashboard's own tokens
(--surface, --border, --accent, ...) are used as they are. */
:root {
--an-radius: 4px;
--an-shadow: 0 1px 2px rgba(12,18,30,.06), 0 1px 1px rgba(12,18,30,.05);
--an-bar: #3987e5; --an-bar-track: #E4E9F0;
/* heat ramp: one blue hue, validated sequential steps (light = low) */
--an-h1:#cde2fb; --an-h2:#9ec5f4; --an-h3:#6da7ec; --an-h4:#3987e5; --an-h5:#256abf; --an-h6:#184f95; --an-h7:#0d366b;
--an-hi1:#12161F; --an-hi2:#12161F; --an-hi3:#12161F; --an-hi4:#FFFFFF; --an-hi5:#FFFFFF; --an-hi6:#FFFFFF; --an-hi7:#FFFFFF;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--an-shadow: 0 1px 2px rgba(0,0,0,.4);
--an-bar: #5C8CFF; --an-bar-track: #1D2430;
--an-h1:#0d366b; --an-h2:#104281; --an-h3:#184f95; --an-h4:#256abf; --an-h5:#3987e5; --an-h6:#6da7ec; --an-h7:#9ec5f4;
--an-hi1:#B8C9E8; --an-hi2:#C3D6F5; --an-hi3:#E7ECF5; --an-hi4:#FFFFFF; --an-hi5:#0A0D13; --an-hi6:#0A0D13; --an-hi7:#0A0D13;
}
}
:root[data-theme="dark"] {
--an-shadow: 0 1px 2px rgba(0,0,0,.4);
--an-bar: #5C8CFF; --an-bar-track: #1D2430;
--an-h1:#0d366b; --an-h2:#104281; --an-h3:#184f95; --an-h4:#256abf; --an-h5:#3987e5; --an-h6:#6da7ec; --an-h7:#9ec5f4;
--an-hi1:#B8C9E8; --an-hi2:#C3D6F5; --an-hi3:#E7ECF5; --an-hi4:#FFFFFF; --an-hi5:#0A0D13; --an-hi6:#0A0D13; --an-hi7:#0A0D13;
}
.an { display: grid; gap: 16px; align-content: start; min-width: 0; font-family: var(--font-ui); font-size: 14px; line-height: 1.5; }
.an * { box-sizing: border-box; }
.an-title { font: 700 18px var(--font-mono); margin: 0; }
.an h2 { font: 700 12px var(--font-mono); text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); margin: 0; }
.an-sub { color: var(--text-muted); font-size: 12.5px; margin: 0; }
.an-panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--an-radius); box-shadow: var(--an-shadow); padding: 14px 16px; display: grid; gap: 12px; min-width: 0; align-content: start; }
.an-panel > * { min-width: 0; }
.an-head { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: baseline; justify-content: space-between; }
.an-runbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.an-runbar label { font: 700 12px var(--font-mono); color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; }
.an select, .an .an-btn { font: 500 13px var(--font-mono); background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: var(--an-radius); padding: 7px 10px; }
.an select { min-width: 0; max-width: 100%; flex: 1 1 160px; }
.an-btn { cursor: pointer; } .an-btn:hover { background: var(--surface-alt); }
.an-tabs { display: inline-flex; gap: 0; }
.an-tabs button { font: 500 12px var(--font-mono); background: var(--surface); color: var(--text-muted); border: 1px solid var(--border); padding: 5px 12px; cursor: pointer; }
.an-tabs button + button { border-left: 0; }
.an-tabs button:first-child { border-radius: var(--an-radius) 0 0 var(--an-radius); } .an-tabs button:last-child { border-radius: 0 var(--an-radius) var(--an-radius) 0; }
.an-tabs button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent-strong); border-color: var(--accent); }
.an-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: 12px; }
.an-kpi { background: var(--surface); border: 1px solid var(--border); border-radius: var(--an-radius); padding: 12px 14px; display: grid; gap: 2px; box-shadow: var(--an-shadow); }
.an-kpi .an-k { font: 700 11px var(--font-mono); text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); }
.an-kpi .an-v { font: 700 26px var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.an-kpi .an-d { font-size: 12px; color: var(--text-muted); }
.an-tag { display: inline-flex; align-items: center; gap: 5px; font: 700 10.5px var(--font-mono); text-transform: uppercase; letter-spacing: .03em; padding: 2px 7px; border-radius: 2px; }
.an-tag::before { content: ""; width: 5px; height: 5px; background: currentColor; border-radius: 1px; }
.an-t-ok { background: var(--success-soft); color: var(--success); } .an-t-warn { background: var(--warning-soft); color: var(--warning); } .an-t-bad { background: var(--danger-soft); color: var(--danger); } .an-t-n { background: var(--neutral-soft); color: var(--neutral); }
.an-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); gap: 16px; }
.an-rows { display: grid; gap: 7px; }
.an-bar-row { display: grid; grid-template-columns: minmax(110px, 38%) minmax(0,1fr) auto; gap: 10px; align-items: center; font-size: 13px; }
.an-bar-row .an-n { overflow-wrap: anywhere; }
.an-track { height: 12px; background: var(--an-bar-track); border-radius: 2px; position: relative; }
.an-fill { height: 100%; background: var(--an-bar); border-radius: 0 4px 4px 0; }
.an-num { font: 500 12.5px var(--font-mono); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.an-scroll { overflow-x: auto; }
.an table { border-collapse: collapse; width: 100%; font-size: 13px; }
.an th .an-hint { cursor: help; color: var(--accent-strong); margin-left: 3px; }
.an th { font: 700 11px var(--font-mono); text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); white-space: nowrap; }
.an th.an-r, .an td.an-r { text-align: right; }
.an td { padding: 6px 8px; border-bottom: 1px solid var(--border-soft); vertical-align: middle; }
.an td.an-a { font-family: var(--font-mono); font-size: 12.5px; white-space: nowrap; }
.an td.an-a a { color: var(--accent-strong); text-decoration: none; } td.an-a a:hover { text-decoration: underline; }
.an-pb { display: grid; grid-template-columns: 70px 40px; gap: 6px; align-items: center; justify-content: end; }
.an-pb .an-track { height: 8px; }
.an-heat td.an-c { padding: 2px; }
.an-cell { display: block; min-width: 76px; text-align: center; font: 500 12px var(--font-mono); font-variant-numeric: tabular-nums; padding: 7px 4px; border-radius: 2px; cursor: default; }
.an-h1{background:var(--an-h1);color:var(--an-hi1)} .an-h2{background:var(--an-h2);color:var(--an-hi2)} .an-h3{background:var(--an-h3);color:var(--an-hi3)} .an-h4{background:var(--an-h4);color:var(--an-hi4)} .an-h5{background:var(--an-h5);color:var(--an-hi5)} .an-h6{background:var(--an-h6);color:var(--an-hi6)} .an-h7{background:var(--an-h7);color:var(--an-hi7)}
.an-legend { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 12px; color: var(--text-muted); }
.an-legend i { display: inline-block; width: 28px; height: 10px; border-radius: 2px; }
.an-vals { display: flex; align-items: flex-end; gap: 4px; height: 120px; border-bottom: 1px solid var(--border); position: relative; }
.an-vals .an-col { flex: 1 1 0; min-width: 0; background: var(--an-bar); border-radius: 3px 3px 0 0; }
.an-vals .an-avg { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--text-muted); }
.an-vlab { display: flex; justify-content: space-between; font: 500 11px var(--font-mono); color: var(--text-faint); }
.an-dq { display: grid; gap: 8px; }
.an-dq .an-row { display: flex; gap: 10px; justify-content: space-between; align-items: baseline; border-bottom: 1px solid var(--border-soft); padding-bottom: 6px; font-size: 13px; }
.an-dq .an-row span:last-child { font: 700 13px var(--font-mono); white-space: nowrap; }
.an-note { font-size: 12px; color: var(--text-muted); }
button.an-kpi { font: inherit; color: inherit; text-align: left; cursor: pointer; width: 100%; }
button.an-kpi:hover { border-color: var(--accent); }
button.an-kpi:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.an-kpi .an-go { font: 700 11px var(--font-mono); color: var(--accent-strong); }
.an dialog { width: min(760px, calc(100% - 32px)); max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); padding: 0; overflow: hidden; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text); box-shadow: 0 16px 40px rgba(0,0,0,.35); }
.an dialog[open] { display: flex; flex-direction: column; }
.an dialog::backdrop { background: rgba(10,14,20,.5); }
.an-dlg { display: flex; flex-direction: column; gap: 12px; padding: 16px; min-height: 0; flex: 1 1 auto; max-height: calc(100vh - 34px); max-height: calc(100dvh - 34px); }
.an-dlg h3 { font: 700 15px var(--font-mono); margin: 0; overflow-wrap: anywhere; flex: 0 0 auto; }
.an-dlg .an-note { flex: 0 0 auto; max-height: 6.5em; overflow: auto; margin: 0; }
.an-dlg .an-dist { flex: 0 0 auto; }
.an-dlg .an-list { flex: 0 1 auto; min-height: 96px; overflow: auto; border: 1px solid var(--border-soft); border-radius: var(--an-radius); }
.an-dlg table { font-size: 12.5px; } .an-dlg th { position: sticky; top: 0; background: var(--surface); }
.an-dlg .an-foot { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; padding-top: 4px; border-top: 1px solid var(--border-soft); }
.an-dlg .an-foot .an-acts { display: flex; gap: 8px; flex-wrap: wrap; }
@media (max-width: 520px) { .an-dlg { padding: 12px; gap: 10px; } .an-dlg .an-foot .an-acts { width: 100%; } .an-dlg .an-foot .an-acts .an-btn { flex: 1 1 auto; } }
@media (max-height: 540px) { .an-dlg .an-dist { display: none; } }
@media (max-height: 640px) { .an-dist .an-cols20 { height: 44px; } .an-dlg .an-note { max-height: 3em; } .an-dlg .an-dist .an-chips { display: none; } }
.an-btn.an-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.an-btn.an-primary:hover { background: var(--accent-strong); }
button.an-bar-row { font: inherit; color: inherit; text-align: left; width: 100%; background: none; border: 0; border-radius: var(--an-radius); padding: 3px 4px; margin: -3px -4px; cursor: pointer; }
button.an-bar-row:hover { background: var(--surface-alt); }
button.an-bar-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.an-dist { display: grid; gap: 8px; }
.an-dist .an-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.an-dist .an-cols20 { display: grid; grid-template-columns: repeat(20, minmax(0, 1fr)); gap: 3px; align-items: end; height: 84px; border-bottom: 1px solid var(--border); }
.an-dist .an-cols20 div { background: var(--an-bar); border-radius: 2px 2px 0 0; min-height: 1px; }
.an-dist .an-lab20 { display: grid; grid-template-columns: repeat(20, minmax(0, 1fr)); gap: 3px; font: 500 9.5px var(--font-mono); color: var(--text-faint); text-align: center; }
/* comparison of two runs: a number that opens a list, and the delta colours */
.an button.an-lnk { font: 500 12.5px var(--font-mono); font-variant-numeric: tabular-nums; color: var(--accent-strong); background: none; border: 0; border-bottom: 1px dashed var(--accent); padding: 0 2px; cursor: pointer; }
.an button.an-lnk:hover { background: var(--surface-alt); }
.an button.an-lnk:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.an-zero { color: var(--text-faint); font: 500 12.5px var(--font-mono); }
.an td.an-good { color: var(--success); font-weight: 700; } .an td.an-bad { color: var(--danger); font-weight: 700; }
#an-tip { position: fixed; z-index: 10; pointer-events: none; background: var(--text); color: var(--bg); font: 500 12px/1.4 var(--font-mono); padding: 6px 9px; border-radius: 4px; max-width: min(360px, calc(100vw - 24px)); white-space: normal; opacity: 0; transition: opacity .08s; }
@media (prefers-reduced-motion: reduce) { #an-tip { transition: none; } }
/* dashboard.css turns every table into stacked cards on a phone; the tables on
this page are data tables and scroll sideways instead. */
@media (max-width: 640px) {
.an-kpi .an-v { font-size: 22px; }
.an thead { display: table-header-group; }
.an table { display: table; width: 100%; }
.an tbody { display: table-row-group; width: auto; }
.an tr, .an tbody tr { display: table-row; width: auto; padding: 0; border-bottom: 0; }
.an td { display: table-cell; width: auto; padding: 6px 8px; border-bottom: 1px solid var(--border-soft); }
}