Files
spa/.planning/ROADMAP.md
T
julian ef18222edf feat: task 1.6 login page
src/ui/pages/login.tsx — LoginPage component:
- Centred card on muted background.
- shadcn Form + FormField with react-hook-form + zodResolver.
  FormMessage renders field-level zod errors automatically.
- Email + password with proper autocomplete attrs for password managers.
  autoFocus on email.
- Submit calls useAuthStore.getState().login(); errors render in a
  destructive Alert above the form.
- In-flight via the auth store's 'authenticating' status (cross-tab safe).
- onAuthenticated callback fires on store transition to authenticated;
  1.7 wires this to a router redirect.

src/App.tsx branches on auth status:
- unknown / authenticating -> "Signing you in..." placeholder
  (avoids flashing the login page on hard refresh while the boot probe
  runs)
- anonymous -> <LoginPage />
- authenticated -> home placeholder (1.7 replaces with router shell)

Deviation: skipped src/routes/login.tsx — requires the router plugin
to have generated routeTree.gen.ts, which is 1.7's territory. The page
works standalone via App.tsx's status branch.

Search-param redirect (?redirect=...) also deferred to 1.7 — no router
yet to expose useSearch. onAuthenticated callback is the seam.
2026-05-02 18:44:57 +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 🟩 PENDING_SHA
1.7 Routing skeleton (TanStack Router + role-aware guards)
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.