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>
56 lines
2.5 KiB
SQL
56 lines
2.5 KiB
SQL
-- 17_fleetops_fuel_view.sql
|
|
-- FleetOps fuel roll-up source: reporting.v_fuel_daily.
|
|
--
|
|
-- Backs GET /analytics/fuel in dashboard_api_rev.py (the FleetOps SPA). It pairs
|
|
-- ACTUAL fuel (trips.fuel_consumed_l, from the /pushtripreport webhook) with an
|
|
-- ESTIMATED figure (distance_km * devices.fuel_100km / 100) so the SPA can show
|
|
-- both and flag the gap.
|
|
--
|
|
-- Why a view (not a direct join in the API): it encapsulates the
|
|
-- reporting.v_trips -> tracksolid.devices join so the read-only staging role only
|
|
-- needs SELECT on this one reporting.* object, not on tracksolid.devices. It reuses
|
|
-- the same per-trip grain + is_meaningful_route filter as the other reporting
|
|
-- summaries (migration 11), and the same imei key v_trips already exposes.
|
|
--
|
|
-- Data state (2026-06-10): devices.fuel_100km is NULL fleet-wide and the /pushoil
|
|
-- + /pushobd webhooks are unregistered, so estimated_fuel_l is NULL today and
|
|
-- actual_fuel_l is sparse. The view is correct now and fills in as data lands —
|
|
-- the API surfaces availability flags rather than faking numbers. Fuel-cost
|
|
-- monetisation is intentionally absent: ops.cost_rates was purged 2026-06-05
|
|
-- (migration 12).
|
|
--
|
|
-- CREATE OR REPLACE + guarded grant -> safe to re-apply.
|
|
|
|
SET search_path = reporting, tracksolid, public;
|
|
|
|
CREATE OR REPLACE VIEW reporting.v_fuel_daily AS
|
|
SELECT t.trip_date,
|
|
t.vehicle_number,
|
|
t.cost_centre,
|
|
t.assigned_city,
|
|
t.assigned_driver,
|
|
t.imei,
|
|
t.distance_km,
|
|
t.fuel_consumed_l AS actual_fuel_l,
|
|
CASE
|
|
WHEN d.fuel_100km IS NOT NULL AND t.distance_km IS NOT NULL
|
|
THEN round(t.distance_km * d.fuel_100km / 100.0, 3)
|
|
ELSE NULL::numeric
|
|
END AS estimated_fuel_l
|
|
FROM reporting.v_trips t
|
|
LEFT JOIN tracksolid.devices d ON d.imei = t.imei
|
|
WHERE t.is_meaningful_route;
|
|
|
|
COMMENT ON VIEW reporting.v_fuel_daily IS
|
|
'Per-trip fuel: actual (trips.fuel_consumed_l) vs estimated (distance_km * devices.fuel_100km/100). '
|
|
'Source for dashboard_api GET /analytics/fuel. Encapsulates the v_trips->devices join so the '
|
|
'read-only staging role needs SELECT only on this view. fuel_100km is NULL fleet-wide as of 2026-06-10.';
|
|
|
|
-- ── grants (guarded: roles may not exist on a fresh DB) ───────────────────────
|
|
DO $grants$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN
|
|
GRANT USAGE ON SCHEMA reporting TO grafana_ro;
|
|
GRANT SELECT ON reporting.v_fuel_daily TO grafana_ro;
|
|
END IF;
|
|
END $grants$;
|