admin: overview redesign, device scope, one formatting contract (milestone 39)
Gates / frontend (push) Successful in 1m57s
Gates / test (push) Successful in 2m34s
Gates / test-aarch64 (push) Successful in 8m9s
Gates / package (push) Successful in 7m14s
Gates / container (push) Successful in 17s
CI / gates (push) Successful in 18m17s
Release / guard (push) Successful in 33s
Gates / test-aarch64 (push) Successful in 7m22s
Gates / container (push) Successful in 11s
Release / gates (push) Successful in 10m35s
Gates / frontend (push) Successful in 2m8s
Gates / test (push) Successful in 2m16s
Gates / package (push) Successful in 44s
Release / publish (push) Successful in 10m4s

The Overview page takes the decided visual language (specs/ui-visual-redesign.md): four centred totals with their Activity links, a smoothed area chart of total and blocked queries with point hover and a tooltip centred beside the point, a stacked client chart in eight distinct hues plus one Other band that is always a series, and a card row with the cache hit rate, the query types as a single-hue ramp ring, and the upstream breakdown. The count axis grows its margin with the widest grouped tick and draws whole-number ticks only.

GET /api/overview takes a client parameter; the scoped read uses idx_query_log_ts and the cache keeps scoped slots. The device selector beside the period selector is URL state, so a scoped view is a link, and the tile links carry the scope into Activity. The route reduces a pasted IPv6 scope to the RFC 5952 spelling the logger stores, mapped addresses included, and drops anything that is not an address. A failed device list says so under the selector with a retry.

All measured quantities go through admin/src/lib/format.ts: grouped counts, two-decimal percentages, one-decimal rates, durations as the two largest nonzero units. Identifiers, configured values and preset labels render as written; the module header states that scope. A sweep test refuses toFixed, toLocaleString, Intl.NumberFormat and padStart anywhere else.

Chrome: one 4px radius from the metrics constants, shared Card with a prominent title and a one-line description on every panel, the settings form sections on the same card with a floated legend, the sidebar grouped into Monitoring and System with a status block (protection, queries per minute on Overview, uptime), keyboard-focusable table scroll wrappers, and the accent darkened to 5.43:1 on its wash.

Not built: the spec's ranked-list primitive, which has no consumer and no API rows. Codex reviewed sessions B to D over five rounds (thirty-three findings fixed, thirteen rejected as non-quantities); the owner skipped a sixth round.

