/** * App — top-level provider stack and route tree. * * Provider order (outermost first): * MantineProvider → QueryClientProvider → BrowserRouter → SessionProvider → routes * * Route tree: * /login → LoginPage (public) * /change-password → ProtectedRoute → ChangePasswordPage (forced password change gate) * / → ProtectedRoute → AppLayout → HomePage * /config → ProtectedRoute → AppLayout → ConfigPage * /records → ProtectedRoute → AppLayout → RecordsPage * /energy → ProtectedRoute → AppLayout → EnergyPage * * AppLayout renders a sidebar (AppSidebar) on the left; page content via on the right. * /login and /change-password have no sidebar layout. */ import { useState } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom' import { MantineProvider, AppShell, Burger } from '@mantine/core' // Mantine requires its CSS to be imported once. import '@mantine/core/styles.css' import { SessionProvider } from './auth/SessionProvider' import { ProtectedRoute } from './auth/ProtectedRoute' import { LoginPage } from './pages/LoginPage' import { HomePage } from './pages/HomePage' import { ConfigPage } from './pages/ConfigPage' import { RecordsPage } from './pages/RecordsPage' import { EnergyPage } from './pages/EnergyPage' import { ChangePasswordPage } from './pages/ChangePasswordPage' import { AppSidebar } from './components/AppSidebar' // --------------------------------------------------------------------------- // TanStack Query client (singleton, created outside render to avoid re-creation) // --------------------------------------------------------------------------- const queryClient = new QueryClient({ defaultOptions: { queries: { // Don't retry on 4xx — we handle 401 in the middleware retry: (failureCount, error) => { if (error instanceof Error && 'status' in error) { const status = (error as unknown as { status: number }).status if (status >= 400 && status < 500) return false } return failureCount < 2 }, }, }, }) // --------------------------------------------------------------------------- // App shell layout (used by all protected pages) // --------------------------------------------------------------------------- function AppLayout() { // Mobile navbar open/close state — controlled here so the Burger and // AppShell share the same state. const [navbarOpen, setNavbarOpen] = useState(false) return ( {/* Burger shown on mobile only (hidden on sm+) */} setNavbarOpen((o) => !o)} size="sm" aria-label="Toggle navigation" data-testid="nav-burger" /> setNavbarOpen(false)} /> ) } // --------------------------------------------------------------------------- // Root app // --------------------------------------------------------------------------- export default function App() { return ( {/* Public routes */} } /> {/* Forced password change — protected (must be logged in) but outside AppLayout */} } /> {/* Protected routes — all nested under AppLayout (sidebar) */} } > } /> } /> } /> } /> ) }