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>
8.6 KiB
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) .envfile 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:
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
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_pgis referenced by the dashboard JSON. Never rename it after deployment.
Step 3 — Dashboard Provider: grafana/provisioning/dashboards/noc_fleet.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
SELECT COUNT(*) AS "Total Vehicles"
FROM tracksolid.devices WHERE enabled_flag = 1;
Online Now
SELECT COUNT(*) AS "Online"
FROM tracksolid.v_fleet_status
WHERE connectivity_status = 'online';
Recent (5-30 min)
SELECT COUNT(*) AS "Recent"
FROM tracksolid.v_fleet_status
WHERE connectivity_status = 'recent';
Offline
SELECT COUNT(*) AS "Offline"
FROM tracksolid.v_fleet_status
WHERE connectivity_status = 'offline';
Moving Now
SELECT COUNT(*) AS "Moving"
FROM tracksolid.v_fleet_status
WHERE speed > 0 AND acc_status = '1' AND connectivity_status = 'online';
Avg Speed
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
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_statusis TEXT ('0'/'1') in the schema — all queries use string comparisons.
Vehicle Status Table
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
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
docker compose up -d grafana
Step 6 — Verify
- Open
http://localhost:3000/d/noc-fleet-live - Map renders with arrow markers on vehicle positions
- Arrows rotate per heading (northbound vehicle = arrow pointing up)
- Tooltip shows: plate, driver name, speed, city, connectivity status
- Table sorts: Online → Recent → Offline
- 30s auto-refresh fires (watch "Last Fix" timestamps)
- Check provisioning logs:
docker compose logs grafana | grep -i provision
Notes
- The
grafana/directory must be committed to git — Coolify's clone must include it :romounts prevent Grafana from writing back into the repoallowUiUpdates: falsein the provider YAML means UI edits are not persisted — the JSON file is the source of truthacc_statusinlive_positionsis TEXT ('0'/'1'), not integer — all queries use string comparisonsuid: tracksolid_pgin the datasource YAML is referenced by the dashboard JSON — never rename it after deployment- ServiceNow ticket integration is deferred to a future phase