Compare commits

...

2 commits

Author SHA1 Message Date
5f24c158e2 Merge pull request #5: Daily Operations dashboard + tracksolid analytics views
Some checks failed
Static Analysis / static (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-04-19 10:47:52 +00:00
David Kiania
85d02c81a5 feat: Daily Operations dashboard + tracksolid analytics views
Some checks failed
Static Analysis / static (push) Has been cancelled
Tests / test (push) Has been cancelled
Static Analysis / static (pull_request) Has been cancelled
Tests / test (pull_request) Has been cancelled
Add a second Grafana dashboard focused on daily operational KPIs and live
dispatch, keeping the NOC Live dashboard untouched.

- grafana/provisioning/dashboards-json/daily_operations_dashboard.json
  New dashboard covering §7 Blueprint Panels 3-8 and the §4 dispatch lens:
  freshness banner, today-at-a-glance stat row, active vehicles map,
  currently-idle table, vehicles-not-moved-today, per-vehicle daily KPI
  roll-up, driver behaviour leaderboard, distance trend, alarm frequency,
  idle cost MTD, utilisation heatmap, SLA row (collapsed, data-gated).

- 07_analytics_views.sql
  Nine views in tracksolid.* wrapping the BA-file [DASHBOARD]-tagged
  queries. Each view carries COMMENT ON VIEW with its spec section.
  SELECT granted to grafana_ro. Smoke-tested against live DB.

- run_migrations.py
  Register 06 and 07 in MIGRATIONS list with idempotent seed checks so
  future fresh deploys apply them correctly.

- CLAUDE.md
  Retire the tracksolid_2 schema references (schema no longer exists);
  §9 Fleet State dated 2026-04-19 with correct pipeline status (running,
  875 runs/24h, 0 failures) and accurate position_history row counts
  (hypertable stats don't show in pg_stat_user_tables).

- docs/superpowers/specs/2026-04-19-daily-operations-dashboard-design.md
  Design spec covering architecture, views, panel layout, deployment,
  rollback, and known data gaps.
2026-04-19 13:44:18 +03:00
5 changed files with 1140 additions and 30 deletions

348
07_analytics_views.sql Normal file
View 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;

View file

@ -55,8 +55,7 @@ See `docs/CONNECTIONS.md` for the full shape. Summary:
- **SSH:** `ssh -i ~/.ssh/id_ed25519 kianiadee@stage.rahamafresh.com` - **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 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) - **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 0206 applied 2026-04-18.
- **⚠ 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 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. - **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: - **Container naming:** Coolify appends a random suffix. Always resolve with:
```bash ```bash
@ -96,29 +95,45 @@ tracksolidApiDocumentation.md # API endpoint reference
## 5. Database Schema — Key Tables ## 5. Database Schema — Key Tables
```sql ```sql
tracksolid_2.devices -- LIVE registry (63 AT4-series devices, 353549* IMEIs, 0 driver names) tracksolid.devices -- Device / driver / vehicle registry (63 rows; 0 driver_name populated)
-- NB: has `assigned_team` (not cost_centre), `city` (not assigned_city) -- IMEI mix: 353549* AT4 (23), 862798* X3/JC400P (23), 865135* X3/JC400P (10), 359857* (7)
tracksolid_2.live_positions -- LIVE positions (19 rows, all stale since 6 Apr 2026) -- Full CSV (144 devices) not yet imported — run import_drivers_csv.py --apply
tracksolid_2.ingestion_log -- LIVE pipeline audit trail tracksolid.live_positions -- Current fix per IMEI (19 rows; refreshed every 60s by ingest_movement)
tracksolid.devices -- Target registry (empty — new stack not yet deployed) tracksolid.position_history -- All GPS fixes (hypertable, partitioned by gps_time). ~519 rows (308 track_list + 211 poll).
tracksolid.live_positions -- Target positions (empty — new stack not yet deployed) -- pg_stat_user_tables shows 0 for hypertables — always COUNT(*) directly.
tracksolid.position_history -- All GPS fixes (hypertable, partitioned by gps_time)
-- source: 'poll' (60s sweep) | 'track_list' (30m high-res) -- source: 'poll' (60s sweep) | 'track_list' (30m high-res)
tracksolid.trips -- Trip summaries: distance_km, driving_time_s, avg/max speed tracksolid.trips -- Trip summaries: distance_km, driving_time_s, avg/max speed
tracksolid.parking_events -- Stop events with duration and address tracksolid.parking_events -- Stop events with duration and address (0 rows — endpoint returning empty)
tracksolid.alarms -- Alarm events (alarm_type, alarm_name, alarm_time) 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.obd_readings -- OBD diagnostics (push only, awaiting webhook registration)
tracksolid.device_events -- Power on/off tamper events tracksolid.device_events -- Power on/off tamper events (push only)
tracksolid.ingestion_log -- API call audit trail per endpoint 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) tracksolid.dispatch_log -- Dispatch decisions for SLA tracking (migration 06; empty until ops integration)
dwh_gold.fact_daily_fleet_metrics -- Nightly ETL aggregates per vehicle per day 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.service_log -- Workshop service history (migration 06)
ops.odometer_readings -- Physical odometer captures (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`. 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 ## 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. 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. 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. 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 | | Metric | Value |
|---|---| |---|---|
| Registered devices (live DB) | 63 AT4-series (`353549*` IMEIs) in `tracksolid_2` | | Registered devices (`tracksolid.devices`) | 63 total — 23 × `353549*` (AT4), 23 × `862798*` + 10 × `865135*` (X3/JC400P), 7 × `359857*` |
| Devices in CSV (not yet in DB) | 144 X3/JC400P (`865135*`, `862798*` IMEIs) | | Devices in CSV not yet imported | 144 (X3/JC400P); `import_drivers_csv.py --apply` will upsert names + plates |
| Driver names populated | 0 — run `import_drivers_csv.py --apply` after new stack deployed | | Driver names populated | 0 / 63 — pending CSV import |
| Live positions | 19 (all stale — last fix 6 Apr 2026) | | Live positions | 19 (latest fix 2026-04-19 10:25 UTC) |
| Trips recorded | 5 (12.8 km total, 46 Apr 2026 only) | | Trips recorded | 8 (latest 2026-04-19 08:34 UTC) |
| Pipeline status | Stopped 6 Apr 2026 — 401 token expiry (fixed in `ts_shared_rev.py`, deploy new stack) | | 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) | | 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) | | 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 | | 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` — 144 X3/JC400P devices with names + plates waiting |
| HIGH | Run `import_drivers_csv.py --apply` after stack deployed (144 devices, names + plates ready) |
| HIGH | Register webhooks: `/pushobd` `/pushoil` `/pushtem` `/pushlbs` `/pushevent` | | HIGH | Register webhooks: `/pushobd` `/pushoil` `/pushtem` `/pushlbs` `/pushevent` |
| HIGH | Investigate X3-63282 in Kampala — legitimate or unauthorised? | | HIGH | Investigate X3-63282 in Kampala — legitimate or unauthorised? |
| MEDIUM | Set `fuel_100km` per vehicle type to activate fuel cost calculations | | 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`) | | MEDIUM | Co-develop client KPI framework (see `docs/KPI_FRAMEWORK.md`) |
| LOW | Populate geofences — depot boundaries, city zones | | 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) |

View file

@ -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 38** — 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 530 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.

View file

@ -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 530 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" }
]
}
]
}
]
}

View file

@ -29,6 +29,8 @@ MIGRATIONS = [
"03_webhook_schema_migration.sql", "03_webhook_schema_migration.sql",
"04_bug_fix_migration.sql", # distance_m → distance_km rename + correction "04_bug_fix_migration.sql", # distance_m → distance_km rename + correction
"05_enhancement_migration.sql", # new tables, OBD columns, dwh_gold expansion "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 ───────────── # ── 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 " "SELECT 1 FROM information_schema.tables "
"WHERE table_schema='tracksolid' AND table_name='device_events'", "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 = [] seeds = []