Skip to content

refactor(dashboard): simplify the client — plain useState, no store, native <details>, zero runtime deps (JEF-411)#233

Merged
thejefflarson merged 5 commits into
mainfrom
thejefflarson/jef-411-dashboard-replace-the-hand-rolled-store-with-plain-usestate
Jul 12, 2026
Merged

refactor(dashboard): simplify the client — plain useState, no store, native <details>, zero runtime deps (JEF-411)#233
thejefflarson merged 5 commits into
mainfrom
thejefflarson/jef-411-dashboard-replace-the-hand-rolled-store-with-plain-usestate

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

Closes JEF-411.

What changed

An architect-designed refactor of the Preact dashboard client (engine/web/) — the server (page.rs, /api/{tab}.json, view_model, security_headers.rs) does NOT change.

  • Local state by default. App owns the ONLY shared state — activeTab, data, strip, status, lastGoodAt — each a plain useState (no reducer, no Context, no signals, no new dep). The hand-rolled observable store (store.js) is deleted.
  • The poll is decoupled to callbacks. startPolling takes { tab, onSnapshot, onStale, liveRegion, … }, feeding App's useState updaters directly.
  • Expansion/disclosure is local & ephemeral. A finding row's expansion is a useState; every "show model prompt"/node-breakdown/judgement disclosure is a native, uncontrolled <details>; the Admission signing-row expander is a useState per row. The sessionStorage persistence (open rows survive a reload) is dropped as an unnecessary constraint.
  • Keyed removal replaces the tombstone. A gone finding is removed by Preact's keyed diff (key={f.id}); reconcile.js + the client tombstone are deleted with no client replacement (a future "recently cleared" cue is server-shipped).
  • Zero runtime npm deps. preact + esbuild-wasm move to devDependencies (build/test-only — the engine include_str!s a compiled bundle and installs nothing); the unused @vitest/mocker + top-level vite pin are dropped; the lockfile is regenerated (npm ci clean).
  • ADR-0028 records the decision (extends 0025/0027, reaffirms 0016/0019).

Invariants preserved (and re-pinned)

  • JEF-408 poll/CSP fix retained verbatim: the default (ms, fn) => setInterval(fn, ms) keeps the handler a FUNCTION (no number→string eval), with a "do not clean up" comment citing JEF-408/ADR-0027. CSP stays strict. Two regression tests kept.
  • JEF-410 strip persistence: the strip is its own useState, decoupled from data, so it survives a tab swap and keeps the last strip if a snapshot omits it. Ported to app-status.test.jsx.
  • Honesty (ADR-0016/0019/0027): zero client honesty derivation — StatusStrip/FindingsEmpty render SERVER tokens (green iff strip["all-clear"]). strip.test.jsx + honesty.test.jsx unchanged and passing.
  • No dangerouslySetInnerHTML, auto-escaping intact, 1000-line cap: the Rust guards in engine/tests/dashboard_web_guards.rs stay green (unchanged).

How I tested

  • npm --prefix engine/web cirun build (from source) + run test: 69/69 green (13 files).
  • cargo test -p protector --test dashboard_web_guards (raw-HTML ban, no off-origin, line cap) and --test dashboard_guards: green.
  • cargo fmt --check clean; cargo clippy -p protector --tests clean (no Rust logic change).
  • Ran /soundcheck:pr-review (no Critical/High findings — the diff adds no injection/eval/off-origin sink; XSS auto-escaping intact, sessionStorage surface reduced) and /simplify (one cleanup applied: dropped an over-clever functional-updater guard in swapTab).

🤖 Generated with Claude Code

https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

thejefflarson and others added 5 commits July 12, 2026 14:28
…tore (JEF-411)

