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,9 @@
* Composes the fail2ban server status bar at the top, a shared time-range
* selector, and the ban list showing aggregate bans from the fail2ban
* database. The time-range selection controls how far back to look.
*
* Sections are wrapped with SectionErrorBoundary to provide graceful
* degradation if individual charts or tables fail to render.
*/
import { Text, makeStyles, tokens } from "@fluentui/react-components";
@@ -14,6 +17,7 @@ import { DashboardFilterBar } from "../components/DashboardFilterBar";
import { ServerStatusBar } from "../components/ServerStatusBar";
import { TopCountriesBarChart } from "../components/TopCountriesBarChart";
import { TopCountriesPieChart } from "../components/TopCountriesPieChart";
import { SectionErrorBoundary } from "../components/SectionErrorBoundary";
import { useCommonSectionStyles } from "../components/commonStyles";
import { useDashboardCountryData } from "../hooks/useDashboardCountryData";
import { DashboardFilterProvider, useDashboardFilters } from "./DashboardFilterProvider";
@@ -69,7 +73,8 @@ const useStyles = makeStyles({
* Main dashboard landing page.
*
* Displays the fail2ban server status, a time-range selector, and the
* ban list table.
* ban list table. Each section is protected with a SectionErrorBoundary
* so that a failure in one section does not crash the entire page.
*/
function DashboardPageContent(): React.JSX.Element {
const styles = useStyles();
@@ -85,7 +90,9 @@ function DashboardPageContent(): React.JSX.Element {
{/* ------------------------------------------------------------------ */}
{/* Server status bar */}
{/* ------------------------------------------------------------------ */}
<ServerStatusBar />
<SectionErrorBoundary sectionName="Server Status">
<ServerStatusBar />
</SectionErrorBoundary>
{/* ------------------------------------------------------------------ */}
{/* Global filter bar */}
@@ -109,11 +116,13 @@ function DashboardPageContent(): React.JSX.Element {
</Text>
</div>
<div className={styles.tabContent}>
<BanTrendChart
timeRange={timeRange}
origin={originFilter}
source={source}
/>
<SectionErrorBoundary sectionName="Ban Trend Chart">
<BanTrendChart
timeRange={timeRange}
origin={originFilter}
source={source}
/>
</SectionErrorBoundary>
</div>
</div>
@@ -127,28 +136,30 @@ function DashboardPageContent(): React.JSX.Element {
</Text>
</div>
<div className={styles.tabContent}>
<ChartStateWrapper
isLoading={countryLoading}
error={countryError}
onRetry={reloadCountry}
isEmpty={!countryLoading && Object.keys(countries).length === 0}
emptyMessage="No ban data for the selected period."
>
<div className={styles.chartsRow}>
<div className={styles.chartCard}>
<TopCountriesPieChart
countries={countries}
countryNames={countryNames}
/>
<SectionErrorBoundary sectionName="Country Charts">
<ChartStateWrapper
isLoading={countryLoading}
error={countryError}
onRetry={reloadCountry}
isEmpty={!countryLoading && Object.keys(countries).length === 0}
emptyMessage="No ban data for the selected period."
>
<div className={styles.chartsRow}>
<div className={styles.chartCard}>
<TopCountriesPieChart
countries={countries}
countryNames={countryNames}
/>
</div>
<div className={styles.chartCard}>
<TopCountriesBarChart
countries={countries}
countryNames={countryNames}
/>
</div>
</div>
<div className={styles.chartCard}>
<TopCountriesBarChart
countries={countries}
countryNames={countryNames}
/>
</div>
</div>
</ChartStateWrapper>
</ChartStateWrapper>
</SectionErrorBoundary>
</div>
</div>
@@ -164,11 +175,13 @@ function DashboardPageContent(): React.JSX.Element {
{/* Ban table */}
<div className={styles.tabContent}>
<BanTable
timeRange={timeRange}
origin={originFilter}
source={source}
/>
<SectionErrorBoundary sectionName="Ban List">
<BanTable
timeRange={timeRange}
origin={originFilter}
source={source}
/>
</SectionErrorBoundary>
</div>
</div>
</div>
@@ -183,3 +196,4 @@ export function DashboardPage(): React.JSX.Element {
);
}