Skip to content
commission

What the dashboard still needs before it leaves beta

relay-80, relay-81 and relay-82 were the design lift the owner asked for: the web view now shares one design language with the product, applies it at operational density, and the inbox is built for triage rather than for reading. This page is the audit afterwards, written by going through the thing as somebody who did not build it.

It is not a victory lap. The recommendation at the bottom is that the beta label stays on, and the reasons are here.

Everything below was reproduced, not guessed. Where a number appears it was measured; where something is deferred it says why and roughly what it would cost.


1. Found and fixed during this pass

Recorded because they show the class of thing that was wrong, and because two of them were invisible to the test suite and only showed up in a screenshot.

whathow it was found
?days=abc on the standup returned a 500 error page — a hand-typed query string was a server faultprobing query strings
?days=1e9 was a 500 by a second route: finite and positive, and still outside the range core accepts. The window is now clamped as well as validatedwriting the test for the first fix
The active project in the nav measured 4.36:1 in dark, below AA at 14px/600measuring every pair
Heading order on the home page was h1 → h3 → h2reading the markup
No prefers-reduced-motion block, though the site calls that rule non-negotiablechecking against site/README.md
Every progress bar rendered on its own line (.meter was an inline span wrapping a block child)reading a screenshot
At 390px, “waiting on checkout-2” was clipped by the panel’s overflow:hiddenreading a screenshot

Each of those is now a test rather than a paragraph — tests/design.test.ts computes every contrast pair from the tokens, walks the heading levels on every page, and requests the malformed query strings. An audit finding that is only written down is a finding somebody has to remember.

2. Not ready, with the reason and the size of the fix

2.1 A cancelled task is invisible — medium, needs core

board() returns in-progress, in-review, ready, blocked, stale and recently-done. A cancelled task is in none of them, so on a board with one cancelled task the only trace is a line in the activity feed. Somebody wondering where a task went cannot find it from the board at all.

Reproduced: a project with one cancelled task renders its title exactly once on /p/:slug, inside the feed.

The fix belongs in src/core/board.ts (a cancelled bucket, or an explicit “this board omits cancelled work” contract) rather than in the view, because a view that queried tasks directly would be a second source of truth. Left for the core owner — noted on relay-83.

2.2 No way to find a task except by scrolling — medium

There is no search, no filter and no jump-to-id. commission search exists on the CLI; the web view has no equivalent, and no URL you can type to reach a task you know the id of except /t/<id>, which is not written down anywhere on the page. On a project with three hundred tasks that is the difference between a tool and a wall chart.

Not a design problem and not small: a search box is a form, and a form on a read-only page is a GET with a query string — which is fine and does not breach the write boundary, but it needs a result page, an empty result state, and a decision about what is searched (titles, bodies, notes). Half a day.

2.3 The board does not scale — medium

Measured: a project with 302 tasks renders a 95KB board with no pagination, truncation or virtualisation, and the page auto-refreshes every 60 seconds. Every task in the project is in the HTML on every load.

There is no design here yet at all. It needs a decision (cap each section with a “and 240 more” disclosure? paginate? collapse sections past N?) before it needs code. Until that decision exists, a big project gets a slow page.

2.4 Every page ships 23KB of uncached CSS — small, needs the server

The stylesheet is inlined into every response: 23,290 bytes, 6,447 gzipped, re-sent on every page view and every 60-second auto-refresh. Inlining is right — constraint 2 in dashboard.md — but nothing compresses the response, so the bad SSH connection this view exists for pays 23KB per page.

Two independent fixes, neither in the view: gzip/brotli on the server (src/server/ mounts the app), or serve the stylesheet from one cacheable GET route with a far-future Cache-Control and a content hash. The second breaks the “no external request” property the CSP-free single file has today, so compression is the one to do first. Left for the server owner — noted on relay-83.

2.5 The auto-refresh is a blunt instrument — small

/, /p/:slug and /p/:slug/standup carry <meta http-equiv="refresh" content="60">. On a slow database that is a full re-render every minute per open tab, and the home page recomputes board() for every project on every render, plus a GROUP BY over the whole events table for the navigation on every request of every page. Nobody has looked at what that costs on a large database, and there is no caching layer of any kind.

