tracksolid_timescale_grafan.../grafanaDeployment.md
David Kiania cd6b2ca81a 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>
2026-04-09 00:01:52 +03:00

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)
  • .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:

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_pg is 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_status is 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

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