feat: task 2.6 MapTrails (bounded ring buffer, polyline rendering)

- src/map/core/map-pref-store.ts: trailMode preference
  ('none' | 'selected' | 'all', default 'selected') + setter, persisted.
- src/map/layers/map-trails.tsx: <MapTrails /> side-effect-only.
  Single GeoJSON source + single line layer. Builds one LineString
  Feature per device whose trail has >= 2 points; filtered by mode.
  Per-device flat colour via deterministic deviceId hash mod a
  6-entry palette.
- src/map/core/trails-toggle.tsx: <TrailsToggle /> floating card
  below <BasemapSwitcher />. Three buttons (None / Selected / All).
- src/routes/_authed/monitor.tsx: renders <TrailsToggle /> +
  <MapTrails /> *before* <MapPositions /> so the line layer is added
  to the map first and renders underneath the symbol layers.

Speed-coloured-per-segment deferred to Phase 3 polish per the task
spec's open-question decision; flat-colour-per-device for v1.

Bundle: main 394KB / 120KB gz — no change from 2.5.
This commit is contained in:
2026-05-02 22:17:10 +02:00
parent d56c5e0e40
commit 28a501c02d
6 changed files with 226 additions and 10 deletions
@@ -276,7 +276,7 @@ Three new files plus Schema work:
**Schema overhaul** in `src/auth/client.ts`:
- Made `Schema` type SDK-compatible. Each entry is an *array* of row types (e.g. `devices: DeviceRow[]`), not a single row — `RegularCollections<Schema>` filters on `Schema[K] extends ArrayLike` and a non-array value collapses to `never`, which broke `readItems('devices', ...)` with `keyof Schema = never`.
- Made `Schema` type SDK-compatible. Each entry is an _array_ of row types (e.g. `devices: DeviceRow[]`), not a single row — `RegularCollections<Schema>` filters on `Schema[K] extends ArrayLike` and a non-array value collapses to `never`, which broke `readItems('devices', ...)` with `keyof Schema = never`.
- Spelled out the composed client type explicitly: `DirectusClient<Schema> & RestClient<Schema> & AuthenticationClient<Schema>` instead of `ReturnType<typeof buildClient>`. Without the explicit annotation Schema didn't reliably flow through the chained `.with(...)` calls to inference at `request()` call sites.
- Added `DeviceRow` and `EventRow` row types to the Schema; barrel-exported via `@/auth`.
@@ -289,6 +289,7 @@ Three new files plus Schema work:
3. Cluster click handler: `getClusterExpansionZoom` is a `Promise`-returning method in maplibre-gl 5.x (was callback-style in older versions). Used the Promise form.
**Smoke check (local `pnpm dev`):**
- `/monitor` loads. `<MapPositions />` mounts. Position store is empty until the event picker (2.7) lands.
- In the browser console: `usePositionStore.getState().applyPositions([{deviceId: 'test-1', lat: 41.327, lon: 19.819, ts: Date.now()}])` produces a marker on the map at the seeded coordinates within ~16ms (one rAF flush).
- Clicking the marker triggers `selectDevice('test-1')` — verifiable by reading the store after.
+19 -1
View File
@@ -153,4 +153,22 @@ If operators want a longer-trail mode: a slider in the prefs (`50 / 200 / 500 /
## Done
(Filled in when the task lands.)
- **`src/map/core/map-pref-store.ts`** — extended with `trailMode: TrailMode` (`'none' | 'selected' | 'all'`, default `'selected'`) and `setTrailMode`. Persisted via the existing zustand `persist` middleware on `'trm-map-prefs'`.
- **`src/map/layers/map-trails.tsx`** — `<MapTrails />` side-effect-only component. Single GeoJSON source + single `'line'` layer. Two-effect pattern (setup + setData on store changes). Reads `trailsByDevice`, `selectedDeviceId`, `trailMode`. Builds one `LineString` Feature per device whose trail has ≥ 2 points; filtered by mode (`none` → empty, `selected` → just the selected device, `all` → every device).
- **`src/map/core/trails-toggle.tsx`** — `<TrailsToggle />` floating card top-right, below `<BasemapSwitcher />` (`top-32`). Three buttons (None / Selected / All); active one highlighted via `bg-accent`. Has a small "Trails" label up top.
- **`src/routes/_authed/monitor.tsx`** — renders `<TrailsToggle />` as a sibling of `<BasemapSwitcher />`, and `<MapTrails />` *before* `<MapPositions />` inside `<MapView>` so the line layer is added to the map first and renders underneath the symbol layers.
**Per-device colouring:** flat colour from a 6-entry palette (`#2563C8 / #2E8C4A / #6B46C1 / #188C8A / #C9296F / #5A5A53`) keyed by a deterministic hash of the deviceId. Same device = same colour across reloads. Distinct enough that two trails don't blend. Speed-coloured-per-segment deferred to a Phase 3 polish task per the original task spec's open-question decision.
**Smoke check (local `pnpm dev`):**
- `/monitor` shows the trails toggle below the basemap switcher.
- Default mode is "Selected" → no trails visible until a device is selected.
- Synthetic positions (push 2+ positions for the same deviceId via the console — `usePositionStore.getState().applyPositions(...)`) and the trail polyline appears immediately.
- Toggling to "All" shows trails for every device with ≥ 2 points.
- Toggling to "None" hides trails.
- Switching basemap → trails reappear after the swap (mapReady gate handles the remount).
- Reloading the page: trail mode persists.
**Bundle:** main bundle 394KB / 120KB gz — no change from 2.5. Trails layer is small.
Landed in `PENDING_SHA`.