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>
Migration 11 was applied out of order on 2026-06-10 (it had never been recorded
applied on prod — the reporting objects were hand-created, then migrations 14/15/16
modified them). Re-running 11 recreated reporting.v_live_positions and
reporting.fn_live_positions at their BASE definitions; 15/16 were skipped as
already-applied, so the live map silently lost migration 15's cost-centre exclusion
(personal/management/mtn) and migration 16's vehicle_type/fleet_segment GeoJSON
fields — the live-map vehicle count jumped 74 -> 80.
Migration 20 re-asserts both objects' intended final definitions (verbatim union of
15 + 16). As the highest-numbered migration it always runs last, so the correct
state wins regardless of apply order. Already hot-fixed on prod by re-running 15+16;
this captures it durably. Idempotent.
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>
migrations/18_grant_reporting_ro.sql — grants USAGE + SELECT on the reporting.*
layer to grafana_ro, with DEFAULT PRIVILEGES so future reporting views are
auto-readable. grafana_ro is the read-only role the staging dashboard_api
connects as; it read tracksolid.* but never reporting.* (the prod dashboard_api
uses the app role), surfacing as "permission denied for view v_filter_drivers /
v_daily_summary" on the staging /analytics/* endpoints. Read-only only — no
write/REFRESH. Registered 18 in run_migrations.py.
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>
fn_live_positions now emits 'vehicle_type' (devices.vehicle_models) and
'fleet_segment' (reporting.fn_fleet_segment) in each GeoJSON feature so FleetNow
can give specialist vehicles (Crane/Motorbike/Pick-Up) their own marker icons.
Additive only — no signature change, STABLE function read immediately by
dashboard_api (no redeploy). Function body reproduced verbatim from prod via
pg_get_functiondef plus the two new properties.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hide personal + management + mtn (Uganda/Kampala) vehicles from the live tracking
map (FleetNow + liveposition SPA). Adds an ops-editable config table
reporting.map_excluded_cost_centres and filters reporting.v_live_positions to drop
any plate whose device(s) carry an excluded cost centre (robust to the tracker/cam
cost_centre inconsistency).
Scope is live-map only; reporting.v_trips (trip history) is intentionally untouched.
The base view feeds reporting.fn_live_positions, so the change propagates to every
live consumer with no dashboard_api redeploy or frontend change. Verified live:
80 -> 74 vehicles, all 6 targets gone (KDU 613A, KDW 781E, UMA 011EK/382EK/418EK/826AB).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add reporting.fn_fleet_segment() and reporting.v_vehicles, splitting the fleet
into ticket-closing field_service vs specialist plant (crane/pick-up/motorbike)
that does not close immediate customer tickets.
The segment is DERIVED from tracksolid.devices.vehicle_models — itself an
authoritative Tracksolid API field (sync_devices maps jimi.user.device.list ->
vehicleModels) — so it stays API-current with no re-seeding; the manual
vehicle_category column is intentionally unused. v_vehicles collapses the
tracker+dashcam device pairs to one row per vehicle by reusing
reporting.normalize_plate() and the same primary-device precedence as
reporting.v_trips / v_live_positions (auto-merges 'KDS 453Y'/'KDS 453 Y',
resolves within-plate model conflicts via the primary tracker).
Verified live: 80 vehicles (61 field_service / 16 specialist / 3 unassigned),
grafana_ro granted. Includes the supporting data-quality report.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the dormant ops (workshop / tickets / dispatch / SLA / odometer)
and dwh_gold (nightly ETL aggregates) schemas plus their dependents —
features never implemented, no live writer or scheduled refresh.
- Prod DB (already applied): DROP SCHEMA ops/dwh_gold CASCADE, plus
tracksolid.dispatch_log, v_sla_inflight, v_utilisation_daily.
- migrations/12_drop_ops.sql + 13_drop_dwh_gold.sql (forward, all
IF EXISTS) registered in run_migrations.py for rebuild durability.
- grafana: removed 8 now-broken panels (In-flight SLA, Idle Cost,
Utilisation Heatmap, Row 7 Field-Service SLAs) from daily_operations;
panel count 21 -> 13.
- docs: scrubbed CLAUDE.md, PLATFORM_OVERVIEW.html (-19KB), DATA_FLOW.md;
pre-drop seed snapshot in docs/reports/260605_ops_purge_backup.md.
The separate tracksolid_dwh server (31.97.44.246:5888) is unrelated
and untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reporting schema (fn_live_positions/fn_vehicle_track/fn_trips_for_map,
the v_trips materialized view + indexes, filter/summary views, refresh_log)
backs the dashboard_api map endpoints but existed only on the prod DB, in no
migration — a rebuild would have lost it. Captured the live DDL into
migrations/11_reporting_schema.sql (idempotent: IF NOT EXISTS / CREATE OR
REPLACE, search_path set for unqualified base-table refs, guarded grants) and
registered it in run_migrations.py. Verified it applies cleanly against prod
inside a rolled-back transaction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>