Fireside Communications · Tracksolid Fleet Intelligence

Merge Live Position + Fleet Intelligence into one map
fleetnow.rahamafresh.com

Implementation plan · v1 · 2026-06-05

New SPA — single self-contained HTML Backend change — CORS only Map — MapLibre GL 4.7.1 Theme — full warm re-skin Existing 2 maps — kept

0Context

The fleet team currently runs two separate MapLibre dashboards off the same read API (fleetapi.rahamafresh.com):

Operators want a single console where they land on the live fleet, then drill into any vehicle's (or any cost-centre's) historical trips for a chosen period — without switching tabs/apps. This plan builds that merged console as a new third dashboard at fleetnow.rahamafresh.com, fully re-skinned to the warm palette in the reference image. The existing two dashboards stay running untouched.

Confirmed decisions

Single-canvas mode switch. Land on Live; selecting a vehicle (or applying filters) switches the map to Trips for that selection and hides the live dots; a Live button (or clearing the selection) returns to the live snapshot.
Filters drive fleet-wide trips. Cost-centre + period (no vehicle) draws all matching trips, exactly like Fleet Intelligence today; clicking a single vehicle narrows to just it.
Keep all three dashboards — fleetnow is additive.
Full re-skin to the warm palette (dark slate base, amber/orange accent, teal-green live).

1Key facts that shape the build

No new backend logic needed. The four existing endpoints cover the whole merged UX:

EndpointReturns / source
GET /webhook/live-positionslive snapshot {summary, geojson}
GET /webhook/live-positions/track1 h trail (LineString Feature)
GET /webhook/fleet-dashboardfilter options {drivers, cost_centres, cities, vehicles}
POST /webhook/fleet-dashboardtrips {summary, geojson} via reporting.fn_trips_for_map(...)

Time presets already supported in dashboard_api_rev.py:252 _preset_to_range: today | 7d (="1 week") | 30d (="1 month") | custom.

One backend change only — CORS. dashboard_api_rev.py:53 _ALLOWED_ORIGINS (env DASHBOARD_CORS_ORIGINS) must include https://fleetnow.rahamafresh.com, otherwise the browser blocks every fetch.

Both SPAs already share: MapLibre GL JS 4.7.1, Carto Voyager basemap (Mapbox dark-v11 scaffolded but inactive), the same CSS design tokens, the same COST_CENTRE_PALETTE + colorForCostCentre() hash, the same SEQ_PALETTE/seqColor(), the Fireside HQ POI, the EAT clock, and escapeHtml. The merge is mostly composition + re-theme, not new algorithms.

2Deliverable

A single self-contained fleetnow.html (one file, inline CSS/JS, MapLibre from unpkg) — matching the existing single-file-SPA pattern so it drops straight into a rustfs bucket behind an nginx proxy. Keep it in the repo for source control, e.g. frontend/fleetnow.html (new frontend/ dir; the existing two live only in rustfs, so this also starts versioning the SPA source).

