The tickets code escaped HTML, but the logistics + fuel renderers and the error
banners interpolated API strings straight into innerHTML. Fuel Log fields
(driver, department, fuel_type, plate) come from WhatsApp messages and
vehicle/driver names from the Tracksolid registry — both user-controlled — so
this was a stored-XSS path into every dispatcher's browser.
- Hoist escapeHtml into HELPERS + add esc(); route every logistics/fuel renderer
and the three error banners through it (21 -> 37 escaped call sites).
- SRI integrity + crossorigin on Chart.js 4.4.1 and maplibre-gl 4.7.1 JS/CSS.
- Caddyfile: CSP (self + pinned CDNs + CARTO basemap + the two fleet APIs),
X-Content-Type-Options, Referrer-Policy, frame-ancestors 'none', -Server.
Validated with `caddy validate` inside the deployed image.
- loadLive(): check r.ok; pause the 15s live poll while hidden or off the Tickets
tab, refresh immediately on return (FO-BUG-01/02).
- Missing-API_BASE fallback flipped staging -> prod, matching the documented
design (FO-OPS-01).
Inline app script passes `node --check`. Audit + plan + work log in docs/260702_*.
Local only; not deployed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The INC/CRQ overview is now a centered "today" snapshot — header + metric tiles
centered. Removed the overview filter row (Cluster/Status/Window/Apply/refresh):
the Ticket explorer below carries the ad-hoc filtering, so it was redundant.
incQs() is fixed to window=today; dropped initIncDropdowns + the removed listeners.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Last updated <ingested_at> EAT" readout under the ticket-explorer Time filter
(right-aligned, its own line) so data freshness is visible while searching, for both
INC and CRQ. Also fix renderIncMetrics to read freshness[DS] instead of the hardcoded
freshness.inc, so the overview "updated" stamp reflects the active dataset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a sub-tab bar under the Tickets tab (INC | CRQ). A DS dataset variable repoints
the dashboard/search/filter-options calls to /webhook/${DS}-* and updates the overview
/ map / legend labels; switching resets the per-dataset dropdowns + explorer and
reloads. Map, SLA legend, popups and the vehicle overlay are dataset-agnostic, so CRQ
renders "just like INC" off reporting.fn_crq_* (fleettickets migration 16).
NOT pushed yet — pushing staging auto-deploys; holding until the CRQ API + DB land.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Engineer is now a <select> of all engineers; Ticket ID is an input with a
datalist of open ticket ids (pick an open ticket, or type any id for substring
search across history). Both populated once from GET /webhook/inc-filter-options;
cluster select also filled from the full cluster list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the five static bottom panels (closures-daily, closures-by-engineer,
by-status, cluster tables) with a queryable Ticket explorer: filter bar (Ticket
ID, Engineer, Cluster, State [Closed/Open/All], Time [today/week/month/all/
custom]) + a results table (ticket, status, cluster·location, engineer, when,
SLA pill, MTTR). Clicking a row flies the map to that ticket and opens its popup;
rows with no geom are flagged non-locatable. Backed by GET /webhook/inc-search
(fn_inc_search). EAT preset ranges resolved client-side.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Midnight reset for ALL today-windowed categories: the dashboard now refetches
when the EAT calendar day changes (60s check), so closed pins/counts, the
day-total, closures-daily and closures-by-engineer all reset for the new day
even on a screen left open overnight (previously only reset on manual Refresh).
- Markers enlarged (open icon-size max 1.0 -> 1.25, closed 0.78 -> 1.0) — bigger
than before but still more compact than the old teardrops.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Top bar: new lead KPI "Total today (open + closed)" = open_now + closed_in_window.
At midnight closed=0 so it shows the start-of-day backlog, then tracks the day's
full workload as tickets close.
- Markers: replace the ticket-stub with a squircle + lightning-bolt (incident/fault
feel; distinct from round vehicle markers), drawn via Path2D.
- SLA palette + labels: Out of SLA = strong maroon red (#A01830), At risk = deep
yellow (#E0A800), Within SLA = deep purple (#6B21A8); legend/popups relabelled
"Out of SLA / At risk / Within SLA". Closed stays a pastel of the same colour.
Daily reset is inherent: the closed layer is windowed to today (closed_at >= today),
so after midnight it empties and the map starts with open tickets by live SLA.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the canvas teardrop pin with an admission-ticket silhouette (side
perforation notches + a dashed tear line, tapering to a bottom point that
anchors on the location) drawn via Path2D at 4x (pixelRatio 4). Keeps every
existing rule: fill = SLA colour, vivid = open / pastel = closed, white outline,
bottom anchor, and the fan-out declutter. Markers now read clearly as tickets
and are distinct from the round vehicle markers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New "Closures by engineer" leaderboard panel (metrics.by_owner): engineer,
closed, breached, avg MTTR. Clicking an engineer toggles a drill-down that
filters the closed map pins to only their closures (and ensures the closed
layer is visible).
- Popups now carry the details dispatchers need: open popups show location_name
and the true coordinates (copyable; the fan-out keeps the real coord even when
the pin is offset); closed popups show "closed by <engineer>".
Backed by fn_inc_dashboard migration 12 (owner case-normalized server-side).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cluster-centroid tickets share an exact coordinate, so the open layer sat on
top of the faded closed pins (23 of 25 closed points were occluded) — the new
closed colouring was correct but invisible. Now pins sharing a coordinate are
fanned into a small deterministic sunflower spiral around it, computed across
BOTH layers so an open and a closed ticket at the same centroid separate.
Active (vivid) and closed (faded) now both show. Also default the Closed layer
on. Offsets are cosmetic (cluster centroids are already approximate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously closed INC tickets all used one flat slate pin, losing the SLA
outcome and reading as inactive/uninformative. Now a closed ticket keeps its
SLA colour but as a light ('pastel') version — Breached → light red, Compliant
→ light green — so active tickets stay vivid and closed ones are a washed-out
same-hue. Makes active-vs-closed and SLA outcome apparent across both layers.
- Faded closed pin images keyed on sla_status (Compliant|Breached, + fallback).
- inc-closed layer matches sla_status → faded pin; popup badge + legend updated
(new "Closed SLA" key); header subtitle reworded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Vertically centre the cluster/status/window filters against the metric tiles
in the INC overview row.
- Overlay only ISP cost-centre vehicles (the teams handling INC); other cost
centres are filtered out. Layer count + labels updated to "ISP vehicles".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fold the filter controls into the INC overview card, right-aligned next to the
metric tiles, and remove the standalone filter bar — better visual balance
(fills the previously empty right side of the overview row).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove the redundant header KPI strip on the Tickets tab (metrics live in the
INC overview card); header stays empty there.
- Enlarge the map (62vh, min 520px) and size the grid cards to their content
(#tk-main align-items:start) so there's no stretched empty space under the
Closures / By-status cards.
- Split the single 26-row "By cluster" list into "Clusters — Nairobi" and
"Clusters — Mombasa / Voi", classified by cluster name (coast keyword set);
bottom row is now four compact span3 cards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render INC tickets as canvas-drawn teardrop map pins via MapLibre symbol layers
(scales to thousands of closed features, unlike DOM markers):
- Open pins coloured by SLA state; larger than the old circles for hierarchy.
- Closed pins use a single muted slate colour irrespective of status (the only
distinction that matters once closed), slightly smaller + under the open layer.
- Legend/popup closed swatch aligned to the new closed colour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Late async loaders (notably the boot-time logistics loadAll) called their KPI
renderer unconditionally, overwriting the shared header strip after the user had
switched to another tab. Guard renderKpis / renderFuelKpis / renderIncKpis so each
only updates the header when its own tab is active.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Overhaul the Tickets tab into the documented INC operations dashboard,
backed by the new GET /webhook/inc-dashboard endpoint (reporting.fn_inc_dashboard):
- Filterbar (cluster / status / window: today|week|month|custom)
- Metric strip: open now, closed in window, open/closed SLA breakdown,
avg MTTR, closures/day + freshness
- Live map: open INC coloured by SLA state, dimmed closed overlay,
FleetNow vehicle markers, layer toggles + SLA legend
- By-status / by-cluster tables + daily closures chart
- Data load decoupled from the basemap so the dashboard renders even if
WebGL/map init is slow or fails
Removes the old combined INC/CRQ map and the /webhook/tickets call (CRQ deferred).
Adds docs/tickets-inc-{overhaul-plan,implementation-guide}.{md,html}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New tab backed by dashboard_api /analytics/fuel-fills(+/recent): KPI strip
(litres, KES spend, fills, KES/L, vehicles), spend+litres daily trend, per-vehicle
table (incl. km/L), by-department breakdown, recent fills. Shares the filter state
plus new department/fuel-type dropdowns; lazy-loads on first open. Recent-fills
time renders the Africa/Nairobi wall-clock value directly (no double tz shift).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #view-tickets is now a MapLibre map: live vehicle DOM markers (ported from
FleetNow) + INC (red) / CRQ (blue) ticket circle layers from /webhook/tickets
- Layers toggle with counts; open/all status filter; lazy-init + map.resize()
- Header KPI strip shows INC/CRQ open + vehicles/moving on the Tickets tab
- Logistics analytics tab unchanged
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Wrap the existing analytics dashboard as the Logistics tab
- Add a scaffolded Tickets tab (per-tab KPIs, recent-tickets + by-status
cards, informative empty state)
- Shared header KPI strip swaps per tab; tickets load lazily on first open
- Ticket data source left as a dashboard_api integration point — no S3
credentials embedded in the static SPA
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs/webhook-auto-deploy.html — step-by-step for push->webhook->Coolify->Traefik
auto-deploy: Coolify Gitea webhook URL/secret, the Forgejo webhook, verifying
deliveries, the staging/prod model, and a troubleshooting table (Auto Deploy
under Advanced, port 80, branch, domain typo, secret). Not served (Dockerfile
copies only src/); repo reference doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auto-deploy verified (push->Forgejo webhook->Coolify->Traefik). Reverting the
test meta marker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Harmless <meta> marker to confirm a push to staging triggers a Coolify
redeploy. Safe to revert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Caddy `templates` only processes text/html + text/plain by default, so the
text/javascript env.js was served with {{env "API_BASE"}} unevaluated (a literal
that's also a JS parse error). Name the JS MIME types in the templates directive
so the API_BASE injection works — required for prod to point at the prod API
(staging worked only via the index.html fallback).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>