diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 35d47f8f..f4c7a31b 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,5 @@
-import { Routes, Route, Navigate } from 'react-router-dom';
-import { useEffect } from 'react';
+import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
+import { useEffect, type ReactNode } from 'react';
import Home from './pages/Home';
import Debugger from './pages/Debugger';
import PayrollScheduler from './pages/PayrollScheduler';
@@ -25,6 +25,20 @@ import AuthCallback from './pages/AuthCallback';
import { useTranslation } from 'react-i18next';
import { contractService } from './services/contracts';
+/**
+ * Wraps a route element in an ErrorBoundary that auto-resets when the user
+ * navigates to a different route path. This ensures a crash on one route
+ * doesn't permanently break navigation to another route.
+ */
+function RouteBoundary({ children, fallback }: { children: ReactNode; fallback?: ReactNode }) {
+ const location = useLocation();
+ return (
+ }>
+ {children}
+
+ );
+}
+
function App() {
const { t } = useTranslation();
@@ -41,7 +55,7 @@ function App() {
-
+
}
/>
-
+
}
/>
-
+
}
/>
- }
+ }
>
-
+
}
/>
}>
+
-
+
}
/>
-
+
}
/>
-
+
}
/>
}>
+
-
+
}
/>
{}} />}>
+
-
+
}
/>
{}} />}>
+
-
+
}
/>
{}} />}>
+
-
+
}
/>
{}} />}>
+
-
+
}
/>
{}} />}>
+
-
+
}
/>
{}} />}>
+
-
+
}
/>
{}} />}>
+
-
+
}
/>
{}} />}>
+
-
+
}
/>
{}} />}>
+
-
+
}
/>
- } />
- } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
} />
);
}
-export default App;
+export default App;
\ No newline at end of file
diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx
index c57a9ff7..d88ce395 100644
--- a/frontend/src/components/ErrorBoundary.tsx
+++ b/frontend/src/components/ErrorBoundary.tsx
@@ -4,12 +4,28 @@ import * as Sentry from '@sentry/react';
type ErrorBoundaryProps = {
fallback: React.ReactNode;
children: React.ReactNode;
+ /** Callback invoked after the boundary resets (e.g. when the user clicks "Try Again"). */
+ onReset?: () => void;
+ /**
+ * When any of these values change between renders, the boundary is reset
+ * (and `onReset` is fired). Useful for keying the boundary to the current
+ * route so navigation away from a crashed route recovers automatically.
+ */
+ resetKeys?: ReadonlyArray;
};
type ErrorBoundaryState = {
hasError: boolean;
};
+/**
+ * Error boundary that isolates render errors to the subtree it wraps. It
+ * renders `fallback` when a child throws, optionally injecting an `onReset`
+ * prop into the fallback so "Try Again" buttons can clear the error state.
+ *
+ * When `resetKeys` change between renders, the boundary recovers
+ * automatically (useful for route-based reset).
+ */
export default class ErrorBoundary extends React.Component {
state: ErrorBoundaryState = {
hasError: false,
@@ -19,7 +35,27 @@ export default class ErrorBoundary extends React.Component 0) {
+ const prevKeys = prevProps.resetKeys;
+ if (
+ !prevKeys ||
+ prevKeys.length !== resetKeys.length ||
+ resetKeys.some((key, index) => !Object.is(key, prevKeys[index]))
+ ) {
+ this.setState({ hasError: false });
+ onReset?.();
+ }
+ }
+ }
+
componentDidCatch(error: unknown, errorInfo: React.ErrorInfo) {
+ // Log to Sentry when configured, and always surface the component stack
+ // in the console for dev-mode debugging.
+ console.error('Uncaught error caught by ErrorBoundary', error, errorInfo);
Sentry.captureException(error, {
extra: {
componentStack: errorInfo.componentStack,
@@ -27,11 +63,28 @@ export default class ErrorBoundary extends React.Component {
+ this.setState({ hasError: false });
+ this.props.onReset?.();
+ };
+
render() {
if (this.state.hasError) {
- return this.props.fallback;
+ const { fallback } = this.props;
+ // If the fallback accepts an onReset handler, wire it up so the
+ // "Try Again" button actually clears the error state.
+ if (React.isValidElement(fallback)) {
+ const fallbackProps = fallback.props as { onReset?: unknown };
+ if (typeof fallbackProps.onReset === 'undefined') {
+ return React.cloneElement(
+ fallback as React.ReactElement<{ onReset?: () => void }>,
+ { onReset: this.handleReset },
+ );
+ }
+ }
+ return fallback;
}
return this.props.children;
}
-}
+}
\ No newline at end of file
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index f84f1879..47820627 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -35,7 +35,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
- {}} />}>
+ }>