Skip to content

feat(dashboard): fixed-stage layout, activity pulse chart and tile navigation - #8

Merged
nickgreengithub merged 1 commit into
mainfrom
feat/dashboard-viz
Aug 17, 2026
Merged

nickgreengithub merged 1 commit into
mainfrom
feat/dashboard-viz

Conversation

@nickgreengithub

Copy link
Copy Markdown
Owner

Rebuilds the dashboard around three asks: no vertical scrolling, a second bar holding the controls, and real visualisations with widgets that navigate.

Layout

  • The page never scrolls. Panels that can grow (feed table, activity list) scroll inside themselves. An e2e test asserts the page has zero scrollable overflow, so this can't silently regress.
  • A second bar replaces the descriptive sentence under the heading, carrying the section nav, window filter, last-updated time and live indicator in the row that prose occupied.
  • Four sections — Overview · Feeds · Clients · Traffic.
  • Tiles are navigation. Each overview tile is a button opening the section that explains it — click the number, not a tab.

The activity pulse

Requests and errors over the window, 48 buckets computed in SQL (a window can hold tens of thousands of rows after a load test). Crosshair snaps to the nearest bucket; one tooltip lists every series; a screen-reader table carries the same values so the hover layer enhances rather than gates.

Series colours were computed, not picked — validated against each theme's panel surface: CVD separation ΔE 23.5 dark / 29.8 light (target 8). The first dark pair failed the lightness band and was re-stepped.

Bugs found by running it, not reasoning about it

  1. Empty chart — SQLite binds a JS number as REAL, so dividing by a bound parameter produced floating-point bucket ids matching no integer bucket. Needed an explicit CAST.
  2. Chart looked frozen — the series was anchored on the window start, leaving the newest bucket past the end of the axis.
  3. Chart collapsed to a strip — it asks for full container height; a block parent with auto height resolves that against content.
  4. State column clipped off the panel edge in the feed table.

Two regressions caught by re-measuring

  • The same a11y defect, reintroduced. label-content-name-mismatch failed on the new KPI tiles: an aria-label replaces the accessible name, so the tiles' visible detail line vanished from it. Now visually-hidden text inside the button, which is additive.
  • Perf 97 → 86, 360 ms blocking. Not the server (130 ms against 62k rows) — toLocaleTimeString builds a fresh Intl formatter per call, ~100 timestamps, every 10s. Cached in lib/format.ts: blocking 360 ms → 20 ms, score → 96.

Verified

12/12 e2e passing · dashboard a11y 100, perf 96, no label/name violations · all four sections checked by hand in light and dark.

🤖 Generated with Claude Code

…vigation

The dashboard was a long scrolling column. An operational view is something
you glance at, so anything below the fold was something nobody would read.

It is now a fixed stage that never scrolls. Panels that can grow without
limit — the feed table, the activity list — scroll inside themselves, and an
end-to-end test asserts the page itself has no scrollable overflow so the
property cannot quietly regress.

The height came from two places. A line of descriptive prose under the
heading became the dashboard's own bar, which carries the section nav, the
window filter, the last-updated time and the live indicator in the row the
sentence occupied. And the panels split into four sections — overview, feeds,
clients, traffic — reached from that bar.

Navigation is also the numbers themselves: each overview tile is a button
that opens the section explaining it, so a reader who wants the detail behind
a figure clicks the figure instead of hunting for the right tab.

The activity pulse is the new visualisation: requests and errors over the
window, bucketed in SQL rather than by reading rows and counting them in
JavaScript, because after a load test a single window holds tens of thousands
of rows. Two series, so a legend is always present and identity never rests
on colour alone; errors take the reserved status colour rather than a third
categorical hue. Hovering gives a crosshair snapped to the nearest bucket and
one tooltip listing every series, and every value it shows is also in a table
rendered for screen readers, so the interaction enhances rather than gates.

The series colours were computed rather than chosen: run through a palette
validator against each theme's panel surface, giving colourblind separation
of 23.5 dark and 29.8 light against a target of 8. The first dark pair failed
the lightness band for its surface and was re-stepped.

Four bugs found by looking at the running page and re-measuring, not by
reasoning about the change:

- Bucketing silently produced an empty chart. SQLite binds a JavaScript
  number as REAL, so dividing by a bound parameter gave floating-point bucket
  ids that matched no integer bucket. It needed an explicit CAST.
- The series was anchored on the start of the window, which left the newest
  traffic one bucket past the end of the axis — the counters moved while the
  chart appeared not to. It anchors on the current bucket now.
- The chart collapsed to a strip at the top of an empty panel: it asks for
  the full height of its container, and a block parent with auto height
  resolves that against its content.
- The feed status table pushed its state column past the panel edge, where it
  read as a truncated word rather than as something scrollable.

And two regressions caught by re-running Lighthouse rather than assuming a
redesign is neutral:

- label-content-name-mismatch was failing again, on the new KPI tiles. Each
  tile carried an aria-label to say where it navigates, and aria-label
  replaces the accessible name rather than adding to it, so the tile's detail
  line was visible on screen and absent from the name. This is the same
  defect already fixed twice here. The hint is now visually hidden text
  inside the button, which is additive.
- Performance fell 97 to 86 with 360 ms of blocking. The server answered in
  130 ms against 62,000 rows; the cost was toLocaleTimeString, which builds a
  fresh Intl formatter per call, across about a hundred timestamps, every ten
  seconds. Two formatters constructed once in lib/format.ts took blocking
  time to 20 ms and the score to 96.

Verified: 12 end-to-end tests pass, Lighthouse reports accessibility 100 and
performance 96 on the dashboard with no label/name violations, and all four
sections were checked by hand in both themes.

Co-Authored-By: Claude <noreply@anthropic.com>
@nickgreengithub
nickgreengithub merged commit c583e9a into main Aug 17, 2026
3 checks passed
@nickgreengithub
nickgreengithub deleted the feat/dashboard-viz branch August 17, 2026 02:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant