Skip to main content

Blank-Screen Auto-Revival — Design

Date: 2026-06-09 Status: Approved

Problem

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 blankspublic/chunk-recovery.js (pre-boot) + vite:preloadError (main.tsx) + the top-level ErrorBoundary auto-reload, all coordinated through lib/stale-recovery.ts (3-attempt cap + healthy-reset).
  • Stuck loading spinnercomponents/ui/module-loading.tsx watchdog.
  • Thrown render crash — top-level ErrorBoundaryGlobalError.
The gap: a screen that renders empty without throwing and without a Suspense fallback — e.g. React is alive but routed to a view that renders 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)

  1. Auto-revive from a black/stuck screen without manual intervention.
  2. Never disrupt in-progress work — must not reload out from under an unsaved invoice/quote/note.
  3. Don’t show scary technical error screens; show a friendly “lost connection · Refresh now” card.
  4. Auto-refresh after 10s if the user doesn’t interact with the card.
  5. 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 === false as a trigger (connection lost).

Escalation

  1. 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.
  2. If still blank after the redirect grace, or offline: render ConnectionLostCard as a fixed full-screen overlay (the watchdog itself has height, so the overlay also ends the “blank” measurement).
  3. 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_reload sessionStorage flag — the post-reload boot in main.tsx reads this to skip the auth-redirect, so the user is never logged out (requirement #5). The reload also routes through stale-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-watchdog detection (height threshold, offline, grace).
  • Manual: navigate to a deliberately-empty view → expect silent /dashboard redirect; toggle offline → expect card + 10s auto-refresh; confirm no card appears while a dialog/form is open (height guard).