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

@@ -4,6 +4,8 @@
* Shows a paginated, filterable table of every ban ever recorded in the
* fail2ban database. Clicking an IP address opens a per-IP timeline view.
* Rows with repeatedly-banned IPs are highlighted in amber.
*
* The history table is wrapped with SectionErrorBoundary for resilience.
*/
import { useCallback, useEffect, useMemo, useState } from "react";
@@ -30,6 +32,7 @@ import {
ChevronRightRegular,
} from "@fluentui/react-icons";
import { DashboardFilterBar } from "../components/DashboardFilterBar";
import { SectionErrorBoundary } from "../components/SectionErrorBoundary";
import { useHistory } from "../hooks/useHistory";
import { IpDetailView } from "./history/IpDetailView";
import { HISTORY_PAGE_SIZE } from "../utils/constants";
@@ -236,12 +239,14 @@ export function HistoryPage(): React.JSX.Element {
if (selectedIp !== null) {
return (
<div className={styles.root}>
<IpDetailView
ip={selectedIp}
onBack={(): void => {
setSelectedIp(null);
}}
/>
<SectionErrorBoundary sectionName="IP Detail View">
<IpDetailView
ip={selectedIp}
onBack={(): void => {
setSelectedIp(null);
}}
/>
</SectionErrorBoundary>
</div>
);
}
@@ -306,38 +311,40 @@ export function HistoryPage(): React.JSX.Element {
{/* DataGrid table */}
{/* ---------------------------------------------------------------- */}
{!loading && !error && (
<div className={styles.tableWrapper}>
<DataGrid
items={items}
columns={columns}
getRowId={(item: HistoryBanItem) => `${item.ip}-${item.banned_at}`}
focusMode="composite"
>
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell }) => (
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
)}
</DataGridRow>
</DataGridHeader>
<DataGridBody<HistoryBanItem>>
{({ item }) => (
<DataGridRow<HistoryBanItem>
key={`${item.ip}-${item.banned_at}`}
className={
item.ban_count >= HIGH_BAN_THRESHOLD
? styles.highBanRow
: undefined
}
>
{({ renderCell }) => (
<DataGridCell>{renderCell(item)}</DataGridCell>
<SectionErrorBoundary sectionName="History Table">
<div className={styles.tableWrapper}>
<DataGrid
items={items}
columns={columns}
getRowId={(item: HistoryBanItem) => `${item.ip}-${item.banned_at}`}
focusMode="composite"
>
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell }) => (
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
)}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>
</div>
</DataGridHeader>
<DataGridBody<HistoryBanItem>>
{({ item }) => (
<DataGridRow<HistoryBanItem>
key={`${item.ip}-${item.banned_at}`}
className={
item.ban_count >= HIGH_BAN_THRESHOLD
? styles.highBanRow
: undefined
}
>
{({ renderCell }) => (
<DataGridCell>{renderCell(item)}</DataGridCell>
)}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>
</div>
</SectionErrorBoundary>
)}
{/* ---------------------------------------------------------------- */}