tracksolid_timescale_grafan.../docs/superpowers/specs/2026-04-19-daily-operations-dashboard-design.md

141 lines
6.7 KiB
Markdown
Raw Normal View History

# 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.