No description
Find a file
david kiania a0022fbeaf fix(security): escape API strings, pin CDN scripts, add CSP (FO-SEC-01/02/03)
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>
2026-07-02 09:47:40 +03:00
docs fix(security): escape API strings, pin CDN scripts, add CSP (FO-SEC-01/02/03) 2026-07-02 09:47:40 +03:00
src fix(security): escape API strings, pin CDN scripts, add CSP (FO-SEC-01/02/03) 2026-07-02 09:47:40 +03:00
.dockerignore feat: FleetOps analytics SPA (Caddy) — initial scaffold 2026-06-10 13:00:31 +03:00
.gitignore feat: FleetOps analytics SPA (Caddy) — initial scaffold 2026-06-10 13:00:31 +03:00
Caddyfile fix(security): escape API strings, pin CDN scripts, add CSP (FO-SEC-01/02/03) 2026-07-02 09:47:40 +03:00
Dockerfile feat: FleetOps analytics SPA (Caddy) — initial scaffold 2026-06-10 13:00:31 +03:00
README.md feat: FleetOps analytics SPA (Caddy) — initial scaffold 2026-06-10 13:00:31 +03:00

FleetOps

Fleet operations analytics for the Tracksolid fleet — fuel, utilisation, distance and driver behaviour. Sibling to FleetNow (live tracking); reuses the same warm-dark ops palette so the two feel like one product.

This is a self-contained single-page app (src/index.html, inline CSS/JS + Chart.js from CDN) served by Caddy. It reads the FleetOps analytics API (the dashboard_api /analytics/* endpoints).

Architecture

fleetops.fivetitude.com   (staging)  ─┐
fleetops.rahamafresh.com  (prod)      ─┼─► this SPA (Caddy :80, behind Traefik/Coolify)
                                        └─► API_BASE → dashboard_api /analytics/*
  • Web server: Caddy. Traefik (via Coolify) terminates TLS, so Caddy is a plain :80 file server. See Caddyfile.
  • Per-environment API base: Caddy's templates directive renders {{env "API_BASE"}} into /env.js at request time, so the same image serves staging and prod — just set the API_BASE env var on each Coolify app:
    • staging → https://fleetapi.fivetitude.com
    • prod → https://fleetapi.rahamafresh.com
    • if unset, index.html falls back to the staging API.

Endpoints consumed

Endpoint Panel
GET /analytics/filters cost-centre / city dropdowns
GET /analytics/fleet-summary KPI strip + per-vehicle table
GET /analytics/utilisation distance & idle daily-trend chart
GET /analytics/driver-behaviour driver leaderboard
GET /analytics/fuel fuel panel (data-gated)

The SPA's origin must be in the API's DASHBOARD_CORS_ORIGINS (staging API already allows fleetops.fivetitude.com).

Deploy

Coolify app (Dockerfile → Caddy), one per environment, bound to a branch:

Env Domain Branch API_BASE
staging fleetops.fivetitude.com staging https://fleetapi.fivetitude.com
prod fleetops.rahamafresh.com main https://fleetapi.rahamafresh.com

Promotion: feature → staging (auto-deploys staging via Forgejo webhook) → main (auto-deploys prod).

Local

docker build -t fleetops . && docker run --rm -p 8080:80 -e API_BASE=https://fleetapi.fivetitude.com fleetops
# open http://localhost:8080  (live data needs the localhost origin in the API CORS allowlist)