Compare commits
2 commits
8d1f40de1c
...
5f24c158e2
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f24c158e2 | |||
|
|
85d02c81a5 |
5 changed files with 1140 additions and 30 deletions
348
07_analytics_views.sql
Normal file
348
07_analytics_views.sql
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
-- 07_analytics_views.sql
|
||||
-- Analytics views backing the Daily Operations dashboard.
|
||||
-- Each view wraps a query block from 01_BusinessAnalytics.md; the section
|
||||
-- is recorded in COMMENT ON VIEW so debuggers can trace any panel back to
|
||||
-- its spec with a single \d+ command.
|
||||
--
|
||||
-- All views are regular (not materialised). v_driver_aggregates_daily is
|
||||
-- the only one with performance risk as position_history grows; convert
|
||||
-- it to a TimescaleDB continuous aggregate once the hypertable exceeds
|
||||
-- ~100k rows.
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- v_fleet_today
|
||||
-- One row per device with today's operational roll-up.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE VIEW tracksolid.v_fleet_today AS
|
||||
WITH today AS (
|
||||
SELECT (NOW() AT TIME ZONE 'Africa/Nairobi')::date AS d
|
||||
),
|
||||
trips_today AS (
|
||||
SELECT
|
||||
t.imei,
|
||||
COUNT(*) AS trips,
|
||||
SUM(t.distance_km) AS km,
|
||||
SUM(t.driving_time_s)::numeric / 3600 AS drive_hours,
|
||||
SUM(t.idle_time_s)::numeric / 3600 AS idle_hours,
|
||||
MIN(t.start_time AT TIME ZONE 'Africa/Nairobi')::time AS first_departure,
|
||||
MAX(COALESCE(t.end_time, t.start_time) AT TIME ZONE 'Africa/Nairobi')::time AS last_return
|
||||
FROM tracksolid.trips t
|
||||
CROSS JOIN today
|
||||
WHERE (t.start_time AT TIME ZONE 'Africa/Nairobi')::date = today.d
|
||||
GROUP BY t.imei
|
||||
),
|
||||
alarms_today AS (
|
||||
SELECT
|
||||
a.imei,
|
||||
COUNT(*) AS alarm_count
|
||||
FROM tracksolid.alarms a
|
||||
CROSS JOIN today
|
||||
WHERE (a.alarm_time AT TIME ZONE 'Africa/Nairobi')::date = today.d
|
||||
GROUP BY a.imei
|
||||
)
|
||||
SELECT
|
||||
d.imei,
|
||||
d.driver_name,
|
||||
d.vehicle_number,
|
||||
d.vehicle_name,
|
||||
COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city,
|
||||
d.enabled_flag,
|
||||
COALESCE(tt.km, 0) AS km_today,
|
||||
COALESCE(tt.trips, 0) AS trips_today,
|
||||
COALESCE(tt.drive_hours, 0) AS drive_hours,
|
||||
COALESCE(tt.idle_hours, 0) AS idle_hours,
|
||||
tt.first_departure,
|
||||
tt.last_return,
|
||||
COALESCE(at.alarm_count, 0) AS alarms_today,
|
||||
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_fix,
|
||||
lp.speed AS last_speed,
|
||||
(tt.imei IS NULL) AS did_not_move
|
||||
FROM tracksolid.devices d
|
||||
LEFT JOIN trips_today tt ON tt.imei = d.imei
|
||||
LEFT JOIN alarms_today at ON at.imei = d.imei
|
||||
LEFT JOIN tracksolid.live_positions lp ON lp.imei = d.imei;
|
||||
|
||||
COMMENT ON VIEW tracksolid.v_fleet_today IS
|
||||
'01_BusinessAnalytics.md §9 Fleet Readiness Scorecard. One row per device with today''s roll-up.';
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- v_vehicles_not_moved_today
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE VIEW tracksolid.v_vehicles_not_moved_today AS
|
||||
SELECT
|
||||
d.imei,
|
||||
d.vehicle_name,
|
||||
d.vehicle_number,
|
||||
d.driver_name,
|
||||
COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city,
|
||||
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_seen,
|
||||
lp.speed
|
||||
FROM tracksolid.devices d
|
||||
LEFT JOIN tracksolid.live_positions lp ON lp.imei = d.imei
|
||||
LEFT JOIN tracksolid.trips t
|
||||
ON t.imei = d.imei
|
||||
AND (t.start_time AT TIME ZONE 'Africa/Nairobi')::date
|
||||
= (NOW() AT TIME ZONE 'Africa/Nairobi')::date
|
||||
WHERE d.enabled_flag = 1
|
||||
AND t.imei IS NULL;
|
||||
|
||||
COMMENT ON VIEW tracksolid.v_vehicles_not_moved_today IS
|
||||
'01_BusinessAnalytics.md §2.3. Enabled vehicles with zero trips today.';
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- v_active_dispatch_map
|
||||
-- Every enabled device with current position, colour-coded status for geomap.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE VIEW tracksolid.v_active_dispatch_map AS
|
||||
SELECT
|
||||
d.imei,
|
||||
d.vehicle_number,
|
||||
d.vehicle_name,
|
||||
d.driver_name,
|
||||
d.driver_phone,
|
||||
COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city,
|
||||
lp.lat,
|
||||
lp.lng,
|
||||
lp.speed,
|
||||
lp.direction,
|
||||
lp.acc_status,
|
||||
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_fix,
|
||||
CASE
|
||||
WHEN lp.gps_time IS NULL THEN 'never_reported'
|
||||
WHEN lp.gps_time < NOW() - INTERVAL '10 minutes' THEN 'stale'
|
||||
WHEN COALESCE(lp.speed, 0) > 5 THEN 'moving'
|
||||
WHEN lp.acc_status = '1' THEN 'idle_ignition_on'
|
||||
ELSE 'parked'
|
||||
END AS status
|
||||
FROM tracksolid.devices d
|
||||
LEFT JOIN tracksolid.live_positions lp ON lp.imei = d.imei
|
||||
WHERE d.enabled_flag = 1;
|
||||
|
||||
COMMENT ON VIEW tracksolid.v_active_dispatch_map IS
|
||||
'01_BusinessAnalytics.md §4.3 All Active Vehicles Map. Geomap source.';
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- v_currently_idle
|
||||
-- Engine on, speed ~0, fresh fix. "Currently burning fuel while parked."
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE VIEW tracksolid.v_currently_idle AS
|
||||
SELECT
|
||||
d.imei,
|
||||
d.vehicle_number,
|
||||
d.driver_name,
|
||||
COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city,
|
||||
lp.lat,
|
||||
lp.lng,
|
||||
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS since,
|
||||
EXTRACT(EPOCH FROM (NOW() - lp.gps_time))::int AS idle_seconds
|
||||
FROM tracksolid.devices d
|
||||
JOIN tracksolid.live_positions lp ON lp.imei = d.imei
|
||||
WHERE d.enabled_flag = 1
|
||||
AND lp.acc_status = '1'
|
||||
AND COALESCE(lp.speed, 0) < 2
|
||||
AND lp.gps_time > NOW() - INTERVAL '15 minutes';
|
||||
|
||||
COMMENT ON VIEW tracksolid.v_currently_idle IS
|
||||
'01_BusinessAnalytics.md §2.2 idle lens. Engine on, speed <2 km/h, fix in last 15m.';
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- v_driver_aggregates_daily
|
||||
-- Per-driver per-day km + speeding + harsh-driving aggregates.
|
||||
-- TODO: Convert to TimescaleDB continuous aggregate once position_history > 100k rows.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE VIEW tracksolid.v_driver_aggregates_daily AS
|
||||
WITH trips_agg AS (
|
||||
SELECT
|
||||
imei,
|
||||
(start_time AT TIME ZONE 'Africa/Nairobi')::date AS day,
|
||||
SUM(distance_km) AS km,
|
||||
COUNT(*) AS trips
|
||||
FROM tracksolid.trips
|
||||
WHERE start_time > NOW() - INTERVAL '31 days'
|
||||
GROUP BY imei, (start_time AT TIME ZONE 'Africa/Nairobi')::date
|
||||
),
|
||||
speeding AS (
|
||||
SELECT
|
||||
imei,
|
||||
(gps_time AT TIME ZONE 'Africa/Nairobi')::date AS day,
|
||||
COUNT(*) FILTER (WHERE speed > 80) AS events_80,
|
||||
COUNT(*) FILTER (WHERE speed > 100) AS events_100,
|
||||
COUNT(*) FILTER (WHERE speed > 120) AS events_120
|
||||
FROM tracksolid.position_history
|
||||
WHERE gps_time > NOW() - INTERVAL '31 days'
|
||||
AND speed IS NOT NULL
|
||||
GROUP BY imei, (gps_time AT TIME ZONE 'Africa/Nairobi')::date
|
||||
),
|
||||
harsh_raw AS (
|
||||
SELECT
|
||||
imei,
|
||||
gps_time,
|
||||
speed,
|
||||
LAG(speed) OVER (PARTITION BY imei ORDER BY gps_time) AS prev_speed,
|
||||
LAG(gps_time) OVER (PARTITION BY imei ORDER BY gps_time) AS prev_time
|
||||
FROM tracksolid.position_history
|
||||
WHERE source = 'track_list'
|
||||
AND gps_time > NOW() - INTERVAL '31 days'
|
||||
),
|
||||
harsh AS (
|
||||
SELECT
|
||||
imei,
|
||||
(gps_time AT TIME ZONE 'Africa/Nairobi')::date AS day,
|
||||
COUNT(*) AS harsh_events
|
||||
FROM harsh_raw
|
||||
WHERE ABS(speed - prev_speed) > 30
|
||||
AND EXTRACT(EPOCH FROM (gps_time - prev_time)) BETWEEN 5 AND 60
|
||||
GROUP BY imei, (gps_time AT TIME ZONE 'Africa/Nairobi')::date
|
||||
)
|
||||
SELECT
|
||||
d.imei,
|
||||
d.driver_name,
|
||||
d.vehicle_number,
|
||||
COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city,
|
||||
t.day,
|
||||
COALESCE(t.km, 0) AS km,
|
||||
COALESCE(t.trips, 0) AS trips,
|
||||
COALESCE(s.events_80, 0) AS events_80,
|
||||
COALESCE(s.events_100, 0) AS events_100,
|
||||
COALESCE(s.events_120, 0) AS events_120,
|
||||
COALESCE(h.harsh_events, 0) AS harsh_events,
|
||||
ROUND(COALESCE(s.events_80, 0) / NULLIF(t.km, 0) * 100, 2) AS speeding_per_100km,
|
||||
ROUND(COALESCE(h.harsh_events, 0)/ NULLIF(t.km, 0) * 100, 2) AS harsh_per_100km
|
||||
FROM trips_agg t
|
||||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||||
LEFT JOIN speeding s ON s.imei = t.imei AND s.day = t.day
|
||||
LEFT JOIN harsh h ON h.imei = t.imei AND h.day = t.day;
|
||||
|
||||
COMMENT ON VIEW tracksolid.v_driver_aggregates_daily IS
|
||||
'01_BusinessAnalytics.md §3.1 (speeding) + §3.2 (harsh driving). Daily grain; panels window via $__timeFilter(day).';
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- v_fleet_km_daily
|
||||
-- Fleet-wide daily distance, broken by assigned_city.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE VIEW tracksolid.v_fleet_km_daily AS
|
||||
SELECT
|
||||
(t.start_time AT TIME ZONE 'Africa/Nairobi')::date AS day,
|
||||
COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city,
|
||||
SUM(t.distance_km) AS km,
|
||||
COUNT(DISTINCT t.imei) AS active_vehicles,
|
||||
COUNT(*) AS trips
|
||||
FROM tracksolid.trips t
|
||||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||||
WHERE t.start_time > NOW() - INTERVAL '90 days'
|
||||
GROUP BY 1, 2;
|
||||
|
||||
COMMENT ON VIEW tracksolid.v_fleet_km_daily IS
|
||||
'01_BusinessAnalytics.md §7 Panel 5 Distance Trend. City-cohort cut built in.';
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- v_alarms_daily
|
||||
-- Daily alarm counts by alarm_name for the time-series panel.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE VIEW tracksolid.v_alarms_daily AS
|
||||
SELECT
|
||||
(alarm_time AT TIME ZONE 'Africa/Nairobi')::date AS day,
|
||||
COALESCE(alarm_name, alarm_type, 'unknown') AS alarm_name,
|
||||
COUNT(*) AS alarm_count
|
||||
FROM tracksolid.alarms
|
||||
WHERE alarm_time > NOW() - INTERVAL '90 days'
|
||||
GROUP BY 1, 2;
|
||||
|
||||
COMMENT ON VIEW tracksolid.v_alarms_daily IS
|
||||
'01_BusinessAnalytics.md §7 Panel 7 Alarm Frequency. Stacked-by-alarm_name time series.';
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- v_utilisation_daily
|
||||
-- Per-vehicle per-day utilisation from dwh_gold.fact_daily_fleet_metrics.
|
||||
-- Empty until dwh_gold.refresh_daily_metrics nightly ETL is scheduled.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE VIEW tracksolid.v_utilisation_daily AS
|
||||
SELECT
|
||||
f.day,
|
||||
d.imei,
|
||||
d.vehicle_number,
|
||||
d.driver_name,
|
||||
COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city,
|
||||
f.total_distance_km,
|
||||
f.total_drive_hours,
|
||||
f.total_idle_hours,
|
||||
f.alarm_count,
|
||||
f.overspeed_count,
|
||||
ROUND(
|
||||
f.total_drive_hours / NULLIF(f.total_drive_hours + f.total_idle_hours, 0) * 100,
|
||||
1
|
||||
) AS utilisation_pct
|
||||
FROM dwh_gold.fact_daily_fleet_metrics f
|
||||
JOIN dwh_gold.dim_vehicles dv ON dv.vehicle_key = f.vehicle_key
|
||||
JOIN tracksolid.devices d ON d.imei = dv.imei;
|
||||
|
||||
COMMENT ON VIEW tracksolid.v_utilisation_daily IS
|
||||
'01_BusinessAnalytics.md §7 Panel 8 Utilisation Heatmap. Empty until nightly ETL runs.';
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- v_sla_inflight
|
||||
-- In-flight field-service tickets with SLA timers, joined to dispatch_log.
|
||||
-- Empty until ops.tickets is populated from Zoho/Freshdesk or equivalent.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE VIEW tracksolid.v_sla_inflight AS
|
||||
SELECT
|
||||
t.ticket_id,
|
||||
t.customer,
|
||||
t.priority,
|
||||
t.job_type,
|
||||
t.status,
|
||||
t.created_at,
|
||||
t.assigned_at,
|
||||
t.closed_at,
|
||||
t.assigned_imei,
|
||||
COALESCE(dl.driver_name, t.driver_name) AS driver_name,
|
||||
dl.first_movement_at,
|
||||
dl.on_site_at,
|
||||
dl.resolved_at,
|
||||
EXTRACT(EPOCH FROM (COALESCE(dl.first_movement_at, NOW()) - t.assigned_at)) / 60 AS dispatch_mins,
|
||||
EXTRACT(EPOCH FROM (COALESCE(dl.on_site_at, NOW()) - dl.first_movement_at)) / 60 AS enroute_mins,
|
||||
EXTRACT(EPOCH FROM (COALESCE(dl.resolved_at, NOW()) - dl.on_site_at)) / 60 AS onsite_mins,
|
||||
EXTRACT(EPOCH FROM (COALESCE(dl.resolved_at, NOW()) - t.created_at)) / 60 AS resolution_mins,
|
||||
CASE
|
||||
WHEN dl.resolved_at IS NOT NULL THEN 'resolved'
|
||||
WHEN dl.cancelled_at IS NOT NULL THEN 'cancelled'
|
||||
WHEN dl.on_site_at IS NOT NULL THEN 'on_site'
|
||||
WHEN dl.first_movement_at IS NOT NULL THEN 'en_route'
|
||||
WHEN t.assigned_at IS NOT NULL THEN 'dispatched'
|
||||
ELSE 'open'
|
||||
END AS ticket_stage
|
||||
FROM ops.tickets t
|
||||
LEFT JOIN tracksolid.dispatch_log dl ON dl.ticket_id = t.ticket_id
|
||||
WHERE t.status NOT IN ('resolved', 'cancelled')
|
||||
OR t.closed_at > NOW() - INTERVAL '24 hours';
|
||||
|
||||
COMMENT ON VIEW tracksolid.v_sla_inflight IS
|
||||
'01_BusinessAnalytics.md §4.5 Field-Service SLA Metrics. Open tickets + last 24h resolved.';
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Read access for Grafana
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
GRANT USAGE ON SCHEMA ops TO grafana_ro;
|
||||
|
||||
GRANT SELECT ON tracksolid.v_fleet_today TO grafana_ro;
|
||||
GRANT SELECT ON tracksolid.v_vehicles_not_moved_today TO grafana_ro;
|
||||
GRANT SELECT ON tracksolid.v_active_dispatch_map TO grafana_ro;
|
||||
GRANT SELECT ON tracksolid.v_currently_idle TO grafana_ro;
|
||||
GRANT SELECT ON tracksolid.v_driver_aggregates_daily TO grafana_ro;
|
||||
GRANT SELECT ON tracksolid.v_fleet_km_daily TO grafana_ro;
|
||||
GRANT SELECT ON tracksolid.v_alarms_daily TO grafana_ro;
|
||||
GRANT SELECT ON tracksolid.v_utilisation_daily TO grafana_ro;
|
||||
GRANT SELECT ON tracksolid.v_sla_inflight TO grafana_ro;
|
||||
|
||||
COMMIT;
|
||||
72
CLAUDE.md
72
CLAUDE.md
|
|
@ -55,8 +55,7 @@ See `docs/CONNECTIONS.md` for the full shape. Summary:
|
|||
|
||||
- **SSH:** `ssh -i ~/.ssh/id_ed25519 kianiadee@stage.rahamafresh.com`
|
||||
- **DB name:** `tracksolid_db` · **DB user:** `postgres` (internal) · `tracksolid_owner` (app) · `grafana_ro` (read-only)
|
||||
- **DB schema:** `tracksolid_2` (current live data, legacy stack) · `tracksolid` (new stack target, currently empty) · `infrastructure` · `dwh_gold` (aggregates)
|
||||
- **⚠ Schema split:** new ingestion code targets `tracksolid`; all live rows are in `tracksolid_2` until the new stack is deployed and `sync_driver_audit.py` has run.
|
||||
- **DB schemas:** `tracksolid` (live, single source of truth) · `dwh_gold` (aggregates) · `ops` (workshop / tickets / odometer) · `infrastructure`. The legacy `tracksolid_2` schema no longer exists — migrations 02–06 applied 2026-04-18.
|
||||
- **DB access:** `DATABASE_URL` points to `timescale_db:5432` (internal Docker network — not reachable locally). Use `docker exec` pattern above. See `docs/CONNECTIONS.md` for full reference.
|
||||
- **Container naming:** Coolify appends a random suffix. Always resolve with:
|
||||
```bash
|
||||
|
|
@ -96,29 +95,45 @@ tracksolidApiDocumentation.md # API endpoint reference
|
|||
## 5. Database Schema — Key Tables
|
||||
|
||||
```sql
|
||||
tracksolid_2.devices -- LIVE registry (63 AT4-series devices, 353549* IMEIs, 0 driver names)
|
||||
-- NB: has `assigned_team` (not cost_centre), `city` (not assigned_city)
|
||||
tracksolid_2.live_positions -- LIVE positions (19 rows, all stale since 6 Apr 2026)
|
||||
tracksolid_2.ingestion_log -- LIVE pipeline audit trail
|
||||
tracksolid.devices -- Target registry (empty — new stack not yet deployed)
|
||||
tracksolid.live_positions -- Target positions (empty — new stack not yet deployed)
|
||||
tracksolid.position_history -- All GPS fixes (hypertable, partitioned by gps_time)
|
||||
tracksolid.devices -- Device / driver / vehicle registry (63 rows; 0 driver_name populated)
|
||||
-- IMEI mix: 353549* AT4 (23), 862798* X3/JC400P (23), 865135* X3/JC400P (10), 359857* (7)
|
||||
-- Full CSV (144 devices) not yet imported — run import_drivers_csv.py --apply
|
||||
tracksolid.live_positions -- Current fix per IMEI (19 rows; refreshed every 60s by ingest_movement)
|
||||
tracksolid.position_history -- All GPS fixes (hypertable, partitioned by gps_time). ~519 rows (308 track_list + 211 poll).
|
||||
-- pg_stat_user_tables shows 0 for hypertables — always COUNT(*) directly.
|
||||
-- source: 'poll' (60s sweep) | 'track_list' (30m high-res)
|
||||
tracksolid.trips -- Trip summaries: distance_km, driving_time_s, avg/max speed
|
||||
tracksolid.parking_events -- Stop events with duration and address
|
||||
tracksolid.alarms -- Alarm events (alarm_type, alarm_name, alarm_time)
|
||||
tracksolid.parking_events -- Stop events with duration and address (0 rows — endpoint returning empty)
|
||||
tracksolid.alarms -- Alarm events (alarm_type, alarm_name, alarm_time) — 10 rows, polling healthy
|
||||
tracksolid.obd_readings -- OBD diagnostics (push only, awaiting webhook registration)
|
||||
tracksolid.device_events -- Power on/off tamper events
|
||||
tracksolid.ingestion_log -- API call audit trail per endpoint
|
||||
tracksolid.dispatch_log -- Dispatch decisions for SLA tracking (migration 06)
|
||||
dwh_gold.fact_daily_fleet_metrics -- Nightly ETL aggregates per vehicle per day
|
||||
tracksolid.device_events -- Power on/off tamper events (push only)
|
||||
tracksolid.ingestion_log -- API call audit trail — 875 runs / 24h, 0 failures at last check (2026-04-19)
|
||||
tracksolid.dispatch_log -- Dispatch decisions for SLA tracking (migration 06; empty until ops integration)
|
||||
tracksolid.schema_migrations -- Applied files: 02,03,04,05,06 (last 06 on 2026-04-18)
|
||||
dwh_gold.fact_daily_fleet_metrics -- Nightly ETL aggregates per vehicle per day (run refresh_daily_metrics)
|
||||
ops.service_log -- Workshop service history (migration 06)
|
||||
ops.odometer_readings -- Physical odometer captures (migration 06)
|
||||
ops.tickets -- Ticket skeleton for ops integration (migration 06)
|
||||
ops.tickets -- Ticket skeleton for ops integration (migration 06; empty)
|
||||
```
|
||||
|
||||
Full DDL: `02_tracksolid_full_schema_rev.sql` + migrations `03`–`06`.
|
||||
|
||||
**Analytics views (migration `07_analytics_views.sql`)** — one view per BA-file query block, readable by `grafana_ro`:
|
||||
|
||||
```sql
|
||||
tracksolid.v_fleet_today -- §9 per-vehicle today roll-up
|
||||
tracksolid.v_vehicles_not_moved_today -- §2.3 alert source
|
||||
tracksolid.v_active_dispatch_map -- §4.3 geomap source
|
||||
tracksolid.v_currently_idle -- §2.2 idle lens
|
||||
tracksolid.v_driver_aggregates_daily -- §3.1 + §3.2 aggression index source
|
||||
tracksolid.v_fleet_km_daily -- §7 Panel 5 distance trend
|
||||
tracksolid.v_alarms_daily -- §7 Panel 7 alarm frequency
|
||||
tracksolid.v_utilisation_daily -- §7 Panel 8 utilisation heatmap (gated on dwh_gold ETL)
|
||||
tracksolid.v_sla_inflight -- §4.5 SLA panels (gated on ops.tickets)
|
||||
```
|
||||
|
||||
All views carry a `COMMENT ON VIEW` referencing their spec — `\d+ tracksolid.v_*` shows the provenance.
|
||||
|
||||
---
|
||||
|
||||
## 6. API Critical Facts
|
||||
|
|
@ -166,20 +181,22 @@ Full DDL: `02_tracksolid_full_schema_rev.sql` + migrations `03`–`06`.
|
|||
7. **Secrets from env only.** Connection strings, API keys, and passwords live in `.env`. Reference variable names from `docs/CONNECTIONS.md`, never values.
|
||||
8. **Two developers, one incoming.** Write code and docs that a second developer (mixed technical/operations background) can follow without prior context.
|
||||
9. **Forgejo API auth:** credentials stored in macOS keychain. Retrieve with `git credential fill` (host=repo.rahamafresh.com). Use basic auth against `https://repo.rahamafresh.com/api/v1` directly — no `tea` or `gh` needed.
|
||||
10. **Check active schema before querying.** Always verify which schema holds live data (`tracksolid` vs `tracksolid_2`) before running or writing queries. Until new stack deploys, live data is in `tracksolid_2`.
|
||||
10. **Single live schema.** All live data lives in `tracksolid`. Aggregates live in `dwh_gold`; workshop/ticket integrations live in `ops`. Do not reintroduce references to the retired `tracksolid_2` schema.
|
||||
|
||||
---
|
||||
|
||||
## 9. Fleet State (as of 2026-04-18)
|
||||
## 9. Fleet State (as of 2026-04-19)
|
||||
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| Registered devices (live DB) | 63 AT4-series (`353549*` IMEIs) in `tracksolid_2` |
|
||||
| Devices in CSV (not yet in DB) | 144 X3/JC400P (`865135*`, `862798*` IMEIs) |
|
||||
| Driver names populated | 0 — run `import_drivers_csv.py --apply` after new stack deployed |
|
||||
| Live positions | 19 (all stale — last fix 6 Apr 2026) |
|
||||
| Trips recorded | 5 (12.8 km total, 4–6 Apr 2026 only) |
|
||||
| Pipeline status | Stopped 6 Apr 2026 — 401 token expiry (fixed in `ts_shared_rev.py`, deploy new stack) |
|
||||
| Registered devices (`tracksolid.devices`) | 63 total — 23 × `353549*` (AT4), 23 × `862798*` + 10 × `865135*` (X3/JC400P), 7 × `359857*` |
|
||||
| Devices in CSV not yet imported | 144 (X3/JC400P); `import_drivers_csv.py --apply` will upsert names + plates |
|
||||
| Driver names populated | 0 / 63 — pending CSV import |
|
||||
| Live positions | 19 (latest fix 2026-04-19 10:25 UTC) |
|
||||
| Trips recorded | 8 (latest 2026-04-19 08:34 UTC) |
|
||||
| Alarms recorded | 10 (latest 2026-04-19 09:15 UTC) |
|
||||
| `position_history` rows | 519 (308 `track_list` + 211 `poll`); hypertable stats don't update in `pg_stat_user_tables` — query directly |
|
||||
| Pipeline status | Running healthy: 875 runs / 24h, 0 failures across all six endpoints |
|
||||
| Cities active | Nairobi (primary), Mombasa (deploying), Kampala (4 devices in CSV) |
|
||||
| Service flags | KDK 829A GP (239,264 km), Belta KCU-647D (235,000 km) |
|
||||
|
||||
|
|
@ -191,12 +208,11 @@ Latest full snapshot: `260412_baseline_report.md`
|
|||
|
||||
| Priority | Item |
|
||||
|---|---|
|
||||
| HIGH | Deploy new ingestion stack (see §8 Step 0 in `01_BusinessAnalytics.md` for full sequence) |
|
||||
| HIGH | Run `import_drivers_csv.py --apply` after stack deployed (144 devices, names + plates ready) |
|
||||
| HIGH | Run `import_drivers_csv.py --apply` — 144 X3/JC400P devices with names + plates waiting |
|
||||
| HIGH | Register webhooks: `/pushobd` `/pushoil` `/pushtem` `/pushlbs` `/pushevent` |
|
||||
| HIGH | Investigate X3-63282 in Kampala — legitimate or unauthorised? |
|
||||
| MEDIUM | Set `fuel_100km` per vehicle type to activate fuel cost calculations |
|
||||
| MEDIUM | Investigate 44 silent devices — SIM installed? Activated? |
|
||||
| MEDIUM | Investigate 44 silent devices (only 19 of 63 reporting) — SIM installed? Activated? |
|
||||
| MEDIUM | Co-develop client KPI framework (see `docs/KPI_FRAMEWORK.md`) |
|
||||
| LOW | Populate geofences — depot boundaries, city zones |
|
||||
| LOW | Run nightly ETL: `SELECT dwh_gold.refresh_daily_metrics(CURRENT_DATE - 1)` |
|
||||
| LOW | Schedule nightly ETL: `SELECT dwh_gold.refresh_daily_metrics(CURRENT_DATE - 1)` (cron or n8n) |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
# Daily Operations Dashboard — Design Spec
|
||||
|
||||
**Date:** 2026-04-19
|
||||
**Author:** David Kiania / Claude
|
||||
**Status:** Approved for implementation
|
||||
**Target:** `stage.rahamafresh.com` · `tracksolid_db` · `tracksolid` schema
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Extend the Grafana deployment with a second dashboard — **Daily Operations — Fleet & Dispatch** — that surfaces the `[DASHBOARD]`-tagged queries from `01_BusinessAnalytics.md`. Leaves the existing NOC Live dashboard untouched.
|
||||
|
||||
Two lenses combined in one dashboard:
|
||||
|
||||
- **Live Dispatch** — active map, currently-idle vehicles, vehicles-not-moved-today, in-flight SLAs.
|
||||
- **§7 Blueprint Panels 3–8** — daily KPI table, driver leaderboard, distance trend, idle cost, alarm frequency, utilisation heatmap.
|
||||
|
||||
Refresh cadence is 1 min. The existing `noc_fleet_dashboard.json` keeps its 30s refresh and its NOC folder.
|
||||
|
||||
## 2. Scope
|
||||
|
||||
### In scope
|
||||
- New dashboard JSON: `grafana/provisioning/dashboards-json/daily_operations_dashboard.json`.
|
||||
- New migration: `07_analytics_views.sql` — nine views in `tracksolid.*` wrapping the BA-file queries.
|
||||
- Provisioning config update: add a second dashboard provider (or extend the existing one) so Grafana picks up the new dashboard on container start.
|
||||
- CLAUDE.md corrections (stale schema + fleet state facts).
|
||||
|
||||
### Out of scope
|
||||
- Populating `ops.tickets` data (blocks SLA panels — they ship empty).
|
||||
- Scheduling the nightly ETL (`dwh_gold.refresh_daily_metrics`) — utilisation heatmap renders empty until it runs.
|
||||
- Any change to the NOC Live dashboard.
|
||||
- Any ingestion code changes.
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
Panels read **only** from views in `tracksolid.*`. No inline SQL in dashboards beyond a `SELECT * FROM tracksolid.v_* WHERE $__timeFilter(day) AND $city_filter`.
|
||||
|
||||
Rationale: `01_BusinessAnalytics.md` §0 says "build panels against the tag, not the SQL text." Views give a single edit point when a metric definition changes.
|
||||
|
||||
Views are **regular** (not materialised). `v_driver_aggregates_daily` is the one with performance risk once `position_history` grows; a `TODO` comment marks the spot for future conversion to a TimescaleDB continuous aggregate.
|
||||
|
||||
## 4. Views
|
||||
|
||||
All created in schema `tracksolid`, owned by `postgres`, `SELECT` granted to `grafana_ro`. Every view carries a `COMMENT ON VIEW` pointing back to the BA-file section.
|
||||
|
||||
| View | Grain | Source | BA § |
|
||||
|---|---|---|---|
|
||||
| `v_fleet_today` | imei × today | `devices`, `trips`, `live_positions`, `alarms` | §9 |
|
||||
| `v_vehicles_not_moved_today` | imei | `devices`, `trips`, `live_positions` | §2.3 |
|
||||
| `v_active_dispatch_map` | imei | `live_positions`, `devices` | §4.3 |
|
||||
| `v_currently_idle` | imei | `live_positions`, `devices` | §2.2 |
|
||||
| `v_driver_aggregates_daily` | imei × day | `position_history`, `trips`, `devices` | §3.1 + §3.2 |
|
||||
| `v_fleet_km_daily` | city × day | `trips`, `devices` | §7 P5 |
|
||||
| `v_alarms_daily` | day × alarm_name | `alarms` | §7 P7 |
|
||||
| `v_utilisation_daily` | imei × day | `dwh_gold.fact_daily_fleet_metrics`, `dwh_gold.dim_vehicles`, `devices` | §7 P8 |
|
||||
| `v_sla_inflight` | ticket | `ops.tickets`, `dispatch_log` | §4.5 |
|
||||
|
||||
## 5. Dashboard
|
||||
|
||||
**File:** `grafana/provisioning/dashboards-json/daily_operations_dashboard.json`
|
||||
**UID:** `daily-ops`
|
||||
**Refresh:** `1m`
|
||||
**Default time range:** `now/d` → `now` (Africa/Nairobi)
|
||||
|
||||
### Variables
|
||||
- `$city` — multi-select, query `SELECT DISTINCT COALESCE(assigned_city, 'unassigned') FROM tracksolid.devices ORDER BY 1`. Default All.
|
||||
- `$active_only` — boolean, default true. Filters `devices.enabled_flag = 1` when true.
|
||||
|
||||
### Layout
|
||||
|
||||
**Freshness banner (top, full width):**
|
||||
- Last GPS fix across all devices; green if < 5 min, amber 5–30 min, red > 30 min.
|
||||
|
||||
**Row 1 — Today at a Glance** (6 stats across)
|
||||
- Vehicles reporting today · Fleet km today · Drive hours · Idle hours · Open alarms (24h) · In-flight SLA jobs.
|
||||
|
||||
**Row 2 — Live Dispatch**
|
||||
- Active Vehicles Map (geomap) · Currently Idle Vehicles (table) · Vehicles Not Moved Today (table).
|
||||
|
||||
**Row 3 — Daily KPI Table**
|
||||
- One row per vehicle: Vehicle · Driver · Km Today · Trips · Drive h · Idle h · First Departure · Last Return · Alarms.
|
||||
|
||||
**Row 4 — Driver Behaviour Leaderboard** (30-day)
|
||||
- Columns: Driver · Vehicle · Km · Speeding/100km · Harsh/100km · Late starts · After-hours trips.
|
||||
- Red/amber/green thresholds per BA-file §3.1 and §3.2 bands.
|
||||
|
||||
**Row 5 — Trends**
|
||||
- Distance Trend 7-day (time series, stacked by city).
|
||||
- Alarm Frequency 30-day (bar chart, stacked by alarm_name).
|
||||
|
||||
**Row 6 — Efficiency**
|
||||
- Idle Cost Tracker (MTD idle hours and estimated KES wasted).
|
||||
- Utilisation Heatmap (Y=vehicle, X=day-of-week).
|
||||
|
||||
**Row 7 — Field-Service SLAs** (collapsed by default)
|
||||
- Four SLA compliance stats + at-risk tickets table. All empty until `ops.tickets` flows.
|
||||
|
||||
## 6. Provisioning
|
||||
|
||||
Extend `grafana/provisioning/dashboards/noc_fleet.yaml` to expose both dashboards, or add a sibling provider file. Both dashboards live under `/etc/grafana/provisioning/dashboards-json/` and are baked into the image via `grafana/Dockerfile`.
|
||||
|
||||
UI edits remain throwaway.
|
||||
|
||||
## 7. Deployment
|
||||
|
||||
1. Commit and push to `repo.rahamafresh.com`.
|
||||
2. Coolify auto-pulls the commit, rebuilds the Grafana image, restarts.
|
||||
3. `run_migrations.py` applies `07_analytics_views.sql` at `ingest_movement` container start.
|
||||
4. New dashboard appears under the NOC folder on first Grafana load.
|
||||
|
||||
### Rollback
|
||||
- Revert the commit and push. Grafana image rebuilds without the new dashboard.
|
||||
- Drop the views manually: `DROP VIEW IF EXISTS tracksolid.v_* CASCADE;`
|
||||
- Remove the `schema_migrations` row for `07_analytics_views.sql`.
|
||||
|
||||
## 8. Testing
|
||||
|
||||
**Pre-push, against live DB:**
|
||||
- Apply `07_analytics_views.sql` with `docker exec -i $DB psql -U postgres -d tracksolid_db`.
|
||||
- `SELECT COUNT(*) FROM tracksolid.v_*` for each view — must return without error. Empty is OK.
|
||||
- `SET ROLE grafana_ro; SELECT * FROM tracksolid.v_fleet_today LIMIT 1;` to confirm read grant.
|
||||
|
||||
**Post-deploy, via browser:**
|
||||
- Log into Grafana at the Coolify URL.
|
||||
- Open "Daily Operations — Fleet & Dispatch".
|
||||
- Confirm no red Grafana query errors. Empty panels acceptable for gated views (`v_utilisation_daily`, `v_sla_inflight`).
|
||||
- Confirm `$city` variable populates (all `unassigned` today).
|
||||
- Confirm freshness banner reflects latest `live_positions.gps_time`.
|
||||
|
||||
## 9. Known Data Gaps (panels ship, light up later)
|
||||
|
||||
| Panel | Blocked on |
|
||||
|---|---|
|
||||
| Utilisation Heatmap | Nightly `dwh_gold.refresh_daily_metrics` job not yet scheduled |
|
||||
| SLA row | `ops.tickets` and `dispatch_log` empty until Zoho/Freshdesk integration |
|
||||
| Driver Leaderboard (harsh) | `position_history` growth; 519 rows today |
|
||||
| Idle Cost Tracker | `dwh_gold.fact_daily_fleet_metrics` (same dependency as heatmap) |
|
||||
|
||||
All queries are written against the target tables so panels light up automatically when upstream data flows.
|
||||
|
|
@ -0,0 +1,594 @@
|
|||
{
|
||||
"title": "Daily Operations — Fleet & Dispatch",
|
||||
"uid": "daily-ops",
|
||||
"schemaVersion": 39,
|
||||
"version": 1,
|
||||
"refresh": "1m",
|
||||
"time": { "from": "now/d", "to": "now" },
|
||||
"timezone": "Africa/Nairobi",
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["30s", "1m", "5m", "15m", "30m", "1h"]
|
||||
},
|
||||
"editable": false,
|
||||
"tags": ["fleet", "daily", "dispatch", "ops"],
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "city",
|
||||
"label": "City",
|
||||
"type": "query",
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"query": "SELECT DISTINCT COALESCE(assigned_city, city, 'unassigned') AS city FROM tracksolid.devices ORDER BY 1",
|
||||
"refresh": 1,
|
||||
"multi": true,
|
||||
"includeAll": true,
|
||||
"allValue": ".*",
|
||||
"current": { "selected": true, "text": "All", "value": "$__all" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"panels": [
|
||||
{
|
||||
"id": 100,
|
||||
"type": "stat",
|
||||
"title": "Last GPS Fix (fleet)",
|
||||
"description": "Most recent live position across all devices. Green < 5 min, amber 5–30 min, red > 30 min.",
|
||||
"gridPos": { "x": 0, "y": 0, "w": 24, "h": 3 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": {
|
||||
"colorMode": "background",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"textMode": "value_and_name"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s",
|
||||
"color": { "mode": "thresholds" },
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 300 },
|
||||
{ "color": "red", "value": 1800 }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT EXTRACT(EPOCH FROM (NOW() - MAX(gps_time)))::int AS \"Seconds since latest fleet fix\" FROM tracksolid.live_positions;",
|
||||
"format": "table",
|
||||
"refId": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"type": "row",
|
||||
"title": "Row 1 — Today at a Glance",
|
||||
"collapsed": false,
|
||||
"gridPos": { "x": 0, "y": 3, "w": 24, "h": 1 },
|
||||
"panels": []
|
||||
},
|
||||
{
|
||||
"id": 110,
|
||||
"type": "stat",
|
||||
"title": "Vehicles reporting today",
|
||||
"gridPos": { "x": 0, "y": 4, "w": 4, "h": 4 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": {
|
||||
"colorMode": "background", "graphMode": "none",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"textMode": "auto"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 1 }] }
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT COUNT(*) FILTER (WHERE trips_today > 0) AS \"Reporting today\" FROM tracksolid.v_fleet_today WHERE assigned_city ~ '${city:regex}';",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 111,
|
||||
"type": "stat",
|
||||
"title": "Fleet km today",
|
||||
"gridPos": { "x": 4, "y": 4, "w": 4, "h": 4 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": {
|
||||
"colorMode": "value", "graphMode": "area",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "lengthkm",
|
||||
"decimals": 1,
|
||||
"color": { "mode": "fixed", "fixedColor": "blue" }
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT ROUND(SUM(km_today)::numeric, 1) AS \"Fleet km today\" FROM tracksolid.v_fleet_today WHERE assigned_city ~ '${city:regex}';",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 112,
|
||||
"type": "stat",
|
||||
"title": "Drive hours today",
|
||||
"gridPos": { "x": 8, "y": 4, "w": 4, "h": 4 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": {
|
||||
"colorMode": "value", "graphMode": "none",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": { "unit": "h", "decimals": 1, "color": { "mode": "fixed", "fixedColor": "green" } }
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT ROUND(SUM(drive_hours)::numeric, 1) AS \"Drive h\" FROM tracksolid.v_fleet_today WHERE assigned_city ~ '${city:regex}';",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 113,
|
||||
"type": "stat",
|
||||
"title": "Idle hours today",
|
||||
"description": "Ignition on, speed ~0. Fuel burn with no movement.",
|
||||
"gridPos": { "x": 12, "y": 4, "w": 4, "h": 4 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": {
|
||||
"colorMode": "value", "graphMode": "none",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "h", "decimals": 1,
|
||||
"color": { "mode": "thresholds" },
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 10 },
|
||||
{ "color": "red", "value": 30 }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT ROUND(SUM(idle_hours)::numeric, 1) AS \"Idle h\" FROM tracksolid.v_fleet_today WHERE assigned_city ~ '${city:regex}';",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 114,
|
||||
"type": "stat",
|
||||
"title": "Open alarms (24h)",
|
||||
"gridPos": { "x": 16, "y": 4, "w": 4, "h": 4 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": {
|
||||
"colorMode": "background", "graphMode": "none",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 1 },
|
||||
{ "color": "red", "value": 10 }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT COUNT(*) AS \"Alarms 24h\" FROM tracksolid.alarms WHERE alarm_time > NOW() - INTERVAL '24 hours';",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 115,
|
||||
"type": "stat",
|
||||
"title": "In-flight SLA jobs",
|
||||
"description": "Tickets currently open and dispatched. Empty until ops.tickets flows.",
|
||||
"gridPos": { "x": 20, "y": 4, "w": 4, "h": 4 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": {
|
||||
"colorMode": "value", "graphMode": "none",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": { "color": { "mode": "fixed", "fixedColor": "purple" } }
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT COUNT(*) AS \"In-flight\" FROM tracksolid.v_sla_inflight WHERE ticket_stage NOT IN ('resolved', 'cancelled');",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 120,
|
||||
"type": "row",
|
||||
"title": "Row 2 — Live Dispatch",
|
||||
"collapsed": false,
|
||||
"gridPos": { "x": 0, "y": 8, "w": 24, "h": 1 },
|
||||
"panels": []
|
||||
},
|
||||
{
|
||||
"id": 121,
|
||||
"type": "geomap",
|
||||
"title": "Active Vehicles Map",
|
||||
"gridPos": { "x": 0, "y": 9, "w": 14, "h": 14 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": {
|
||||
"basemap": { "config": { "theme": "dark" }, "name": "Basemap", "type": "carto" },
|
||||
"controls": { "mouseWheelZoom": true, "showAttribution": true, "showScale": true, "showZoom": true },
|
||||
"layers": [
|
||||
{
|
||||
"config": {
|
||||
"showLegend": true,
|
||||
"style": {
|
||||
"color": { "field": "status", "fixed": "green", "mode": "field" },
|
||||
"opacity": 0.9,
|
||||
"rotation": { "field": "direction", "fixed": 0, "max": 360, "min": -360, "mode": "field" },
|
||||
"size": { "fixed": 14, "max": 15, "min": 2, "mode": "fixed" },
|
||||
"symbol": { "fixed": "img/icons/marker/circle.svg", "mode": "fixed" }
|
||||
}
|
||||
},
|
||||
"filterData": { "id": "byRefId", "options": "A" },
|
||||
"location": { "latitude": "lat", "longitude": "lng", "mode": "coords" },
|
||||
"name": "Vehicles", "tooltip": true, "type": "markers"
|
||||
}
|
||||
],
|
||||
"tooltip": { "mode": "details" },
|
||||
"view": { "allLayers": true, "id": "coords", "lat": -1.5, "lon": 36.5, "zoom": 6 }
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": { "color": { "mode": "palette-classic-by-name" } },
|
||||
"overrides": [
|
||||
{ "matcher": { "id": "byName", "options": "status" },
|
||||
"properties": [
|
||||
{ "id": "mappings", "value": [{ "type": "value", "options": {
|
||||
"moving": { "color": "green", "index": 0, "text": "Moving" },
|
||||
"idle_ignition_on": { "color": "yellow", "index": 1, "text": "Idle (engine on)" },
|
||||
"parked": { "color": "blue", "index": 2, "text": "Parked" },
|
||||
"stale": { "color": "orange", "index": 3, "text": "Stale > 10m" },
|
||||
"never_reported": { "color": "red", "index": 4, "text": "Never reported" }
|
||||
} }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT imei, vehicle_number, driver_name, assigned_city, lat, lng, speed, direction, status, last_fix FROM tracksolid.v_active_dispatch_map WHERE lat IS NOT NULL AND lng IS NOT NULL AND assigned_city ~ '${city:regex}';",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 122,
|
||||
"type": "table",
|
||||
"title": "Currently Idle (engine on, speed < 2)",
|
||||
"gridPos": { "x": 14, "y": 9, "w": 10, "h": 7 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": { "cellHeight": "sm", "showHeader": true, "footer": { "show": false } },
|
||||
"fieldConfig": {
|
||||
"defaults": { "custom": { "align": "auto", "filterable": true } },
|
||||
"overrides": [
|
||||
{ "matcher": { "id": "byName", "options": "idle_seconds" },
|
||||
"properties": [{ "id": "unit", "value": "s" }, { "id": "displayName", "value": "Idle for" }] }
|
||||
]
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT vehicle_number, driver_name, assigned_city, since, idle_seconds FROM tracksolid.v_currently_idle WHERE assigned_city ~ '${city:regex}' ORDER BY idle_seconds DESC;",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 123,
|
||||
"type": "table",
|
||||
"title": "Vehicles Not Moved Today",
|
||||
"gridPos": { "x": 14, "y": 16, "w": 10, "h": 7 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": { "cellHeight": "sm", "showHeader": true, "footer": { "show": false } },
|
||||
"fieldConfig": {
|
||||
"defaults": { "custom": { "align": "auto", "filterable": true } }
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT vehicle_number, driver_name, assigned_city, last_seen FROM tracksolid.v_vehicles_not_moved_today WHERE assigned_city ~ '${city:regex}' ORDER BY last_seen DESC NULLS LAST;",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 130,
|
||||
"type": "row",
|
||||
"title": "Row 3 — Daily KPI Table",
|
||||
"collapsed": false,
|
||||
"gridPos": { "x": 0, "y": 23, "w": 24, "h": 1 },
|
||||
"panels": []
|
||||
},
|
||||
{
|
||||
"id": 131,
|
||||
"type": "table",
|
||||
"title": "Per-Vehicle Daily Roll-up",
|
||||
"gridPos": { "x": 0, "y": 24, "w": 24, "h": 12 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": { "cellHeight": "sm", "showHeader": true, "footer": { "show": false } },
|
||||
"fieldConfig": {
|
||||
"defaults": { "custom": { "align": "auto", "filterable": true } },
|
||||
"overrides": [
|
||||
{ "matcher": { "id": "byName", "options": "km_today" },
|
||||
"properties": [{ "id": "unit", "value": "lengthkm" }, { "id": "decimals", "value": 1 }] },
|
||||
{ "matcher": { "id": "byName", "options": "drive_hours" },
|
||||
"properties": [{ "id": "unit", "value": "h" }, { "id": "decimals", "value": 1 }] },
|
||||
{ "matcher": { "id": "byName", "options": "idle_hours" },
|
||||
"properties": [{ "id": "unit", "value": "h" }, { "id": "decimals", "value": 1 }] },
|
||||
{ "matcher": { "id": "byName", "options": "did_not_move" },
|
||||
"properties": [
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-background" } },
|
||||
{ "id": "mappings", "value": [{ "type": "value", "options": {
|
||||
"true": { "color": "red", "index": 0, "text": "No" },
|
||||
"false": { "color": "transparent", "index": 1, "text": "Yes" }
|
||||
} }] },
|
||||
{ "id": "displayName", "value": "Moved?" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT vehicle_number, driver_name, assigned_city, km_today, trips_today, drive_hours, idle_hours, first_departure, last_return, alarms_today, did_not_move FROM tracksolid.v_fleet_today WHERE enabled_flag = 1 AND assigned_city ~ '${city:regex}' ORDER BY km_today DESC NULLS LAST;",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 140,
|
||||
"type": "row",
|
||||
"title": "Row 4 — Driver Behaviour Leaderboard (30-day)",
|
||||
"collapsed": false,
|
||||
"gridPos": { "x": 0, "y": 36, "w": 24, "h": 1 },
|
||||
"panels": []
|
||||
},
|
||||
{
|
||||
"id": 141,
|
||||
"type": "table",
|
||||
"title": "Driver Leaderboard",
|
||||
"description": "Rolling 30-day aggression index. Red/amber/green per BA-file §3.1–§3.2 thresholds.",
|
||||
"gridPos": { "x": 0, "y": 37, "w": 24, "h": 12 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": { "cellHeight": "sm", "showHeader": true, "footer": { "show": false } },
|
||||
"fieldConfig": {
|
||||
"defaults": { "custom": { "align": "auto", "filterable": true } },
|
||||
"overrides": [
|
||||
{ "matcher": { "id": "byName", "options": "km" },
|
||||
"properties": [{ "id": "unit", "value": "lengthkm" }, { "id": "decimals", "value": 0 }] },
|
||||
{ "matcher": { "id": "byName", "options": "speeding_per_100km" },
|
||||
"properties": [
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-background" } },
|
||||
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 1 },
|
||||
{ "color": "red", "value": 5 }
|
||||
] } }
|
||||
]
|
||||
},
|
||||
{ "matcher": { "id": "byName", "options": "harsh_per_100km" },
|
||||
"properties": [
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-background" } },
|
||||
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 0.5 },
|
||||
{ "color": "red", "value": 2 }
|
||||
] } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT driver_name, vehicle_number, assigned_city, SUM(km)::numeric(10,0) AS km, SUM(events_80) AS events_80, SUM(events_100) AS events_100, SUM(events_120) AS events_120, SUM(harsh_events) AS harsh_events, ROUND(SUM(events_80)::numeric / NULLIF(SUM(km), 0) * 100, 2) AS speeding_per_100km, ROUND(SUM(harsh_events)::numeric / NULLIF(SUM(km), 0) * 100, 2) AS harsh_per_100km FROM tracksolid.v_driver_aggregates_daily WHERE day > CURRENT_DATE - INTERVAL '30 days' AND assigned_city ~ '${city:regex}' GROUP BY driver_name, vehicle_number, assigned_city ORDER BY harsh_per_100km DESC NULLS LAST;",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 150,
|
||||
"type": "row",
|
||||
"title": "Row 5 — Trends",
|
||||
"collapsed": false,
|
||||
"gridPos": { "x": 0, "y": 49, "w": 24, "h": 1 },
|
||||
"panels": []
|
||||
},
|
||||
{
|
||||
"id": 151,
|
||||
"type": "timeseries",
|
||||
"title": "Fleet Distance — 7-day (by city)",
|
||||
"gridPos": { "x": 0, "y": 50, "w": 12, "h": 9 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "lengthkm",
|
||||
"custom": { "drawStyle": "bars", "fillOpacity": 60, "lineWidth": 1 }
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT day::timestamptz AS time, assigned_city AS metric, km AS value FROM tracksolid.v_fleet_km_daily WHERE day > CURRENT_DATE - INTERVAL '7 days' AND assigned_city ~ '${city:regex}' ORDER BY day;",
|
||||
"format": "time_series", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 152,
|
||||
"type": "timeseries",
|
||||
"title": "Alarm Frequency — 30-day (by type)",
|
||||
"gridPos": { "x": 12, "y": 50, "w": 12, "h": 9 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short",
|
||||
"custom": { "drawStyle": "bars", "fillOpacity": 60, "lineWidth": 1, "stacking": { "mode": "normal", "group": "A" } }
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT day::timestamptz AS time, alarm_name AS metric, alarm_count AS value FROM tracksolid.v_alarms_daily WHERE day > CURRENT_DATE - INTERVAL '30 days' ORDER BY day;",
|
||||
"format": "time_series", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 160,
|
||||
"type": "row",
|
||||
"title": "Row 6 — Efficiency",
|
||||
"collapsed": false,
|
||||
"gridPos": { "x": 0, "y": 59, "w": 24, "h": 1 },
|
||||
"panels": []
|
||||
},
|
||||
{
|
||||
"id": 161,
|
||||
"type": "stat",
|
||||
"title": "Idle Cost (month-to-date)",
|
||||
"description": "Sum of idle hours × 0.8 L/h × 180 KES/L across fleet for this month. Empty until nightly ETL refreshes dwh_gold.",
|
||||
"gridPos": { "x": 0, "y": 60, "w": 12, "h": 6 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": {
|
||||
"colorMode": "value", "graphMode": "area", "textMode": "value_and_name",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "currencyKES", "decimals": 0,
|
||||
"color": { "mode": "thresholds" },
|
||||
"thresholds": { "mode": "absolute", "steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 50000 },
|
||||
{ "color": "red", "value": 200000 }
|
||||
] }
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT ROUND(SUM(total_idle_hours) * 0.8 * 180) AS \"Idle cost KES (MTD)\" FROM tracksolid.v_utilisation_daily WHERE day >= DATE_TRUNC('month', CURRENT_DATE) AND assigned_city ~ '${city:regex}';",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 162,
|
||||
"type": "heatmap",
|
||||
"title": "Utilisation Heatmap (30-day)",
|
||||
"description": "Per-vehicle daily utilisation %. Empty until dwh_gold.fact_daily_fleet_metrics is refreshed by the nightly ETL.",
|
||||
"gridPos": { "x": 12, "y": 60, "w": 12, "h": 9 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": {
|
||||
"calculate": false,
|
||||
"cellGap": 2,
|
||||
"color": { "mode": "scheme", "scheme": "RdYlGn", "steps": 64 },
|
||||
"yAxis": { "axisLabel": "Vehicle" }
|
||||
},
|
||||
"fieldConfig": { "defaults": { "custom": { "scaleDistribution": { "type": "linear" } } } },
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT day::timestamptz AS time, vehicle_number AS metric, utilisation_pct AS value FROM tracksolid.v_utilisation_daily WHERE day > CURRENT_DATE - INTERVAL '30 days' AND assigned_city ~ '${city:regex}' ORDER BY day;",
|
||||
"format": "time_series", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 170,
|
||||
"type": "row",
|
||||
"title": "Row 7 — Field-Service SLAs (data-gated)",
|
||||
"collapsed": true,
|
||||
"gridPos": { "x": 0, "y": 69, "w": 24, "h": 1 },
|
||||
"panels": [
|
||||
{
|
||||
"id": 171,
|
||||
"type": "stat",
|
||||
"title": "Dispatch SLA (median mins, 24h)",
|
||||
"gridPos": { "x": 0, "y": 70, "w": 6, "h": 5 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": {
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||
},
|
||||
"fieldConfig": { "defaults": { "unit": "m", "decimals": 0 } },
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY dispatch_mins) AS \"Dispatch p50 (min)\" FROM tracksolid.v_sla_inflight WHERE created_at > NOW() - INTERVAL '24 hours';",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 172,
|
||||
"type": "stat",
|
||||
"title": "En-route SLA (median mins, 24h)",
|
||||
"gridPos": { "x": 6, "y": 70, "w": 6, "h": 5 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": { "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false } },
|
||||
"fieldConfig": { "defaults": { "unit": "m", "decimals": 0 } },
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY enroute_mins) AS \"En-route p50 (min)\" FROM tracksolid.v_sla_inflight WHERE created_at > NOW() - INTERVAL '24 hours';",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 173,
|
||||
"type": "stat",
|
||||
"title": "On-site SLA (median mins, 24h)",
|
||||
"gridPos": { "x": 12, "y": 70, "w": 6, "h": 5 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": { "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false } },
|
||||
"fieldConfig": { "defaults": { "unit": "m", "decimals": 0 } },
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY onsite_mins) AS \"On-site p50 (min)\" FROM tracksolid.v_sla_inflight WHERE created_at > NOW() - INTERVAL '24 hours';",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 174,
|
||||
"type": "stat",
|
||||
"title": "Resolution SLA (median mins, 24h)",
|
||||
"gridPos": { "x": 18, "y": 70, "w": 6, "h": 5 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": { "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false } },
|
||||
"fieldConfig": { "defaults": { "unit": "m", "decimals": 0 } },
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY resolution_mins) AS \"Resolution p50 (min)\" FROM tracksolid.v_sla_inflight WHERE created_at > NOW() - INTERVAL '24 hours';",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 175,
|
||||
"type": "table",
|
||||
"title": "At-risk tickets",
|
||||
"gridPos": { "x": 0, "y": 75, "w": 24, "h": 10 },
|
||||
"datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"options": { "cellHeight": "sm", "showHeader": true, "footer": { "show": false } },
|
||||
"fieldConfig": { "defaults": { "custom": { "align": "auto", "filterable": true } } },
|
||||
"targets": [
|
||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||
"rawSql": "SELECT ticket_id, customer, priority, ticket_stage, driver_name, created_at, dispatch_mins, enroute_mins, onsite_mins, resolution_mins FROM tracksolid.v_sla_inflight WHERE ticket_stage NOT IN ('resolved', 'cancelled') ORDER BY resolution_mins DESC NULLS LAST LIMIT 50;",
|
||||
"format": "table", "refId": "A" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -27,8 +27,10 @@ DATABASE_URL = os.environ["DATABASE_URL"]
|
|||
MIGRATIONS = [
|
||||
"02_tracksolid_full_schema_rev.sql",
|
||||
"03_webhook_schema_migration.sql",
|
||||
"04_bug_fix_migration.sql", # distance_m → distance_km rename + correction
|
||||
"05_enhancement_migration.sql", # new tables, OBD columns, dwh_gold expansion
|
||||
"04_bug_fix_migration.sql", # distance_m → distance_km rename + correction
|
||||
"05_enhancement_migration.sql", # new tables, OBD columns, dwh_gold expansion
|
||||
"06_business_analytics_migration.sql", # ops schema, dispatch_log, assigned_city
|
||||
"07_analytics_views.sql", # Grafana-facing views in tracksolid.*
|
||||
]
|
||||
|
||||
# ── Tables that must exist before the service is allowed to start ─────────────
|
||||
|
|
@ -105,6 +107,16 @@ def seed_pre_tracking_migrations(conn):
|
|||
"SELECT 1 FROM information_schema.tables "
|
||||
"WHERE table_schema='tracksolid' AND table_name='device_events'",
|
||||
),
|
||||
(
|
||||
"06_business_analytics_migration.sql",
|
||||
"SELECT 1 FROM information_schema.tables "
|
||||
"WHERE table_schema='ops' AND table_name='tickets'",
|
||||
),
|
||||
(
|
||||
"07_analytics_views.sql",
|
||||
"SELECT 1 FROM information_schema.views "
|
||||
"WHERE table_schema='tracksolid' AND table_name='v_fleet_today'",
|
||||
),
|
||||
]
|
||||
|
||||
seeds = []
|
||||
|
|
|
|||
Loading…
Reference in a new issue