From 09b38607068215584706e310610741010a8ff61c Mon Sep 17 00:00:00 2001 From: David Kiania Date: Fri, 10 Apr 2026 22:57:36 +0300 Subject: [PATCH] Add fleet business analytics document Covers fleet utilisation, driver behaviour (speeding, harsh driving, tardiness, after-hours movement), real-time dispatch queries, km per driver per day, full business question inventory, Grafana dashboard blueprint, and the 5-step roadmap to unlock remaining capabilities. Co-Authored-By: Claude Sonnet 4.6 --- 01_BusinessAnalytics.md | 769 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 769 insertions(+) create mode 100644 01_BusinessAnalytics.md diff --git a/01_BusinessAnalytics.md b/01_BusinessAnalytics.md new file mode 100644 index 0000000..4463798 --- /dev/null +++ b/01_BusinessAnalytics.md @@ -0,0 +1,769 @@ +# Fireside Communications — Fleet Business Analytics +## Tracksolid Pro · Field Operations & Logistics Intelligence Assessment +### April 2026 + +--- + +## Table of Contents + +1. [Data Foundation Summary](#1-data-foundation-summary) +2. [Fleet Utilisation](#2-fleet-utilisation) +3. [Driver Behaviour](#3-driver-behaviour) +4. [Real-Time Dispatch — Nearest Vehicle to Job](#4-real-time-dispatch--nearest-vehicle-to-job) +5. [Distance per Driver per Day](#5-distance-per-driver-per-day) +6. [Business Questions Now Answerable](#6-business-questions-now-answerable) +7. [Grafana Dashboard Blueprint](#7-grafana-dashboard-blueprint) +8. [What Unlocks the Remaining 30%](#8-what-unlocks-the-remaining-30) + +--- + +## 1. Data Foundation Summary + +The ingestion stack currently populates the following data sources, each feeding the analytics layer: + +| Table | Content | Frequency | +|---|---|---| +| `tracksolid.live_positions` | Current position of every vehicle | Every 60 seconds | +| `tracksolid.position_history` (source: `poll`) | Fleet position snapshot | Every 60 seconds | +| `tracksolid.position_history` (source: `track_list`) | Every GPS waypoint per device | Every 30 minutes (35-min window) | +| `tracksolid.trips` | Trip summaries: distance, speed, duration, idle | Every 15 minutes | +| `tracksolid.parking_events` | Stop/idle events with address and duration | Every 15 minutes | +| `tracksolid.alarms` | Alarm events with type, severity, location | Every 5 minutes | +| `tracksolid.devices` | Vehicle and driver registry | Daily at 02:00 | +| `dwh_gold.fact_daily_fleet_metrics` | Daily KPI aggregates per vehicle | Nightly ETL | + +**Position history density** increased significantly with the addition of `poll_track_list` (POLL-01): + +| Before | After | +|---|---| +| ~1 fix per minute per vehicle | 2–6 fixes per minute per active vehicle | +| Route gaps of 1–2 km between points | Continuous accurate path traces | +| Speed deltas invisible at 60s intervals | Harsh driving events detectable at 10–30s intervals | + +All timestamps are stored in UTC and displayed in `Africa/Nairobi` (EAT = UTC+3) throughout this document. + +--- + +## 2. Fleet Utilisation + +### 2.1 Utilisation Rate + +The percentage of working hours a vehicle is actively driving versus sitting idle or unused. Calculated per vehicle per day: + +```sql +SELECT + t.imei, + d.driver_name, + d.vehicle_number, + DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS working_day, + ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours, + ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours, + ROUND( + SUM(t.driving_time_s) / (10.0 * 3600) * 100, 1 + ) AS utilisation_pct +FROM tracksolid.trips t +JOIN tracksolid.devices d ON d.imei = t.imei +WHERE t.start_time >= CURRENT_DATE AT TIME ZONE 'Africa/Nairobi' + AND t.end_time IS NOT NULL +GROUP BY t.imei, d.driver_name, d.vehicle_number, working_day +ORDER BY utilisation_pct DESC; +``` + +**Benchmark targets:** + +| Rate | Interpretation | Action | +|---|---|---| +| > 70% | Excellent — asset working hard | Monitor driver fatigue | +| 55–70% | Good — healthy operational range | No action required | +| 40–55% | Below average — investigate stops | Review route planning | +| < 40% | Poor — asset underutilised | Reassign or investigate | +| 0% | Vehicle did not move today | Verify not broken down or abandoned | + +> **Note:** The denominator (10 hours) should be adjusted to match your actual contractual shift length. + +--- + +### 2.2 Daily Revenue-Generating Hours vs Fuel-Wasting Idle + +Engine-on-but-stationary time is direct cost with no output. At Kenya diesel prices (~KES 180/litre) and typical 8 L/100 km consumption, a stationary diesel engine burns approximately **0.8 L/hour** at idle. + +```sql +SELECT + imei, + SUM(total_drive_hours) AS drive_hours, + SUM(total_idle_hours) AS idle_hours, + ROUND( + SUM(total_idle_hours) * 0.8 * 180, 0 + ) AS idle_fuel_cost_kes, + ROUND( + SUM(total_idle_hours) / + NULLIF(SUM(total_drive_hours + total_idle_hours), 0) * 100, 1 + ) AS idle_pct +FROM dwh_gold.fact_daily_fleet_metrics +WHERE day >= CURRENT_DATE - INTERVAL '7 days' +GROUP BY imei +ORDER BY idle_fuel_cost_kes DESC; +``` + +**Fleet-wide idle cost this month:** + +```sql +SELECT + ROUND(SUM(total_idle_hours), 1) AS fleet_idle_hours, + ROUND(SUM(total_idle_hours) * 0.8 * 180) AS estimated_wasted_kes +FROM dwh_gold.fact_daily_fleet_metrics +WHERE day >= DATE_TRUNC('month', CURRENT_DATE); +``` + +--- + +### 2.3 Vehicles That Did Not Move Today + +```sql +SELECT + d.imei, + d.vehicle_name, + d.vehicle_number, + d.driver_name, + lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_seen, + lp.speed +FROM tracksolid.devices d +LEFT JOIN tracksolid.live_positions lp ON lp.imei = d.imei +LEFT JOIN tracksolid.trips t + ON t.imei = d.imei + AND DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') = CURRENT_DATE +WHERE d.enabled_flag = 1 + AND t.imei IS NULL +ORDER BY d.imei; +``` + +--- + +## 3. Driver Behaviour + +### 3.1 Speeding + +Counts position fixes where speed exceeded threshold, normalised per 100 km to avoid penalising drivers who simply drive more. + +```sql +WITH driver_speed AS ( + SELECT + ph.imei, + COUNT(*) FILTER (WHERE ph.speed > 80) AS fixes_over_80, + COUNT(*) FILTER (WHERE ph.speed > 100) AS fixes_over_100, + COUNT(*) FILTER (WHERE ph.speed > 120) AS fixes_over_120, + COUNT(*) AS total_fixes + FROM tracksolid.position_history ph + WHERE ph.gps_time > NOW() - INTERVAL '7 days' + AND ph.gps_time < NOW() + AND ph.speed IS NOT NULL + GROUP BY ph.imei +), +driver_km AS ( + SELECT imei, SUM(distance_km) AS total_km + FROM tracksolid.trips + WHERE start_time > NOW() - INTERVAL '7 days' + GROUP BY imei +) +SELECT + ds.imei, + d.driver_name, + d.vehicle_number, + ROUND(dk.total_km, 1) AS km_driven, + ds.fixes_over_80 AS events_80_kmh, + ds.fixes_over_100 AS events_100_kmh, + ds.fixes_over_120 AS events_120_kmh, + ROUND(ds.fixes_over_80 / NULLIF(dk.total_km, 0) * 100, 2) AS rate_per_100km +FROM driver_speed ds +JOIN driver_km dk ON dk.imei = ds.imei +JOIN tracksolid.devices d ON d.imei = ds.imei +ORDER BY rate_per_100km DESC; +``` + +**Severity banding:** + +| Speed | Classification | Response | +|---|---|---| +| 80–100 km/h | Warning | Log, notify supervisor if persistent | +| 100–120 km/h | Serious | Formal driver warning | +| > 120 km/h | Critical | Immediate management escalation | + +--- + +### 3.2 Harsh Driving — Hard Braking and Sudden Acceleration + +Requires `track_list` data (POLL-01). Identifies speed changes greater than 30 km/h within a 60-second window — the signature of hard braking or sudden acceleration. Both events cause tyre wear, brake wear, fuel spikes, and increase accident probability. + +```sql +WITH ordered AS ( + SELECT + imei, + gps_time, + speed, + LAG(speed) OVER (PARTITION BY imei ORDER BY gps_time) AS prev_speed, + LAG(gps_time) OVER (PARTITION BY imei ORDER BY gps_time) AS prev_time + FROM tracksolid.position_history + WHERE source = 'track_list' + AND gps_time > NOW() - INTERVAL '7 days' + AND gps_time < NOW() +) +SELECT + imei, + gps_time AT TIME ZONE 'Africa/Nairobi' AS event_time, + prev_speed AS speed_before, + speed AS speed_after, + ABS(speed - prev_speed) AS delta_kmh, + CASE + WHEN speed > prev_speed THEN 'hard_acceleration' + ELSE 'hard_braking' + END AS event_type +FROM ordered +WHERE ABS(speed - prev_speed) > 30 + AND EXTRACT(EPOCH FROM (gps_time - prev_time)) BETWEEN 5 AND 60 +ORDER BY event_time DESC; +``` + +**Driver aggression index** — normalised harsh events per 100 km: + +```sql +WITH harsh AS ( + SELECT + imei, + COUNT(*) AS harsh_events + FROM ( + SELECT + imei, + speed, + LAG(speed) OVER (PARTITION BY imei ORDER BY gps_time) AS prev_speed, + LAG(gps_time) OVER (PARTITION BY imei ORDER BY gps_time) AS prev_time, + gps_time + FROM tracksolid.position_history + WHERE source = 'track_list' + AND gps_time > NOW() - INTERVAL '30 days' + ) sub + WHERE ABS(speed - prev_speed) > 30 + AND EXTRACT(EPOCH FROM (gps_time - prev_time)) BETWEEN 5 AND 60 + GROUP BY imei +), +km AS ( + SELECT imei, SUM(distance_km) AS total_km + FROM tracksolid.trips + WHERE start_time > NOW() - INTERVAL '30 days' + GROUP BY imei +) +SELECT + h.imei, + d.driver_name, + d.vehicle_number, + h.harsh_events, + ROUND(k.total_km, 0) AS km_driven, + ROUND(h.harsh_events / NULLIF(k.total_km, 0) * 100, 2) AS aggression_index +FROM harsh h +JOIN km k ON k.imei = h.imei +JOIN tracksolid.devices d ON d.imei = h.imei +ORDER BY aggression_index DESC; +``` + +> An aggression index below **0.5** is good. Above **2.0** warrants a driver coaching conversation. Above **5.0** is a safety concern. + +--- + +### 3.3 Tardiness — Late Starts and Early Knock-Off + +**Late starts** (first ignition-on after scheduled shift start): + +```sql +SELECT + f.vehicle_key AS imei, + d.driver_name, + d.vehicle_number, + f.day, + f.day_start_time, + CASE + WHEN f.day_start_time > '07:45:00' THEN + EXTRACT(EPOCH FROM (f.day_start_time - '07:30:00'::TIME)) / 60 + ELSE 0 + END::INT AS minutes_late +FROM dwh_gold.fact_daily_fleet_metrics f +JOIN tracksolid.devices d ON d.imei = f.vehicle_key +WHERE f.day >= CURRENT_DATE - INTERVAL '30 days' + AND f.day_start_time > '07:45:00' +ORDER BY minutes_late DESC; +``` + +**Early knock-off** (last trip ended before scheduled shift end): + +```sql +SELECT + f.vehicle_key AS imei, + d.driver_name, + f.day, + f.day_end_time, + CASE + WHEN f.day_end_time < '17:00:00' THEN + EXTRACT(EPOCH FROM ('17:00:00'::TIME - f.day_end_time)) / 60 + ELSE 0 + END::INT AS minutes_early +FROM dwh_gold.fact_daily_fleet_metrics f +JOIN tracksolid.devices d ON d.imei = f.vehicle_key +WHERE f.day >= CURRENT_DATE - INTERVAL '30 days' + AND f.day_end_time < '17:00:00' + AND f.total_trips > 0 +ORDER BY minutes_early DESC; +``` + +> Adjust `'07:30:00'` and `'17:00:00'` to match your actual contracted shift times. + +**Chronic late starters — monthly pattern:** + +```sql +SELECT + f.vehicle_key AS imei, + d.driver_name, + COUNT(*) AS late_days, + ROUND(AVG( + EXTRACT(EPOCH FROM (f.day_start_time - '07:30:00'::TIME)) / 60 + ), 0) AS avg_minutes_late +FROM dwh_gold.fact_daily_fleet_metrics f +JOIN tracksolid.devices d ON d.imei = f.vehicle_key +WHERE f.day >= DATE_TRUNC('month', CURRENT_DATE) + AND f.day_start_time > '07:45:00' +GROUP BY f.vehicle_key, d.driver_name +HAVING COUNT(*) >= 3 +ORDER BY late_days DESC, avg_minutes_late DESC; +``` + +--- + +### 3.4 After-Hours Movement + +Any trip starting or ending outside contracted hours. Flags unauthorised vehicle use, night deliveries not on schedule, or potential vehicle theft. + +```sql +SELECT + t.imei, + d.driver_name, + d.vehicle_number, + t.start_time AT TIME ZONE 'Africa/Nairobi' AS departure_nairobi, + t.end_time AT TIME ZONE 'Africa/Nairobi' AS arrival_nairobi, + ROUND(t.distance_km::numeric, 1) AS distance_km, + CASE + WHEN EXTRACT(HOUR FROM t.start_time AT TIME ZONE 'Africa/Nairobi') < 6 THEN 'pre-dawn departure' + WHEN EXTRACT(HOUR FROM t.start_time AT TIME ZONE 'Africa/Nairobi') >= 20 THEN 'night departure' + ELSE 'after-hours return' + END AS flag +FROM tracksolid.trips t +JOIN tracksolid.devices d ON d.imei = t.imei +WHERE ( + EXTRACT(HOUR FROM t.start_time AT TIME ZONE 'Africa/Nairobi') < 6 + OR EXTRACT(HOUR FROM t.start_time AT TIME ZONE 'Africa/Nairobi') >= 20 + OR EXTRACT(HOUR FROM t.end_time AT TIME ZONE 'Africa/Nairobi') >= 21 +) + AND t.start_time > NOW() - INTERVAL '30 days' +ORDER BY t.start_time DESC; +``` + +--- + +### 3.5 Km Covered per Driver per Day + +```sql +SELECT + t.imei, + d.driver_name, + d.vehicle_number, + DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS working_day, + ROUND(SUM(t.distance_km)::numeric, 1) AS km_driven, + COUNT(*) AS trips, + ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours, + ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours, + MAX(t.max_speed_kmh) AS peak_speed_kmh, + MIN(t.start_time AT TIME ZONE 'Africa/Nairobi')::TIME AS first_departure, + MAX(t.end_time AT TIME ZONE 'Africa/Nairobi')::TIME AS last_return +FROM tracksolid.trips t +JOIN tracksolid.devices d ON d.imei = t.imei +WHERE t.start_time >= CURRENT_DATE AT TIME ZONE 'Africa/Nairobi' + AND t.end_time IS NOT NULL +GROUP BY t.imei, d.driver_name, d.vehicle_number, working_day +ORDER BY km_driven DESC; +``` + +**Expected daily km benchmarks by vehicle type:** + +| Vehicle Type | Expected Daily km | Flag: Below | Flag: Above | +|---|---|---|---| +| Urban delivery van | 80–150 km | < 40 km | > 300 km | +| Long-haul truck | 300–500 km | < 150 km | > 700 km | +| Field/supervisor vehicle | 50–120 km | < 20 km | > 250 km | +| Motorcycle courier | 60–120 km | < 30 km | > 200 km | + +A driver consistently covering 250 km/day in an urban van either has a legitimately large route or is running personal errands between jobs. Both scenarios need different responses. + +**Weekly km trend per driver:** + +```sql +SELECT + t.imei, + d.driver_name, + DATE_TRUNC('week', t.start_time AT TIME ZONE 'Africa/Nairobi') AS week_start, + ROUND(SUM(t.distance_km)::numeric, 1) AS total_km, + COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS days_active, + ROUND(SUM(t.distance_km)::numeric / + NULLIF(COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')), 0), 1 + ) AS avg_km_per_day +FROM tracksolid.trips t +JOIN tracksolid.devices d ON d.imei = t.imei +WHERE t.start_time > NOW() - INTERVAL '90 days' + AND t.end_time IS NOT NULL +GROUP BY t.imei, d.driver_name, week_start +ORDER BY t.imei, week_start; +``` + +--- + +## 4. Real-Time Dispatch — Nearest Vehicle to Job + +### 4.1 Find the 5 Closest Available Vehicles + +Given a new job at a known location, this query returns the nearest active vehicles with a fresh GPS fix. Runs in milliseconds against the `live_positions` table with the PostGIS spatial index. + +```sql +-- Replace :job_lat and :job_lng with the job coordinates +SELECT + lp.imei, + d.vehicle_name, + d.vehicle_number, + d.driver_name, + d.driver_phone, + d.vehicle_category, + lp.acc_status, + lp.speed, + ROUND( + ST_Distance( + lp.geom::geography, + ST_SetSRID(ST_MakePoint(:job_lng, :job_lat), 4326)::geography + ) / 1000.0, 2 + ) AS distance_km, + ROUND( + ST_Distance( + lp.geom::geography, + ST_SetSRID(ST_MakePoint(:job_lng, :job_lat), 4326)::geography + ) / 1000.0 / 30.0 * 60, 0 + ) AS eta_minutes_urban, + lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_seen +FROM tracksolid.live_positions lp +JOIN tracksolid.devices d ON d.imei = lp.imei +WHERE lp.acc_status = '1' + AND lp.speed < 5 + AND lp.gps_time > NOW() - INTERVAL '5 minutes' +ORDER BY distance_km ASC +LIMIT 5; +``` + +**ETA speed assumptions** — adjust the divisor to match the route type: + +| Route type | Speed (km/h) | Formula | +|---|---|---| +| Nairobi CBD | 20 km/h | `/ 20.0 * 60` | +| Nairobi urban | 30 km/h | `/ 30.0 * 60` | +| Peri-urban | 50 km/h | `/ 50.0 * 60` | +| Highway | 80 km/h | `/ 80.0 * 60` | + +--- + +### 4.2 Dispatch Logic for n8n or API Integration + +The recommended workflow when a new job/ticket arrives: + +1. **Trigger:** New job created (webhook from job management system or n8n) +2. **Force-refresh positions:** Call `get_device_locations()` for the top 10 candidate IMEIs to get sub-second fresh positions before committing +3. **Run dispatch query** above with job coordinates +4. **Filter by vehicle type** if the job requires specific capacity (`AND d.vehicle_category = 'van'`) +5. **Exclude vehicles with open alarms:** `AND NOT EXISTS (SELECT 1 FROM tracksolid.alarms a WHERE a.imei = lp.imei AND a.alarm_time > NOW() - INTERVAL '1 hour')` +6. **Present top 3 candidates** to dispatcher (or auto-assign #1 if fully automated) +7. **Log dispatch decision** to a separate `dispatch_log` table for SLA tracking + +--- + +### 4.3 All Active Vehicles Map — Live Fleet View + +Returns all vehicles with a position fix in the last 10 minutes, suitable for a Grafana Geomap panel with auto-refresh at 30 seconds. + +```sql +SELECT + lp.imei, + COALESCE(d.vehicle_name, d.vehicle_number, lp.imei) AS label, + d.driver_name, + lp.lat, + lp.lng, + lp.speed, + lp.acc_status, + CASE + WHEN lp.speed > 5 THEN 'moving' + WHEN lp.acc_status = '1' THEN 'idle' + ELSE 'parked' + END AS vehicle_state, + lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_seen +FROM tracksolid.live_positions lp +JOIN tracksolid.devices d ON d.imei = lp.imei +WHERE lp.gps_time > NOW() - INTERVAL '10 minutes' +ORDER BY lp.imei; +``` + +--- + +## 5. Distance per Driver per Day + +### 5.1 Today's Summary + +```sql +SELECT + t.imei, + COALESCE(d.driver_name, 'Unassigned') AS driver, + COALESCE(d.vehicle_number, t.imei) AS vehicle, + ROUND(SUM(t.distance_km)::numeric, 1) AS km_today, + COUNT(*) AS trips_today, + ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours, + ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours, + MIN(t.start_time AT TIME ZONE 'Africa/Nairobi')::TIME AS first_departure, + MAX(t.end_time AT TIME ZONE 'Africa/Nairobi')::TIME AS last_return +FROM tracksolid.trips t +JOIN tracksolid.devices d ON d.imei = t.imei +WHERE t.start_time >= CURRENT_DATE AT TIME ZONE 'Africa/Nairobi' + AND t.end_time IS NOT NULL +GROUP BY t.imei, d.driver_name, d.vehicle_number +ORDER BY km_today DESC; +``` + +### 5.2 30-Day Driver Performance Scorecard + +Combines distance, behaviour, and punctuality into a single view per driver. + +```sql +WITH km_summary AS ( + SELECT + imei, + COUNT(DISTINCT DATE(start_time AT TIME ZONE 'Africa/Nairobi')) AS days_active, + ROUND(SUM(distance_km)::numeric, 1) AS total_km, + ROUND(AVG(distance_km)::numeric, 1) AS avg_km_per_trip, + MAX(max_speed_kmh) AS peak_speed + FROM tracksolid.trips + WHERE start_time > NOW() - INTERVAL '30 days' + AND end_time IS NOT NULL + GROUP BY imei +), +alarm_summary AS ( + SELECT imei, COUNT(*) AS alarm_count + FROM tracksolid.alarms + WHERE alarm_time > NOW() - INTERVAL '30 days' + GROUP BY imei +), +late_summary AS ( + SELECT vehicle_key AS imei, COUNT(*) AS late_days + FROM dwh_gold.fact_daily_fleet_metrics + WHERE day > CURRENT_DATE - 30 + AND day_start_time > '07:45:00' + GROUP BY vehicle_key +) +SELECT + k.imei, + d.driver_name, + d.vehicle_number, + k.days_active, + k.total_km, + ROUND(k.total_km / NULLIF(k.days_active, 0), 1) AS avg_km_per_day, + k.peak_speed AS peak_speed_kmh, + COALESCE(a.alarm_count, 0) AS alarms_30d, + COALESCE(l.late_days, 0) AS late_starts_30d +FROM km_summary k +JOIN tracksolid.devices d ON d.imei = k.imei +LEFT JOIN alarm_summary a ON a.imei = k.imei +LEFT JOIN late_summary l ON l.imei = k.imei +ORDER BY k.total_km DESC; +``` + +--- + +## 6. Business Questions Now Answerable + +| Business Question | Primary Data Source | Confidence | +|---|---|---| +| Which vehicles are moving right now? | `live_positions` | High | +| Who started work latest today? | `fact_daily_fleet_metrics.day_start_time` | High | +| Who drove the most km this week? | `trips` + `devices` | High | +| Which vehicle spent the most time idling? | `trips.idle_time_s` | High | +| How much fuel was wasted on idle today? | `trips.idle_time_s` × est. rate | Medium (needs `fuel_100km` set) | +| Which driver triggered the most alarms this month? | `alarms` + `devices` | High | +| What is total fleet distance this month? | `trips` | High | +| Which vehicles did not move at all today? | `trips` LEFT JOIN `devices` | High | +| Who is nearest to a new job right now? | `live_positions` + PostGIS | High | +| Did any vehicle leave depot after hours? | `trips` time filter | High | +| What is the speeding rate per driver per week? | `position_history` speed filter | High | +| Which driver has the harshest driving style? | `position_history` delta query | High (needs 1–2 weeks of `track_list` data to accumulate) | +| Are vehicles on approved routes? | `position_history` + `geofences` | Low (pending geofence population) | +| Is cold chain in temperature range? | `temperature_readings` | Low (pending webhook registration) | +| How much fuel is consumed per route? | `fuel_readings` + `trips` | Low (pending fuel sensor webhook) | +| What is the real odometer per vehicle? | `live_positions.current_mileage` | Medium (depends on tracker calibration) | +| How many km to next service interval? | `live_positions.current_mileage` - last service | Open (requires service log) | +| Did any vehicle enter a restricted zone? | `alarms` (geofence type) + `geofences` | Low (pending geofence setup) | +| Which drivers are consistently late on Mondays? | `fact_daily_fleet_metrics` day-of-week filter | High | +| What percentage of the fleet was utilised today? | `trips` + `devices` count | High | + +--- + +## 7. Grafana Dashboard Blueprint + +### Panel 1 — Real-Time Fleet Map (auto-refresh: 30s) +- **Type:** Geomap +- **Source:** `live_positions` joined to `devices` +- **Colour coding:** + - Green = moving (speed > 5 km/h) + - Amber = ignition on, stationary (acc_status = '1', speed ≤ 5) + - Red = offline (last fix > 10 minutes ago) +- **Tooltip:** driver name, vehicle number, speed, last seen + +### Panel 2 — Fleet Status Summary Row (auto-refresh: 1m) +| Stat | Query | +|---|---| +| Vehicles active now | COUNT WHERE acc_status = '1' AND gps_time > NOW() - 5m | +| Vehicles moving | COUNT WHERE speed > 5 AND gps_time > NOW() - 5m | +| Vehicles offline | COUNT WHERE gps_time < NOW() - 10m | +| Open alarms | COUNT FROM alarms WHERE alarm_time > NOW() - 1h | +| Fleet km today | SUM(distance_km) WHERE start_time >= today | + +### Panel 3 — Daily KPI Table (refresh: 1h) +Columns: Vehicle · Driver · Km Today · Trips · Drive Hours · Idle Hours · First Departure · Last Return · Alarms + +### Panel 4 — Driver Behaviour Leaderboard (refresh: 1h) +Ranked by aggression index (harsh events per 100 km), speeding events, and late starts. Colour-coded red/amber/green per threshold. + +### Panel 5 — Distance Trend (7-day bar chart) +- X-axis: Date +- Y-axis: Total km +- Series: one bar per vehicle or fleet total with daily breakdown + +### Panel 6 — Idle Cost Tracker (refresh: 1h) +- Running total of idle hours and estimated KES wasted this month +- Trend line showing improvement or deterioration week-over-week + +### Panel 7 — Alarm Frequency (30-day time series) +- Line chart: alarm count per day +- Breakdown by alarm type (overspeed, geofence, harsh braking) + +### Panel 8 — Utilisation Heatmap (weekly) +- Y-axis: Vehicle/driver +- X-axis: Day of week +- Colour: utilisation % (green > 60%, amber 40–60%, red < 40%) + +--- + +## 8. What Unlocks the Remaining 30% + +The data foundation is in place. The following five steps activate the remaining analytics capabilities: + +### Step 1 — Register Webhooks in Tracksolid Pro Account *(Blocker)* +Without registration, the following tables remain empty regardless of code: + +| Webhook | Table | Unlocks | +|---|---|---| +| `/pushobd` | `obd_readings` | Engine health, fuel level per fix, RPM | +| `/pushoil` | `fuel_readings` | Fuel theft detection, tank level trend | +| `/pushtem` | `temperature_readings` | Cold chain compliance alerts | +| `/pushlbs` | `lbs_readings` | Positions when GPS signal lost | +| `/pushevent` | `device_events` | Device powered off/on events (tamper detection) | +| `/pushtripreport` | `trips` (push source) | Real-time trip completion events | + +**Action:** Log into Tracksolid Pro → Account Settings → Webhook Configuration → add server URL for each endpoint. + +--- + +### Step 2 — Set `fuel_100km` per Vehicle Type + +Currently null for all 63 devices. Once set, all fuel cost calculations activate automatically. + +```sql +-- Example: set consumption rates by vehicle category +UPDATE tracksolid.devices SET fuel_100km = 8.5 WHERE vehicle_category = 'truck'; +UPDATE tracksolid.devices SET fuel_100km = 7.0 WHERE vehicle_category = 'van'; +UPDATE tracksolid.devices SET fuel_100km = 4.5 WHERE vehicle_category = 'motorcycle'; +UPDATE tracksolid.devices SET fuel_100km = 9.0 WHERE vehicle_category = 'car'; +``` + +--- + +### Step 3 — Populate Vehicle Names and Driver Names + +Currently all 63 devices show blank fields. Reports display IMEI numbers instead of human-readable identities. + +```sql +-- Update individually or import from CSV via COPY +UPDATE tracksolid.devices +SET vehicle_name = 'KBZ 123A', + vehicle_number = 'KBZ 123A', + driver_name = 'John Kamau', + driver_phone = '+254700000001', + vehicle_category = 'van' +WHERE imei = '352093080000001'; +``` + +--- + +### Step 4 — Define Geofences + +Populate `tracksolid.geofences` with: +- **Depot boundaries** — alert when vehicles leave outside working hours +- **Approved route corridors** — alert when vehicles deviate from assigned routes +- **Restricted zones** — alert when vehicles enter prohibited areas (e.g. competitor premises, residential zones during noise hours) + +```sql +-- Example: circular depot geofence +INSERT INTO tracksolid.geofences (fence_id, fence_name, fence_type, geom, radius_m) +VALUES ( + 'depot_nairobi_main', + 'Main Nairobi Depot', + 'circle', + ST_SetSRID(ST_MakePoint(36.8219, -1.2921), 4326), + 200 +); +``` + +--- + +### Step 5 — Run Migrations and Deploy Updated Containers + +```bash +# 1. Run distance correction migration (fixes historical data) +docker exec timescale_db-bo3nov2ija7g8wn9b1g2paxs-210508774107 \ + psql -U postgres -d tracksolid_db -f /migrations/04_bug_fix_migration.sql + +# 2. Run schema enhancement migration (new tables + columns) +docker exec timescale_db-bo3nov2ija7g8wn9b1g2paxs-210508774107 \ + psql -U postgres -d tracksolid_db -f /migrations/05_enhancement_migration.sql + +# 3. Rebuild and restart ingestion containers with updated code +docker compose up -d --build ingest_movement ingest_events webhook_receiver + +# 4. Schedule nightly ETL +# Add to cron or n8n: +# SELECT dwh_gold.refresh_daily_metrics(CURRENT_DATE - 1); +``` + +--- + +## Appendix — Key Metric Thresholds Reference + +| Metric | Green | Amber | Red | +|---|---|---|---| +| Fleet utilisation rate | > 60% | 40–60% | < 40% | +| Idle time as % of shift | < 15% | 15–30% | > 30% | +| Speeding events per 100 km | < 0.5 | 0.5–2.0 | > 2.0 | +| Harsh driving index per 100 km | < 0.5 | 0.5–2.0 | > 2.0 | +| Late starts per month (per driver) | 0–1 | 2–4 | ≥ 5 | +| Days vehicle not used (per month) | 0–2 | 3–5 | > 5 | +| GPS fix age (live_positions) | < 2 min | 2–10 min | > 10 min | +| Alarm rate per vehicle per week | 0–2 | 3–7 | > 7 | + +--- + +*Document generated: 2026-04-10 · Stack: TimescaleDB 2.15 + PostGIS + Tracksolid Pro Open Platform API* +*Ingestion pipeline: `ingest_movement_rev.py` v2.2 · `ingest_events_rev.py` · `webhook_receiver_rev.py`*