# 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 service** — `docs/wiki/entities/react-spa.md` - **Map subsystem** — `docs/wiki/concepts/maps-architecture.md`, `docs/wiki/sources/traccar-maps-architecture.md` - **Live channel** — `docs/wiki/concepts/live-channel-architecture.md`, `docs/wiki/synthesis/processor-ws-contract.md` - **Auth + business plane** — `docs/wiki/entities/directus.md`, `docs/wiki/synthesis/directus-schema-draft.md` - **System architecture** — `docs/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 `
`. 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`**](./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)](./phase-1-foundation/02-stack-rounding-out.md) | 🟩 | `9918418` | | 1.3 | [Vite dev proxy + path aliases + tsconfig hardening](./phase-1-foundation/03-vite-dev-proxy.md) | 🟩 | `39b60c9` | | 1.4 | [Runtime config endpoint](./phase-1-foundation/04-runtime-config.md) | 🟩 | `8e2151a` | | 1.5 | [Directus auth client (cookie mode + refresh)](./phase-1-foundation/05-directus-auth-client.md) | 🟩 | `38fe2e3` | | 1.6 | [Login page](./phase-1-foundation/06-login-page.md) | 🟩 | `PENDING_SHA` | | 1.7 | [Routing skeleton (TanStack Router + role-aware guards)](./phase-1-foundation/07-routing-skeleton.md) | ⬜ | — | | 1.8 | [Logout flow](./phase-1-foundation/08-logout-flow.md) | ⬜ | — | | 1.9 | [Gitea CI + Dockerfile + nginx static serve](./phase-1-foundation/09-gitea-ci-and-dockerfile.md) | ⬜ | — | | 1.10 | [Compose service block in trm/deploy](./phase-1-foundation/10-deploy-compose-block.md) | ⬜ | — | ### 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-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-3-dogfood-readiness/README.md) ### Phase 4 — Future / optional **Status:** ❄️ Not committed [**See `phase-4-future/README.md`**](./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.