feat(dashboard): move all body HTML to the client (root-only server shell); fix the dead poll interval (JEF-408)#231
Merged
thejefflarson merged 1 commit intoJul 12, 2026
Conversation
…hell); fix the dead poll interval (JEF-408)
The server now emits a ROOT-ONLY document shell — `<head>` + `<div id="dash-root">` +
the deferred bundle. ALL body HTML (the status strip, the tab nav, and every view body)
renders in the Preact client, reconciling from `/api/{tab}.json`. This supersedes ADR-0025's
server-rendered strip/nav; recorded as ADR-0027.
Two production bug fixes:
- Dead recurring poll + blank tab-swaps + a CSP eval violation, all one root cause: `poll.js`
called its interval as `(ms, fn)` but defaulted to native `setInterval` (`(fn, ms)`), so it
passed the number 5000 as the handler — the recurring poll never fired, and the browser
coerced the number to the string "5000" and took the legacy string-handler eval path, which
the strict CSP correctly blocked. The default is now `(ms, fn) => setInterval(fn, ms)`; the
CSP stays strict. A client tab-swap also restarts the poll so the new tab refetches immediately.
- The status strip + tab nav moved to the client (`strip.jsx`), a 1:1 port of the retired maud
`status_strip.rs`, reusing the existing `dashboard.css` classes verbatim. The judging axis is
chosen by a single server-derived `judging-state` token so the honesty DERIVATION stays
server-side (ADR-0025 contract); the client performs zero honesty derivation.
Honesty preserved: green renders ONLY when `judging-state === "all-clear"`; watching/warming/
no-model/blind and any awaiting/uncertain counts are non-green. A blank before the first fetch is
honest (absent ≠ green). SSR/hydration of the strip is a noted follow-up.
Retired `dashboard/components/` (status_strip.rs, nav.rs). Tests: Rust page_tests (body root-only),
serialize_tests (judging-state token across states), dashboard_guards (root-only body); JS strip.test.jsx,
poll.test.js regression (ticks >1 + function handler), app-refetch.test.jsx.
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Closes JEF-408.
What changed
The server now emits a ROOT-ONLY document shell —
<head>+<div id="dash-root" data-tab=…>+ the deferred bundle<script>. All body HTML — the status strip, the tab nav, and every view body — renders in the Preact client, reconciling from/api/{tab}.json. This supersedes ADR-0025's "strip + nav stay SERVER-RENDERED"; recorded as ADR-0027.Two production bug fixes (one root cause)
The reversed-args
setIntervalbug caused three symptoms at once, all fixed by the correct arg order (setIntervalImpl = (ms, fn) => setInterval(fn, ms)):setInterval(POLL_MS, tick)passed the number5000as the handler, so only the initialtick()ran."5000"and took the legacy string-handler eval path, which the strict CSP (script-src 'self', nounsafe-eval) correctly blocked.The CSP stays strict — the bug was our reversed args, not the policy. A client tab-swap now also restarts the poll so the swapped-to tab refetches immediately (no up-to-5s blank).
Client strip move
strip.jsxis a 1:1 port of the retired maudcomponents/status_strip.rs, reusing the existingdashboard.cssclasses verbatim (no new palette). The judging axis is chosen by a single server-derivedjudging-statetoken (all-clear|watching|judging|warming|no-model|blind) added to the strip props' hand-writtenSerialize— so the honesty derivation stays server-side and the client performs zero honesty derivation. Retireddashboard/components/(status_strip.rs,nav.rs) and its module.Honesty invariant (ADR-0016/0019) — preserved
Green renders only when
judging-state === "all-clear".watching/warming/no-model/blindand any awaiting/uncertain counts are non-green. A blank before the first fetch is honest (absent ≠ green — the connection banner already says "connecting…"). SSR/hydration of the strip to close the pre-fetch blank is a noted follow-up (ADR-0027).How I tested
cargo test -p protector,cargo fmt,cargo clippywarnings-as-errors, all green):page_tests.rsrewritten for the root-only body (#dash-rootpresent, no.strip/.tabs,<title>still carries the cluster);serialize_tests.rsadds thejudging-statetoken across every state + an "all-clear iff green" invariant sweep;dashboard_guards.rsflipped to assert the body is root-only and thecomponents/module stays retired.npm test, 84 pass): newstrip.test.jsx(eachjudging-state→ its class/glyph/text; counts + escalation + signing-regression chip; coverage present/degraded/absent; green iff all-clear);poll.test.jsregression (default interval ticks >1 with fake timers, and nativesetIntervalreceives a function handler — locks out the string-eval regression);app-refetch.test.jsx(a tab-swap restarts the poll → immediate refetch).npm run build) socargocaninclude_str!it.Notes
/soundcheck:pr-review: no Critical/High findings (presentation-only; all untrusted text auto-escaped by Preact/maud, nodangerouslySetInnerHTML/eval, CSP untouched, same-origin poll unchanged).🤖 Generated with Claude Code
https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP