import { Component, useState, type ErrorInfo, type ReactNode } from "react"; import { AlertTriangle, Check, Copy, Home, RefreshCw, Sparkles } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { isChunkLoadError, reloadForNewDeploy } from "@/lib/chunk"; import { cn } from "@/lib/utils"; // ErrorBoundary keeps one broken render from unmounting the whole panel. The // root boundary (main.tsx) covers the providers and the shell; the one around // AppShell's Outlet keeps the navigation alive and resets on route change, so a // crash on one page never strands the user there. interface Props { children: ReactNode; /** Changing it clears a caught error (the Outlet passes the pathname). */ resetKey?: unknown; /** Replaces the crash page, e.g. a flat fleet grid for the 3D card. */ fallback?: (error: unknown, reset: () => void) => ReactNode; /** "screen" fills the viewport (root); "page" sits inside the shell. */ variant?: "screen" | "page"; } interface State { failed: boolean; error: unknown; } export class ErrorBoundary extends Component { state: State = { failed: false, error: null }; static getDerivedStateFromError(error: unknown): State { return { failed: true, error }; } componentDidCatch(error: unknown, info: ErrorInfo) { console.error("felis panel: render failed", error, info.componentStack); if (isChunkLoadError(error)) reloadForNewDeploy(); } componentDidUpdate(prev: Props) { if (this.state.failed && !Object.is(prev.resetKey, this.props.resetKey)) { this.setState({ failed: false, error: null }); } } reset = () => this.setState({ failed: false, error: null }); render() { if (!this.state.failed) return this.props.children; if (this.props.fallback) return this.props.fallback(this.state.error, this.reset); return ; } } function errorText(error: unknown): string { if (error instanceof Error) return `${error.name}: ${error.message}`; try { return typeof error === "string" ? error : JSON.stringify(error); } catch { return String(error); } } function CrashPage({ error, onRetry, screen }: { error: unknown; onRetry: () => void; screen: boolean }) { const { t } = useTranslation("common"); const [copied, setCopied] = useState(false); const updated = isChunkLoadError(error); const detail = errorText(error); async function copy() { try { await navigator.clipboard.writeText(`${detail}\n${window.location.href}`); setCopied(true); window.setTimeout(() => setCopied(false), 1500); } catch { // Clipboard denied: the text stays selectable in the details block. } } return (
{updated ? : }

{updated ? t("crash_updated_title") : t("crash_title")}

{updated ? t("crash_updated_body") : t("crash_body")}

{!updated && ( )} {/* A plain anchor: the root boundary renders outside the router. */}
{!updated && (
{t("crash_details")}
{detail}
)}
); }