Improve error boundary granularity with page and section level boundaries

Implement three-level error boundary strategy:
- Top-level (app shell): catches critical failures
- Page-level: preserves navigation when page crashes
- Section-level: graceful degradation for charts/tables

Create new components:
- PageErrorBoundary: wraps page routes
- SectionErrorBoundary: wraps data-heavy sections

Enhance ErrorBoundary with customizable titles, messages, and reload behavior.

Apply page boundaries to all route handlers in App.tsx.

Apply section boundaries to:
- DashboardPage: server status, ban trend, country charts, ban list
- JailsPage: jail overview, ban/unban form, IP lookup
- MapPage: world map, ban table
- ConfigPage: configuration editor
- HistoryPage: history table, IP detail view
- BlocklistsPage: sources, schedule, import log

Update Web-Development.md with error boundary strategy documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-28 08:33:39 +02:00
parent 42beb9cf3b
commit da6433b2cf
12 changed files with 453 additions and 143 deletions

View File

@@ -17,6 +17,11 @@
* - `/history` — event history (protected)
* - `/blocklists` — blocklist management (protected)
* All unmatched paths redirect to `/`.
*
* Error Boundaries:
* - Top-level ErrorBoundary wraps the entire app shell (rare full-page reload).
* - Each page route wrapped in PageErrorBoundary (page fails but nav persists).
* - Risky sections within pages wrapped in SectionErrorBoundary (graceful degradation).
*/
import { lazy, Suspense } from "react";
@@ -29,6 +34,7 @@ import { TimezoneProvider } from "./providers/TimezoneProvider";
import { RequireAuth } from "./components/RequireAuth";
import { SetupGuard } from "./components/SetupGuard";
import { ErrorBoundary } from "./components/ErrorBoundary";
import { PageErrorBoundary } from "./components/PageErrorBoundary";
import { MainLayout } from "./layouts/MainLayout";
const SetupPage = lazy(() => import("./pages/SetupPage").then((m) => ({ default: m.SetupPage })));
@@ -50,21 +56,34 @@ function AppContents(): React.JSX.Element {
return (
<FluentProvider theme={theme}>
<ErrorBoundary>
<ErrorBoundary
title="Application Error"
message="The application encountered a critical error. Reloading may help."
isFullPage={true}
>
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<Suspense fallback={<Spinner size="large" label="Loading…" />}>
<AuthProvider>
<Routes>
{/* Setup wizard — always accessible; redirects to /login if already done */}
<Route path="/setup" element={<SetupPage />} />
<Route
path="/setup"
element={
<PageErrorBoundary pageName="Setup">
<SetupPage />
</PageErrorBoundary>
}
/>
{/* Login — requires setup to be complete */}
<Route
path="/login"
element={
<SetupGuard>
<LoginPage />
</SetupGuard>
<PageErrorBoundary pageName="Login">
<SetupGuard>
<LoginPage />
</SetupGuard>
</PageErrorBoundary>
}
/>
@@ -80,13 +99,62 @@ function AppContents(): React.JSX.Element {
</SetupGuard>
}
>
<Route index element={<DashboardPage />} />
<Route path="/map" element={<MapPage />} />
<Route path="/jails" element={<JailsPage />} />
<Route path="/jails/:name" element={<JailDetailPage />} />
<Route path="/config" element={<ConfigPage />} />
<Route path="/history" element={<HistoryPage />} />
<Route path="/blocklists" element={<BlocklistsPage />} />
<Route
index
element={
<PageErrorBoundary pageName="Dashboard">
<DashboardPage />
</PageErrorBoundary>
}
/>
<Route
path="/map"
element={
<PageErrorBoundary pageName="Map">
<MapPage />
</PageErrorBoundary>
}
/>
<Route
path="/jails"
element={
<PageErrorBoundary pageName="Jails">
<JailsPage />
</PageErrorBoundary>
}
/>
<Route
path="/jails/:name"
element={
<PageErrorBoundary pageName="Jail Details">
<JailDetailPage />
</PageErrorBoundary>
}
/>
<Route
path="/config"
element={
<PageErrorBoundary pageName="Configuration">
<ConfigPage />
</PageErrorBoundary>
}
/>
<Route
path="/history"
element={
<PageErrorBoundary pageName="History">
<HistoryPage />
</PageErrorBoundary>
}
/>
<Route
path="/blocklists"
element={
<PageErrorBoundary pageName="Blocklists">
<BlocklistsPage />
</PageErrorBoundary>
}
/>
</Route>
{/* Fallback — redirect unknown paths to dashboard */}