Files
siftlode/frontend/src/components/ui/CrashBoundary.tsx
T
peter d44d8d6dda fix(gate): knip sees the watch entry, the build must emit watch.html, and two hand copies are held
- The raw-source contract tests read files from disk (rawSources.testkit)
  instead of import.meta.glob, which knip counted as importing every src file —
  its unused-files lane could never name an orphan. knip now names
  src/watchMain.tsx as an entry (registered in docs/gates.md).
- vite build fails when index.html or watch.html is not emitted.
- The watch-entry import walk follows dynamic import() too.
- audit_metrics.py's plan-code patterns are held equal to the guard's.
- CrashBoundary is domain-free: it moves into components/ui.
2026-10-06 03:19:33 +02:00

63 lines
2.1 KiB
TypeScript

import { Component, type ErrorInfo, type ReactNode } from "react";
import { errorAttrs, log } from "../../lib/log";
import { reloadingForNewBuild } from "../../lib/staleBuild";
import { Button } from "./Button";
export interface CrashCopy {
title: string;
subtitle: string;
reload: string;
}
interface Props {
children: ReactNode;
/** Read at crash time, so the screen speaks the language active then. */
copy: () => CrashCopy;
/** What the entry does beyond the log line (the app keeps a fatal notice in its center). */
onCrash?: () => void;
}
interface State {
crashed: boolean;
}
// Catches render-time crashes anywhere in the tree, logs them and offers a reload. Free of the app's
// i18n and notification store, so the public /watch entry wraps its page in it without loading
// either; `ErrorBoundary` is the app's wrapper around it.
export class CrashBoundary extends Component<Props, State> {
state: State = { crashed: false };
static getDerivedStateFromError(): State {
return { crashed: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
// The tab is already reloading onto a newer build (lib/staleBuild): this is the chunk it could not
// load, not a crash — no fatal notice, which would outlive the reload.
if (reloadingForNewBuild()) return;
log.fatal("ui.crash", "the app crashed while rendering", {
...errorAttrs(error),
...(info.componentStack ? { component_stack: info.componentStack } : {}),
});
this.props.onCrash?.();
}
render() {
if (this.state.crashed) {
if (reloadingForNewBuild()) return null;
const copy = this.props.copy();
return (
<div className="min-h-dvh grid place-items-center text-muted p-6 text-center">
<div>
<div className="text-lg font-semibold text-fg mb-1">{copy.title}</div>
<div className="text-sm mb-3">{copy.subtitle}</div>
<Button variant="link" onClick={() => location.reload()} className="text-sm">
{copy.reload}
</Button>
</div>
</div>
);
}
return this.props.children;
}
}