diff --git a/docs/adr/0027-dashboard-root-only-shell-client-strip.md b/docs/adr/0027-dashboard-root-only-shell-client-strip.md new file mode 100644 index 0000000..8964c0e --- /dev/null +++ b/docs/adr/0027-dashboard-root-only-shell-client-strip.md @@ -0,0 +1,70 @@ +# 0027. Dashboard: the server emits a ROOT-ONLY shell; the status strip + nav render in the client + +- Status: Accepted +- Date: 2026-07-12 +- Supersedes (in part): [0025](0025-dashboard-v4-preact-client-render.md) — its decision that the + persistent **status strip** and the **tab nav** stay SERVER-RENDERED for a calm-when-blind first + paint. Everything else in 0025 (Preact reconciler, view_model/props as the serde JSON contract, + built-from-source bundle, zero-egress, server-derived honesty tokens) stands. +- Reaffirms: [0016](0016-severity-vs-urgency.md) — presentation is a view, never a gate; and the + honesty axes of [0019](0019-dashboard-v3-presentation-architecture.md)/0025 (blind ≠ green). + +## Context + +Under ADR-0025 the engine went Preact-only for every view *body*, but kept TWO parts server-rendered +in maud — the status strip and the tab nav — so the honest calm-when-blind banner would paint before +any JS ran. That split had two concrete costs that surfaced in production (JEF-408): + +1. **A dead recurring poll masqueraded as a working one.** `poll.js` called its injected interval as + `(ms, fn)`, but the default was native `setInterval` (`(fn, ms)`), so `setInterval(POLL_MS, tick)` + handed the *number* 5000 as the handler. The recurring poll never fired — only the initial tick — + so a client tab-swap blanked the view forever ("clicking does nothing"). Worse, the browser + coerced the numeric handler to the string `"5000"` and took the legacy string-handler path, which + compiles the string as code (an eval); the strict CSP (`script-src 'self'`, no `unsafe-eval`) + correctly blocked it. One reversed-args bug, three symptoms. + +2. **Two rendering stacks for the same honesty derivation.** The maud `status_strip.rs` and the + (client) view bodies both had to agree on the judging axis, kept in sync by hand. Any maud-only + strip meant the strip and the body could drift, and the shell carried body HTML the client would + have to reconcile around. + +The calm-when-blind justification for a server-rendered strip is weaker than it first appears: a +blank document before the first fetch is *honest* (absent is not a green all-clear), and the strip's +honesty is a **derivation**, not a render — as long as that derivation stays server-side, WHERE the +strip DOM is produced is immaterial to the honesty contract. + +## Decision + +We will move ALL body HTML — the status strip and the tab nav included — to the Preact client, and +have the server emit a **ROOT-ONLY** document shell. + +- **Server shell (`page.rs`):** the body is just `
` + the deferred + bundle `