Files
BanGUI/frontend/src/App.tsx
Lukas c8b48b5b65 fix(api): correlation ID survives HMR; fix endpoint template literal typos
- client.ts: store correlation ID in sessionStorage so HMR (module re-eval)
  does not generate a new ID mid-session; add clearSessionCorrelationId()
- endpoints.ts: fix 3 template literal trailing-quote bugs (missing ')' chars);
  replace template literals with string concat for encodeURIComponent calls
- AuthProvider.tsx: call clearSessionCorrelationId() on logout
- App.tsx: reorder ThemeProvider import before AuthProvider per PROVIDER_ORDER.md;
  indent Routes inside AuthProvider to match expected tree structure
- Tasks.md: update task status
- providerTreeOrder.test.tsx: add integration tests for provider nesting order

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-03 23:35:18 +02:00

223 lines
10 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, useEffect } 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 { ThemeProvider, useThemeMode } from "./providers/ThemeProvider";
import { AuthProvider } from "./providers/AuthProvider";
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";
import { initializeWebVitals } from "./utils/metrics";
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();
// Initialize web vitals tracking on component mount
useEffect(() => {
initializeWebVitals();
}, []);
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>
{/* 8. 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;