/**
* 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) */}
}
>
} />
} />
} />
} />
)
}