2026-06-10 10:00:31 +00:00
|
|
|
# FleetOps — static analytics SPA served by Caddy.
|
|
|
|
|
# Traefik (via Coolify) terminates TLS, so Caddy is a plain :80 file server.
|
|
|
|
|
# The only moving part is runtime API-base injection: Caddy's `templates`
|
|
|
|
|
# directive evaluates {{env "API_BASE"}} inside /env.js at request time, so the
|
|
|
|
|
# SAME image serves staging (fleetapi.fivetitude.com) and prod
|
|
|
|
|
# (fleetapi.rahamafresh.com) — set API_BASE per Coolify app.
|
|
|
|
|
:80 {
|
|
|
|
|
root * /srv
|
|
|
|
|
encode zstd gzip
|
|
|
|
|
|
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 06:47:40 +00:00
|
|
|
# Security headers (FO-SEC-03). CSP allows self + the two pinned CDNs, the
|
|
|
|
|
# CARTO basemap (styles/tiles/fonts) and the fleet APIs; SRI in index.html
|
|
|
|
|
# pins the CDN payloads themselves. frame-ancestors 'none' = no clickjacking.
|
|
|
|
|
# script-src keeps 'unsafe-inline' because the whole app is one inline
|
|
|
|
|
# <script> block — the CSP's job here is limiting external script origins
|
|
|
|
|
# and exfil targets (connect-src), not inline policy.
|
|
|
|
|
header {
|
|
|
|
|
X-Content-Type-Options "nosniff"
|
|
|
|
|
Referrer-Policy "strict-origin-when-cross-origin"
|
|
|
|
|
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com; style-src 'self' 'unsafe-inline' https://unpkg.com; img-src 'self' data: blob: https://*.cartocdn.com; font-src https://*.cartocdn.com; connect-src 'self' https://fleetapi.rahamafresh.com https://fleetapi.fivetitude.com https://*.cartocdn.com; worker-src blob:; frame-ancestors 'none'"
|
|
|
|
|
-Server
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 10:00:31 +00:00
|
|
|
# Health endpoint for Coolify / Traefik probes.
|
|
|
|
|
handle /healthz {
|
|
|
|
|
respond "ok" 200
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Runtime config: Caddy renders {{env "API_BASE"}} into env.js. Never cache it.
|
2026-06-10 10:23:26 +00:00
|
|
|
# `templates` only acts on text/html + text/plain by default, so JS responses
|
|
|
|
|
# are skipped unless their MIME type is named explicitly here.
|
2026-06-10 10:00:31 +00:00
|
|
|
handle /env.js {
|
|
|
|
|
header Cache-Control "no-store"
|
2026-06-10 10:23:26 +00:00
|
|
|
templates {
|
|
|
|
|
mime text/javascript application/javascript
|
|
|
|
|
}
|
2026-06-10 10:00:31 +00:00
|
|
|
file_server
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# SPA: never cache the shell, fall back to index.html for client routes.
|
|
|
|
|
handle {
|
|
|
|
|
header /index.html Cache-Control "no-store"
|
|
|
|
|
try_files {path} /index.html
|
|
|
|
|
file_server
|
|
|
|
|
}
|
|
|
|
|
}
|