feat(dashboard): rebuild the dashboard as a security operations screen - #287
Open
bihius wants to merge 3 commits into
Open
feat(dashboard): rebuild the dashboard as a security operations screen#287bihius wants to merge 3 commits into
bihius wants to merge 3 commits into
Conversation
Adds GET /stats/overview, /stats/timeseries and /stats/top so the dashboard can be driven by server-side aggregation instead of counting client-side. - overview: KPI counters for a 1h/24h/7d/30d window, each compared against the immediately preceding window of the same length. Replaces the five round-trips the frontend used to make, two of which fetched entire vhost and policy lists just to count them. - timeseries: dense allow/deny/monitor buckets (12-30 bars per window) with empty intervals returned as explicit zeros. - top: most frequently triggered rules, denied source IPs and targeted vhosts, with ban state attached for admins. The banned-IP count reads HAProxy stick-tables and is the only part that can fail independently, so it degrades to null rather than failing the request - an unreachable stats socket must not blank out database-backed metrics.
Replaces the bootstrap placeholder - five all-time counters and a hardcoded "Recent activity" list describing the frontend's own build progress - with a screen driven by the new /stats endpoints. - A 1h/24h/7d/30d window, kept in the URL so a view can be shared, drives every section at once. - Hand-rolled SVG stacked bar chart of allow/monitor/deny over time. No charting dependency: the shape is simple and drawing it directly keeps every colour on the existing semantic tokens, so both themes work. The same numbers are mirrored in a visually hidden table, so nothing in the chart is conveyed by colour alone. - KPI tiles now show window-over-window deltas, with direction carried by a glyph and wording rather than colour. - Top triggered rules and top denied source IPs, each deep-linking into the pre-filtered log viewer; LogsPage learned to hydrate its filters from the query string to make that work. - System status folds in pending-config detection: comparing the generated checksum against the one HAProxy last reloaded answers "is what I edited actually live?". - Every section owns its loading, empty and error state, so one failing endpoint no longer decides what the rest of the page shows. Also fixes the font stack: --font-sans asked for Inter while index.html loaded DM Sans, so the app had been rendering in a system fallback.
bihius
force-pushed
the
feat/dashboard-redesign
branch
from
July 30, 2026 07:29
574202c to
446e904
Compare
Arrow keys moved focus synchronously inside the key handler, before React had committed the selection change. Because the selected window lives in the URL, that commit re-renders the whole screen, so anything disturbing focus mid-render left the roving tabindex pointing at a blurred element — every later arrow key then landed on the document and did nothing. Move the focus into an effect keyed on the selection so it runs after the commit, guarded by a containment check so a refresh elsewhere on the page cannot pull focus into the tablist. Adds TimeRangeTabs tests that walk the full control rather than asserting a single move, plus coverage for the policies list edit modal, which had none and was reported as broken from the field.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The dashboard was still the bootstrap placeholder: five all-time counters, a
hardcoded "Recent activity" list narrating the frontend's own build progress,
and a checksum panel. It answered none of the questions an operator actually
opens a WAF console with — is something attacking me right now, what is being
blocked, is my config live? — and roughly 40% of the screen below the fold was
empty.
It was also expensive for what it showed: the five counters cost five requests,
two of which fetched entire vhost and policy lists client-side just to call
.lengthon them, and two of which wereGET /logs?page_size=1reads oftotal. There was no aggregation endpoint anywhere in the backend.Backend — new
/statsendpointsapp/routers/stats.py+app/services/stats_service.py+app/schemas/stats.py,all behind
get_current_user:GET /stats/overview1h/24h/7d/30dwindow, each compared against the immediately preceding window of the same lengthGET /stats/timeseriesGET /stats/topNotes on the implementation:
counters, rather than eight
COUNT(*)queries. Every grouped column(
event_at,action,severity,source_ip,rule_id,vhost) isalready indexed.
string, so 5-minute and 6-hour buckets share one code path. The dialect
branch (
strftime/extract epoch) is isolated in one helper so a futurePostgres move doesn't mean rewriting the grouping logic.
delta_pctisnullwhen the previous window was empty — a dashboard shouldnever render "+∞%". The UI shows "new" instead.
rather than the database, so it is the only one that can fail independently.
It degrades to
nullinstead of failing the request: an unreachable statssocket must not blank out database-backed metrics.
only ever render purged, empty buckets.
Recent blocks reuse
GET /logs?action=deny&page_size=8; no new endpoint needed.Frontend
?window=7d) drives every section at once.Implemented as a real tablist, so arrow keys move between ranges and the view
can be linked to.
simple, and drawing it directly keeps every colour on the existing semantic
tokens, so both
emeraldandfrostwork without a parallel styling system.The same numbers are mirrored in a visually hidden
<table>, so nothing inthe chart is conveyed by colour alone.
and by wording, not by colour — the tiles still read correctly in greyscale.
A rise in blocked traffic is treated as a warning; the same shape of change
on allowed traffic is neutral.
LogsPagelearned to hydrate its filters from the query string to make thatwork (
filtersFromSearchParams, with the enum and numeric values validatedrather than trusted).
checksum against the one HAProxy last reloaded answers "is what I edited
actually live?".
endpoint no longer decides what the rest of the page shows, and an all-zero
chart is replaced by an empty state with a route into vhost management.
Visual/token changes
Additive only — existing
--primary/--backgroundare untouched, so the otherfour screens are unaffected.
--font-sansasked for Inter whileindex.htmlloaded DM Sansand IBM Plex Sans, so the app had been rendering in a system fallback the
whole time. Now DM Sans, plus IBM Plex Mono for figures, checksums and IPs.
--color-chart-allow/monitor/deny/grid) so action →colour means one thing everywhere.
prefers-reduced-motionblock, which the stylesheet previously had none of.use-dashboard-stats.tsis deleted;listAllVHosts/listAllPoliciesare keptbecause other screens still use them.
Testing
uv run pytest --cov=app— 713 passed, 6 skipped.stats_service.pyat 96%.New:
tests/unit/test_stats_service.py,tests/integration/test_stats_router.py(window maths, dense bucket filling, null delta without a baseline, 401/422,
banned_ipshidden from viewers, and an unreachable Runtime API degrading tonullrather than failing the response).uv run mypy app/anduv run ruff check app/— clean.pnpm type-check,pnpm lint,pnpm test— clean, 165 tests. The oldDashboardPage.test.tsxasserted on exactly five loading skeletons and mockedfour API modules, so it was rewritten; new tests cover the chart primitive,
the formatters and the log URL-filter parsing.
sensible payloads,
window=99d→ 422, no token → 401.haproxy -cwas not run — HAProxy is not installed on this machine, and noHAProxy config is touched by this change.