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.