README: add a "Map overlay layers" section (how the toggleable layer system works + how to add a layer in ~2 min), document fleet segmentation / department colours / legend / POIs, refresh the file tree (layers/), and correct the deploy note (Coolify auto-deploys on push to main). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.1 KiB
FleetNow
A single-file map console that merges live vehicle positions and historical trips into one view for the Fireside Communications / Tracksolid fleet.
Status: v2 — live at https://fleetnow.rahamafresh.com. Deployed from this repo via Coolify (Dockerfile → nginx). Reads the
dashboard_apiread-API atfleetapi.rahamafresh.com.2026-06-08 additions: fleet segmentation (specialist vehicle icons that never cluster), per-department colour coordination + collapsible Key legend, persistent POIs (Fireside HQ, Safaricom HQ), and a toggleable map-overlay layer system (first layer: 232 Shell fuel stations). See Map overlay layers.
Deploy auto-fires on push to
mainvia Coolify (~2–3 min build); if it lags, hit Redeploy in Coolify.
What it does
- Land on the live fleet — markers carry the department (cost-centre) colour,
a heading arrow, the plate tail, and a hover popup (status, driver, reverse-geocoded
address, heading, odometer, last fix, source). Markers scale with the zoom level.
Shape encodes recency so stakeholders read activity at a glance:
- ● circle (full colour + heading arrow) — moving right now
- ■ square (pastel colour, no arrow, ~half the size of a moving marker) — active within the last 24h, now stopped
- grey ● — offline (no fix in 24h)
- One pin per vehicle (tracker + camera dedup). Every vehicle carries a GPS
tracker (X3 / GT06E / AT4) and a dashcam (JC400P) that share the same
number plate. FleetNow collapses the pair to a single marker/dropdown entry:
the tracker is primary; if the tracker isn't reporting (>24h), it falls
back to the camera; if both report, the tracker wins. Pairing is by
normalised plate, so a stray space (
KDS 453 YvsKDS 453Y) still merges. - Clustering (zoomed out). Vehicles group into amber count-bubbles (Folium /
Leaflet.MarkerCluster style, via
supercluster); click a bubble to zoom and expand it. Clusters disband into individual pins at ~city zoom (z11). Clustering honours the active filter and applies to the live view only (not trip routes). - Fleet segmentation & department colours. Specialist vehicles (crane / motorbike / pick-up) get their own marker icons and are never clustered (always individually visible); every cost centre has a fixed, distinct colour, with a collapsible Key legend (bottom-left) listing only the centres on screen. Non-operational vehicles (personal / management / Uganda-MTN) are filtered out upstream in the read-API, so the live map shows the operational fleet only.
- Persistent POIs — Fireside HQ and Safaricom HQ (Waiyaki Way) as labelled reference markers.
- Filters (bottom-right card) apply to the live map instantly:
- Number plate — multi-select, sorted A→Z; picking a vehicle auto-fills its cost centre + city.
- Cost centre and Assigned city — narrow the live fleet (and the KPI bar recomputes to match).
- Time (Today / 1 week / 1 month / Custom) — applies to trips, not live.
- Drill into history: click a vehicle's dot → Show trips, or set plate / cost centre / city + period and hit Show trips for a fleet-wide pull. The map switches to seq-coloured trip routes with start/end markers and a click-to-animate replay; the ● Live pill returns to the live snapshot.
- In trips view a context bar (below the header) summarises the active filter (vehicle / cost centre / city) plus the first trip and last trip — each with its reverse-geocoded location and timestamp — alongside the KPI totals (trips, km, driving/idle hours, vehicles, drivers, date range).
- Full-width map + two-tier bottom dock (no floating/side panels). All controls
live in a bottom dock with two tiers: a filter tier on top and a trip-card
tier beneath (selection → results hierarchy). In live mode the filter row
is expanded and there are no cards; in trips mode the filters collapse to a
one-line summary (
Filters: KCA 542Q · roll out · nairobi · Last 1 month, with Edit to expand) and the trip cards show beneath — keeping the map tall. - Plate picker is a searchable combobox — type to filter, click to add a removable chip (multi-select), instead of a tall scrolling list. Cost centre / city / time stay single-line; date pickers appear only for a custom range. Trip cards scroll horizontally; click a card to fit + animate that route.
Live: https://fleetnow.rahamafresh.com
Architecture
The whole app is one self-contained index.html (inline CSS + JS; MapLibre
GL JS loaded from a CDN). It has no build step and no local assets. It reads from
the existing dashboard read-API — it does not talk to the database directly.
index.html → the entire SPA
layers/*.geojson → static overlay data (gas stations, …), served at /layers/
Dockerfile → bakes index.html + layers/ into an nginx:alpine image (port 80)
nginx.conf → static serve + /healthz + no-cache on index.html
Backend it depends on
const API_BASE = 'https://fleetapi.rahamafresh.com'; (top of the <script> in
index.html). That service (dashboard_api_rev.py in the tracksolid repo)
exposes:
| Endpoint | Use |
|---|---|
GET /webhook/live-positions |
live snapshot {summary, geojson} |
GET /webhook/live-positions/track?vehicle_number=&hours= |
1 h trail |
GET /webhook/fleet-dashboard |
filter options (plates, cost centres) |
POST /webhook/fleet-dashboard |
trips for a selection {summary, geojson} |
CORS: the API must allow the https://fleetnow.rahamafresh.com origin
(DASHBOARD_CORS_ORIGINS). It is in the code default; make sure the deployed
dashboard_api container's env includes it, then restart that container.
Map overlay layers
Toggleable reference overlays (gas stations, etc.) sit behind the Layers
control (top-right, collapsed, all off by default). Each is a static GeoJSON
in layers/, rendered as a MapLibre symbol layer that auto-declutters
(icon-allow-overlap: false — sparse when zoomed out, all points reveal as you
zoom in), with a zoom-scaled icon (~8→16 px) and a hover label (one reused
popup, so only ever one is visible). Overlays render under the vehicle markers.
Shipped layers:
| Layer | Data | Points |
|---|---|---|
| Shell stations | layers/shell_stations.geojson (OSM kenya-260605) |
232 |
To add a layer (≈2 min):
- Drop its point GeoJSON in
layers/<name>.geojson. - Add one entry to the
OVERLAYSarray near the top of the<script>inindex.html:{ id: '<name>', label: '<Label>', url: 'layers/<name>.geojson', iconSvg: <40×40 SVG string>, nameKey: 'name', defaultOn: false }iconSvgis registered as the marker image (reuseSHELL_ICON_SVGas a template). Nothing else to wire —addOverlay()builds the source + symbol layer, and the Layers control lists it automatically. - Commit + push. The
DockerfilealreadyCOPYslayers/into nginx.
The Shell layer was extracted from a Kenya OSM
.pbf— the reproducible workflow (filteramenity=fuel,brand=Shell) lives in thetracksolidrepo:scripts/export_osm_pois.py+docs/OSM_POI_EXPORT.md.
Deploy (Coolify, git-based)
- In Coolify, create a new Application from this git repo
(
https://repo.rahamafresh.com/kianiadee/fleetnow.git), branchmain, build pack Dockerfile. - Set the port to
80. - Add the domain
fleetnow.rahamafresh.com(HTTPS / Let's Encrypt). Coolify wires Traefik on thecoolifynetwork automatically. - Point DNS
fleetnow.rahamafresh.com→ the VPS (31.97.44.246) if not already. - Deploy. Every push to
mainredeploys;index.htmlis servedno-cacheso changes appear immediately.
Local preview
docker build -t fleetnow . && docker run --rm -p 8080:80 fleetnow
# open http://localhost:8080
Loading from
localhostwill be CORS-blocked by the live API unless that origin is allow-listed. For pure UI work, run a same-origin proxy that forwards/webhook/*tofleetapi.rahamafresh.com.