docs/waves/usability_resilience_backlog.md
Waves Usability & Resilience Backlog (2026-07-24)
Follow-up backlog from a usability/graceful-failure audit of the Waves browser frontend, grounded against Chromium’s own stated design principles (“no god objects,” “content not chrome,” keyboard/screen-reader access as core not afterthought), Firefox’s network-error-page pattern (plain-language error + visible retry), and Nielsen Norman’s usability heuristics (visibility of system status; help users recognize, diagnose, and recover from errors; consistency and standards).
Sources: Chromium browser design principles, Chromium User Experience, NN/g 10 usability heuristics, Firefox network error page design.
The higher-severity items from this same audit pass (WMLScript dialog visibility, frozen first-load skeleton, toast queueing, uniform fetch-failure toasting, focused-link/softkey ARIA) were fixed directly — see the PR that lands alongside this doc. This file tracks what was deliberately deferred.
How to use
Same convention as MAINTENANCE_WORK_ITEMS.md: each entry is a
self-contained story with status/priority/files/finding/recommendation/
acceptance. Pull one into a branch when picked up; update status in place.
Current queue
U1 No progress indication on repeat navigations
Status:donePriority:P2Files:
browser/frontend/src/app/browser-controller.ts(navigation trigger sites)browser/frontend/src/app/browser-presenter.tsbrowser/frontend/src/components/status-panel.ts
Finding:
- The shimmering skeleton loading state only ever shows for the very first deck render (
needsFirstRenderSkeleton = !this.presenter.hasRenderedDeck()). Every subsequent navigation (link click, reload, back/forward, external intent) has zero in-progress indicator beyond a status-panel text color change — the viewport itself just sits static with the old content until the new response lands. - Under real network latency (
WAVES_CONFIG.transportFetchTimeoutMs= 5000ms plus a retry), a slow-but-working load is visually indistinguishable from a stuck one unless the user is actively watching the status panel text.
Recommendation:
- Reuse the existing skeleton/progress affordance (or a lighter inline spinner) for every navigation, not just the first, gated by a short delay (e.g. only show if the fetch hasn’t resolved within ~150-200ms) so instant local-mode loads don’t flash it unnecessarily.
Accept:
- Any navigation that takes longer than the short delay threshold shows a visible in-progress state in the viewport itself, not just the status panel.
U2 Deck-parse and script-trap errors are indistinguishable from network errors
Status:donePriority:P2Files:
browser/frontend/src/app/navigation-state.ts(EngineRuntimeSnapshotfields:lastScriptExecutionOk,lastScriptExecutionTrap,lastScriptExecutionErrorClass,lastScriptExecutionErrorCategory)browser/frontend/src/app/browser-controller.tsbrowser/frontend/src/app/waves-copy.ts
Finding:
- The engine already exposes a proper error taxonomy for script traps (
lastScriptExecutionErrorClass/errorCategory), but the frontend only reads these fields for render change-detection (engineSnapshotsEqual) — never to change a status message or visual state. A malformed WML payload, a network timeout, and a WMLScript fatal trap all produce the exact same generic “error” status-panel text today.
Recommendation:
- Branch the status/toast message on the actual failure category (network vs. malformed-payload vs. script-trap) using data the engine already computes, so users (and anyone debugging a deck) can tell which layer failed without opening the dev drawer.
Accept:
- The three failure classes produce visibly distinct, correctly-worded status messages.
U3 Back button never reflects “no history” state
Status:donePriority:P3Files:
browser/frontend/src/app/browser-controller.tsbrowser/frontend/src/app/browser-shell-template.ts
Finding:
#btn-backis never disabled, even when there is provably no back history (navigateBackWithFallbackreturns'none'). Both Chrome and Firefox dim/disable their back button at the start of history as a passive “don’t bother” signal rather than requiring the user to click and read a status message.
Recommendation:
- Track back-availability in render state and toggle
disabled/aria-disabledon#btn-backaccordingly. Low effort, matches an established platform convention.
Accept:
- Back button is disabled exactly when there is no engine or host history to go back to.
U4 Color palette hardcoded and duplicated, no shared design tokens
Status:donePriority:P3Files:
browser/frontend/src/styles.cssbrowser/frontend/src/components/status-panel.tsbrowser/frontend/src/components/surface-panel.tsbrowser/frontend/src/vendor/win95/win95.css(read-only reference, third-party)
Finding:
- Only
--motion-fast/--motion-base/--ease-standard/--waves-tealexist as CSS custom properties. Everything else (status-panel tones, toast tones, win95-gray palette) is hardcoded hex, repeated independently instyles.cssand inline in each Lit component’sstatic styles.
Recommendation:
- Extract a small shared token set (status/toast semantic colors at minimum) into
:rootcustom properties, consumed by both plain CSS and the Lit components’csstemplates.
Accept:
- Status/toast/error colors are defined once and referenced everywhere, not restated.
U5 No runtime-configurable timeout/contrast/font-size
Status:todoPriority:P4(likely won’t do — flagging for a scope decision, not recommending)Files:
browser/frontend/src/app/waves-config.ts
Finding:
- All UX-relevant timing/behavior constants (
transportFetchTimeoutMs,transportFetchRetries,networkProbeMaxAttempts/DelayMs/TimeoutMs,toastTtlMs,engineTimerTickMs) are compile-time constants with no UI to change them at runtime; same for any visual accessibility preference (contrast, font size).
Recommendation:
- Before investing here, get an explicit call on scope: AGENTS.md frames this repo as a deterministic MVP emulator, not a modern-browser feature-parity target. This may be intentionally out of scope. Do not build a settings panel speculatively.
Accept:
- N/A until a scope decision is made.
U6 BrowserPresenter has no dispose() lifecycle symmetry
Status:donePriority:P3Files:
browser/frontend/src/app/browser-presenter.tsbrowser/frontend/src/app/browser-controller.ts
Finding:
BrowserPresenter’s constructor registers adevDrawerEl.addEventListener('toggle', ...)with no matching teardown;BrowserController.dispose()never disposes its presenter. Currently harmless becausemountBrowserShellreplaces#app.innerHTMLwholesale on every bootstrap/HMR cycle, dropping the old listener with the old DOM — but the class has no lifecycle symmetry with the controller that owns it.
Recommendation:
- Add a
dispose()toBrowserPresenterthat removes its listener(s); call it fromBrowserController.dispose().
Accept:
- Presenter listeners are torn down explicitly, not just implicitly via DOM replacement.
U7 Debug-panel full-JSON reserialize on every mutation
Status:donePriority:P3Files:
browser/frontend/src/app/browser-presenter.ts(flushDeveloperPanels,scheduleDeveloperPanelFlush)browser/frontend/src/app/browser-presenter.test.ts
Finding:
- When the dev drawer is open, every session/snapshot/timeline mutation re-serializes the entire object graph via
JSON.stringify(..., null, 2)rather than diffing. Gated behinddevDrawerEl.openand dirty flags already (better than the original 2026-03-15 review’s “every update” finding), but still O(n) per mutation while open.
Recommendation:
- Not urgent — developer-facing panel, not user-facing. Revisit only if it shows up in real profiling, or bundle into a future
M1-08follow-up pass.
Accept:
- N/A — low priority, informational.
Notes:
- Fixed as part of the
M1-08follow-up pass.setSessionState/clearTimeline/recordTimeline/setSnapshot/setTransportResponseno longer callflushDeveloperPanels()synchronously and immediately; they call a newscheduleDeveloperPanelFlush(), which coalesces same-tick mutations (e.g.patchSessionState’ssetSessionState+recordTimelinepair, or severalrecordTimelinecalls made back-to-back while stepping through a navigation) into a single microtask-deferred flush instead of one fullJSON.stringifypass per call. The dev-drawertogglelistener still callsflushDeveloperPanels()directly/synchronously so opening the drawer shows current state immediately rather than a tick late.flushDeveloperPanels()itself is unchanged (still per-field dirty-gated, still a no-op while the drawer is closed). Verified with a new regression test (coalesces a burst of same-tick timeline mutations into a single reserialize) that spies onJSON.stringifyand asserts three synchronousrecordTimelinecalls produce exactly one reserialize of the timeline array, plus a companion test confirming mutations in separate ticks still each flush. Low-risk, no behavior change to what’s displayed.
U8 marketing-site/global.css is a single 1261-line unsplit stylesheet
Status:donePriority:P4Files:
marketing-site/src/styles/global.cssmarketing-site/src/components/*.astro(new)marketing-site/src/pages/index.astro
Finding:
- No component-level splitting; will get harder to maintain as the site grows. Not broken today.
Recommendation:
- Defer until the next marketing-site change that touches styling meaningfully; not worth a standalone pass.
Accept:
- N/A — cosmetic, low priority.
Resolution:
- Extracted the nine previously inline page sections (header/nav, hero, signal strip, manifesto, stack, capabilities, shipping, final CTA, footer) into
marketing-site/src/components/*.astro, each carrying its own scoped<style>block for the rules that belong to it alone, including that section’s responsive breakpoint overrides.global.csswas trimmed to ~380 lines of genuinely cross-cutting rules: design tokens, the base reset, page-shell utilities, the shared brand mark (header+footer), shared button styles (hero+final-cta), shared section-heading/typography rules, and shared entrance-animation keyframes.index.astronow just composes the components; its reveal/nav-highlight<script>is unchanged. Verified via a structural diff of the built CSS (665/665 normalized declarations match between the old monolithic output and the new split output; the one apparent mismatch is a cosmetic artifact of Astro’s own compiler rewriting.hero-copy > *to an equivalent attribute-selector form) and a full diff of the builtindex.html(identical DOM/classes/ids/text/script; the only additions are Astro’s own inertdata-astro-cid-*scoping attributes).pnpm --dir marketing-site --ignore-workspace run buildsucceeds.
Also tracked elsewhere (not duplicated here)
BrowserControllergod-file decomposition — tracked asM1-08indocs/waves/MAINTENANCE_WORK_ITEMS.md(status: done — residual DOM-listener-wiring and keyboard-intent-routing extraction landed).network::wspdead-code-vs-live-fetch-path split — tracked asM1-18.fetch_policy.rsshallow destination-check hardening, gateway-bridgedAllowPrivateinvariant pinning, andfetch_host.rsgateway-transport-fallback host-scope confirmation — transport/tauri-layer hardening items surfaced in the same audit; see the maintenance doc entriesM1-19/M1-20/M1-21added alongside this file.- Full field-level IPC response-shape validation (currently only a null/undefined boundary guard) — deliberately partial; revisit as its own scoped design decision (schema library vs. hand-rolled checks), not bundled here.