Foundations for the sprint 1 backend wiring. No UI behavior change beyond
the loading state in AppShell, but everything below the wire is now real:
- vite.config.ts adds `server.proxy['/api']` → http://localhost:5000
(overridable via VITE_DEV_API_TARGET). In prod Caddy routes /api → backend
on the same origin, so the same `/api/v1/...` paths work without changes.
- src/types/api.ts hand-rolled against the backend Pydantic schemas.
User / Engagement / EngagementCreate / Login / ApiError / ApiValidationError.
Should be regenerated from OpenAPI once backend exposes it.
- src/lib/api.ts: thin fetch wrapper. Always credentials:'include' so the
HttpOnly session cookie travels. 4xx/5xx normalize into ApiClientError
with typed `body` (ApiError | ApiValidationError | null). No retry loop —
that's TanStack Query's policy.
- src/session/sessionApi.ts: 1:1 functions for /auth/me, /auth/login,
/auth/logout. fetchMe maps 401 → null so "unauthenticated" is data, not
an error.
- src/session/useSession.ts: now a TanStack Query hook against
SESSION_QUERY_KEY (`['session']`). Returns { user, isLoading, isError,
signOut, isSigningOut }. Cookie is the source of truth, server is the
resolver, query is the cache.
- Drop sessionStorage mock layer entirely: src/mocks/session.ts,
src/session/SessionContext.{tsx,context.ts}, src/routing/Root.tsx all
removed. No more provider tree — QueryClientProvider in App.tsx is the
only global state container.
- AppShell renders a "resolving session" state during /auth/me's first
flight so users with a valid cookie don't see a /login flash on direct
navigation to a protected URL.
- StatusRail gains an optional `sessionState="resolving"` slot used by
the loading shell.
- Sidebar's Sign-out wires POST /auth/logout, invalidates the session
cache, and always navigates to /login regardless of the call outcome
(a failed logout still expires the local cache so users aren't stuck
on a broken cookie).
- types/roles.ts loses SessionUser (replaced by api.ts User which is the
authoritative shape).
M1 — Single SessionProvider via nested router.
The previous router had two route entries with `path: '/'`
(Navigate, AppShell) plus a separate `/login` entry, each wrapped in
its own RootLayout. That instantiated SessionProvider three times,
forking state the moment session writes diverged across siblings.
Replaced by one Root route with SessionProvider + <Outlet />, and
index/login/AppShell-children nested underneath. RootLayout (the
per-tree wrapper) is now obsolete and deleted; the new Root component
lives in src/routing/Root.tsx (addresses NIT N4 as a side effect).
M2 — Typo: "pollign" → "polling" in LiveCockpitPage masthead.
M3 — Replace asymmetric `?? 'rt_operator'` / `?? 'soc_analyst'`
fallbacks in LiveCockpitPage with an explicit `if (!user) return null;`
guard placed after all hooks (rules-of-hooks). AppShell already
redirects unauthenticated visitors to /login, so the guard documents
the invariant rather than introducing one.
NITs N1-N3, N5-N7 recorded in tasks/todo.md as sprint 1+ follow-ups.
- Role enum (rt_operator, rt_lead, soc_analyst) aligned with spec §3 / F11.
Frontend predicates (isRT, isLead, isSOC) drive layout only — backend
remains the source of truth for permissions (D-008).
- SessionContext split into Provider (TSX) and hook (useSession) to satisfy
react-refresh/only-export-components.
- AppShell composes StatusRail (link health, active run, UTC clock, build) +
Sidebar (role-conditional nav with keyboard shortcut hints) + Outlet.
Unauthenticated visitors redirect to /login.
- StatusRail uses pulsing status-dot pattern and label-system micro-typo
(uppercase 10px, 0.08em tracking) to evoke an instrument-panel header.
- Router (createBrowserRouter): /login outside the shell, all app routes
nested inside the shell. RootLayout extracted to its own file for
fast-refresh compliance.
Routes (sprint 0, flat):
/login LoginPage
/engagements EngagementsPage
/library TtpLibraryPage (RT only — gated client-side, will
be re-enforced by backend RBAC)
/scenarios ScenarioComposerPage (RT only)
/runs LiveCockpitPage
/reports ReportPage
/audit AuditPage (lead RT only)
Sub-routes under /engagements/:eid land in sprint 1+ when real scoping
arrives.