From 423ed9b3a327bde74de6958ae459e58f4194ee70 Mon Sep 17 00:00:00 2001 From: Niklas Ye Date: Fri, 25 Sep 2026 13:02:08 +0200 Subject: [PATCH] Add a Stats page to the web UI Statistics used to live only in terdut-tui; the account page said so. The page shows the same figures as the TUI's Stats tab -- incident counts, MTTA and MTTR, top alerts, and alert frequency by hour (UTC) and by day of week -- and adds a range picker (Today, 7d, 30d, 90d, All) that the TUI does not have. The ranges are day-granular because the server reads from/to as whole UTC dates, so there is no 24h chip. No server change: the page uses the existing /api/stats/* endpoints, which already scope to the caller's teams. Charts are inline SVG and plain elements sized from script, because the CSP forbids inline styles, inline scripts and CDN libraries. Removes the "statistics are in terdut-tui" notes from the account page and the README. --- README.md | 5 +- internal/web/static/app.css | 37 +++++++ internal/web/static/index.html | 5 + internal/web/static/js/account.js | 1 - internal/web/static/js/api.js | 6 ++ internal/web/static/js/app.js | 5 +- internal/web/static/js/stats.js | 168 ++++++++++++++++++++++++++++++ internal/web/static/js/ui.js | 1 + internal/web/stats_test.go | 26 +++++ 9 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 internal/web/static/js/stats.js create mode 100644 internal/web/stats_test.go diff --git a/README.md b/README.md index c6c02f8..50fa583 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,9 @@ archive), who is on call, the alert feed, and an *Account* tab for your own password and the ntfy topic your pages go to. It is built for a phone first. On a phone it navigates through a hamburger menu and has a sticky action bar, it follows the system's dark mode, and it can be added to the home screen. From 900px wide it switches -to a sidebar with the queue and the incident side by side. Statistics remain in -[terdut-tui](https://github.com/yeniklas/terdut-tui) for now. +to a sidebar with the queue and the incident side by side. The Stats page shows +incident counts, MTTA and MTTR, and alert frequency by name, hour and day over a +chosen range. You sign in with a username and password. Users have no password until one is set, and a user without one can only use API keys: diff --git a/internal/web/static/app.css b/internal/web/static/app.css index c6f42c6..29a860c 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -867,3 +867,40 @@ button.rota-day:hover { background: var(--surface-2); } .overview-head { display: flex; align-items: baseline; gap: 8px; } .overview-count { margin-left: auto; color: var(--muted); font-size: 18px; font-weight: 700; } .overview-item p { margin: 4px 0 0; } + +/* ---------- stats page ---------- */ + +#view-stats .chips { padding-left: 0; padding-right: 0; } +.stats { display: grid; gap: 14px; padding-bottom: 16px; } +.stat-tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } +.stat-tile { + --sev: var(--border-strong); + background: var(--surface); border: 1px solid var(--border); + border-left: 3px solid var(--sev); border-radius: var(--radius); + box-shadow: var(--shadow); padding: 12px; +} +.stat-tile.st-triggered { --sev: var(--crit); } +.stat-tile.st-acknowledged { --sev: var(--warn); } +.stat-tile.st-resolved { --sev: var(--ok); } +.stat-value { font-size: 24px; font-weight: 700; font-variant-numeric: tabular-nums; } +.stat-label { margin-top: 2px; font-size: 12px; color: var(--muted); } +.chart-card + .chart-card { margin-top: 0; } +.chart-title { + margin-bottom: 10px; font-size: 13px; font-weight: 700; + text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); +} +.hbars { list-style: none; display: grid; gap: 6px; } +.hbar { display: grid; grid-template-columns: minmax(80px, 34%) 1fr auto; align-items: center; gap: 8px; font-size: 13px; } +.hbar-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.hbar-track { height: 10px; border-radius: 5px; background: var(--surface-2); overflow: hidden; } +.hbar-fill { display: block; height: 100%; border-radius: 5px; background: var(--ok); } +.hbar-count { min-width: 2ch; text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; } +.columns { display: block; width: 100%; height: auto; } +.columns .axis { stroke: var(--border-strong); stroke-width: 1; } +.columns .col-hit { fill: transparent; } +.columns .col-bar { fill: var(--accent); } +.columns .col:hover .col-bar { opacity: 0.75; } +.columns .col-label { fill: var(--faint); font-size: 10px; font-family: var(--font); } +@media (min-width: 900px) { + .stat-tiles { grid-template-columns: repeat(3, 1fr); } +} diff --git a/internal/web/static/index.html b/internal/web/static/index.html index 2174927..284493d 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -90,6 +90,10 @@ Alerts + + + Stats + Team @@ -128,6 +132,7 @@ +