Adds a searchable single-select vehicle/plate pulldown. Picking a vehicle
sets the cost-centre & assigned-city filters to match, frames its day's
route on the map, and opens its trip dock. Backed by a persistent session
registry so every vehicle stays findable after a filter narrows the view.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Vendor maplibre-gl 4.7.1 (js+css) and serve from /vendor instead of the unpkg CDN — no external dependency/SRI gap for the core map.
- Projector skips duplicate (imei, occurred_at) history rows via NOT EXISTS (parked devices re-report the same gpsTime each poll); migration 23 dedupes existing rows and adds a unique index.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When viewing one driver's day, each trip is drawn in its own colour
from a 12-colour palette (cycling past 12). The matching colour shows
as the left edge of the trip card so card ↔ polyline pair up at a
glance.
Trip selection moves from border-swap to an outline, so the
trip-colour swatch stays visible when a card is selected.
Multi-mode (>1 vehicle) still uses per-vehicle SELECTION_PALETTE so a
fleet comparison reads as one colour per driver.
Plain-click on a vehicle marker: single-vehicle mode, full trip-card
list, click a card → animated playback (unchanged behaviour).
⌘/Ctrl/Shift-click: add/remove the vehicle from the selection. Each
selected vehicle's day routes are drawn on the map as a polyline in a
distinct colour from an 8-colour selection palette. Trip dock switches
to a compact per-vehicle row layout with ✕ remove buttons; the header
shows aggregate trip count + distance + drive / idle / stop minutes
summed across the selection.
Date change re-fetches every selected vehicle; CSV button downloads one
file per selected vehicle. Map auto-fits to the union of bounds.
Click a vehicle row in multi-mode → map flies to just that vehicle's
trips. Removing the last vehicle empties the dock; the X button closes
it entirely.
Internals: replaced singular `_tripState` with a `_selection` Map keyed
by vehicle_id. Single-trip animation layers still exist for the
single-mode trip-card playback; multi-mode uses per-vehicle line-only
layers (vroute-line-{id}) with no marker animation.
- Migration 20: collapse `Nairobi`/`nairobi` in domain.vehicles → 'nairobi'
- Remove the SLO panel from the top band (filter + tile rows stay)
- Offline vehicles render as solid grey instead of dim-cost-centre tint;
opacity now only differentiates moving (1.0) vs parked (0.75) vs
offline (0.55) so colour carries identity + state cleanly
Restructure:
- FLEET NOW tiles + SLOs + Filters in a horizontal top band; full-width map
- Trip panel moved to a bottom dock that slides up; trips render as a
horizontally-scrollable card strip instead of a vertical right-panel list
Multi-select filter widgets:
- cost_centre + assigned_city are now dropdowns with an "All …" toggle
and per-option checkboxes
- cost_centre options carry a colour swatch matching the marker tint —
the filter doubles as a live colour legend
- Server-side filter still applies when exactly one option is picked;
multi-selection within a widget is narrowed client-side via setFilter
so the existing serve.fn_live_view contract is unchanged
Cost-centre tint always visible:
- circle-color now uses cost_centre_color unconditionally
- operational_state is shown via opacity (moving=1.0 / parked=0.7 /
offline=0.35), keeping colour as a stable identity cue
applyClientFilter() is a new exported helper called by the page after
each refresh to narrow markers by multi-selection state.
Click any vehicle on the map to open a 360px slide-in panel showing:
- reporting time (first ACC_ON of the day)
- day totals: trip count, distance, drive/idle/stop minutes
- per-trip rows with start/end/duration/distance/idling, click to
select; selected trip renders its polyline + animates a marker
along it over 10 seconds
- end-reason badge per trip (work stop, reporting silence, long gap,
day end) with colour-coded accent
- date picker (defaults to today EAT)
- CSV download button → /trips.csv?date=...
Map clicks query rendered features across circle/arrow/label layers and
take the topmost — single click handler, no per-layer duplicates. The
existing hover popup remains untouched.
Wraps #map in #map-container so the panel can absolute-position over
the right side without disturbing the existing left-aside grid layout.
authClient gets a getToken() helper so the CSV download path can attach
the Authorization header for a plain fetch (apiFetch returns JSON only).