Passthrough over reporting.fn_inc_filter_options (fleettickets migration 14):
engineers, clusters, open ticket ids for the ticket-explorer dropdowns.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Thin passthrough over reporting.fn_inc_search (fleettickets migration 13):
ad-hoc ticket lookup by ticket_id / owner / cluster / status / state / time for
the FleetOps ticket explorer. Mirrors /webhook/inc-dashboard (read-only conn,
jsonb passthrough); validates state in {open,closed,all} and ISO-8601 from/to.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Thin passthrough over reporting.fn_inc_dashboard (fleettickets migration 09),
powering the FleetOps live INC map (open + windowed-closed + metrics). Mirrors
/webhook/tickets for auth/connection and returns the function's jsonb unchanged.
Params: cluster, status, window (today|week|month|custom), from/to (custom).
Validates window enum, custom-requires-from/to, ISO-8601 from/to, and from<to
(400 on failure). Matches docs/dashboard-api-contract.md from the fleettickets repo.
Verified end-to-end against the prod DB (read-only): default/week/cluster
filters return correct payloads; bad window and custom-without-bounds → 400.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_preset_to_range only special-cased today/30d/custom and fell through to 7d for
everything else, so the Fuel Log tab's default "90d" silently returned 7 days.
Parse any positive `Nd` preset into a today-(N-1)..today window. Backward-
compatible: today/30d/custom and the 7d default are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reads reporting.v_fuel_fills / v_fuel_efficiency (owned by the fleetfuel module,
which ingests the rustfs `fuel` bucket). Adds GET /analytics/fuel-fills (totals,
per-vehicle rows incl. km/L, by_department, daily trend, unmatched-plate status)
and /analytics/fuel-fills/recent, reusing _analytics_window + _dim_filters.
Extends /analytics/filters with departments + fuel_types, savepoint-guarded so a
not-yet-migrated v_fuel_fills can't break the existing trips dropdowns.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `tickets` schema (INC/CRQ map), its migrations, and the ingest/geocode loader
moved to their own repo: repo.rahamafresh.com/kianiadee/fleettickets.git.
- remove migrations 21-23 and tools/import_tickets.py
- run_migrations.py: drop the 21-23 entries (fleettickets owns them now)
- dashboard_api keeps GET /webhook/tickets, calling reporting.fn_tickets_for_map
which fleettickets defines
- move geocoder env-var docs to fleettickets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Collapse the backend from 7 Coolify services to 4 app services + the DB.
- Merge ingest_movement + ingest_events into a single ingest_worker:
split each poller's main() into reusable startup_catchup()/register_jobs()
and drive both from one schedule loop in new ingest_worker_rev.py
(standalone entrypoints retained for local debug).
- docker-compose.yaml: replace the two poller services with ingest_worker;
remove the pgbouncer service (dormant; transaction-mode pooling is unsafe
for the advisory-lock'd v_trips refresher) and the grafana service +
grafana-data volume (redundant with the FleetOps SPA).
- Add reporting.v_ingest_health (migration 19) + dashboard_api GET
/health/ingest as the pipeline-freshness surface that replaces Grafana's
health panels.
webhook_receiver stays isolated so a poller fault can't drop inbound pushes.
timescale_db and db_backup are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enables stage-2: the prod dashboard_api request pool connects as the READ-ONLY
dashboard_ro role (DATABASE_URL) while the v_trips refresher keeps a privileged
connection via REFRESH_DATABASE_URL (falls back to DATABASE_URL when unset, so
single-role/staging deploys are unchanged). Avoids the FIX-D02 trap (a read-only
role cannot REFRESH).
Adds deploy_dashboard_api.sh (the prod bridge deploy, now version-controlled):
strips inherited DATABASE_URL, sets REFRESH_DATABASE_URL=<app role> +
DATABASE_URL=dashboard_ro, CORS incl. fleetops.rahamafresh.com.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the read-only /analytics/* surface the FleetOps SPA will consume, plus
the migration that backs the fuel roll-up. All endpoints SELECT the indexed
reporting.* / tracksolid.v_* views and never write, so the forthcoming staging
instance can serve them against the prod DB as grafana_ro.
dashboard_api_rev.py:
- GET /analytics/fleet-summary per-vehicle + per-cost-centre roll-up
- GET /analytics/utilisation per-vehicle utilisation + daily fleet trend
- GET /analytics/driver-behaviour per-driver speeding / harsh index
- GET /analytics/fuel actual vs estimated litres (data-gated flags)
- GET /analytics/filters dropdown options (alias of GET /webhook/fleet-dashboard)
- responses run through jsonable_encoder (Decimal->float, date->ISO)
- VTRIPS_REFRESH_INTERVAL_S<=0 now DISABLES the v_trips refresher, so a
read-only staging instance never attempts REFRESH (prod still owns it).
migrations/17_fleetops_fuel_view.sql:
- reporting.v_fuel_daily encapsulates the v_trips->devices join (so the
read-only role needs SELECT only on the view) and grants it to grafana_ro.
Registered 17 in run_migrations.py. Note: live migration head is 16, not 13
as CLAUDE.md implies. Endpoints are unit-compilable but untested live until
the staging bridge (Phase 1) exists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The merged FleetNow dashboard (separate repo, Coolify) reads this read-API, so
its origin must be in DASHBOARD_CORS_ORIGINS. Added to the code default; live
config is set via the env in ~/deploy_dashboard_api.sh on the host.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Fleet Trips dashboard reads reporting.v_trips (a materialized view).
Its refresh was a scheduled n8n workflow; when n8n was retired the matview
froze (last refresh 2026-06-01) so the dashboard showed no recent trips
even though tracksolid.trips kept ingesting live.
Move the refresh into the owned stack: a background loop in dashboard_api
runs REFRESH MATERIALIZED VIEW CONCURRENTLY reporting.v_trips every
VTRIPS_REFRESH_INTERVAL_S (default 300s). Safe across uvicorn --workers
via a pg advisory lock (one worker refreshes per tick); runs in a thread
so the ~9s refresh never blocks the event loop; logs to
reporting.refresh_log (source='dashboard_api') for continuity. Uses a
dedicated autocommit connection because REFRESH ... CONCURRENTLY cannot
run inside a transaction block.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Fleet Trips SPA posts application/x-www-form-urlencoded, but the
POST /webhook/fleet-dashboard handler read the body with request.json().
That threw on every request, the except swallowed it to body={}, and all
filters (vehicle_numbers, cost_centre, assigned_city) plus period/dates
were dropped — so every query returned the full unfiltered fleet (1,266
trips) regardless of the dropdowns. The map/KPIs/trips never changed,
which read as "the dropdowns don't work."
Parse by Content-Type: urllib.parse.parse_qs for form bodies (no new
dependency — avoids python-multipart), JSON still accepted defensively
for n8n-compat callers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Live Positions SPA calls GET /webhook/live-positions/track, but the
read-API only exposed /webhook/vehicle-track. Clicking a vehicle to view its
1-hour trail therefore 404'd even after repointing N8N_BASE. Register the SPA's
actual path as a route alias to the same handler (vehicle-track kept as alias),
so the only frontend change remains the base URL. Docstring updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
n8n was a thin HTTP->SQL proxy for the Live Position and Fleet Trips maps and
proved fragile (credential reloads, :latest drift, shared connection limits).
This service calls the same proven reporting.* functions directly, reusing the
existing psycopg2 pool / Docker image / Coolify deploy.
Endpoints mirror the n8n webhook paths so the only frontend change is N8N_BASE:
GET /webhook/live-positions -> {summary, geojson} (fn_live_positions)
GET /webhook/vehicle-track -> GeoJSON Feature (fn_vehicle_track)
GET /webhook/fleet-dashboard -> filter options
POST /webhook/fleet-dashboard -> trips payload (fn_trips_for_map)
Response shapes replicate the n8n "Build response JSON" nodes exactly; empty
filters/sentinels ('', null, undefined) normalize to SQL wildcards. CORS limited
to the dashboard origins. Added dashboard_api service to docker-compose (port
8890, Coolify-routed). SQL contracts validated against prod.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>