- src/live/last-seen.ts: formatLastSeen(ts, now) — "now" / "Ns ago" /
"Nm ago" / "HH:MM".
- src/live/use-staleness.ts: useStalenessTick(intervalMs=1000) hook
re-renders subscribers every interval with the current epoch ms.
- src/ui/components/connection-chip.tsx: <ConnectionChip /> bottom-left,
three states (connected / connecting+reconnecting / disconnected),
aria-live polite + role status.
- src/map/layers/map-positions.tsx: every FeatureProps now carries
staleSec; both symbol layers interpolate icon-opacity (and text-opacity
on the non-selected layer) on staleSec — 0-60s full opacity,
5min faded, 30min very faded. Update effect rebuilds features at the
staleness tick rate (1Hz).
- src/routes/_authed/monitor.tsx: renders <ConnectionChip />.
- src/live/index.ts: re-exports formatLastSeen + useStalenessTick.
Strategy A (faded marker via interpolation) chosen over Strategy B
(separate warning-badge layer) per the task's open-question
resolution; simpler and easier to extend later.
Deviations:
1. stalenessTick is its own hook in src/live/use-staleness.ts rather
than a property on the position store — keeps the store clean of
UI-driven re-render concerns; the hook is reusable.
2. <DeviceLastSeen deviceId> standalone component skipped — the SPA
doesn't have a sidebar yet (Phase 3.4); the per-marker fade IS the
indicator for v1.
Bundle: main 396KB / 121KB gz — small bump from 2.8.
🎉 Phase 2 — Live monitoring map — complete. All 9 tasks shipped.
End-to-end: login -> /monitor -> event auto-selects -> snapshot
positions render -> live updates flow via WS through the rAF
coalescer -> staleness fades stale markers -> connection chip
surfaces WS state. Dogfood-blocking work for Rally Albania 2026 done.
- pnpm add maplibre-gl + -D @types/geojson.
- src/map/core/styles.ts: defaultStyle (OSM raster bootstrap; 2.2
replaces with the basemap-switcher descriptor table).
- src/map/core/map-view.tsx: module-level Map singleton lazily created
on first <MapView> mount, attached to a class="trm-map-host" detached
<div> that React refs append/remove on mount/unmount. Style-data
lifecycle flips mapReady false on every styledata event, polls
loaded() at 33ms intervals, flips ready true once the style is
loaded — the canonical MapLibre style-swap dance.
- Exports getMap()/getMapReady()/subscribeMapReady()/useMapReady (via
useSyncExternalStore for SSR-safe + concurrent-safe reads). getMap()
throws if called pre-mount; the explicit failure mode beats a
null-able top-level export.
- src/routes/_authed/monitor.tsx: new /monitor route, full-viewport
<MapView /> for 2.1 (no children — subsequent tasks plug in here).
- src/routes/_authed/index.tsx: home-page card now links to /monitor.
- eslint.config.js: override for src/map/** + src/live/** disables
react-refresh/only-export-components. Same pattern as the existing
overrides for shadcn primitives and route files.
Deviation: spec sketched a top-level `map` constant export; implemented
as `getMap(): MapLibreMap` (a function) so the singleton stays lazy
until <MapView> mounts. Top-level constant would either force eager
init (breaks SSR/tests) or be nullable (footgun). The function form
throws a clear error if called pre-mount.
Bundle: /monitor lazy chunk is 1MB raw / 274KB gzipped (MapLibre + CSS).
Other routes unaffected. Vite chunk-size warning is harmless.
Nine task files matching Phase 1's shape (Goal / Deliverables / Spec /
Acceptance / Risks / Done). README updated with full sequencing diagram,
files-modified outline, tech stack additions, design rules, and phase
acceptance.
| # | Task |
| --- | --------------------------------------------------------------------- |
| 2.1 | MapView singleton + mapReady gate |
| 2.2 | Tile-source switcher (Esri / OpenTopoMap / OSM / optional Google) |
| 2.3 | Sprite preload — 7 racing categories x 4 colour variants |
| 2.4 | WS client + rAF coalescer + Zustand position store + connection store |
| 2.5 | MapPositions — clustered + selected sources |
| 2.6 | MapTrails — bounded ring buffer, polyline rendering |
| 2.7 | Event picker — TanStack Query + WS subscription orchestration |
| 2.8 | Camera control trio — default-fit / selected-follow / one-shot |
| 2.9 | Connection status + per-device last-seen indicators |
Sequencing: 2.1 and 2.4 are parallel foundations (singleton vs data
pipeline). Once both land, 2.5 / 2.6 / 2.7 / 2.9 fan out independently.
2.2 / 2.3 only need 2.1. 2.8 sits at the end on top of 2.1 + 2.5.
Each task documents its deliverables down to file paths + interface
shapes, includes concrete code sketches in the Specification, lists
explicit out-of-scope items, and surfaces risks for the implementer
to think about. An agent (or future me) can pick up any single task
and ship it without re-deriving the design from the wiki.
Resolved Phase 2 design decisions baked into the task files:
- Trails: flat-colour-per-device for v1, defer speed-coloured segments
to a Phase 3 polish task.
- Cluster params: 14/50 (traccar default); tune after seeing real data.
- Event picker placement: top-left dropdown.
- Multi-event: out — single-select, one event at a time.
- Stale-position visual: fade icon opacity; defer warning badges.