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 <noreply@anthropic.com>
This commit is contained in:
David Kiania 2026-04-09 00:01:52 +03:00
parent 2f3879aa2a
commit cd6b2ca81a
5 changed files with 949 additions and 0 deletions

View file

@ -60,10 +60,14 @@ services:
depends_on: depends_on:
timescale_db: timescale_db:
condition: service_healthy condition: service_healthy
env_file: .env
environment: environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} - 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: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
# COOLIFY DOMAIN LOGIC: # COOLIFY DOMAIN LOGIC:
# You will set the actual URL in the Coolify UI, # You will set the actual URL in the Coolify UI,
# but the service needs to expose port 3000 internally. # but the service needs to expose port 3000 internally.

View file

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

View file

@ -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

View file

@ -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

324
grafanaDeployment.md Normal file
View file

@ -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=<password for grafana_ro postgres user>
```
---
## 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