Adds a navigation-aware request cancellation mechanism that automatically aborts all route-specific API requests when the user navigates to a different route. This prevents silent state-update errors from responses arriving after component unmount and conserves bandwidth by cancelling now-irrelevant requests. Key additions: - NavigationCancellationContext: Context for managing route-specific signals - NavigationCancellationProvider: Provider that detects route changes and aborts all signals from the previous route - useNavigationAbortSignal hook: Allows components to subscribe to navigation-aware cancellation signals - Comprehensive tests for the cancellation lifecycle - Documentation in Web-Development.md for request lifecycle policy The provider is placed in the app hierarchy between BrowserRouter and AuthProvider, ensuring consistent cancellation behavior across all routes. Long-lived background tasks (polling, session validation) can opt-out by managing their own AbortController lifecycle. Closes #23 from Tasks.md: No global cancellation policy on route transitions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
217 lines
9.8 KiB
TypeScript
217 lines
9.8 KiB
TypeScript
/**
|
|
* Application root component.
|
|
*
|
|
* Provider order (see `src/providers/PROVIDER_ORDER.md` for detailed contract):
|
|
* 1. `ThemeProvider` — OUTERMOST; provides theme context to AppContents
|
|
* 2. `FluentProvider` — supplies Fluent UI theme and design tokens
|
|
* 3. `NotificationProvider` — provides notification service to all descendants
|
|
* 4. `ErrorBoundary` — catches catastrophic errors
|
|
* 5. `BrowserRouter` — enables client-side routing via React Router
|
|
* 6. `NavigationCancellationProvider` — manages route-aware request cancellation
|
|
* 7. `AuthProvider` — manages session state; validates on mount; uses useNavigate()
|
|
* 8. `TimezoneProvider` — INNERMOST (inside protected routes); fetches timezone after auth
|
|
*
|
|
* CRITICAL: Provider order is order-sensitive. See PROVIDER_ORDER.md before refactoring.
|
|
*
|
|
* Routes:
|
|
* - `/setup` — first-run setup wizard (always accessible; redirects to /login if already done)
|
|
* - `/login` — master password login (redirects to /setup if not done)
|
|
* - `/` — dashboard (protected, inside MainLayout)
|
|
* - `/map` — world map (protected)
|
|
* - `/jails` — jail list (protected)
|
|
* - `/jails/:name` — jail detail (protected)
|
|
* - `/config` — configuration editor (protected)
|
|
* - `/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";
|
|
import { FluentProvider, Spinner } from "@fluentui/react-components";
|
|
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
|
import { darkTheme, lightTheme } from "./theme/customTheme";
|
|
import { AuthProvider } from "./providers/AuthProvider";
|
|
import { ThemeProvider, useThemeMode } from "./providers/ThemeProvider";
|
|
import { TimezoneProvider } from "./providers/TimezoneProvider";
|
|
import { NavigationCancellationProvider } from "./providers/NavigationCancellationProvider";
|
|
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";
|
|
import { injectSkeletonStyles } from "./utils/skeletonStyles";
|
|
|
|
const SetupPage = lazy(() => import("./pages/SetupPage").then((m) => ({ default: m.SetupPage })));
|
|
const LoginPage = lazy(() => import("./pages/LoginPage").then((m) => ({ default: m.LoginPage })));
|
|
const DashboardPage = lazy(() => import("./pages/DashboardPage").then((m) => ({ default: m.DashboardPage })));
|
|
const MapPage = lazy(() => import("./pages/MapPage").then((m) => ({ default: m.MapPage })));
|
|
const JailsPage = lazy(() => import("./pages/JailsPage").then((m) => ({ default: m.JailsPage })));
|
|
const JailDetailPage = lazy(() => import("./pages/JailDetailPage").then((m) => ({ default: m.JailDetailPage })));
|
|
const ConfigPage = lazy(() => import("./pages/ConfigPage").then((m) => ({ default: m.ConfigPage })));
|
|
const HistoryPage = lazy(() => import("./pages/HistoryPage").then((m) => ({ default: m.HistoryPage })));
|
|
const BlocklistsPage = lazy(() => import("./pages/BlocklistsPage").then((m) => ({ default: m.BlocklistsPage })));
|
|
|
|
/**
|
|
* Root application component — mounts providers and top-level routes.
|
|
*
|
|
* Provider stack (see PROVIDER_ORDER.md for detailed contract):
|
|
* - FluentProvider (2) — receives theme from useThemeMode()
|
|
* - NotificationProvider (3) — provides notification service
|
|
* - ErrorBoundary (4) — catches catastrophic errors
|
|
* - BrowserRouter (5) — enables routing
|
|
* - NavigationCancellationProvider (6) — manages route-aware request cancellation
|
|
* - AuthProvider (7) — session validation; uses useNavigate()
|
|
* - TimezoneProvider (8) — inside protected routes only
|
|
*/
|
|
function AppContents(): React.JSX.Element {
|
|
const { colorMode } = useThemeMode();
|
|
const theme = colorMode === "dark" ? darkTheme : lightTheme;
|
|
|
|
// Inject skeleton animation styles once at app startup
|
|
injectSkeletonStyles();
|
|
|
|
return (
|
|
// 2. FluentProvider — supplies Fluent UI theme and tokens
|
|
<FluentProvider theme={theme}>
|
|
{/* 3. NotificationProvider — makes notification service available */}
|
|
<NotificationProvider>
|
|
{/* 4. ErrorBoundary — catches catastrophic errors that would crash the app */}
|
|
<ErrorBoundary
|
|
title="Application Error"
|
|
message="The application encountered a critical error. Reloading may help."
|
|
isFullPage={true}
|
|
>
|
|
{/* Notification container must be rendered inside ErrorBoundary */}
|
|
<NotificationContainer />
|
|
{/* 5. BrowserRouter — enables routing; required by AuthProvider's useNavigate() */}
|
|
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
|
{/* 6. NavigationCancellationProvider — manages route-aware request cancellation */}
|
|
<NavigationCancellationProvider>
|
|
<Suspense fallback={<Spinner size="large" label="Loading…" />}>
|
|
{/* 7. AuthProvider — validates session on mount; must be inside BrowserRouter */}
|
|
<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">
|
|
<SetupGuard>
|
|
<LoginPage />
|
|
</SetupGuard>
|
|
</PageErrorBoundary>
|
|
}
|
|
/>
|
|
|
|
{/* Protected routes — require setup AND authentication */}
|
|
<Route
|
|
element={
|
|
<SetupGuard>
|
|
<RequireAuth>
|
|
{/* 7. TimezoneProvider — INNERMOST; fetches timezone after auth validation */}
|
|
<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>
|
|
</NavigationCancellationProvider>
|
|
</BrowserRouter>
|
|
</ErrorBoundary>
|
|
</NotificationProvider>
|
|
</FluentProvider>
|
|
);
|
|
}
|
|
|
|
function App(): React.JSX.Element {
|
|
// ThemeProvider (1. OUTERMOST) — provides theme context needed by AppContents
|
|
return (
|
|
<ThemeProvider>
|
|
<AppContents />
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
export default App;
|