Decouple the poll from any state container: startPolling now takes plain
{ tab, onSnapshot, onStale, liveRegion, … } callbacks so App can wire them to
its useState updaters. Everything else is byte-identical — POLL_MS=5000, the
synchronous first tick(), the stale-on-failure paths, the mid-selection defer
guard, and (verbatim) the JEF-408/ADR-0027 `(ms, fn) => setInterval(fn, ms)`
default that keeps the handler a function (no number→string eval under the
strict CSP).

Tests: poll.test.js ports to spy the callbacks; the two JEF-408 regressions
(>1-tick recurring poll + native setInterval gets a FUNCTION handler) and the
selection-guard defer test are retained.

Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… the store (JEF-411)

App holds the only shared client state — activeTab, data, strip, status,
lastGoodAt — each a plain useState (no reducer, no Context, no signals). The
status transitions are small updaters: a snapshot goes live + persists the
strip (keeping the last if omitted — JEF-410), stale never fires before the
first snapshot, and a tab swap nulls `data` but never touches `strip` (its own
useState, so the header persists across swaps — JEF-410). main.jsx renders
<App initialTab=…>; the hand-rolled store.js is deleted.

The poll effect keys on [activeTab] so a swap restarts it (immediate refetch —
JEF-408). Honesty is untouched: the strip/empty-state still render server tokens.

Tests: app/app-refetch driven via the DOM (5×-pushState intercept, modified
click, immediate-refetch-on-swap); a new app-status.test.jsx ports the store's
surviving invariants (the three status transitions incl. never-stale-before-
first-snapshot; the JEF-410 strip-persists-across-swap AND keeps-last-strip
blocks). store.test.js deleted; transitional-states drives the banner via <App>.

Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…laces the tombstone (JEF-411)

Every view is now a pure `view`-only render (the store prop is gone). Expansion
and disclosure are LOCAL, ephemeral state: a finding row's expansion is a
useState in the row; every "show model prompt"/node-breakdown/judgement
disclosure is a native, uncontrolled <details>; the Admission signing-row
expander is a useState per row (useRowDisclosure drops its store arg). Preact's
keyed diff keeps an open row open across a poll for free — the sessionStorage
persistence (open rows survive a reload) is dropped as an unnecessary constraint.

reconcile.js and the client tombstone are deleted: a finding that vanishes is
removed by the keyed diff (key={f.id}), with no client farewell row (a future
"recently cleared" cue is server-shipped; the strip's cleared-count carries it).

Tests: the load-bearing state-preservation test drives expansion via a click +
an open native <details> + focus and survives a new snapshot keyed by id (the
two tombstone tests deleted); escaping drives expansion by clicking the row
(tombstone-id XSS test deleted); the action/admission/readiness/alerts tests
drop the store and drive disclosures via the DOM, keeping keying + honesty.
reconcile.test.js deleted.

Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ps (JEF-411)

preact and esbuild-wasm move to devDependencies (both are build/test-only — the
running engine include_str!s a compiled bundle and installs nothing). The unused
@vitest/mocker (transitive; vi.mock still works via vitest) and the top-level
vite pin are dropped; jsdom + @testing-library/preact + vitest stay. The lockfile
is regenerated so `npm ci` and the supply-chain guard match — the root manifest
now has zero runtime dependencies.

Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Record the decision: local state by default (App holds the 5 shared fields + the
callback-decoupled poll as plain useState, no reducer/Context/deps); expansion is
ephemeral (persistence removed); keyed removal replaces the tombstone (a future
cleared-cue is server-shipped); the JEF-408 poll/CSP fix + JEF-410 strip
persistence + server-derived honesty are retained; deps pruned to build+test only
(zero runtime). Extends 0025/0027 (both stand), reaffirms 0016/0019. Update the
ADR index.

Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@thejefflarson thejefflarson merged commit 976877b into main Jul 12, 2026
5 checks passed
@thejefflarson thejefflarson deleted the thejefflarson/jef-411-dashboard-replace-the-hand-rolled-store-with-plain-usestate branch July 12, 2026 21:36
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