Files
nxdns/specs/milestone-39.md
T
mokhtar 85b8be50a0
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
admin: overview redesign, device scope, one formatting contract (milestone 39)
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
2026-09-07 23:11:50 +02:00

6.0 KiB

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.