Files
spa/.planning/phase-2-live-map/README.md
T
julian 26e059fc20 feat: planning structure + task 1.2 stack rounding-out
Add .planning/ scaffolding:
- ROADMAP.md (4 phases, 8 non-negotiable design rules)
- phase-1-foundation/ README + 9 task files (1.2-1.10)
- phase-2-live-map / phase-3-dogfood-readiness / phase-4-future README placeholders

Task 1.2 — stack rounding-out:
- Tailwind 4 via @tailwindcss/vite + src/styles/globals.css
- shadcn/ui (slate, new-york) primitives in src/ui/primitives/:
  button, input, label, form, card, alert
- TanStack Router 1.169 + Query 5.100 (devtools + plugin in devDeps)
- Zustand 5, @directus/sdk 21, zod 4, react-hook-form 7 + resolvers
- Prettier 3 + eslint-config-prettier + eslint-plugin-prettier
- ESLint override disabling react-refresh/only-export-components for
  src/ui/primitives/** (intentional dual-exports in shadcn primitives)
- Path alias @/* -> ./src/* in tsconfig.json + tsconfig.app.json
  (TS 6 deprecates baseUrl; paths now resolve relative to config file).
  Pulled forward from 1.3 because shadcn add CLI needs it resolvable.
- Scripts: dev, build, preview, lint, typecheck, format, format:check,
  test (placeholder)
- App.tsx Tailwind smoke test (centred card + shadcn Button)
- README.md rewritten with stack/scripts/shadcn-add docs

All four gates green: typecheck, lint, format:check, build (222KB / 70KB gz).
2026-05-02 18:41:54 +02:00

59 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 2 — Live monitoring map
**Status:** ⬜ Not started — depends on [[processor]] Phase 1.5 landing
The dogfood-day deliverable. After Phase 2, an operator opens the SPA, picks the active event, and watches the field move on a real-time map. Inherits the architecture documented in `docs/wiki/concepts/maps-architecture.md` from `docs/wiki/sources/traccar-maps-architecture.md`, with the deliberate divergences (rAF coalescer, Zustand, longer trail default, racing sprite set, native PostGIS GeoJSON) baked in from day one.
## Outcome statement
When Phase 2 is done:
- A `MapLibre GL JS` singleton renders inside a detached `<div>` mounted by `<MapView>`. Sources and layers are added by side-effect-only `Map*` components.
- The user can switch basemap between Esri World Imagery (satellite), OpenTopoMap, OSM raster, and (if a Google Maps key is in runtime config) Google Satellite via the `maplibre-google-maps` adapter.
- Sprite registry is preloaded at app boot: `rally-car / quad / ssv / motorcycle / runner / hiker / default` × `success / error / neutral / info`.
- The SPA opens a WebSocket to Processor (per `docs/wiki/synthesis/processor-ws-contract.md`), sends `subscribe` for the active event, receives a snapshot, then streams positions.
- An rAF-coalescer buffers incoming positions per-device; one `setData` call per frame regardless of message rate.
- `MapPositions` renders devices on two GeoJSON sources (clustered non-selected + unclustered selected). Cluster expansion and selection-on-click work.
- `MapTrails` renders a per-device bounded ring buffer of recent positions as a polyline. Default 200 points; configurable.
- An event picker (sidebar or top bar) lets the operator switch between events they have access to.
- Camera control trio (`MapCamera` / `MapDefaultCamera` / `MapSelectedDevice`) handles fit-on-load, follow-selected, and manual interaction.
- A connection-status indicator shows WS state (connected / reconnecting / offline) and last-message-received age per device (subtle UI; doesn't dominate).
## Why this is a separate phase
- **Foundation must be solid.** Auth, routing, deploy, runtime config — all must work before adding the live channel. Phase 1 ships an empty shell on stage; Phase 2 fills it.
- **Depends on the processor side.** The WS contract is locked, but the producing endpoint must exist before the SPA can connect to it. [[processor]] Phase 1.5 is the gating dependency.
- **Map architecture is non-trivial.** The singleton + side-effect-component + rAF coalescer + GeoJSON setData stack is a coherent pattern that works as a whole. Bundling it into Phase 1 would inflate Phase 1 dramatically.
## Tasks (sketched, not detailed)
These get full task files when Phase 2 starts. For now, this is the planned shape:
| # | Task | Notes |
| --- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| 2.1 | MapLibre singleton + `<MapView>` + `mapReady` gate | Module-level instance, detached `<div>`, listener Set for ready transitions |
| 2.2 | Tile-source switcher | Esri / OpenTopoMap / OSM / optional Google via `maplibre-google-maps`; reads from runtime config |
| 2.3 | Sprite preload | Pre-rasterised at `devicePixelRatio` × 4 colour variants; re-`addImage`'d after every style swap |
| 2.4 | WS client + rAF coalescer + Zustand position store | The throughput-discipline core. Per-device latest-position map + per-device bounded trail ring buffer |
| 2.5 | `MapPositions` (clustered + selected sources) | Symbol layer + direction arrow + cluster bubble; selection on click |
| 2.6 | `MapTrails` (bounded ring buffer, polyline rendering) | Default 200 points/device; possibly speed-coloured per segment (post-decision) |
| 2.7 | Event picker + sidebar | Reads events from Directus REST via TanStack Query; subscribes / unsubscribes the WS on switch |
| 2.8 | Camera control trio | One-shot fit / initial framing / reactive follow |
| 2.9 | Connection-status + per-device last-seen indicators | Small chrome elements; non-dominant |
## Architectural boundaries to maintain
- `src/map/` is a self-contained module. Imports `@/auth` (for the WS cookie) and `@/config` (for the runtime config), nothing else. The map subsystem must be deletable as a unit if we ever need to reroute (we won't).
- `src/live/` houses the WS client, position store, and the coalescer. Decoupled from the map module so the map renders whatever's in the store, regardless of source.
- No domain logic. The map shows positions; it doesn't know about classes, entries, penalties, or stages. Phase 2.5+ is when domain awareness lands.
## Open questions blocking task-level detail
(These get answered when Phase 2 starts.)
1. **Live trail colouring.** Flat colour by device or speed-coloured per segment (Traccar's replay style)? Speed-coloured live is novel and informative for race operators; needs a decision before 2.6.
2. **Event-picker placement.** Top bar (always visible) or sidebar (collapsible)? Depends on the rest of the operator chrome.
3. **Cluster parameters.** Traccar uses `clusterMaxZoom: 14, clusterRadius: 50`. Rally density (50500 vehicles spread across a country-scale stage) may want different values. Defer until we see real positions on the map.
4. **Per-device sidebar list.** Out of scope for v1 of Phase 2 or in scope? Leaning out — the map is the focus; a list is supplementary.
5. **What happens when the user has access to multiple events.** Picker shows all? Only the active ones (between `starts_at` and `ends_at`)? Decide before 2.7.