141 lines
6.7 KiB
Markdown
141 lines
6.7 KiB
Markdown
|
|
# 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.
|