It also silently discards a reader’s scroll position, and it will interrupt someone mid-<details>. The inbox already opts out of it for exactly this reason (“the one page with a half-typed sentence in it”); the reasoning applies more widely than it is applied.

2.6 Commission’s vocabulary is unexplained — small, but a judgement call

A stranger reads: gate: tier1, unblocks 2, stale claims, routine events, in review, blocked (derived), scoped to dana, supervised, human_only. Every one of those is defined in docs/concepts/, and none of them is linked from the page that uses it. site/README.md argues at length that this vocabulary is the reference’s, not the buyer’s — the same argument applies to a person opening the dashboard for the first time.

Cheap version: a title on the six worst offenders. Honest version: link each section heading to its concept page. Neither is done.

2.7 No theme toggle — deferred, with the reason

The view follows prefers-color-scheme and cannot be overridden. A toggle without client JS means a route that writes a cookie, and the read-only boundary — four write routes, structurally enforced, perturbation-tested — is worth more than a toggle. Deliberately deferred, not an oversight, and recorded here so nobody re-litigates it by accident.

2.8 Undesigned states that are now handled

Enumerated so the list is a list rather than a shrug. Each was reproduced:

statenow
no projects at alldesigned empty state naming commission init
a project with no tasksdesigned empty state naming commission apply
an empty inboxdesigned as the goal (relay-82), with the in-flight work under it
an inbox with one itemcaptured and checked; no layout that needs a second item
nothing moving on the standupdesigned empty state, pointing at ?days= and the board
no activity at all“nothing has happened here yet”
only routine activitypre-existing disclosure, kept
a very long titlewraps; rows reflow rather than clip, at 390px and 1440px
a failed sync / a divergencepre-existing; edged in --c-refusal, links the resolving command
a stale claimpre-existing board section, keeps its status chip
an unknown project or task404 through the same layout, with a way back
a refusal from a judgement routecomes back as a prompt on the page, not an error page
?days= / ?limit= / ?actor= nonsenseignored rather than fatal (?days=abc was a 500)

2.9 Undesigned states still outstanding

statewhy it is deferred
hundreds of tasks on one board§2.3 — needs a decision before it needs code
hundreds of projects in the navigationoverflow menu is a flat <details> list with no search; fine at ten, unusable at two hundred
a database that is locked or slowno timeout, no degraded render, no message — the page just takes as long as it takes
a project whose vision is 5,000 wordsthe disclosure holds it, but nothing caps it
an actor name long enough to break a chipchips white-space:nowrap; a 60-character actor name will push a row wide
two people acting on the same item at oncesecond one gets core’s refusal, which is correct, but the page does not say “somebody else just did this”

3. Accessibility pass

Done by hand against the rendered markup, plus computed contrast for every pair this redesign introduced.

Contrast — measured, WCAG 2.1, against the surface each colour actually sits on. Every text pair clears AA in both themes; the two tightest are named.

pairlightdark
chip in_progress (accent on 12% tint)5.49:17.12:1
chip in_review (violet on 14% tint)4.61:15.44:1
chip done (ok on 12% tint)5.45:17.10:1
chip cancelled (refusal on 12% tint)5.40:15.61:1
chip gate (warn on 14% tint)4.86:17.27:1
unblocks N (warn on surface)5.93:19.42:1
the judgement button (on-brand on violet)5.70:17.15:1
active project in the nav (ink on brand-soft)15.32:113.80:1
the item’s context line (muted on surface)6.57:17.32:1
the rank (faint on surface)5.17:14.75:1
section labels and footer (faint on canvas)4.99:15.04:1

The in_review chip at 4.61:1 in light is the tightest pair on the surface. It passes, and it is 12px, which is where AA is least forgiving. If the violet is ever retuned, re-measure this one first.

