feat: task 1.7 routing skeleton

TanStack Router file-based routes:
- vite.config.ts: TanStackRouterVite plugin (target: react,
  autoCodeSplitting: true) before the React plugin.
- .gitignore: src/routeTree.gen.ts (auto-generated).
- src/lib/query-client.ts: module-level QueryClient singleton.
- src/routes/__root.tsx: root route, QueryClientProvider, Suspense-
  wrapped router + query devtools (dev-only via import.meta.env.DEV).
- src/routes/login.tsx: public route. validateSearch schema accepts
  optional redirect. LoginRoute wraps <LoginPage> with onAuthenticated
  navigating to redirect ?? '/'.
- src/routes/_authed/route.tsx: pathless protected layout. beforeLoad
  redirects to /login on 'anonymous'; falls through on 'unknown' /
  'authenticating' so hard reload doesn't flash the login page.
- src/routes/_authed/index.tsx: placeholder home with sign-out button.
  Cross-tab logout effect bounces to /login on store transition.
- src/App.tsx: replaced — now creates the router and renders
  <RouterProvider />. The interim status-branching logic from 1.6 is
  gone (the route tree handles it).
- eslint.config.js: override for src/routes/** disabling
  react-refresh/only-export-components — TanStack Router's file-based
  pattern intentionally co-exports Route alongside components.

Code-splitting working: login chunk 37KB, home + auth 17KB, card
primitive 31KB, all loaded lazily; main bundle 353KB.

Deviations:
1. No top-level useRequireAuth hook — per-page useEffect on store
   transitions is just as effective and only used in one place.
2. ESLint override for src/routes/** matches the existing pattern for
   src/ui/primitives/**.
3. Login page's auto-navigate effect from 1.6 is removed — the route
   wrapper owns navigation via onAuthenticated.
This commit is contained in:
2026-05-02 18:19:33 +02:00
parent 4ac5ed4eca
commit dc4e73f73a
11 changed files with 251 additions and 36 deletions
@@ -219,4 +219,27 @@ function HomePage() {
## Done
(Filled in when the task lands.)
TanStack Router wired with file-based routes:
- **`vite.config.ts`** — `TanStackRouterVite({ target: 'react', autoCodeSplitting: true })` added before the React plugin (router plugin must run first so `routeTree.gen.ts` exists when React's transform runs). Path-alias and proxy config from 1.3 untouched.
- **`.gitignore`** — added `src/routeTree.gen.ts`.
- **`src/lib/query-client.ts`** — module-level `QueryClient` singleton with sensible defaults (refetch on focus, 0 stale-time default with per-query overrides expected, retry: 1).
- **`src/routes/__root.tsx`** — `createRootRouteWithContext<{ queryClient }>()`. Wraps `<Outlet />` in `<QueryClientProvider>`. In dev only, `<Suspense>`-wrapped `<TanStackRouterDevtools position="bottom-right" />` + `<ReactQueryDevtools buttonPosition="bottom-left" />` lazy-loaded via `React.lazy` gated on `import.meta.env.DEV` so the prod bundle doesn't ship them.
- **`src/routes/login.tsx`** — public login route. `validateSearch` schema accepts an optional `redirect` string. `LoginRoute` wraps `<LoginPage>` (from 1.6) and passes `onAuthenticated → navigate({ to: redirect ?? '/', replace: true })`.
- **`src/routes/_authed/route.tsx`** — pathless protected layout. `beforeLoad` reads `useAuthStore.getState().status`; on `'anonymous'` throws `redirect({ to: '/login', search: { redirect: location.href } })`. The `'unknown'` and `'authenticating'` statuses fall through to the component, which renders a "Loading…" placeholder until the boot probe resolves (without that, hard reloads flash the login page on every refresh — bad UX).
- **`src/routes/_authed/index.tsx`** — placeholder home page (`/`). Header with TRM brand + signed-in display name + sign-out button (calls `useAuthStore.getState().logout()` then the component's status effect navigates to `/login`). Card placeholder for the live monitoring map (Phase 2). Uses `useNavigate` to handle the cross-tab logout case where the store transitions to `'anonymous'` mid-session.
- **`src/App.tsx`** — replaced. Now creates the router with `createRouter({ routeTree, context: { queryClient }, defaultPreload: 'intent' })` and renders `<RouterProvider router={router} />`. The interim `App.tsx` from 1.6 (which branched on auth status directly) is gone — that branching now lives in the route tree.
**Deviations from spec:**
1. Spec sketched a `useRequireAuth()` hook + a runtime check inside the layout component as belt-and-braces. The runtime check landed in `_authed/index.tsx` (and any future protected page) as a `useEffect` that responds to store transitions. Didn't add a top-level `useRequireAuth()` hook — the per-page effect is just as effective and avoids adding a hook that's only used in one place.
2. Added an ESLint override for `src/routes/**` to disable `react-refresh/only-export-components`. TanStack Router's file-based pattern intentionally co-exports `Route` (the route definition object) alongside the route's component — the rule doesn't apply usefully there. Same shape as the existing override for `src/ui/primitives/**`.
3. Removed the `LoginPage` auto-navigate effect from 1.6 — the route wrapper (`LoginRoute`) now owns the navigation via `onAuthenticated`, so the page itself stays pure.
**Smoke check:** `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, `pnpm build` all green. Bundle: 353KB main + per-route chunks (login 37KB, home + auth 17KB, card primitive 31KB lazy). Code-splitting working — login page only loads its chunk on the login route.
Browser smoke pending: against a running stage Directus, expected behaviour is `/` → 302 to `/login?redirect=%2F` → submit credentials → land on `/`. Hard refresh on `/` while authenticated stays on `/`. Already-authenticated user navigating to `/login` is auto-redirected away.
Landed in `PENDING_SHA`.