From cd6b2ca81a29867489e07e90f1fdb4de6f5cdd24 Mon Sep 17 00:00:00 2001 From: David Kiania Date: Thu, 9 Apr 2026 00:01:52 +0300 Subject: [PATCH] Add Grafana NOC fleet dashboard with provisioning Adds a fully-provisioned Grafana dashboard for NOC operators to monitor 80 vehicles in real-time: live geomap with direction arrows, speed, driver info, and color-coded plates. Includes datasource and dashboard provider YAMLs, dashboard JSON (schemaVersion 39 / Grafana 11.0.0), and docker-compose updates to mount provisioning at container start. Co-Authored-By: Claude Sonnet 4.6 --- docker-compose.yaml | 4 + .../dashboards-json/noc_fleet_dashboard.json | 589 ++++++++++++++++++ .../provisioning/dashboards/noc_fleet.yaml | 12 + .../datasources/tracksolid_postgres.yaml | 20 + grafanaDeployment.md | 324 ++++++++++ 5 files changed, 949 insertions(+) create mode 100644 grafana/provisioning/dashboards-json/noc_fleet_dashboard.json create mode 100644 grafana/provisioning/dashboards/noc_fleet.yaml create mode 100644 grafana/provisioning/datasources/tracksolid_postgres.yaml create mode 100644 grafanaDeployment.md diff --git a/docker-compose.yaml b/docker-compose.yaml index cd5cd27..1f58276 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -60,10 +60,14 @@ services: depends_on: timescale_db: condition: service_healthy + env_file: .env environment: - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} + - GF_USERS_DEFAULT_THEME=dark + - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards-json/noc_fleet_dashboard.json volumes: - grafana-data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning:ro # COOLIFY DOMAIN LOGIC: # You will set the actual URL in the Coolify UI, # but the service needs to expose port 3000 internally. diff --git a/grafana/provisioning/dashboards-json/noc_fleet_dashboard.json b/grafana/provisioning/dashboards-json/noc_fleet_dashboard.json new file mode 100644 index 0000000..09aa827 --- /dev/null +++ b/grafana/provisioning/dashboards-json/noc_fleet_dashboard.json @@ -0,0 +1,589 @@ +{ + "title": "NOC Fleet Operations — Live", + "uid": "noc-fleet-live", + "schemaVersion": 39, + "version": 1, + "refresh": "30s", + "time": { "from": "now-1h", "to": "now" }, + "timepicker": { + "refresh_intervals": ["10s", "30s", "1m", "5m"] + }, + "editable": false, + "tags": ["noc", "fleet", "live"], + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "panels": [ + { + "id": 1, + "type": "stat", + "title": "Total Vehicles", + "gridPos": { "x": 0, "y": 0, "w": 4, "h": 3 }, + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "text", "value": null }] + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "rawSql": "SELECT COUNT(*) AS \"Total Vehicles\"\nFROM tracksolid.devices WHERE enabled_flag = 1;", + "format": "table", + "refId": "A" + } + ] + }, + { + "id": 2, + "type": "stat", + "title": "Online Now", + "description": "GPS fix within last 5 minutes", + "gridPos": { "x": 4, "y": 0, "w": 4, "h": 3 }, + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "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 } + ] + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "rawSql": "SELECT COUNT(*) AS \"Online\"\nFROM tracksolid.v_fleet_status\nWHERE connectivity_status = 'online';", + "format": "table", + "refId": "A" + } + ] + }, + { + "id": 3, + "type": "stat", + "title": "Recent (5-30 min)", + "description": "GPS fix between 5 and 30 minutes ago", + "gridPos": { "x": 8, "y": 0, "w": 4, "h": 3 }, + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "text", "value": null }, + { "color": "yellow", "value": 1 } + ] + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "rawSql": "SELECT COUNT(*) AS \"Recent\"\nFROM tracksolid.v_fleet_status\nWHERE connectivity_status = 'recent';", + "format": "table", + "refId": "A" + } + ] + }, + { + "id": 4, + "type": "stat", + "title": "Offline", + "description": "GPS fix older than 30 minutes", + "gridPos": { "x": 12, "y": 0, "w": 4, "h": 3 }, + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 1 } + ] + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "rawSql": "SELECT COUNT(*) AS \"Offline\"\nFROM tracksolid.v_fleet_status\nWHERE connectivity_status = 'offline';", + "format": "table", + "refId": "A" + } + ] + }, + { + "id": 5, + "type": "stat", + "title": "Moving Now", + "description": "Vehicles with speed > 0 and engine on", + "gridPos": { "x": 16, "y": 0, "w": 4, "h": 3 }, + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "text", "value": null }, + { "color": "green", "value": 1 } + ] + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "rawSql": "SELECT COUNT(*) AS \"Moving\"\nFROM tracksolid.v_fleet_status\nWHERE speed > 0 AND acc_status = '1' AND connectivity_status = 'online';", + "format": "table", + "refId": "A" + } + ] + }, + { + "id": 6, + "type": "stat", + "title": "Avg Speed (km/h)", + "description": "Average speed of currently moving vehicles", + "gridPos": { "x": 20, "y": 0, "w": 4, "h": 3 }, + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "unit": "velocitykmh", + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 80 }, + { "color": "red", "value": 120 } + ] + } + }, + "overrides": [] + }, + "targets": [ + { + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "rawSql": "SELECT ROUND(AVG(speed)::numeric, 1) AS \"Avg Speed km/h\"\nFROM tracksolid.v_fleet_status\nWHERE speed > 0 AND acc_status = '1' AND connectivity_status = 'online';", + "format": "table", + "refId": "A" + } + ] + }, + { + "id": 7, + "type": "geomap", + "title": "Live Vehicle Locations", + "gridPos": { "x": 0, "y": 3, "w": 24, "h": 16 }, + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "options": { + "basemap": { + "config": { "theme": "dark" }, + "name": "Basemap", + "type": "carto" + }, + "controls": { + "mouseWheelZoom": true, + "showAttribution": true, + "showDebug": false, + "showScale": true, + "showZoom": true + }, + "layers": [ + { + "config": { + "showLegend": false, + "style": { + "color": { + "field": "vehicle_number", + "fixed": "dark-green", + "mode": "field" + }, + "opacity": 1, + "rotation": { + "field": "direction", + "fixed": 0, + "max": 360, + "min": -360, + "mode": "field" + }, + "size": { + "fixed": 18, + "max": 15, + "min": 2, + "mode": "fixed" + }, + "symbol": { + "fixed": "img/icons/marker/arrow-up.svg", + "mode": "fixed" + }, + "symbolAlign": { + "h": "center", + "v": "center" + } + } + }, + "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": "lat" }, + "properties": [ + { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } + ] + }, + { + "matcher": { "id": "byName", "options": "lng" }, + "properties": [ + { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } + ] + }, + { + "matcher": { "id": "byName", "options": "imei" }, + "properties": [ + { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": false } } + ] + }, + { + "matcher": { "id": "byName", "options": "direction" }, + "properties": [ + { "id": "unit", "value": "degree" }, + { "id": "displayName", "value": "Heading" } + ] + }, + { + "matcher": { "id": "byName", "options": "speed" }, + "properties": [ + { "id": "unit", "value": "velocitykmh" }, + { "id": "displayName", "value": "Speed" } + ] + }, + { + "matcher": { "id": "byName", "options": "vehicle_number" }, + "properties": [ + { "id": "displayName", "value": "Plate" } + ] + }, + { + "matcher": { "id": "byName", "options": "driver_name" }, + "properties": [ + { "id": "displayName", "value": "Driver" } + ] + }, + { + "matcher": { "id": "byName", "options": "driver_phone" }, + "properties": [ + { "id": "displayName", "value": "Phone" } + ] + }, + { + "matcher": { "id": "byName", "options": "vehicle_name" }, + "properties": [ + { "id": "displayName", "value": "Vehicle" } + ] + }, + { + "matcher": { "id": "byName", "options": "connectivity_status" }, + "properties": [ + { "id": "displayName", "value": "Status" }, + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "online": { "color": "green", "index": 0, "text": "Online" }, + "recent": { "color": "yellow", "index": 1, "text": "Recent" }, + "offline": { "color": "red", "index": 2, "text": "Offline" } + } + } + ] + } + ] + }, + { + "matcher": { "id": "byName", "options": "acc_status" }, + "properties": [ + { "id": "displayName", "value": "ACC" }, + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "1": { "text": "On", "color": "green", "index": 0 }, + "0": { "text": "Off", "color": "red", "index": 1 } + } + } + ] + } + ] + }, + { + "matcher": { "id": "byName", "options": "gps_time" }, + "properties": [ + { "id": "displayName", "value": "Last Fix" } + ] + }, + { + "matcher": { "id": "byName", "options": "loc_desc" }, + "properties": [ + { "id": "displayName", "value": "Location" } + ] + } + ] + }, + "targets": [ + { + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "rawSql": "SELECT\n d.imei,\n d.vehicle_number,\n d.vehicle_name,\n d.driver_name,\n d.driver_phone,\n d.city,\n d.device_group,\n lp.lat,\n lp.lng,\n lp.speed,\n lp.direction,\n lp.acc_status,\n lp.loc_desc,\n lp.gps_time,\n CASE\n WHEN lp.gps_time >= NOW() - INTERVAL '5 minutes' THEN 'online'\n WHEN lp.gps_time >= NOW() - INTERVAL '30 minutes' THEN 'recent'\n ELSE 'offline'\n END AS connectivity_status\nFROM tracksolid.devices d\nINNER JOIN tracksolid.live_positions lp USING (imei)\nWHERE d.enabled_flag = 1\n AND lp.lat IS NOT NULL\n AND lp.lng IS NOT NULL\nORDER BY d.vehicle_number;", + "format": "table", + "refId": "A" + } + ] + }, + { + "id": 8, + "type": "table", + "title": "Vehicle Status", + "gridPos": { "x": 0, "y": 19, "w": 24, "h": 10 }, + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "options": { + "cellHeight": "sm", + "footer": { "countRows": false, "fields": "", "reducer": ["sum"], "show": false }, + "showHeader": true, + "sortBy": [] + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { "type": "auto" }, + "filterable": true, + "inspect": false + } + }, + "overrides": [ + { + "matcher": { "id": "byName", "options": "Status" }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { "type": "color-background", "mode": "basic" } + }, + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "Online": { "color": "green", "index": 0 }, + "Recent": { "color": "yellow", "index": 1 }, + "Offline": { "color": "red", "index": 2 } + } + } + ] + } + ] + }, + { + "matcher": { "id": "byName", "options": "Speed (km/h)" }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { "type": "color-text" } + }, + { + "id": "color", + "value": { "mode": "thresholds" } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 80 }, + { "color": "red", "value": 120 } + ] + } + } + ] + }, + { + "matcher": { "id": "byName", "options": "Last Fix" }, + "properties": [ + { "id": "unit", "value": "dateTimeAsLocal" } + ] + }, + { + "matcher": { "id": "byName", "options": "Min Since Fix" }, + "properties": [ + { "id": "custom.width", "value": 110 }, + { "id": "displayName", "value": "Min Ago" } + ] + }, + { + "matcher": { "id": "byName", "options": "Driver Phone" }, + "properties": [ + { "id": "custom.width", "value": 130 } + ] + }, + { + "matcher": { "id": "byName", "options": "Plate" }, + "properties": [ + { "id": "custom.width", "value": 110 } + ] + } + ] + }, + "targets": [ + { + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "rawSql": "SELECT\n d.vehicle_number AS \"Plate\",\n d.vehicle_name AS \"Vehicle\",\n d.driver_name AS \"Driver\",\n d.driver_phone AS \"Driver Phone\",\n d.city AS \"City\",\n ROUND(lp.speed::numeric, 0) AS \"Speed (km/h)\",\n lp.loc_desc AS \"Last Location\",\n lp.gps_time AS \"Last Fix\",\n CASE\n WHEN lp.gps_time >= NOW() - INTERVAL '5 minutes' THEN 'Online'\n WHEN lp.gps_time >= NOW() - INTERVAL '30 minutes' THEN 'Recent'\n ELSE 'Offline'\n END AS \"Status\",\n EXTRACT(EPOCH FROM (NOW() - lp.gps_time))::int / 60 AS \"Min Since Fix\"\nFROM tracksolid.devices d\nLEFT JOIN tracksolid.live_positions lp USING (imei)\nWHERE d.enabled_flag = 1\nORDER BY\n CASE\n WHEN lp.gps_time >= NOW() - INTERVAL '5 minutes' THEN 0\n WHEN lp.gps_time >= NOW() - INTERVAL '30 minutes' THEN 1\n ELSE 2\n END,\n d.vehicle_number;", + "format": "table", + "refId": "A" + } + ] + }, + { + "id": 9, + "type": "table", + "title": "Ingestion Health", + "collapsed": true, + "gridPos": { "x": 0, "y": 29, "w": 24, "h": 8 }, + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "options": { + "cellHeight": "sm", + "footer": { "countRows": false, "fields": "", "reducer": ["sum"], "show": false }, + "showHeader": true + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { "type": "auto" }, + "filterable": false, + "inspect": false + } + }, + "overrides": [ + { + "matcher": { "id": "byName", "options": "Result" }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { "type": "color-background", "mode": "basic" } + }, + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "OK": { "color": "green", "index": 0 }, + "FAIL": { "color": "red", "index": 1 } + } + } + ] + } + ] + } + ] + }, + "targets": [ + { + "datasource": { "type": "postgres", "uid": "tracksolid_pg" }, + "rawSql": "SELECT\n endpoint AS \"Endpoint\",\n TO_CHAR(run_at, 'HH24:MI DD-Mon') AS \"Last Run\",\n CASE WHEN success THEN 'OK' ELSE 'FAIL' END AS \"Result\",\n error_message AS \"Error\",\n seconds_ago AS \"Lag (s)\"\nFROM tracksolid.v_ingestion_health\nORDER BY endpoint;", + "format": "table", + "refId": "A" + } + ] + } + ] +} diff --git a/grafana/provisioning/dashboards/noc_fleet.yaml b/grafana/provisioning/dashboards/noc_fleet.yaml new file mode 100644 index 0000000..6b03735 --- /dev/null +++ b/grafana/provisioning/dashboards/noc_fleet.yaml @@ -0,0 +1,12 @@ +apiVersion: 1 + +providers: + - name: NOC Fleet Dashboards + orgId: 1 + folder: NOC + type: file + disableDeletion: true + updateIntervalSeconds: 30 + allowUiUpdates: false + options: + path: /etc/grafana/provisioning/dashboards-json diff --git a/grafana/provisioning/datasources/tracksolid_postgres.yaml b/grafana/provisioning/datasources/tracksolid_postgres.yaml new file mode 100644 index 0000000..4fa5965 --- /dev/null +++ b/grafana/provisioning/datasources/tracksolid_postgres.yaml @@ -0,0 +1,20 @@ +apiVersion: 1 + +datasources: + - name: TracksolidDB + type: postgres + uid: tracksolid_pg + url: timescale_db:5432 + database: tracksolid_db + user: grafana_ro + secureJsonData: + password: ${GRAFANA_DB_RO_PASSWORD} + jsonData: + sslmode: disable + maxOpenConns: 5 + maxIdleConns: 2 + connMaxLifetime: 14400 + postgresVersion: 1600 + timescaledb: true + editable: false + isDefault: true diff --git a/grafanaDeployment.md b/grafanaDeployment.md new file mode 100644 index 0000000..fe79001 --- /dev/null +++ b/grafanaDeployment.md @@ -0,0 +1,324 @@ +# Grafana NOC Fleet Dashboard — Deployment Guide + +## Overview + +Single-region NOC dashboard monitoring 80 vehicles (Nairobi, Mombasa, Kampala) via Tracksolid/Jimi GPS. +Live location, direction arrows, speed, and driver info. Auto-refreshes every 30 seconds. + +--- + +## Prerequisites + +- Docker Compose stack running (`timescale_db`, `grafana`) +- `.env` file with all existing variables +- Add one new variable to `.env`: + ``` + GRAFANA_DB_RO_PASSWORD= + ``` + +--- + +## File Structure + +``` +grafana/ +└── provisioning/ + ├── datasources/ + │ └── tracksolid_postgres.yaml + ├── dashboards/ + │ └── noc_fleet.yaml + └── dashboards-json/ + └── noc_fleet_dashboard.json +``` + +--- + +## Step 1 — docker-compose.yaml (grafana service) + +Replace the existing `grafana` service block with: + +```yaml +grafana: + image: grafana/grafana:11.0.0 + restart: always + depends_on: + timescale_db: + condition: service_healthy + env_file: .env + environment: + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} + - GF_USERS_DEFAULT_THEME=dark + - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards-json/noc_fleet_dashboard.json + volumes: + - grafana-data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning:ro +``` + +--- + +## Step 2 — Datasource: `grafana/provisioning/datasources/tracksolid_postgres.yaml` + +```yaml +apiVersion: 1 + +datasources: + - name: TracksolidDB + type: postgres + uid: tracksolid_pg + url: timescale_db:5432 + database: tracksolid_db + user: grafana_ro + secureJsonData: + password: ${GRAFANA_DB_RO_PASSWORD} + jsonData: + sslmode: disable + maxOpenConns: 5 + maxIdleConns: 2 + connMaxLifetime: 14400 + postgresVersion: 1600 + timescaledb: true + editable: false + isDefault: true +``` + +> **Note:** `uid: tracksolid_pg` is referenced by the dashboard JSON. Never rename it after deployment. + +--- + +## Step 3 — Dashboard Provider: `grafana/provisioning/dashboards/noc_fleet.yaml` + +```yaml +apiVersion: 1 + +providers: + - name: NOC Fleet Dashboards + orgId: 1 + folder: NOC + type: file + disableDeletion: true + updateIntervalSeconds: 30 + allowUiUpdates: false + options: + path: /etc/grafana/provisioning/dashboards-json +``` + +--- + +## Step 4 — Dashboard JSON: `grafana/provisioning/dashboards-json/noc_fleet_dashboard.json` + +### Dashboard Settings + +| Setting | Value | +|---------|-------| +| UID | `noc-fleet-live` | +| Auto-refresh | 30s | +| Theme | Dark | +| Schema version | 39 (Grafana 11.0.0) | +| Editable | false | +| Bookmark URL | `/d/noc-fleet-live` | + +--- + +### Panel Layout + +| Row | Panel | Type | Notes | +|-----|-------|------|-------| +| 1 | Total Vehicles | Stat | Count all enabled devices | +| 1 | Online Now | Stat | GPS fix < 5 min — green | +| 1 | Recent (5-30m) | Stat | GPS fix 5-30 min — amber | +| 1 | Offline | Stat | GPS fix > 30 min — red | +| 1 | Moving Now | Stat | speed > 0 AND acc on | +| 1 | Avg Speed (km/h) | Stat | Moving vehicles only | +| 2 | Live Vehicle Locations | Geomap | Direction arrows, color by plate | +| 3 | Vehicle Status Table | Table | All vehicles, sorted Online first | +| 4 | Ingestion Health | Table | Collapsed by default | + +--- + +### SQL Queries + +**Total Vehicles** +```sql +SELECT COUNT(*) AS "Total Vehicles" +FROM tracksolid.devices WHERE enabled_flag = 1; +``` + +**Online Now** +```sql +SELECT COUNT(*) AS "Online" +FROM tracksolid.v_fleet_status +WHERE connectivity_status = 'online'; +``` + +**Recent (5-30 min)** +```sql +SELECT COUNT(*) AS "Recent" +FROM tracksolid.v_fleet_status +WHERE connectivity_status = 'recent'; +``` + +**Offline** +```sql +SELECT COUNT(*) AS "Offline" +FROM tracksolid.v_fleet_status +WHERE connectivity_status = 'offline'; +``` + +**Moving Now** +```sql +SELECT COUNT(*) AS "Moving" +FROM tracksolid.v_fleet_status +WHERE speed > 0 AND acc_status = '1' AND connectivity_status = 'online'; +``` + +**Avg Speed** +```sql +SELECT ROUND(AVG(speed)::numeric, 1) AS "Avg Speed km/h" +FROM tracksolid.v_fleet_status +WHERE speed > 0 AND acc_status = '1' AND connectivity_status = 'online'; +``` + +**Geomap — Live Vehicle Locations** +```sql +SELECT + d.imei, + d.vehicle_number, + d.vehicle_name, + d.driver_name, + d.driver_phone, + d.city, + d.device_group, + lp.lat, + lp.lng, + lp.speed, + lp.direction, + lp.acc_status, + lp.loc_desc, + lp.gps_time, + CASE + WHEN lp.gps_time >= NOW() - INTERVAL '5 minutes' THEN 'online' + WHEN lp.gps_time >= NOW() - INTERVAL '30 minutes' THEN 'recent' + ELSE 'offline' + END AS connectivity_status +FROM tracksolid.devices d +INNER JOIN tracksolid.live_positions lp USING (imei) +WHERE d.enabled_flag = 1 + AND lp.lat IS NOT NULL + AND lp.lng IS NOT NULL +ORDER BY d.vehicle_number; +``` + +> INNER JOIN — only vehicles with a valid GPS fix appear on the map. +> `acc_status` is TEXT ('0'/'1') in the schema — all queries use string comparisons. + +**Vehicle Status Table** +```sql +SELECT + d.vehicle_number AS "Plate", + d.vehicle_name AS "Vehicle", + d.driver_name AS "Driver", + d.driver_phone AS "Driver Phone", + d.city AS "City", + ROUND(lp.speed::numeric, 0) AS "Speed (km/h)", + lp.loc_desc AS "Last Location", + lp.gps_time AS "Last Fix", + CASE + WHEN lp.gps_time >= NOW() - INTERVAL '5 minutes' THEN 'Online' + WHEN lp.gps_time >= NOW() - INTERVAL '30 minutes' THEN 'Recent' + ELSE 'Offline' + END AS "Status", + EXTRACT(EPOCH FROM (NOW() - lp.gps_time))::int / 60 AS "Min Since Fix" +FROM tracksolid.devices d +LEFT JOIN tracksolid.live_positions lp USING (imei) +WHERE d.enabled_flag = 1 +ORDER BY + CASE + WHEN lp.gps_time >= NOW() - INTERVAL '5 minutes' THEN 0 + WHEN lp.gps_time >= NOW() - INTERVAL '30 minutes' THEN 1 + ELSE 2 + END, + d.vehicle_number; +``` + +> LEFT JOIN — offline vehicles (no GPS fix) still appear in the table showing their null status. + +**Ingestion Health** +```sql +SELECT + endpoint AS "Endpoint", + TO_CHAR(run_at, 'HH24:MI DD-Mon') AS "Last Run", + CASE WHEN success THEN 'OK' ELSE 'FAIL' END AS "Result", + error_message AS "Error", + seconds_ago AS "Lag (s)" +FROM tracksolid.v_ingestion_health +ORDER BY endpoint; +``` + +--- + +### Geomap Configuration + +| Property | Value | +|----------|-------| +| Basemap | Carto Dark | +| Location mode | `coords` — fields `lat` / `lng` | +| Marker symbol | `img/icons/marker/arrow-up.svg` (built-in Grafana 11) | +| Rotation | field: `direction` (0° = North, clockwise) | +| Color | field: `vehicle_number`, scheme: `palette-classic-by-name` | +| Tooltip hidden fields | `lat`, `lng`, `imei` | +| Direction unit override | `degree` (shows `245°` in tooltip) | +| Initial view | East Africa center — auto-fits to vehicle positions | + +### Connectivity Status Color Mapping + +| Value | Color | +|-------|-------| +| Online / online | Green | +| Recent / recent | Amber | +| Offline / offline | Red | + +### Stat Panel Thresholds + +| Panel | Green | Amber | Red | +|-------|-------|-------|-----| +| Online Now | > 0 | — | = 0 | +| Recent | — | > 0 | — | +| Offline | = 0 | — | > 0 | +| Moving Now | > 0 | — | — | +| Avg Speed | < 80 km/h | 80-120 | > 120 | + +All stat panels: `colorMode: background`, `graphMode: none`. + +--- + +## Step 5 — Deploy + +```bash +docker compose up -d grafana +``` + +--- + +## Step 6 — Verify + +1. Open `http://localhost:3000/d/noc-fleet-live` +2. Map renders with arrow markers on vehicle positions +3. Arrows rotate per heading (northbound vehicle = arrow pointing up) +4. Tooltip shows: plate, driver name, speed, city, connectivity status +5. Table sorts: Online → Recent → Offline +6. 30s auto-refresh fires (watch "Last Fix" timestamps) +7. Check provisioning logs: + ```bash + docker compose logs grafana | grep -i provision + ``` + +--- + +## Notes + +- The `grafana/` directory must be committed to git — Coolify's clone must include it +- `:ro` mounts prevent Grafana from writing back into the repo +- `allowUiUpdates: false` in the provider YAML means UI edits are not persisted — the JSON file is the source of truth +- `acc_status` in `live_positions` is TEXT (`'0'`/`'1'`), not integer — all queries use string comparisons +- `uid: tracksolid_pg` in the datasource YAML is referenced by the dashboard JSON — never rename it after deployment +- ServiceNow ticket integration is deferred to a future phase