tracksolid_timescale_grafan.../01_BusinessAnalytics.md
David Kiania 40e452e156 Replace hardcoded container names with dynamic lookup
Coolify regenerates the container suffix on every redeploy, making
hardcoded names stale. All three docs now use:
  TS_DB=$(docker ps --filter "name=timescale_db" --format "{{.Names}}" | head -1)

OPERATIONS_MANUAL.md: replaced bare connection string with full
  tsdb() shell function, one-liner pattern, and multi-container
  label-filter guidance.
tracksolid_DB_manual.md: updated header and connection example.
01_BusinessAnalytics.md: updated Step 5 migration commands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 23:09:01 +03:00

27 KiB
Raw Blame History

Fireside Communications — Fleet Business Analytics

Tracksolid Pro · Field Operations & Logistics Intelligence Assessment

April 2026


Table of Contents

  1. Data Foundation Summary
  2. Fleet Utilisation
  3. Driver Behaviour
  4. Real-Time Dispatch — Nearest Vehicle to Job
  5. Distance per Driver per Day
  6. Business Questions Now Answerable
  7. Grafana Dashboard Blueprint
  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 26 fixes per minute per active vehicle
Route gaps of 12 km between points Continuous accurate path traces
Speed deltas invisible at 60s intervals Harsh driving events detectable at 1030s 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:

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
5570% Good — healthy operational range No action required
4055% 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.

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:

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

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.

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
80100 km/h Warning Log, notify supervisor if persistent
100120 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.

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:

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

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

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:

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.

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

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 80150 km < 40 km > 300 km
Long-haul truck 300500 km < 150 km > 700 km
Field/supervisor vehicle 50120 km < 20 km > 250 km
Motorcycle courier 60120 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:

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.

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

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

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.

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 12 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 4060%, 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.

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

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

# Resolve container name dynamically (survives Coolify redeployments)
TS_DB=$(docker ps --filter "name=timescale_db" --format "{{.Names}}" | head -1)

# 1. Run distance correction migration (fixes historical data)
docker exec -i "$TS_DB" psql -U postgres -d tracksolid_db \
  < /migrations/04_bug_fix_migration.sql

# 2. Run schema enhancement migration (new tables + columns)
docker exec -i "$TS_DB" psql -U postgres -d tracksolid_db \
  < /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% 4060% < 40%
Idle time as % of shift < 15% 1530% > 30%
Speeding events per 100 km < 0.5 0.52.0 > 2.0
Harsh driving index per 100 km < 0.5 0.52.0 > 2.0
Late starts per month (per driver) 01 24 ≥ 5
Days vehicle not used (per month) 02 35 > 5
GPS fix age (live_positions) < 2 min 210 min > 10 min
Alarm rate per vehicle per week 02 37 > 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