- pnpm add maplibre-gl + -D @types/geojson.
- src/map/core/styles.ts: defaultStyle (OSM raster bootstrap; 2.2
replaces with the basemap-switcher descriptor table).
- src/map/core/map-view.tsx: module-level Map singleton lazily created
on first <MapView> mount, attached to a class="trm-map-host" detached
<div> that React refs append/remove on mount/unmount. Style-data
lifecycle flips mapReady false on every styledata event, polls
loaded() at 33ms intervals, flips ready true once the style is
loaded — the canonical MapLibre style-swap dance.
- Exports getMap()/getMapReady()/subscribeMapReady()/useMapReady (via
useSyncExternalStore for SSR-safe + concurrent-safe reads). getMap()
throws if called pre-mount; the explicit failure mode beats a
null-able top-level export.
- src/routes/_authed/monitor.tsx: new /monitor route, full-viewport
<MapView /> for 2.1 (no children — subsequent tasks plug in here).
- src/routes/_authed/index.tsx: home-page card now links to /monitor.
- eslint.config.js: override for src/map/** + src/live/** disables
react-refresh/only-export-components. Same pattern as the existing
overrides for shadcn primitives and route files.
Deviation: spec sketched a top-level `map` constant export; implemented
as `getMap(): MapLibreMap` (a function) so the singleton stays lazy
until <MapView> mounts. Top-level constant would either force eager
init (breaks SSR/tests) or be nullable (footgun). The function form
throws a clear error if called pre-mount.
Bundle: /monitor lazy chunk is 1MB raw / 274KB gzipped (MapLibre + CSS).
Other routes unaffected. Vite chunk-size warning is harmless.
CI runners configure pnpm with --store-dir=.pnpm-store (project-local
CAS) for cross-run caching. Prettier and ESLint were scanning into it
and finding 333 unformatted JSON files.
Added to .prettierignore and eslint.config.js globalIgnores:
- .pnpm-store / .pnpm — pnpm content-addressable store variants
- coverage — future test runs (Vitest in Phase 3)
- .cache — generic build cache directory
Local Windows trees aren't affected (pnpm uses the global store at
~/.local/share/pnpm/store) but the rules cover both paths.
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.