julian dc4e73f73a 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.
2026-05-02 18:45:55 +02:00
2026-05-02 18:45:55 +02:00
2026-05-02 18:45:55 +02:00
2026-05-02 18:45:55 +02:00
2026-05-02 16:56:15 +02:00
2026-05-02 16:53:12 +02:00
2026-05-02 18:45:55 +02:00

trm/spa

End-user single-page app for the TRM platform: race directors, marshals, timekeepers, and (post-Phase 4 of trm/directus) public-facing participants. Talks to trm/directus for REST/GraphQL + business-plane WebSocket and to trm/processor for the live-position WebSocket firehose.

Architectural anchors live in trm/docs/wiki/:

  • wiki/entities/react-spa.md — this service.
  • wiki/concepts/maps-architecture.md — map subsystem (Phase 2).
  • wiki/synthesis/processor-ws-contract.md — live-position wire spec (Phase 2).
  • wiki/synthesis/directus-schema-draft.md — schema this consumes.

Implementation planning lives in .planning/ — see .planning/ROADMAP.md.

Stack

  • Vite 8 + React 19 + TypeScript 6 — SPA build, no SSR.
  • Tailwind CSS 4 via @tailwindcss/vite.
  • shadcn/ui primitives (slate base, new-york style) — copy-in components owned by us, in src/ui/primitives/.
  • TanStack Router + Query — file-based routes, server-state cache.
  • Zustand — high-frequency client state (auth, live positions in Phase 2).
  • @directus/sdk — typed Directus client (REST + WS).
  • zod — runtime validation (config, forms, WS protocol).
  • react-hook-form + @hookform/resolvers — forms.

Common scripts

Command Purpose
pnpm dev Start the Vite dev server on :5173.
pnpm build Type-check + production build into dist/.
pnpm preview Serve the built dist/ for smoke testing.
pnpm typecheck Type-check only (no build).
pnpm lint ESLint over the repo.
pnpm format Prettier auto-format.
pnpm format:check Prettier check only (CI gate).
pnpm test Test runner (Phase 3 wires Vitest).

Adding a shadcn primitive

pnpm dlx shadcn@latest add <component>

Components land in src/ui/primitives/ per components.json's alias config. Edit them freely — they're our code from the moment they land.

Local dev

The Vite dev server (pnpm dev on :5173) proxies three path namespaces to local services so everything runs same-origin — the same topology stage/prod gets via Traefik, no CORS workarounds needed.

Path Forwards to Notes
/api/* ${VITE_DEV_DIRECTUS_URL}/* Directus REST + GraphQL. Default http://localhost:8055.
/ws-business ${VITE_DEV_DIRECTUS_URL}/websocket Directus business-plane WebSocket (auto-derived ws:// scheme).
/ws-live ${VITE_DEV_PROCESSOR_WS_URL}/ Processor live-position WebSocket. Default ws://localhost:8081 (Phase 1.5).

Override per-developer by copying .env.example to .env.local and editing — Vite auto-loads .env.local in any mode and the values flow into vite.config.ts via loadEnv.

If the dev port 5173 collides with another Vite app, run with VITE_PORT=5174 pnpm dev (or pin a different port in vite.config.ts).

Runtime config

/config.json is fetched at app boot, validated with zod, and held in a React context (useRuntimeConfig() hook). One image, multiple environments — the deploy stack overrides the file via a Docker volume mount; the SPA never rebuilds for an env-only change.

Field Type Notes
directusUrl URL/path Directus REST + GraphQL base. Same-origin path or absolute URL.
liveWsUrl URL/path Processor live-position WebSocket URL.
businessWsUrl URL/path Directus business-plane WebSocket URL.
googleMapsKey string? Optional. If absent, Google tile sources are hidden in the UI.
env enum dev / stage / prod — used in log labels and feature-flag conditionals.

URLs may be absolute (http(s)://, ws(s)://) or absolute paths starting with /. Relative paths work because the SPA is always same-origin to its backends. The committed public/config.json uses path-only defaults that match the dev proxy.

In stage/prod the deploy stack mounts an override file at /usr/share/nginx/html/config.json; see trm/deploy/README.md (lands in Phase 1.10).

CI

.gitea/workflows/build.yml (lands in Phase 1.9) runs the four gates — typecheck, lint, format:check, build — and publishes a Docker image on push to main.

S
Description
No description provided
Readme 701 KiB
Languages
TypeScript 49.1%
HTML 24.7%
JavaScript 20.3%
CSS 5.6%
Dockerfile 0.3%