Layout (per the user's spec; palette only from the image)

No left panel. The map is full-bleed under the top bar; everything else floats over it as cards — filters bottom-right (always), trip list bottom-left (trips mode only).

┌───────────────────────────────────────────────────────────────────┐
│ TOP BAR — metrics for current mode + [● Live] pill + EAT clock      │
├───────────────────────────────────────────────────────────────────┤
│  MAP (full-bleed, centred on East Africa)                          │
│                                                                     │
│  ┌───────────────────┐                   ┌──────────────────────┐  │
│  │ TRIP LIST          │                   │ FILTERS (bottom-right)│ │
│  │ (trips mode only,  │                   │  • Number plate      │  │
│  │  bottom-left)      │                   │  • Cost centre       │  │
│  │  ◀ Live            │                   │  • Time: Today▾      │  │
│  │  #1 KDK 829A · 8km │                   │    (Today/1wk/1mo/   │  │
│  │  #2 KDK 829A · 3km │                   │     Custom)          │  │
│  └───────────────────┘                   └──────────────────────┘  │
└───────────────────────────────────────────────────────────────────┘

State model (the merge core)

One mode variable: 'live' | 'trips'.

Keep live and trip layers as separate sources/layers toggled by visibility rather than a shared source — it mirrors how each SPA already manages its own source and avoids id collisions.

Palette re-skin (full)

Replace the CSS :root tokens. Derived from the reference image (warm dark ops console):

--bg
#161a23 base
--panel
#1e232e
--border
#2c333f
--text
#ECEFF4
--muted
#93a0b4
--accent
#E8954A amber
--live
#2dd4a7 teal
--parked
#6b7280
--offline
#b4791f
--danger
#ef5b5b

Keep the categorical COST_CENTRE_PALETTE and SEQ_PALETTE distinct/high-contrast for legibility (don't collapse them into the warm ramp) — but reorder so the first slots lead with the brand amber/teal. Re-tone marker outlines, popups, chips, buttons, and the HQ POI to the new tokens. "Moving/active" state colour → --live teal; arrows stay white-on-halo.

3Backend change

In dashboard_api_rev.py:53, add https://fleetnow.rahamafresh.com to the DASHBOARD_CORS_ORIGINS default list (durable, for when the service is folded into Coolify), AND set it live on the running standalone bridge container's env. Per the deploy notes, that container is not Coolify-managed:

# on twala host (user runs SSH — IP-whitelisted):
#   update DASHBOARD_CORS_ORIGINS to include fleetnow, then:
docker restart dashboard_api

4Deployment (user runs the SSH/host steps — sandbox IP isn't whitelisted)

Mirror the existing liveposition/fleetintelligence wiring (single index.html object in rustfs + nginx reverse-proxy + Traefik route on the coolify net + DNS):

  1. Create rustfs bucket fleetnow; upload fleetnow.html as index.html (boto3 against https://s3.rahamafresh.com, path-style — same method already used to repoint the other two).
  2. Add fleetnow-proxy nginx config (clone ~/fleetintelligence-proxy/nginx.conf, point at rustfs-...:9000/fleetnow/index.html); attach Traefik labels for https://fleetnow.rahamafresh.com (entrypoints http/https, certresolver letsencrypt, traefik.docker.network=coolify).
  3. Ensure DNS fleetnow.rahamafresh.com31.97.44.246 (LE will issue on first hit).
  4. Apply the CORS change above + docker restart dashboard_api.

In the SPA, set const API_BASE = 'https://fleetapi.rahamafresh.com'; (rename from the legacy N8N_BASE for clarity).

5Verification (end to end)

  1. Local smoke: open fleetnow.html from disk (or python -m http.server). Live dots load (CORS will block until fleetnow origin is allow-listed — for local dev, temporarily add http://localhost:* to DASHBOARD_CORS_ORIGINS, or test against a curl-proxied copy).
  2. API contracts unchanged (curl, already-allowed origin):
    curl https://fleetapi.rahamafresh.com/webhook/live-positions
    # → {summary, geojson} with features
    
    curl -X POST https://fleetapi.rahamafresh.com/webhook/fleet-dashboard \
      -H 'Content-Type: application/x-www-form-urlencoded' \
      -d 'vehicle_numbers=KCA 542Q&period=30d'
    # → trips for one vehicle (~232 trips for that plate per fix history)
  3. Mode switch: land on Live (dots + arrows + cost-centre colours + polling). Click a vehicle → map switches to that vehicle's trips for "Today", top bar shows trip KPIs, Live pill appears. Click a trip row → route animates. Click Live → returns to live snapshot, polling resumes.
  4. Fleet-wide filters: pick a cost centre + "1 week", apply → all matching trips draw; clear → back to Live.
  5. Post-deploy: load https://fleetnow.rahamafresh.com in a browser — no CORS errors in console; existing liveposition + fleetintelligence still load (regression check).

6Critical files

FileRole
frontend/fleetnow.htmlNEWThe merged single-file SPA — the bulk of the work.
dashboard_api_rev.py (line 53)EDITAdd fleetnow to CORS default; redeploy bridge container env + restart.
Live SPA (liveposition)REUSECopy patterns: makeArrowImage, vehicleState, live layers, showPopup + button-wiring, geocode queue, trail. renderVehicleList intentionally not reused.
Trips SPA (fleetintelligence)REUSECopy patterns: periodToRange, loadFilters/VEHICLE_META, trip line/endpoint layers, animateTrip.
Deploy notes (memory)REFdashboard-api-map-fix + prod-twala-deploy-topology: rustfs S3 endpoint, nginx-proxy + Traefik convention, "scp + docker restart" gotcha for the bridge container.

7Phased implementation roadmap

Each phase is a reviewable checkpoint. The order de-risks the hard part (the live⇄trips mode switch) by getting each half working in isolation first. Phases 0–5 are local; Phase 6 is the only one touching prod and is run by you (host is IP-whitelisted). No production push until you confirm.

0Scaffold & theme foundationLOCAL

Create frontend/fleetnow.html: shell = top bar + full-bleed map + empty floating-card slots. Warm palette :root tokens, API_BASE, EAT clock, MapLibre init centred on East Africa ([37.5,-3.0], zoom ~5.2), Carto Voyager basemap, HQ POI.

✅ Map loads, themed, no data.

1Live mode (locked look)LOCAL

Live polling (GET /webhook/live-positions, 15s), the pin marker + direction chevron + plate-tail pill, cost-centre colouring, hover popup with the locked field set + trail button, live KPIs + staleness chip, reverse-geocoding queue.

✅ Live fleet renders matching the screenshot design (warm-toned).

2Filter card (bottom-right)LOCAL

loadFilters; plate / cost-centre / time-period dropdowns (default Today; 1 week / 1 month / Custom), vehicle→cc/city auto-fill.

✅ Dropdowns populate; no behaviour wired.

3Trips mode + mode switch (core)LOCAL

mode state machine (live ⇄ trips): stopPolling → hide live layers → POST /webhook/fleet-dashboard → seq-coloured trip lines + start/end markers; floating trip list bottom-left (◀ Live head) → click row → fit + animate; top bar → trip KPIs + bookends; ● Live pill = return. Wire all three entry paths (map-dot "Show trips" button, plate dropdown, fleet-wide cc/period).

✅ Full round trip works: Live → vehicle/filter → trips + animation → back to Live.

4Polish & resilienceLOCAL

Empty/error states (no trips, feed down), transitions, responsive floating cards, attribution, offline-vehicle styling. Final pass against locked design + palette.

✅ The merged SPA is done and self-reviewed.

5Backend CORS + local integration testEDIT

Add https://fleetnow.rahamafresh.com to DASHBOARD_CORS_ORIGINS default in dashboard_api_rev.py; test locally (temp localhost origin) against the live API.

✅ Every endpoint returns 200 to the new SPA, no CORS errors.

6Deploy (you run; I prep + verify)PROD

rustfs fleetnow bucket upload, fleetnow-proxy nginx + Traefik route, DNS, docker restart dashboard_api with the new origin.

✅ fleetnow live; liveposition + fleetintelligence still load (regression check).