diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index f3a7754..14acf40 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -1,5 +1,5 @@ import { createBrowserRouter, Navigate } from 'react-router-dom'; -import { RootLayout } from '@/router/RootLayout'; +import { Root } from '@/routing/Root'; import { AppShell } from '@/components/shell/AppShell'; import { LoginPage } from '@/screens/login/LoginPage'; import { EngagementsPage } from '@/screens/engagements/EngagementsPage'; @@ -11,48 +11,34 @@ import { AuditPage } from '@/screens/audit/AuditPage'; /** * Routes mirror spec §9 (UI Web) with sprint 0 placeholders. - * Once real engagement scoping lands, sub-routes nest under /engagements/:eid. - * Sprint 0 keeps the URLs flat so the wireframes are reachable directly. + * + * The Root route mounts SessionProvider once for the entire tree. All + * top-level paths (login, app shell, fallback) are children of that + * single Root so they share one session state — no provider forking + * between routes. + * + * Once real engagement scoping lands, sub-routes nest under + * /engagements/:eid (spec §9). Sprint 0 keeps URLs flat so the + * wireframes are reachable directly. */ export const router = createBrowserRouter([ { - path: '/', - element: ( - - - - ), - }, - { - path: '/login', - element: ( - - - - ), - }, - { - path: '/', - element: ( - - - - ), + element: , children: [ - { path: 'engagements', element: }, - { path: 'library', element: }, - { path: 'scenarios', element: }, - { path: 'runs', element: }, - { path: 'reports', element: }, - { path: 'audit', element: }, + { index: true, element: }, + { path: 'login', element: }, + { + element: , + children: [ + { path: 'engagements', element: }, + { path: 'library', element: }, + { path: 'scenarios', element: }, + { path: 'runs', element: }, + { path: 'reports', element: }, + { path: 'audit', element: }, + ], + }, + { path: '*', element: }, ], }, - { - path: '*', - element: ( - - - - ), - }, ]); diff --git a/frontend/src/router/RootLayout.tsx b/frontend/src/router/RootLayout.tsx deleted file mode 100644 index bca8088..0000000 --- a/frontend/src/router/RootLayout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type { ReactNode } from 'react'; -import { SessionProvider } from '@/session/SessionContext'; - -/** - * Wraps every top-level route with the session provider. Kept in its own - * file so the router config can stay export-pure (fast-refresh friendly). - */ -export function RootLayout({ children }: { children: ReactNode }) { - return {children}; -} diff --git a/frontend/src/routing/Root.tsx b/frontend/src/routing/Root.tsx new file mode 100644 index 0000000..ec978f8 --- /dev/null +++ b/frontend/src/routing/Root.tsx @@ -0,0 +1,16 @@ +import { Outlet } from 'react-router-dom'; +import { SessionProvider } from '@/session/SessionContext'; + +/** + * Root route element. Mounts SessionProvider once for the entire app so + * every nested route — login, app shell, fallback — shares one session + * state. Kept in its own file so router.tsx exports only the router + * config (fast-refresh friendly). + */ +export function Root() { + return ( + + + + ); +} diff --git a/frontend/src/screens/cockpit/LiveCockpitPage.tsx b/frontend/src/screens/cockpit/LiveCockpitPage.tsx index 8d987e6..9886469 100644 --- a/frontend/src/screens/cockpit/LiveCockpitPage.tsx +++ b/frontend/src/screens/cockpit/LiveCockpitPage.tsx @@ -46,6 +46,11 @@ export function LiveCockpitPage() { return { total, done, detected, missed, partial }; }, [steps]); + // AppShell guarantees a session before this screen mounts; the early + // return keeps that invariant explicit and removes ambiguous fallbacks + // when reading user.role below. Placed after all hooks per rules-of-hooks. + if (!user) return null; + return (
{/* Run header + control bar */} @@ -54,7 +59,7 @@ export function LiveCockpitPage() { style={{ borderColor: 'var(--line-default)', backgroundColor: 'var(--surface-1)' }} >
-
// Live run · pollign 500 ms
+
// Live run · polling 500 ms

RUN-2026-05-21-A03 · Démo Client X

@@ -74,7 +79,7 @@ export function LiveCockpitPage() {
- {isLead(user?.role ?? 'rt_operator') && ( + {isLead(user.role) && (