Keyboard and structure — what holds:

  • Skip link to #main, lang="en", header/nav/main/footer landmarks, the project nav labelled, aria-current="page" on the active project.
  • One <h1> per page and no skipped levels (fixed this pass).
  • A visible :focus-visible ring, 2px, never removed, on every interactive element including the disclosures.
  • Everything interactive is a link, a <button>, a <summary> or an <input> — there is no div with a click handler anywhere, because there is no JavaScript at all. Focus order is source order.
  • Status dots are aria-hidden; the word always travels with the colour, and where a row’s chip is suppressed the section heading is the word.
  • Every input carries an aria-label: the judgement fields name the task and the question, the unlock field names what it wants. No control is labelled by its placeholder alone.
  • The three questions each inbox item answers are still announced (.vh labels) even though they are no longer drawn.

Not good enough yet:

gapsize
Several metadata elements carry information only in a title attribute (the exact timestamp behind “3d”, the touches overlap warning behind a , the binding’s observed-at). title is not reliably announced and is unreachable by touchmedium
The glyph on an overlapping ready task has no accessible name at allsmall
The inbox context line is 12px. It clears contrast, but 12px as the resting size for a sentence is small, and it is the third thing a reader with low vision will hitmedium — it is a density trade-off that deserves a second opinion
Nothing has been tested with an actual screen reader, and no automated a11y suite runs in CI. Everything above is a hand auditmedium
Zoom to 200% and 400% has not been checkedsmall
The 60s auto-refresh will interrupt a screen-reader user mid-page, with no way to turn it offmedium — see §2.5

4. On a phone

Captured at 390×844 in both themes, every page, with no horizontal overflow — the capture rig fails the run if scrollWidth > clientWidth, so this is checked rather than eyeballed.

What is genuinely fine: rows reflow instead of clipping, the inbox item stacks in a sensible order, the judgement form is reachable and usable, the nav wraps.

What is not designed for a phone so much as tolerated: the project nav is the same row of pills and will wrap to three lines with six projects; the activity feed’s three-column row becomes very narrow; and nothing anywhere is a touch target of the recommended 44px — the tab strip, the disclosure triangles and the nav pills are all around 28–32px high. That last one is a real finding and it is the density decision working against touch.

5. Recommendation: the beta label stays on

The design lift was real and it is done: one design language with a test that fails on drift, an operational system that is its own thing rather than a squashed landing page, a measurably denser board, and an inbox a person can triage. If the question were “does this look like a product?”, the answer is now yes.

But the beta label is not a claim about how something looks. It is a promise about what a stranger can rely on, and three things below the line are load bearing:

  1. A cancelled task disappears (§2.1). Somebody will lose a task and not believe the tool again. That is a correctness problem wearing a design problem’s clothes.
  2. Nothing here has been tried on a large project (§2.3, §2.5). 95KB and no pagination at 300 tasks is not a scaling curve anybody has looked at, and “we never tested it past our own repository” is exactly what a beta label is for.
  3. You cannot find anything (§2.2). A dashboard without search is a dashboard you can only read top to bottom.

None of the three is hard. All three are unfinished, and a label that comes off while they are unfinished costs more than a label that stays on another month — because the first person who loses a cancelled task will not file a bug, they will stop opening the page.

Suggested exit criteria, so the next person is not re-arguing this:

  • cancelled work is visible somewhere on the board, or the board says in words that it omits it;
  • a project of 500 tasks renders in under 100KB and somebody has looked at the page;
  • a task is findable by id or by title from the web view;
  • responses are compressed;
  • one pass with a real screen reader, and the title-only metadata given a visible or announced home.

Everything else on this page is worth doing and none of it should hold the label.

6. Reproducing this audit

bun run scripts/shoot-dashboard.ts .screenshots/dashboard
bun test tests/design.test.ts tests/web-inbox.test.ts

The capture rig reports page height and horizontal overflow for every page × viewport × theme and exits non-zero if anything scrolls sideways. The numbers in §2.3 and §2.4 came from rendering a 302-task project through webRoutes in process and measuring the response; the contrast table came from computing WCAG 2.1 relative luminance for each pair against the surface it sits on, the same method site/README.md uses.

This page is docs/design/dashboard-beta.md in the Commission repository, rendered in place — the site keeps no copy of it. The repository is private, so there is no edit link to follow.