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>
156 lines
8.1 KiB
Markdown
156 lines
8.1 KiB
Markdown
# 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_api` read-API at
|
||
> `fleetapi.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 `main`** via 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 Y` vs `KDS 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):**
|
||
1. Drop its point GeoJSON in `layers/<name>.geojson`.
|
||
2. Add one entry to the `OVERLAYS` array near the top of the `<script>` in
|
||
`index.html`:
|
||
```js
|
||
{ id: '<name>', label: '<Label>', url: 'layers/<name>.geojson',
|
||
iconSvg: <40×40 SVG string>, nameKey: 'name', defaultOn: false }
|
||
```
|
||
`iconSvg` is registered as the marker image (reuse `SHELL_ICON_SVG` as a
|
||
template). Nothing else to wire — `addOverlay()` builds the source + symbol
|
||
layer, and the Layers control lists it automatically.
|
||
3. Commit + push. The `Dockerfile` already `COPY`s `layers/` into nginx.
|
||
|
||
> The Shell layer was extracted from a Kenya OSM `.pbf` — the reproducible
|
||
> workflow (filter `amenity=fuel`, `brand=Shell`) lives in the `tracksolid` repo:
|
||
> `scripts/export_osm_pois.py` + `docs/OSM_POI_EXPORT.md`.
|
||
|
||
## Deploy (Coolify, git-based)
|
||
|
||
1. In Coolify, create a new **Application** from this git repo
|
||
(`https://repo.rahamafresh.com/kianiadee/fleetnow.git`), branch `main`,
|
||
build pack **Dockerfile**.
|
||
2. Set the **port** to `80`.
|
||
3. Add the domain **`fleetnow.rahamafresh.com`** (HTTPS / Let's Encrypt). Coolify
|
||
wires Traefik on the `coolify` network automatically.
|
||
4. Point DNS `fleetnow.rahamafresh.com` → the VPS (`31.97.44.246`) if not already.
|
||
5. Deploy. Every push to `main` redeploys; `index.html` is served `no-cache` so
|
||
changes appear immediately.
|
||
|
||
## Local preview
|
||
|
||
```bash
|
||
docker build -t fleetnow . && docker run --rm -p 8080:80 fleetnow
|
||
# open http://localhost:8080
|
||
```
|
||
|
||
> Loading from `localhost` will 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/*` to `fleetapi.rahamafresh.com`.
|