catch render errors instead of showing a white screen

The repo had no error boundary anywhere, so a single malformed stored layout took the whole app down
and the only recovery was a psql session. Two boundaries, because "recover" means different things:

- around the routed screen in DashboardLayout, with the dock and header deliberately left outside so
  navigating away is itself a way out, plus a two-click reset of every `screens/*` layout for when it
  fails again in the same place. Dashboards are not touched — they are user-created and hold content.
- around each panel app in PanelSlot, so one bad app leaves the rest of the workspace running. Its
  recovery is "clear this panel", offered only when the layout is the user's to edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 09:04:03 +00:00
co-authored by Claude Opus 5
parent 30eef86972
commit 64961d49f5
5 changed files with 224 additions and 3 deletions
@@ -1,6 +1,9 @@
import { useMemo, useRef } from 'react';
import { useLocation } from 'react-router';
import { useDock, MusicPlayerHost } from 'officerdev';
import { useCapabilities } from 'hooks/useCapabilities';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { ScreenErrorFallback } from './ScreenErrorFallback';
import { Background } from './Background';
import { Header } from './Header';
import { Dock, ALL_DOCK_ITEMS, DEFAULT_DOCK_PATHS } from './Dock';
@@ -19,6 +22,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
const permitted = useMemo(() => ALL_DOCK_ITEMS.filter((item) => canVisit(item.to)), [canVisit]);
const { items: visibleItems } = useDock(permitted, DEFAULT_DOCK_PATHS);
const isTouch = useIsTouch();
const { pathname } = useLocation();
usePageTitleSync();
// The content region shrinks when the (in-flow) music dock takes its space; the nav dock measures its
// reveal boundary from this element, so it always sits just above whatever's at the bottom.
@@ -35,7 +39,16 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
<section ref={regionRef} className="relative min-h-0 flex-1 snap-start overflow-clip">
<Background />
{!isTouch && <Dock items={visibleItems} boundaryRef={regionRef} className="hidden md:flex" />}
<div className="absolute inset-0 z-2 pt-[52px] md:pt-[64px] pb-2 overflow-y-auto">{children}</div>
<div className="absolute inset-0 z-2 pt-[52px] md:pt-[64px] pb-2 overflow-y-auto">
{/* Chrome stays outside: whatever broke, the dock and the header still navigate you off it.
Keyed on the pathname so leaving a broken screen is itself a recovery. */}
<ErrorBoundary
resetKeys={[pathname]}
fallback={({ error, reset }) => <ScreenErrorFallback error={error} reset={reset} />}
>
{children}
</ErrorBoundary>
</div>
</section>
<MusicPlayerHost />
</div>