Skip to content

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 into
mainfrom
thejefflarson/jef-408-dashboard-move-all-body-html-to-the-client-root-only-server
Jul 12, 2026
Merged

feat(dashboard): move all body HTML to the client (root-only server shell); fix the dead poll interval (JEF-408)#231
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-408-dashboard-move-all-body-html-to-the-client-root-only-server

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

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 setInterval bug caused three symptoms at once, all fixed by the correct arg order (setIntervalImpl = (ms, fn) => setInterval(fn, ms)):

  1. Dead recurring pollsetInterval(POLL_MS, tick) passed the number 5000 as the handler, so only the initial tick() ran.
  2. Blank tab-swaps forever — the recurring poll never refired ("clicking does nothing").
  3. CSP eval violation — the browser coerced the numeric handler to the string "5000" and took the legacy string-handler eval path, which the strict CSP (script-src 'self', no unsafe-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.jsx is a 1:1 port of the retired maud components/status_strip.rs, reusing the existing dashboard.css classes verbatim (no new palette). The judging axis is chosen by a single server-derived judging-state token (all-clear | watching | judging | warming | no-model | blind) added to the strip props' hand-written Serialize — so the honesty derivation stays server-side and the client performs zero honesty derivation. Retired dashboard/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 / blind and 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

  • Rust (cargo test -p protector, cargo fmt, cargo clippy warnings-as-errors, all green): page_tests.rs rewritten for the root-only body (#dash-root present, no .strip/.tabs, <title> still carries the cluster); serialize_tests.rs adds the judging-state token across every state + an "all-clear iff green" invariant sweep; dashboard_guards.rs flipped to assert the body is root-only and the components/ module stays retired.
  • JS (npm test, 84 pass): new strip.test.jsx (each judging-state → its class/glyph/text; counts + escalation + signing-regression chip; coverage present/degraded/absent; green iff all-clear); poll.test.js regression (default interval ticks >1 with fake timers, and native setInterval receives a function handler — locks out the string-eval regression); app-refetch.test.jsx (a tab-swap restarts the poll → immediate refetch).
  • Built the bundle from source (npm run build) so cargo can include_str! it.

Notes

  • Operator: please visually confirm post-deploy — a code/test audit can't see the render. Verify the strip paints, tab-swaps update immediately, and no CSP eval warning appears in the console.
  • /soundcheck:pr-review: no Critical/High findings (presentation-only; all untrusted text auto-escaped by Preact/maud, no dangerouslySetInnerHTML/eval, CSP untouched, same-origin poll unchanged).

🤖 Generated with Claude Code

https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

…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>
@thejefflarson thejefflarson enabled auto-merge (squash) July 12, 2026 17:34
@thejefflarson thejefflarson merged commit 7c016a6 into main Jul 12, 2026
5 checks passed
@thejefflarson thejefflarson deleted the thejefflarson/jef-408-dashboard-move-all-body-html-to-the-client-root-only-server branch July 12, 2026 17:39
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