Claude-Session: https://claude.ai/code/session_01VTgx3a1zz1R78o4K55kkwR
This commit is contained in:
2026-09-07 23:11:50 +02:00
parent e656670dd4
commit 85b8be50a0
77 changed files with 2869 additions and 1163 deletions
+58
View File
@@ -0,0 +1,58 @@
# Milestone 39: Overview visual migration
Apply the decided visual language (`ui-visual-redesign.md`, 2026-09-03) to the real admin: tokens, shared chrome, the Overview page in full, and a first-pass sweep of the other pages. Two functional additions ride along by owner ruling: a client scope filter on Overview (server + UI), and the sidebar status block. One cross-cutting contract: all numeric formatting goes through one module with fixed rules.
Owner rulings (2026-09-03): eight named clients + Other (the existing `/api/overview` count — the prototype's nine is corrected to eight); the client filter ships in this milestone, not deferred; the status block ships in this milestone; this is a milestone, not an addendum; formatting must be consistent everywhere — "6d 04h" vs "06d 4h" vs a percentage shown "15.4%" when it is 15.44% are all defects.
## Formatting contract (applies to every session)
One module, `admin/src/lib/format.ts`, owns every user-visible number. No call site formats inline. Rules:
- **Counts**: thousands-grouped, `18,432`. Existing `Intl.NumberFormat` usage consolidates here.
- **Percentages**: always two decimals, round half up, `15.44%`, `0.00%`, `100.00%`. Never one decimal, never trailing-zero trimming.
- **Durations (uptime, ages)**: the two largest nonzero units, no zero padding, single space: `6d 4h`, `4h 12m`, `12m 5s`, `45s`. Below 1s: `<1s`.
- **Rates**: one decimal, `12.8`.
- **Timestamps**: unchanged — browser timezone display, per `ui-redesign.md` §Time scoping.
Unit tests pin each rule including the rounding. A sweep removes every ad-hoc `toFixed`/`toLocaleString`/hand-built duration in `admin/src`; the milestone is not done while one survives outside `format.ts`.
## Sessions
A (server: client scope) is independent. B (tokens + shared chrome + format module) first on the frontend; C (Overview) needs A and B; D (sweep) needs B and lands after C. Orchestrator writes the changelog.
---
## Session A: client scope on `/api/overview`
`GET /api/overview?period=…&client=<ip>` — optional `client`, validated as an address the same way the queries filters validate theirs. Semantics: every field of the response (totals, buckets, clients, other, types, routes, coverage) describes only that client's rows.
Engineering call, stated up front: the m36 projection tables aggregate across clients and cannot answer a scoped request for types/routes/totals. A scoped request therefore takes the raw-row scan path the 1h window already uses, over `idx_query_log_ts`, for every period. Household scale wears that cost; per-client projection tables would be overengineering. The web-layer response cache keys gain the client value.
Scoped response shape stays identical: `clients` carries at most that one client, `other` is present and all-zero. `coverage` is unchanged. OpenAPI, generated sample, TypeScript types, deterministic handler tests (scoped vs unscoped agreement on a seeded fixture: scoped totals sum to the client's slice; an unknown client returns zeros, not 404).
## Session B: tokens, format module, shared chrome
- `format.ts` per the contract above, with its tests, plus the removal sweep across `admin/src`.
- `tokens.stylex.ts`: radius token 4px; `danger`/chart red split — chart red becomes `oklch(0.65 0.19 25)` (3:1 floor: non-text and ≥24px text only; `dangerText` keeps its darker value for small text); the eight client palette colors + Other gray `oklch(0.80 0.01 260)` as named tokens (values in `ui-visual-redesign.md`); card metrics.
- Card chrome: every panel becomes a white hairline-bordered card, 4px radius, with a title (1.4rem, weight 650, ink) and a one-line muted description. Selector specificity note from the decision record applies.
- Sidebar: MONITORING (Overview, Activity, Clients) / SYSTEM (Configuration, Diagnostics) group labels; foot status block reading the existing `/api/health` protection condition (Active/Paused dot + blocking state) and `/api/version` `uptime_seconds` (formatted per contract). Queries/min derives from the current Overview response (`totals.queries / window minutes`, rate format) and renders only where that response is live; other pages show the block without the rate row. Mobile drawer keeps the same content.
- Toolbar selector styles (rectangular, radius token, 44px), ranked-list styles (full-bleed, red rule variant, no hover), progress-bar card style.
## Session C: Overview page
- Toolbar: client selector (from the clients list; "All devices" default) + period selector. Client is URL state beside period: `/overview?period=24h&client=…`, validated in the route search; the picker navigates.
- Stat tiles restyled per the decision record (centered 2.5rem numerals, semantic color, lowercase captions). Percentage tile uses the two-decimal contract.
- `chartKit.tsx` rework: smooth area chart (blue and red line+fill pairs, edge-to-edge x mapping, point hover with dots and guide line, side-flipping tooltip that never covers the hovered bucket, SVG `hidden` handled via attributes); stacked bars keep band hover, gain the 0.5-unit anti-seam bleed; palette tokens drive series colors, Other keys to the gray token; donut legend restyle.
- Cache/progress presentation and the mobile ≤800px stack (top-bar shell, single column, merged stat card) per the decision record.
- Tests: route search validation for `client`, chart geometry units where practical, axe pass, existing page tests updated.
## Session D: sweep
Activity, Clients, Configuration, Diagnostics adopt the tokens, card chrome, list styles and format module. No layout redesign of those pages in this milestone — chrome and formatting only. Byte budget check; screenshots of every page on the smoke server for owner acceptance before commit.
## Acceptance
- `/api/overview` scoped and unscoped agree on seeded fixtures; contract artifacts regenerated.
- No numeric formatting outside `format.ts`; the pinned format tests pass.
- Overview matches the decision record at 1440 and 420 widths (screenshot review).
- All existing suites green; bundle within budget; no new dependency.
+35
View File
@@ -0,0 +1,35 @@
# UI visual redesign — decision record
Decided 2026-09-03 from the throwaway prototype at `scratchpad/proto-overview/index.html` (session-local; the screenshots and this record are what survive). This settles the visual language only; the IA, routes and API are `ui-redesign.md`'s and stay as built.
## Direction
Pi-hole's structure × NextDNS's visual language, on the light theme. The owner supplied both references directly; four earlier exploration rounds (below) were rejected.
## Chosen values
- **Ground and cards.** Near-white ground; white cards with hairline borders; radius **4px**; card padding ~28px; density 1.00; no panel header bars.
- **Card head.** Every card: a 1.4rem weight-650 ink title plus a one-line muted description sentence (0.875rem). The title selector must outrank any heading reset (a `.v h2` reset silently ate the title styles in the prototype).
- **Stat tiles.** Four tiles: one centered 2.5rem regular-weight numeral over a lowercase muted caption. Semantic color on the numeral itself: ink for totals, red for blocked, muted gray for the percentage, ink for clients. No icons, no borders inside.
- **Toolbar.** Two rectangular dropdown selectors (radius token, 44px min-height, hairline border): client scope left, period right. Not pills — the owner rejected the pill shape.
- **Queries chart.** Smooth Catmull-Rom area. Total: 2px blue line + 13% blue fill. Blocked: 2px red line + 35% red fill — both series are line-plus-backdrop pairs. Points spread **edge to edge**: the first bucket sits on the left plot edge, the last on the right; the curve never extends past its own data. Hover: NextDNS-style point hover — a 4px dot on each series (white 2px stroke) plus a thin vertical guide line; no bucket-band highlight. Tooltip sits ~16px **beside** the point, flipping to the left near the right edge; it never covers the hovered bucket. SVG note: `hidden` is attribute-only on SVG elements — toggle with `setAttribute`/`removeAttribute`, never the `.hidden` property.
- **Client activity chart.** Stacked bars, band-centered, band hover highlight kept. Segments bleed 0.5 viewBox units into the segment below so antialiasing cannot open white seams. Series: the top **eight** named clients (the `/api/overview` contract's own count) plus one aggregated flat "Other" band (gray `oklch(0.80 0.01 260)`) on top. Legend lists the nine names + Other; the tooltip ranks the top four for the bucket, then an Other row, then the total.
- **Client palette** (categorical, reserved red at hue 25 excluded): blue `oklch(0.60 0.14 258)`, teal `oklch(0.68 0.12 190)`, violet `oklch(0.62 0.15 300)`, amber `oklch(0.75 0.13 80)`, green `oklch(0.65 0.13 150)`, magenta `oklch(0.66 0.14 340)`, orange `oklch(0.70 0.14 55)`, olive `oklch(0.72 0.11 120)`, sky `oklch(0.78 0.09 230)`. Known-closest pair: amber/orange.
- **Red.** `oklch(0.65 0.19 25)` (#ED5350, 3.54:1 vs white). Compliant for non-text UI and large text (3:1 floor); it must never color small text.
- **Accent.** Blue, hue 258, for nav, links and the total-queries series. Green (hue 150) stays the cache/success color. Green and red are fixed semantics, never derived from the accent hue.
- **Ranked lists.** Full-bleed rows, hairline dividers, name left with a muted sub-line (list/IP), right-aligned tabular count. Blocked list: a 3px red rule flush on each row's left edge. **No hover effect** — the rows are not interactive.
- **Progress cards.** Cache hit rate as a full-width rounded bar with a colored percentage figure under it (NextDNS's Encrypted DNS pattern).
- **Query types.** Donut + legend list with counts and percentages, single-hue lightness/chroma ramp off the accent.
- **Sidebar.** Grouped section labels (MONITORING / SYSTEM), the active item as a soft accent-tinted block, and a compact status block at the foot: Active dot + blocking state, queries/min, uptime.
- **Mobile (≤800px).** The sidebar becomes a sticky top bar (brand + status pinned, nav scrolls horizontally); everything stacks single-column; the stat tiles merge into one card — numbers side by side, the percentage on a line under.
- **Type.** System font stack. No webfont.
## Rejected along the way
- **Round 1** (Quiet / Console / Instrument / Signal): dark themes rejected outright; Console's density liked, its darkness not.
- **Round 2** (Ledger / Paper / Grid): a Ledger+Grid mix was "disliked least", never liked.
- **Round 3** (convergence with toggles): the settled config (panels frame, system labels, blue accent, 3px radius) read as competent but had no signature — "not an immediate this-is-it".
- **Round 4** (signatures: IBM Plex type-led, a blocked-share hero Meter, a chart-first Geist): all three rejected; the direct Pi-hole × NextDNS reference replaced them.
- **Chart details rejected in the blend round:** pill-shaped selectors; bucket-band hover on the area chart; tooltip centered above the point (covers the data); a solid 0.9-opacity red band (one-color, inconsistent with the blue pair); the curve overhanging the last bucket.
- **Client colors rejected:** 15 hues at 24° spacing (rainbow, neighbors indistinguishable); 6 colors + 9 graduated grays (the gray stripes merge into a dull cap). Codex concurred: nine tiny series cannot be made simultaneously legible through color; aggregate the tail.
- **Not adopted from Codex's option list:** hue families with lightness steps (15 still exceed discrimination), small multiples (a different card, not a fix), hover isolation (owner declined), top-N-per-bucket recoloring and streamgraphs (both distort trend reading).