Files
spa/.planning/ROADMAP.md
T
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

7.9 KiB

spa — Roadmap

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

This file is the single navigation hub for all SPA implementation planning. Each phase has its own folder with a README and granular task files. Update statuses here as work lands.

Status legend

Symbol Meaning
Not started
🟦 Planned (designed, not coded)
🟨 In progress
🟩 Done
Paused / blocked
❄️ Frozen / future / optional

Architectural anchors

The SPA is specified by the wiki at ../docs/wiki/. Implementing agents should read these pages before starting any task:

  • This servicedocs/wiki/entities/react-spa.md
  • Map subsystemdocs/wiki/concepts/maps-architecture.md, docs/wiki/sources/traccar-maps-architecture.md
  • Live channeldocs/wiki/concepts/live-channel-architecture.md, docs/wiki/synthesis/processor-ws-contract.md
  • Auth + business planedocs/wiki/entities/directus.md, docs/wiki/synthesis/directus-schema-draft.md
  • System architecturedocs/wiki/sources/gps-tracking-architecture.md, docs/wiki/concepts/plane-separation.md

Non-negotiable design rules

These rules govern every task. Any deviation must be discussed and documented as a decision before code lands.

  1. Singleton MapLibre. A single maplibregl.Map instance lives in a module-level variable, attached to a detached <div>. React mounts/unmounts the div via refs across page navigation. WebGL context never recreates. Pattern is documented in docs/wiki/concepts/maps-architecture.md.
  2. Side-effect-only Map* components. Every map-participating component returns null and uses useEffect for setup + cleanup, with a separate effect for setData updates. No DOM marker components, no react-map-gl.
  3. rAF coalescer at the WS boundary. Position messages buffer per-device; one requestAnimationFrame tick flushes the latest snapshot to the Zustand store. Per-message dispatch is the failure mode traccar-web exhibits — we don't replicate it.
  4. Same-origin everything. SPA bundle, directus REST/WS, and processor WS all served under one origin via reverse proxy. Vite dev proxy in dev; Traefik in stage/prod. Cross-origin breaks the cookie auth flow.
  5. In-memory access token, httpOnly refresh cookie. Access token never touches localStorage. Refresh cookie is httpOnly + Secure + SameSite=Lax. Refresh on 401 via Directus's /auth/refresh.
  6. Role-aware UI shape from day one. Even though everyone's admin until directus Phase 4, the route guards and conditional rendering check user.role rather than hard-coding "everyone sees everything." Retrofitting is painful; baking it in costs nothing.
  7. Runtime config, not build-time. Per-environment values (Directus URL, processor WS URL, optional Google Maps key) come from /config.json served by the proxy. One image, multiple environments. No rebuild to switch envs.
  8. Native PostGIS GeoJSON. Geometry round-trips through ST_AsGeoJSON(geometry) on the server, not WKT. The SPA never imports wellknown or runs WKT parsing in the client.

Phases

Phase 1 — Foundation

Status: 🟨 In progress (1.1 done) Outcome: A deployable empty-shell SPA: scaffold + stack rounded out + Directus cookie auth flow + protected routes + Gitea CI + compose deploy block. End state: an operator can browse to https://stage.trmtracking.org, log in with a Directus credential, see a placeholder home page, log out. No live map yet — that's Phase 2.

See phase-1-foundation/README.md

# Task Status Landed in
1.1 Project scaffold (Vite + React + TS) 🟩 (manual)
1.2 Stack rounding-out (Tailwind + shadcn/ui + deps + Prettier) 🟩 9918418
1.3 Vite dev proxy + path aliases + tsconfig hardening 🟩 39b60c9
1.4 Runtime config endpoint 🟩 8e2151a
1.5 Directus auth client (cookie mode + refresh) 🟩 38fe2e3
1.6 Login page 🟩 7215cb5
1.7 Routing skeleton (TanStack Router + role-aware guards) 🟩 PENDING_SHA
1.8 Logout flow
1.9 Gitea CI + Dockerfile + nginx static serve
1.10 Compose service block in trm/deploy

Phase 2 — Live monitoring map

Status: Not started — depends on processor Phase 1.5 landing Outcome: The dogfood-day deliverable. MapLibre singleton + tile-source switcher + sprite preload + WS client with rAF coalescer + Zustand position store + MapPositions (clustered + selected) + MapTrails (bounded ring buffer) + event picker + camera control trio + connection-status indicator. Operators on race day open one page, pick the active event, and watch their field move in real time.

See phase-2-live-map/README.md

Phase 3 — Dogfood readiness

Status: Not started Outcome: Operational polish a closed pilot needs but a demo doesn't: error boundaries that don't blank-screen the map, connection-state UI that tells the operator "WS reconnecting", mobile-responsive baseline (doesn't crash on a phone in the field), per-device detail panel, operator-friendly empty/loading states.

See phase-3-dogfood-readiness/README.md

Phase 4 — Future / optional

Status: ❄️ Not committed See phase-4-future/README.md

Ideas on radar: geometry editor (depends on directus Phase 2 collections), replay mode, heatmaps / deck.gl, i18n (Albanian), dark mode, E2E tests.

Operating model

  • Implementation agent contract. Each task file is self-sufficient: goal, deliverables, specification, acceptance criteria. An agent should be able to complete one task without reading the whole wiki — but should skim the wiki references at the top of the task before starting.
  • Sequence within a phase. Task numbering reflects intended order. Soft dependencies are explicit in each task's "Depends on" field. Tasks with no dependencies on each other can be done in parallel.
  • Status updates. When a task is started, change its row in this ROADMAP to 🟨 and the task file's status badge accordingly. When done, 🟩 + a one-line note in the task file's "Done" section pointing at the merging commit/PR.
  • Drift control. If implementation diverges from a task's spec, update the task file before the diverging code lands, with a note explaining why. Do not let plans rot.