feat: centralized error notification service (issue #15)

- Create NotificationService with context provider for centralized error/success messaging
- Add NotificationContainer component to render notification stack
- Integrate NotificationProvider into App root
- Refactor BanUnbanForm to use notification service instead of local error state
- Update fetchError utility to optionally use notification callbacks
- Add comprehensive error handling guidelines to Web-Development.md
- Prevent duplicate notifications with deduplication logic
- Support auto-dismiss with configurable TTL per notification type

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-28 08:41:33 +02:00
parent da6433b2cf
commit ae34d98859
8 changed files with 557 additions and 152 deletions

View File

@@ -31,10 +31,12 @@ import { darkTheme, lightTheme } from "./theme/customTheme";
import { AuthProvider } from "./providers/AuthProvider";
import { ThemeProvider, useThemeMode } from "./providers/ThemeProvider";
import { TimezoneProvider } from "./providers/TimezoneProvider";
import { NotificationProvider } from "./services/notificationService";
import { RequireAuth } from "./components/RequireAuth";
import { SetupGuard } from "./components/SetupGuard";
import { ErrorBoundary } from "./components/ErrorBoundary";
import { PageErrorBoundary } from "./components/PageErrorBoundary";
import { NotificationContainer } from "./components/NotificationContainer";
import { MainLayout } from "./layouts/MainLayout";
const SetupPage = lazy(() => import("./pages/SetupPage").then((m) => ({ default: m.SetupPage })));
@@ -56,114 +58,117 @@ function AppContents(): React.JSX.Element {
return (
<FluentProvider theme={theme}>
<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={
<PageErrorBoundary pageName="Setup">
<SetupPage />
</PageErrorBoundary>
}
/>
<NotificationProvider>
<ErrorBoundary
title="Application Error"
message="The application encountered a critical error. Reloading may help."
isFullPage={true}
>
<NotificationContainer />
<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={
<PageErrorBoundary pageName="Setup">
<SetupPage />
</PageErrorBoundary>
}
/>
{/* Login — requires setup to be complete */}
<Route
path="/login"
element={
<PageErrorBoundary pageName="Login">
{/* Login — requires setup to be complete */}
<Route
path="/login"
element={
<PageErrorBoundary pageName="Login">
<SetupGuard>
<LoginPage />
</SetupGuard>
</PageErrorBoundary>
}
/>
{/* Protected routes — require setup AND authentication */}
<Route
element={
<SetupGuard>
<LoginPage />
<RequireAuth>
<TimezoneProvider>
<MainLayout />
</TimezoneProvider>
</RequireAuth>
</SetupGuard>
</PageErrorBoundary>
}
/>
}
>
<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>
{/* Protected routes — require setup AND authentication */}
<Route
element={
<SetupGuard>
<RequireAuth>
<TimezoneProvider>
<MainLayout />
</TimezoneProvider>
</RequireAuth>
</SetupGuard>
}
>
<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 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</AuthProvider>
</Suspense>
</BrowserRouter>
</ErrorBoundary>
{/* Fallback — redirect unknown paths to dashboard */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</AuthProvider>
</Suspense>
</BrowserRouter>
</ErrorBoundary>
</NotificationProvider>
</FluentProvider>
);
}