Blank-Screen Auto-Revival — Design
Date: 2026-06-09 Status: ApprovedProblem
The app can occasionally land on a blank/black screen that the existing recovery layers don’t catch. Today’s recovery already handles:- Deploy/chunk blanks —
public/chunk-recovery.js(pre-boot) +vite:preloadError(main.tsx) + the top-levelErrorBoundaryauto-reload, all coordinated throughlib/stale-recovery.ts(3-attempt cap + healthy-reset). - Stuck loading spinner —
components/ui/module-loading.tsxwatchdog. - Thrown render crash — top-level
ErrorBoundary→GlobalError.
null
(stale/odd URL), or a transient network drop leaves the root empty. No error,
no spinner → no existing watchdog fires → the user is stranded on black.
Requirements (from the user)
- Auto-revive from a black/stuck screen without manual intervention.
- Never disrupt in-progress work — must not reload out from under an unsaved invoice/quote/note.
- Don’t show scary technical error screens; show a friendly “lost connection · Refresh now” card.
- Auto-refresh after 10s if the user doesn’t interact with the card.
- Never log the user out — recovery reload must preserve auth.
Design
Safety-by-construction
The watchdog triggers on a truly empty root (#root rendered height ≈ 0).
This is what makes requirement #2 automatic: if the root has ~0 height, nothing
is on screen — there is no open form/dialog and therefore no work to lose. A
real screen (login, dashboard shell, any module) always has height, so the
trigger can’t fire over live content.
Component: BlankScreenWatchdog
Mounted once inside the router (sibling to <Routes>, so it stays alive
regardless of which route matches and can do a client-side redirect). It is
invisible until it detects trouble.
Heartbeat (every ~2s):
- Measures
document.getElementById('root')bounding height. - Considers the screen blank when height <
BLANK_MIN_PX(40px) and no Suspense fallback / loading skeleton is present (checked by a data marker or absence of known spinner nodes). - Requires the blank state to persist for
BLANK_GRACE_MS(8s) before acting — rides out legitimate transitions. - Also treats
navigator.onLine === falseas a trigger (connection lost).
Escalation
- Silent self-heal (once): client-side
navigate('/dashboard', { replace: true }). No reload, login/state preserved, nothing shown. Fixes the stale-URL-blank case invisibly. Tracked by a ref so it’s attempted at most once per blank episode. - If still blank after the redirect grace, or offline: render
ConnectionLostCardas a fixed full-screen overlay (the watchdog itself has height, so the overlay also ends the “blank” measurement). - The card never appears over live content (guaranteed by the height trigger).
Component: ConnectionLostCard
Premium branded card (dark, centered, OdontoX mark), copy:
Hmm, we lost connection
Your work is safe. Reconnect to pick up right where you left off.
[ ↻ Refresh now ]
- 10s countdown → auto-refresh if no interaction (requirement #4). A visible countdown (“Refreshing in 9…”) makes it non-jarring.
- Refresh now button reloads immediately.
- Both paths call a protected reload that sets the existing
vite_chunk_reloadsessionStorage flag — the post-reload boot inmain.tsxreads this to skip the auth-redirect, so the user is never logged out (requirement #5). The reload also routes throughstale-recovery’s cap so a genuinely broken deploy can’t loop forever.
Stale-URL self-heal (defense in depth)
useDashboardView already redirects unknown view values to the default. No
change needed there; the watchdog covers the residual permission-gated /
silent-null cases generically.
Out of scope
- React-failed-to-mount (no React at all): already covered by
public/chunk-recovery.js+ the pre-boot error listeners. - Per-module error UIs: unchanged.
Files
ui/src/lib/blank-screen-watchdog.ts— pure detection helpers (testable).ui/src/components/shared/BlankScreenWatchdog.tsx— heartbeat + escalation.ui/src/components/shared/ConnectionLostCard.tsx— the card + countdown.ui/src/App.tsx— mount<BlankScreenWatchdog />inside the router.- Reuses
ui/src/lib/stale-recovery.ts(cap/flag) — no change.
Testing
- Unit:
blank-screen-watchdogdetection (height threshold, offline, grace). - Manual: navigate to a deliberately-empty view → expect silent
/dashboardredirect; toggle offline → expect card + 10s auto-refresh; confirm no card appears while a dialog/form is open (height guard).

