344 lines
28 KiB
HTML
344 lines
28 KiB
HTML
|
|
<!doctype html>
|
|||
|
|
<html lang="en">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8" />
|
|||
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|||
|
|
<title>fleetnow.rahamafresh.com — Implementation Plan v1 · 2026-06-05</title>
|
|||
|
|
<style>
|
|||
|
|
:root {
|
|||
|
|
--bg: #161a23;
|
|||
|
|
--panel: #1e232e;
|
|||
|
|
--panel-2: #232a36;
|
|||
|
|
--border: #2c333f;
|
|||
|
|
--text: #ECEFF4;
|
|||
|
|
--muted: #93a0b4;
|
|||
|
|
--accent: #E8954A; /* amber/orange */
|
|||
|
|
--accent-hover:#d97b2c;
|
|||
|
|
--live: #2dd4a7; /* teal-green */
|
|||
|
|
--offline: #b4791f;
|
|||
|
|
--warn: #f0a93b;
|
|||
|
|
--danger: #ef5b5b;
|
|||
|
|
--code-bg: #11151c;
|
|||
|
|
}
|
|||
|
|
* { box-sizing: border-box; }
|
|||
|
|
html, body { margin: 0; }
|
|||
|
|
body {
|
|||
|
|
background: var(--bg);
|
|||
|
|
color: var(--text);
|
|||
|
|
font: 15px/1.65 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|||
|
|
padding: 0 0 80px;
|
|||
|
|
}
|
|||
|
|
.wrap { max-width: 920px; margin: 0 auto; padding: 0 24px; }
|
|||
|
|
header.doc {
|
|||
|
|
border-bottom: 1px solid var(--border);
|
|||
|
|
background: linear-gradient(180deg, var(--panel) 0%, var(--bg) 100%);
|
|||
|
|
padding: 40px 0 28px;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
header.doc .wrap { padding-top: 0; padding-bottom: 0; }
|
|||
|
|
.eyebrow {
|
|||
|
|
text-transform: uppercase; letter-spacing: 2px; font-size: 11px;
|
|||
|
|
color: var(--accent); font-weight: 700; margin: 0 0 10px;
|
|||
|
|
}
|
|||
|
|
h1 {
|
|||
|
|
font-size: 30px; line-height: 1.2; margin: 0 0 14px; font-weight: 700;
|
|||
|
|
}
|
|||
|
|
h1 .url { color: var(--accent); }
|
|||
|
|
.sub { color: var(--muted); font-size: 14px; margin: 0; }
|
|||
|
|
.pills { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
|
|||
|
|
.pill {
|
|||
|
|
font-size: 12px; padding: 5px 11px; border-radius: 999px;
|
|||
|
|
border: 1px solid var(--border); background: var(--panel-2); color: var(--muted);
|
|||
|
|
}
|
|||
|
|
.pill b { color: var(--text); font-weight: 600; }
|
|||
|
|
h2 {
|
|||
|
|
font-size: 20px; margin: 38px 0 12px; padding-top: 10px;
|
|||
|
|
border-top: 1px solid var(--border); color: var(--text);
|
|||
|
|
}
|
|||
|
|
h2 .n {
|
|||
|
|
display: inline-grid; place-items: center;
|
|||
|
|
width: 26px; height: 26px; border-radius: 7px;
|
|||
|
|
background: rgba(232,149,74,.15); color: var(--accent);
|
|||
|
|
font-size: 13px; font-weight: 700; margin-right: 10px; vertical-align: 2px;
|
|||
|
|
}
|
|||
|
|
h3 { font-size: 15px; margin: 24px 0 8px; color: var(--accent); }
|
|||
|
|
p { margin: 10px 0; }
|
|||
|
|
a { color: var(--accent); }
|
|||
|
|
code {
|
|||
|
|
background: var(--code-bg); border: 1px solid var(--border);
|
|||
|
|
border-radius: 4px; padding: 1px 6px; font-size: 13px;
|
|||
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|||
|
|
color: #e6c9a8;
|
|||
|
|
}
|
|||
|
|
pre {
|
|||
|
|
background: var(--code-bg); border: 1px solid var(--border);
|
|||
|
|
border-left: 3px solid var(--accent);
|
|||
|
|
border-radius: 8px; padding: 16px 18px; overflow-x: auto;
|
|||
|
|
font-size: 12.5px; line-height: 1.55;
|
|||
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|||
|
|
color: #cdd6e4;
|
|||
|
|
}
|
|||
|
|
pre code { background: none; border: 0; padding: 0; color: inherit; }
|
|||
|
|
ul, ol { margin: 10px 0; padding-left: 24px; }
|
|||
|
|
li { margin: 6px 0; }
|
|||
|
|
strong { color: var(--text); }
|
|||
|
|
.muted { color: var(--muted); }
|
|||
|
|
.card {
|
|||
|
|
background: var(--panel); border: 1px solid var(--border);
|
|||
|
|
border-radius: 10px; padding: 18px 20px; margin: 16px 0;
|
|||
|
|
}
|
|||
|
|
.card.context { border-left: 3px solid var(--live); }
|
|||
|
|
.decisions { display: grid; gap: 10px; counter-reset: d; }
|
|||
|
|
.decision {
|
|||
|
|
background: var(--panel); border: 1px solid var(--border);
|
|||
|
|
border-radius: 8px; padding: 12px 14px 12px 46px; position: relative;
|
|||
|
|
}
|
|||
|
|
.decision::before {
|
|||
|
|
counter-increment: d; content: counter(d);
|
|||
|
|
position: absolute; left: 12px; top: 12px;
|
|||
|
|
width: 22px; height: 22px; border-radius: 50%;
|
|||
|
|
background: var(--accent); color: #1a1009; font-weight: 700; font-size: 12px;
|
|||
|
|
display: grid; place-items: center;
|
|||
|
|
}
|
|||
|
|
.decision b { color: var(--accent); }
|
|||
|
|
table { width: 100%; border-collapse: collapse; margin: 14px 0; font-size: 13.5px; }
|
|||
|
|
th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|||
|
|
th { color: var(--muted); text-transform: uppercase; font-size: 11px; letter-spacing: .6px; }
|
|||
|
|
td code { white-space: nowrap; }
|
|||
|
|
.swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px,1fr)); gap: 8px; margin: 12px 0; }
|
|||
|
|
.sw { display: flex; align-items: center; gap: 9px; font-size: 12px; padding: 7px 9px; background: var(--panel); border: 1px solid var(--border); border-radius: 7px; }
|
|||
|
|
.sw .chip { width: 22px; height: 22px; border-radius: 5px; border: 1px solid rgba(255,255,255,.15); flex: none; }
|
|||
|
|
.sw .hex { color: var(--muted); font-family: ui-monospace, monospace; font-size: 11px; }
|
|||
|
|
.ascii { font-size: 11.5px; line-height: 1.35; }
|
|||
|
|
.phases { display: grid; gap: 0; margin: 16px 0; position: relative; }
|
|||
|
|
.phase {
|
|||
|
|
position: relative; padding: 4px 0 18px 0; margin-left: 13px;
|
|||
|
|
border-left: 2px solid var(--border); padding-left: 26px;
|
|||
|
|
}
|
|||
|
|
.phase:last-child { border-left-color: transparent; }
|
|||
|
|
.ph-head { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
|
|||
|
|
.ph-head b { font-size: 15px; }
|
|||
|
|
.ph-n {
|
|||
|
|
position: absolute; left: -15px; top: 2px;
|
|||
|
|
width: 28px; height: 28px; border-radius: 50%;
|
|||
|
|
background: var(--panel-2); border: 2px solid var(--accent); color: var(--accent);
|
|||
|
|
display: grid; place-items: center; font-weight: 700; font-size: 13px;
|
|||
|
|
}
|
|||
|
|
.phase p { margin: 6px 0; }
|
|||
|
|
.ck { color: var(--live); font-size: 13px; font-weight: 500; }
|
|||
|
|
footer.doc { margin-top: 50px; padding-top: 18px; border-top: 1px solid var(--border); color: var(--muted); font-size: 12.5px; }
|
|||
|
|
.tag { display:inline-block; font-size:11px; padding:2px 8px; border-radius:5px; background:rgba(45,212,167,.14); color:var(--live); border:1px solid rgba(45,212,167,.3); }
|
|||
|
|
.tag.edit { background:rgba(240,169,59,.14); color:var(--warn); border-color:rgba(240,169,59,.3); }
|
|||
|
|
.tag.new { background:rgba(232,149,74,.16); color:var(--accent); border-color:rgba(232,149,74,.3); }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
|
|||
|
|
<header class="doc">
|
|||
|
|
<div class="wrap">
|
|||
|
|
<p class="eyebrow">Fireside Communications · Tracksolid Fleet Intelligence</p>
|
|||
|
|
<h1>Merge Live Position + Fleet Intelligence into one map<br><span class="url">fleetnow.rahamafresh.com</span></h1>
|
|||
|
|
<p class="sub">Implementation plan · v1 · 2026-06-05</p>
|
|||
|
|
<div class="pills">
|
|||
|
|
<span class="pill"><b>New SPA</b> — single self-contained HTML</span>
|
|||
|
|
<span class="pill"><b>Backend change</b> — CORS only</span>
|
|||
|
|
<span class="pill"><b>Map</b> — MapLibre GL 4.7.1</span>
|
|||
|
|
<span class="pill"><b>Theme</b> — full warm re-skin</span>
|
|||
|
|
<span class="pill"><b>Existing 2 maps</b> — kept</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<div class="wrap">
|
|||
|
|
|
|||
|
|
<h2><span class="n">0</span>Context</h2>
|
|||
|
|
<div class="card context">
|
|||
|
|
<p>The fleet team currently runs <strong>two separate MapLibre dashboards</strong> off the same read API (<code>fleetapi.rahamafresh.com</code>):</p>
|
|||
|
|
<ul>
|
|||
|
|
<li><strong>liveposition.rahamafresh.com</strong> — real-time snapshot: one dot per vehicle, coloured by cost centre, direction arrow, plate-tail label, hover popup with reverse-geocoded address, polls <code>GET /webhook/live-positions</code> every 15 s, optional 1 h trail.</li>
|
|||
|
|
<li><strong>fleetintelligence.rahamafresh.com</strong> — historical trips: pick vehicle(s)/cost-centre/city + time period, <code>POST /webhook/fleet-dashboard</code> draws seq-coloured trip lines with start/end markers and an animated dot that runs the route when a trip row is clicked.</li>
|
|||
|
|
</ul>
|
|||
|
|
<p>Operators want a <strong>single console</strong> 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 <strong>fleetnow.rahamafresh.com</strong>, fully re-skinned to the warm palette in the reference image. The existing two dashboards stay running untouched.</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<h3>Confirmed decisions</h3>
|
|||
|
|
<div class="decisions">
|
|||
|
|
<div class="decision"><b>Single-canvas mode switch.</b> Land on <strong>Live</strong>; selecting a vehicle (or applying filters) switches the map to <strong>Trips</strong> for that selection and hides the live dots; a <strong>Live</strong> button (or clearing the selection) returns to the live snapshot.</div>
|
|||
|
|
<div class="decision"><b>Filters drive fleet-wide trips.</b> Cost-centre + period (no vehicle) draws <em>all</em> matching trips, exactly like Fleet Intelligence today; clicking a single vehicle narrows to just it.</div>
|
|||
|
|
<div class="decision"><b>Keep all three</b> dashboards — fleetnow is additive.</div>
|
|||
|
|
<div class="decision"><b>Full re-skin</b> to the warm palette (dark slate base, amber/orange accent, teal-green live).</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<h2><span class="n">1</span>Key facts that shape the build</h2>
|
|||
|
|
<p><strong>No new backend logic needed.</strong> The four existing endpoints cover the whole merged UX:</p>
|
|||
|
|
<table>
|
|||
|
|
<thead><tr><th>Endpoint</th><th>Returns / source</th></tr></thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr><td><code>GET /webhook/live-positions</code></td><td>live snapshot <code>{summary, geojson}</code></td></tr>
|
|||
|
|
<tr><td><code>GET /webhook/live-positions/track</code></td><td>1 h trail (LineString Feature)</td></tr>
|
|||
|
|
<tr><td><code>GET /webhook/fleet-dashboard</code></td><td>filter options <code>{drivers, cost_centres, cities, vehicles}</code></td></tr>
|
|||
|
|
<tr><td><code>POST /webhook/fleet-dashboard</code></td><td>trips <code>{summary, geojson}</code> via <code>reporting.fn_trips_for_map(...)</code></td></tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
<p>Time presets already supported in <code>dashboard_api_rev.py:252</code> <code>_preset_to_range</code>: <code>today</code> | <code>7d</code> (="1 week") | <code>30d</code> (="1 month") | <code>custom</code>.</p>
|
|||
|
|
<p><strong>One backend change only — CORS.</strong> <code>dashboard_api_rev.py:53</code> <code>_ALLOWED_ORIGINS</code> (env <code>DASHBOARD_CORS_ORIGINS</code>) must include <code>https://fleetnow.rahamafresh.com</code>, otherwise the browser blocks every fetch.</p>
|
|||
|
|
<p>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 <code>COST_CENTRE_PALETTE</code> + <code>colorForCostCentre()</code> hash, the same <code>SEQ_PALETTE</code>/<code>seqColor()</code>, the Fireside HQ POI, the EAT clock, and <code>escapeHtml</code>. The merge is mostly composition + re-theme, not new algorithms.</p>
|
|||
|
|
|
|||
|
|
<h2><span class="n">2</span>Deliverable</h2>
|
|||
|
|
<p>A single self-contained <code>fleetnow.html</code> (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. <code>frontend/fleetnow.html</code> (new <code>frontend/</code> dir; the existing two live only in rustfs, so this also starts versioning the SPA source).</p>
|
|||
|
|
|
|||
|
|
<h3>Layout <span class="muted">(per the user's spec; palette only from the image)</span></h3>
|
|||
|
|
<p><strong>No left panel.</strong> 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).</p>
|
|||
|
|
<pre class="ascii"><code>┌───────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 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) │ │
|
|||
|
|
│ └───────────────────┘ └──────────────────────┘ │
|
|||
|
|
└───────────────────────────────────────────────────────────────────┘</code></pre>
|
|||
|
|
|
|||
|
|
<ul>
|
|||
|
|
<li><strong>Top bar</strong> swaps with mode:
|
|||
|
|
<ul>
|
|||
|
|
<li><em>Live:</em> Vehicles · Moving · Parked · Offline 24h+ · Median kmh + last-batch staleness chip (reuse live SPA's <code>applyFilters</code> summary recompute).</li>
|
|||
|
|
<li><em>Trips:</em> Trips · Distance · Driving h · Idle h · Vehicles · Drivers + the date-range / "first trip → last trip" bookend context (reuse trips SPA's <code>renderResult</code> summary block).</li>
|
|||
|
|
</ul>
|
|||
|
|
</li>
|
|||
|
|
<li><strong>Live marker + hover popup — preserve existing look & feel <span class="tag">LOCKED</span></strong> Keep the deployed live-map design: a rounded <strong>pin</strong> filled with the <strong>cost-centre colour</strong> containing a <strong>white direction chevron rotated to heading</strong>, with the <strong>last-4-of-plate</strong> in a small dark pill below the pin (e.g. <code>690F</code>, <code>453Y</code>). The hover popup keeps its exact field set + order: plate (bold) + <code>MOVING · NN KMH</code> badge, driver, <code>costcentre · city</code>, reverse-geocoded address, <code>heading NNN° · gps signal N</code>, <code>NN km on the clock</code>, <code>last fix Ns ago · timestamp</code>, <code>source <mc_type> · <device_kind></code>, and the action button. Only re-map colour tokens onto the warm theme (status green → <code>--live</code> teal, button blue → <code>--accent</code> amber); structure/feel unchanged. <span class="muted">(Ref screenshot 2026-06-05.)</span></li>
|
|||
|
|
<li><strong>Selecting a single vehicle (both paths):</strong>
|
|||
|
|
<ul>
|
|||
|
|
<li><em>Map-click:</em> click a live dot → reuse the live SPA's <code>showPopup</code>; add a <strong>"Show trips"</strong> button (same pattern as its existing "Show last 1 h trail" button) → enters Trips mode for that vehicle at the selected period.</li>
|
|||
|
|
<li><em>Plate dropdown:</em> pick the plate in the bottom-right filter card → same result.</li>
|
|||
|
|
</ul>
|
|||
|
|
</li>
|
|||
|
|
<li><strong>Floating filter card (bottom-right, always visible):</strong> Number plate (single/multi select), Cost centre, Time period (default <strong>Today</strong>). Applying a cost-centre/period (no plate) → fleet-wide Trips mode; picking a plate → that vehicle's Trips. Reuse <code>loadFilters</code>, <code>fillVehicleSelect</code>/<code>VEHICLE_META</code> auto-fill, <code>periodToRange</code> (labels: Today / 1 week / 1 month / Custom).</li>
|
|||
|
|
<li><strong>Floating trip list (bottom-left, trips mode only):</strong> a back affordance (<code>◀ Live</code>) at its head + the seq-coloured trip list (reuse <code>renderTripList</code>); clicking a row fits + animates the route (reuse <code>animateTrip</code>). Hidden in Live mode.</li>
|
|||
|
|
<li class="muted">The live "Tracked Fleet" side-list (<code>renderVehicleList</code>) is <strong>dropped</strong> — with no left panel, live vehicle selection is map-dot + plate-dropdown only.</li>
|
|||
|
|
</ul>
|
|||
|
|
|
|||
|
|
<h3>State model <span class="muted">(the merge core)</span></h3>
|
|||
|
|
<p>One <code>mode</code> variable: <code>'live'</code> | <code>'trips'</code>.</p>
|
|||
|
|
<ul>
|
|||
|
|
<li><strong>Boot →</strong> <code>mode='live'</code>. Start the 15 s poll (<code>startPolling</code>), map centred on East Africa (<code>center:[37.5,-3.0], zoom:5.2</code> so Kenya + Uganda + Mombasa + Kampala all fit). Render live layers (<code>live-body</code> circles, <code>live-arrow</code> symbols, <code>live-label</code> text — copied verbatim).</li>
|
|||
|
|
<li><strong>Enter trips</strong> (vehicle row click, or filter "apply"): <code>stopPolling()</code>, hide/remove the live source+layers (or toggle <code>visibility:none</code>), <code>POST /webhook/fleet-dashboard</code> with the selection, draw <code>trip-lines</code> + <code>trip-starts</code>/<code>trip-ends</code> (copied verbatim), populate the left trip list, swap the top bar to trip KPIs, light up the <code>[● Live]</code> pill as a clickable "return" control.</li>
|
|||
|
|
<li><strong>Return to live</strong> (<code>Live</code> pill / clear selection): cancel any animation (<code>clearAnim</code>), remove trip layers, restore live layers, <code>startPolling()</code> again.</li>
|
|||
|
|
</ul>
|
|||
|
|
<p>Keep live and trip layers as <strong>separate sources/layers</strong> toggled by visibility rather than a shared source — it mirrors how each SPA already manages its own source and avoids id collisions.</p>
|
|||
|
|
|
|||
|
|
<h3>Palette re-skin <span class="muted">(full)</span></h3>
|
|||
|
|
<p>Replace the CSS <code>:root</code> tokens. Derived from the reference image (warm dark ops console):</p>
|
|||
|
|
<div class="swatches">
|
|||
|
|
<div class="sw"><span class="chip" style="background:#161a23"></span><div><div>--bg</div><div class="hex">#161a23 base</div></div></div>
|
|||
|
|
<div class="sw"><span class="chip" style="background:#1e232e"></span><div><div>--panel</div><div class="hex">#1e232e</div></div></div>
|
|||
|
|
<div class="sw"><span class="chip" style="background:#2c333f"></span><div><div>--border</div><div class="hex">#2c333f</div></div></div>
|
|||
|
|
<div class="sw"><span class="chip" style="background:#ECEFF4"></span><div><div>--text</div><div class="hex">#ECEFF4</div></div></div>
|
|||
|
|
<div class="sw"><span class="chip" style="background:#93a0b4"></span><div><div>--muted</div><div class="hex">#93a0b4</div></div></div>
|
|||
|
|
<div class="sw"><span class="chip" style="background:#E8954A"></span><div><div>--accent</div><div class="hex">#E8954A amber</div></div></div>
|
|||
|
|
<div class="sw"><span class="chip" style="background:#2dd4a7"></span><div><div>--live</div><div class="hex">#2dd4a7 teal</div></div></div>
|
|||
|
|
<div class="sw"><span class="chip" style="background:#6b7280"></span><div><div>--parked</div><div class="hex">#6b7280</div></div></div>
|
|||
|
|
<div class="sw"><span class="chip" style="background:#b4791f"></span><div><div>--offline</div><div class="hex">#b4791f</div></div></div>
|
|||
|
|
<div class="sw"><span class="chip" style="background:#ef5b5b"></span><div><div>--danger</div><div class="hex">#ef5b5b</div></div></div>
|
|||
|
|
</div>
|
|||
|
|
<p>Keep the <strong>categorical</strong> <code>COST_CENTRE_PALETTE</code> and <code>SEQ_PALETTE</code> 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 → <code>--live</code> teal; arrows stay white-on-halo.</p>
|
|||
|
|
|
|||
|
|
<h2><span class="n">3</span>Backend change</h2>
|
|||
|
|
<p>In <code>dashboard_api_rev.py:53</code>, add <code>https://fleetnow.rahamafresh.com</code> to the <code>DASHBOARD_CORS_ORIGINS</code> 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 <strong>not</strong> Coolify-managed:</p>
|
|||
|
|
<pre><code># on twala host (user runs SSH — IP-whitelisted):
|
|||
|
|
# update DASHBOARD_CORS_ORIGINS to include fleetnow, then:
|
|||
|
|
docker restart dashboard_api</code></pre>
|
|||
|
|
|
|||
|
|
<h2><span class="n">4</span>Deployment <span class="muted">(user runs the SSH/host steps — sandbox IP isn't whitelisted)</span></h2>
|
|||
|
|
<p>Mirror the existing liveposition/fleetintelligence wiring (single <code>index.html</code> object in rustfs + nginx reverse-proxy + Traefik route on the <code>coolify</code> net + DNS):</p>
|
|||
|
|
<ol>
|
|||
|
|
<li>Create rustfs bucket <code>fleetnow</code>; upload <code>fleetnow.html</code> as <code>index.html</code> (boto3 against <code>https://s3.rahamafresh.com</code>, path-style — same method already used to repoint the other two).</li>
|
|||
|
|
<li>Add <code>fleetnow-proxy</code> nginx config (clone <code>~/fleetintelligence-proxy/nginx.conf</code>, point at <code>rustfs-...:9000/fleetnow/index.html</code>); attach Traefik labels for <code>https://fleetnow.rahamafresh.com</code> (entrypoints http/https, certresolver <code>letsencrypt</code>, <code>traefik.docker.network=coolify</code>).</li>
|
|||
|
|
<li>Ensure DNS <code>fleetnow.rahamafresh.com</code> → <code>31.97.44.246</code> (LE will issue on first hit).</li>
|
|||
|
|
<li>Apply the CORS change above + <code>docker restart dashboard_api</code>.</li>
|
|||
|
|
</ol>
|
|||
|
|
<p>In the SPA, set <code>const API_BASE = 'https://fleetapi.rahamafresh.com';</code> (rename from the legacy <code>N8N_BASE</code> for clarity).</p>
|
|||
|
|
|
|||
|
|
<h2><span class="n">5</span>Verification <span class="muted">(end to end)</span></h2>
|
|||
|
|
<ol>
|
|||
|
|
<li><strong>Local smoke:</strong> open <code>fleetnow.html</code> from disk (or <code>python -m http.server</code>). Live dots load (CORS will block until fleetnow origin is allow-listed — for local dev, temporarily add <code>http://localhost:*</code> to <code>DASHBOARD_CORS_ORIGINS</code>, or test against a curl-proxied copy).</li>
|
|||
|
|
<li><strong>API contracts unchanged</strong> (curl, already-allowed origin):
|
|||
|
|
<pre><code>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)</code></pre>
|
|||
|
|
</li>
|
|||
|
|
<li><strong>Mode switch:</strong> 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, <code>Live</code> pill appears. Click a trip row → route animates. Click <code>Live</code> → returns to live snapshot, polling resumes.</li>
|
|||
|
|
<li><strong>Fleet-wide filters:</strong> pick a cost centre + "1 week", apply → all matching trips draw; clear → back to Live.</li>
|
|||
|
|
<li><strong>Post-deploy:</strong> load <code>https://fleetnow.rahamafresh.com</code> in a browser — no CORS errors in console; existing liveposition + fleetintelligence still load (regression check).</li>
|
|||
|
|
</ol>
|
|||
|
|
|
|||
|
|
<h2><span class="n">6</span>Critical files</h2>
|
|||
|
|
<table>
|
|||
|
|
<thead><tr><th>File</th><th></th><th>Role</th></tr></thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr><td><code>frontend/fleetnow.html</code></td><td><span class="tag new">NEW</span></td><td>The merged single-file SPA — the bulk of the work.</td></tr>
|
|||
|
|
<tr><td><code>dashboard_api_rev.py</code> <span class="muted">(line 53)</span></td><td><span class="tag edit">EDIT</span></td><td>Add fleetnow to CORS default; redeploy bridge container env + restart.</td></tr>
|
|||
|
|
<tr><td>Live SPA <span class="muted">(liveposition)</span></td><td><span class="tag">REUSE</span></td><td>Copy patterns: <code>makeArrowImage</code>, <code>vehicleState</code>, live layers, <code>showPopup</code> + button-wiring, geocode queue, trail. <code>renderVehicleList</code> intentionally <em>not</em> reused.</td></tr>
|
|||
|
|
<tr><td>Trips SPA <span class="muted">(fleetintelligence)</span></td><td><span class="tag">REUSE</span></td><td>Copy patterns: <code>periodToRange</code>, <code>loadFilters</code>/<code>VEHICLE_META</code>, trip line/endpoint layers, <code>animateTrip</code>.</td></tr>
|
|||
|
|
<tr><td>Deploy notes <span class="muted">(memory)</span></td><td><span class="tag">REF</span></td><td><code>dashboard-api-map-fix</code> + <code>prod-twala-deploy-topology</code>: rustfs S3 endpoint, nginx-proxy + Traefik convention, "scp + docker restart" gotcha for the bridge container.</td></tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
|
|||
|
|
<h2><span class="n">7</span>Phased implementation roadmap</h2>
|
|||
|
|
<p>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. <strong>Phases 0–5 are local; Phase 6 is the only one touching prod</strong> and is run by you (host is IP-whitelisted). No production push until you confirm.</p>
|
|||
|
|
|
|||
|
|
<div class="phases">
|
|||
|
|
<div class="phase">
|
|||
|
|
<div class="ph-head"><span class="ph-n">0</span><b>Scaffold & theme foundation</b><span class="tag new">LOCAL</span></div>
|
|||
|
|
<p>Create <code>frontend/fleetnow.html</code>: shell = top bar + full-bleed map + empty floating-card slots. Warm palette <code>:root</code> tokens, <code>API_BASE</code>, EAT clock, MapLibre init centred on East Africa (<code>[37.5,-3.0]</code>, zoom ~5.2), Carto Voyager basemap, HQ POI.</p>
|
|||
|
|
<p class="ck">✅ Map loads, themed, no data.</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="phase">
|
|||
|
|
<div class="ph-head"><span class="ph-n">1</span><b>Live mode (locked look)</b><span class="tag new">LOCAL</span></div>
|
|||
|
|
<p>Live polling (<code>GET /webhook/live-positions</code>, 15s), the pin marker + direction chevron + plate-tail pill, cost-centre colouring, hover popup with the <strong>locked field set</strong> + trail button, live KPIs + staleness chip, reverse-geocoding queue.</p>
|
|||
|
|
<p class="ck">✅ Live fleet renders matching the screenshot design (warm-toned).</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="phase">
|
|||
|
|
<div class="ph-head"><span class="ph-n">2</span><b>Filter card (bottom-right)</b><span class="tag new">LOCAL</span></div>
|
|||
|
|
<p><code>loadFilters</code>; plate / cost-centre / time-period dropdowns (default <strong>Today</strong>; 1 week / 1 month / Custom), vehicle→cc/city auto-fill.</p>
|
|||
|
|
<p class="ck">✅ Dropdowns populate; no behaviour wired.</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="phase">
|
|||
|
|
<div class="ph-head"><span class="ph-n">3</span><b>Trips mode + mode switch <span class="muted" style="font-weight:400">(core)</span></b><span class="tag new">LOCAL</span></div>
|
|||
|
|
<p><code>mode</code> state machine (<code>live ⇄ trips</code>): <code>stopPolling</code> → hide live layers → <code>POST /webhook/fleet-dashboard</code> → seq-coloured trip lines + start/end markers; floating trip list bottom-left (<code>◀ Live</code> head) → click row → fit + animate; top bar → trip KPIs + bookends; <code>● Live</code> pill = return. Wire <strong>all three entry paths</strong> (map-dot "Show trips" button, plate dropdown, fleet-wide cc/period).</p>
|
|||
|
|
<p class="ck">✅ Full round trip works: Live → vehicle/filter → trips + animation → back to Live.</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="phase">
|
|||
|
|
<div class="ph-head"><span class="ph-n">4</span><b>Polish & resilience</b><span class="tag new">LOCAL</span></div>
|
|||
|
|
<p>Empty/error states (no trips, feed down), transitions, responsive floating cards, attribution, offline-vehicle styling. Final pass against locked design + palette.</p>
|
|||
|
|
<p class="ck">✅ The merged SPA is done and self-reviewed.</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="phase">
|
|||
|
|
<div class="ph-head"><span class="ph-n">5</span><b>Backend CORS + local integration test</b><span class="tag edit">EDIT</span></div>
|
|||
|
|
<p>Add <code>https://fleetnow.rahamafresh.com</code> to <code>DASHBOARD_CORS_ORIGINS</code> default in <code>dashboard_api_rev.py</code>; test locally (temp <code>localhost</code> origin) against the live API.</p>
|
|||
|
|
<p class="ck">✅ Every endpoint returns 200 to the new SPA, no CORS errors.</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="phase">
|
|||
|
|
<div class="ph-head"><span class="ph-n">6</span><b>Deploy <span class="muted" style="font-weight:400">(you run; I prep + verify)</span></b><span class="tag" style="background:rgba(239,91,91,.14);color:var(--danger);border-color:rgba(239,91,91,.3)">PROD</span></div>
|
|||
|
|
<p>rustfs <code>fleetnow</code> bucket upload, <code>fleetnow-proxy</code> nginx + Traefik route, DNS, <code>docker restart dashboard_api</code> with the new origin.</p>
|
|||
|
|
<p class="ck">✅ fleetnow live; liveposition + fleetintelligence still load (regression check).</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<footer class="doc">
|
|||
|
|
Generated 2026-06-05 · Plan v1 · fleetnow.rahamafresh.com · No production push without explicit confirmation.
|
|||
|
|
</footer>
|
|||
|
|
|
|||
|
|
</div>
|
|||
|
|
</body>
|
|||
|
|
</html>
|