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>
1544 lines
69 KiB
Markdown
1544 lines
69 KiB
Markdown
# Tracksolid Database Manual
|
||
|
||
**Database:** `tracksolid_db`
|
||
**Host:** `kianiadee@stage.rahamafresh.com`
|
||
**Container:** `timescale_db-*` (Coolify-generated suffix — changes on redeploy, use dynamic lookup below)
|
||
**Engine:** TimescaleDB (PostgreSQL 16 + TimescaleDB 2.15)
|
||
**Timezone note:** All timestamps are stored in UTC. Always cast to `AT TIME ZONE 'Africa/Nairobi'` (EAT = UTC+3) when displaying to users or building reports.
|
||
|
||
To connect from the host server:
|
||
```bash
|
||
# Dynamic — works after every Coolify redeploy
|
||
tsdb # if ~/.zshrc function is installed (see OPERATIONS_MANUAL.md)
|
||
|
||
# Or inline:
|
||
TS_DB=$(docker ps --filter "name=timescale_db" --format "{{.Names}}" | head -1)
|
||
docker exec -it "$TS_DB" psql -U postgres -d tracksolid_db
|
||
```
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Schema Overview](#1-schema-overview)
|
||
2. [tracksolid.devices](#2-tracksoliddevices)
|
||
3. [tracksolid.position_history](#3-tracksolidposition_history)
|
||
4. [tracksolid.trips](#4-tracksolidtrips)
|
||
5. [tracksolid.live_positions](#5-tracksolidlive_positions)
|
||
6. [tracksolid.alarms](#6-tracksolidalarms)
|
||
7. [tracksolid.ingestion_log](#7-tracksolidingestion_log)
|
||
8. [tracksolid.heartbeats](#8-tracksolidheartbeats)
|
||
9. [tracksolid.obd_readings](#9-tracksolidobd_readings)
|
||
10. [tracksolid.parking_events](#10-tracksolidparking_events)
|
||
11. [tracksolid.fault_codes](#11-trackshopfault_codes)
|
||
12. [dwh_gold.dim_vehicles](#12-dwh_golddim_vehicles)
|
||
13. [dwh_gold.fact_daily_fleet_metrics](#13-dwh_goldfact_daily_fleet_metrics)
|
||
14. [Business Intelligence Queries](#14-business-intelligence-queries)
|
||
15. [Today's Metrics — From 00:00 Nairobi Time to Now](#15-todays-metrics--from-0000-nairobi-time-to-now)
|
||
16. [Daily Analytics](#16-daily-analytics)
|
||
17. [Weekly Analytics](#17-weekly-analytics)
|
||
18. [Monthly Analytics](#18-monthly-analytics)
|
||
19. [Quarterly Analytics](#19-quarterly-analytics)
|
||
20. [New Tables from Migration 05](#20-new-tables-from-migration-05)
|
||
21. [Known Data Issues](#21-known-data-issues)
|
||
|
||
---
|
||
|
||
## 1. Schema Overview
|
||
|
||
The database is organised into three schemas:
|
||
|
||
| Schema | Purpose |
|
||
|---|---|
|
||
| `tracksolid` | Raw operational data ingested from the Tracksolid/Jimi Open Platform API |
|
||
| `dwh_gold` | Pre-aggregated data warehouse layer for reporting and dashboards |
|
||
| `public` | PostGIS spatial reference tables (system-managed) |
|
||
|
||
Data is pulled from the Jimi/Tracksolid API on a continuous polling schedule. The `ingestion_log` table records every API call so you can audit pipeline health.
|
||
|
||
### List all tables with sizes
|
||
|
||
This query gives a quick inventory of every table in the two business schemas along with its on-disk size. Useful as a first step when connecting to the database to understand what exists and which tables hold the most data.
|
||
|
||
```sql
|
||
SELECT
|
||
schemaname,
|
||
tablename,
|
||
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS size
|
||
FROM pg_tables
|
||
WHERE schemaname IN ('tracksolid', 'dwh_gold')
|
||
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC;
|
||
```
|
||
|
||
### Count rows in all tables
|
||
|
||
A fast sanity check to see which tables are populated and which are still empty. Run this after deployments or data migrations to confirm data is flowing as expected.
|
||
|
||
```sql
|
||
SELECT 'tracksolid.trips' AS tbl, COUNT(*) FROM tracksolid.trips
|
||
UNION ALL SELECT 'tracksolid.position_history', COUNT(*) FROM tracksolid.position_history
|
||
UNION ALL SELECT 'tracksolid.alarms', COUNT(*) FROM tracksolid.alarms
|
||
UNION ALL SELECT 'tracksolid.devices', COUNT(*) FROM tracksolid.devices
|
||
UNION ALL SELECT 'tracksolid.heartbeats', COUNT(*) FROM tracksolid.heartbeats
|
||
UNION ALL SELECT 'tracksolid.obd_readings', COUNT(*) FROM tracksolid.obd_readings
|
||
UNION ALL SELECT 'tracksolid.parking_events', COUNT(*) FROM tracksolid.parking_events
|
||
UNION ALL SELECT 'tracksolid.live_positions', COUNT(*) FROM tracksolid.live_positions
|
||
UNION ALL SELECT 'tracksolid.fault_codes', COUNT(*) FROM tracksolid.fault_codes
|
||
UNION ALL SELECT 'tracksolid.device_events', COUNT(*) FROM tracksolid.device_events
|
||
UNION ALL SELECT 'tracksolid.fuel_readings', COUNT(*) FROM tracksolid.fuel_readings
|
||
UNION ALL SELECT 'tracksolid.temperature_readings', COUNT(*) FROM tracksolid.temperature_readings
|
||
UNION ALL SELECT 'tracksolid.lbs_readings', COUNT(*) FROM tracksolid.lbs_readings
|
||
UNION ALL SELECT 'tracksolid.geofences', COUNT(*) FROM tracksolid.geofences
|
||
UNION ALL SELECT 'tracksolid.ingestion_log', COUNT(*) FROM tracksolid.ingestion_log
|
||
UNION ALL SELECT 'dwh_gold.dim_vehicles', COUNT(*) FROM dwh_gold.dim_vehicles
|
||
UNION ALL SELECT 'dwh_gold.fact_daily_fleet_metrics', COUNT(*) FROM dwh_gold.fact_daily_fleet_metrics;
|
||
```
|
||
|
||
---
|
||
|
||
## 2. tracksolid.devices
|
||
|
||
This table is the master registry of all GPS tracking devices in the fleet. Each row represents one physical GPS tracker unit installed in a vehicle. Key fields include the device's IMEI (its unique hardware identifier), vehicle registration details (name, plate number, brand, model), driver assignment, SIM card details, and subscription lifecycle timestamps. The `current_mileage_km` field reflects the latest odometer reading received from the device. At time of audit, 63 devices were registered, all in a single "Default group", but `vehicle_name`, `vehicle_number`, and `driver_name` were not yet populated — these need to be filled in for reports to be human-readable.
|
||
|
||
The `fuel_100km` column is particularly important: it is the reference value used to estimate `fuel_consumed_l` in the trips table. If it is null, fuel calculations will not work.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d tracksolid.devices
|
||
```
|
||
|
||
### List all active devices with odometer readings
|
||
|
||
Returns all enabled devices ordered by highest mileage. Use this to identify high-utilisation vehicles that may need servicing, and to verify that vehicle metadata (name, plate, driver) has been filled in. Vehicles with very high odometer readings but blank names are a data quality flag.
|
||
|
||
```sql
|
||
SELECT
|
||
imei,
|
||
vehicle_name,
|
||
vehicle_number,
|
||
driver_name,
|
||
mc_type,
|
||
device_group,
|
||
status,
|
||
current_mileage_km,
|
||
last_synced_at AT TIME ZONE 'Africa/Nairobi' AS last_synced_nairobi
|
||
FROM tracksolid.devices
|
||
WHERE enabled_flag = 1
|
||
ORDER BY current_mileage_km DESC NULLS LAST;
|
||
```
|
||
|
||
---
|
||
|
||
## 3. tracksolid.position_history
|
||
|
||
This is the core time-series table and is implemented as a **TimescaleDB hypertable**, automatically partitioned by time into chunks for efficient storage and querying of large volumes of GPS data. Two separate ingestion paths write to this table:
|
||
|
||
| Source | `source` value | Frequency | What it captures |
|
||
|---|---|---|---|
|
||
| `poll_live_positions` | `poll` | Every 60 seconds | Latest position snapshot across entire fleet |
|
||
| `poll_track_list` | `track_list` | Every 30 minutes (35-min window) | Every waypoint the device logged — typically 1 fix per 10–30 seconds while moving |
|
||
|
||
The `track_list` source (added in FIX-M14) fills the critical gap where a vehicle can travel several kilometres between 60-second fleet sweeps with no trace. Combined, both sources yield approximately **2–6 fixes per minute per active vehicle**.
|
||
|
||
Each row captures the device's location (latitude, longitude, and PostGIS geometry), speed, heading, ignition status, satellite count, and running odometer. The `acc_status` field indicates whether the vehicle's ignition/accessory circuit is on (`1`) or off (`0`). The table uses a composite primary key of `(imei, gps_time)`, ensuring no duplicates regardless of which path writes the row. Older chunks are transparently compressed by TimescaleDB to save disk space.
|
||
|
||
**Important:** `altitude` is present in the schema but not currently populated by either ingestion path.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d tracksolid.position_history
|
||
```
|
||
|
||
### Most recent GPS pings per vehicle (Nairobi time)
|
||
|
||
Returns the latest position record for each device. Use this to quickly check which vehicles are currently active, where they last reported from, and whether the GPS lock is good (satellite count ≥ 8 is acceptable; ≥ 12 is excellent).
|
||
|
||
```sql
|
||
SELECT
|
||
ph.imei,
|
||
ph.gps_time AT TIME ZONE 'Africa/Nairobi' AS gps_nairobi,
|
||
ph.lat,
|
||
ph.lng,
|
||
ph.speed,
|
||
ph.direction,
|
||
ph.acc_status,
|
||
ph.current_mileage,
|
||
ph.altitude,
|
||
ph.satellite
|
||
FROM tracksolid.position_history ph
|
||
ORDER BY ph.gps_time DESC
|
||
LIMIT 15;
|
||
```
|
||
|
||
### All GPS pings up to the current moment
|
||
|
||
Returns position history records with a `gps_time` strictly before `NOW()`. `NOW()` returns the current timestamp in UTC, which matches the stored timezone of the column directly — no conversion needed in the `WHERE` clause. This filter is useful when building live queries or scheduled reports that must not accidentally include future-dated records from a faulty device clock. Combine with a lower-bound interval to avoid scanning the entire table; the example below limits results to the last 24 hours.
|
||
|
||
```sql
|
||
SELECT
|
||
ph.imei,
|
||
ph.gps_time AT TIME ZONE 'Africa/Nairobi' AS gps_nairobi,
|
||
ph.lat,
|
||
ph.lng,
|
||
ph.speed,
|
||
ph.direction,
|
||
ph.acc_status,
|
||
ph.current_mileage,
|
||
ph.altitude,
|
||
ph.satellite
|
||
FROM tracksolid.position_history ph
|
||
WHERE ph.gps_time > NOW() - INTERVAL '24 hours'
|
||
AND ph.gps_time < NOW()
|
||
ORDER BY ph.gps_time DESC;
|
||
```
|
||
|
||
To widen or narrow the window, replace the interval:
|
||
|
||
| Interval | Example |
|
||
|---|---|
|
||
| Last hour | `NOW() - INTERVAL '1 hour'` |
|
||
| Last 24 hours | `NOW() - INTERVAL '24 hours'` |
|
||
| Last 7 days | `NOW() - INTERVAL '7 days'` |
|
||
| Since midnight Nairobi time | See today's metrics section below |
|
||
|
||
---
|
||
|
||
### Full position trail for a specific vehicle on a given day
|
||
|
||
Replaces `'YOUR_IMEI_HERE'` and the date with actual values. Useful for replaying a vehicle's route through the day, debugging missing trip segments, or feeding data into a mapping tool.
|
||
|
||
```sql
|
||
SELECT
|
||
gps_time AT TIME ZONE 'Africa/Nairobi' AS gps_nairobi,
|
||
lat,
|
||
lng,
|
||
speed,
|
||
direction,
|
||
acc_status,
|
||
current_mileage,
|
||
satellite
|
||
FROM tracksolid.position_history
|
||
WHERE imei = 'YOUR_IMEI_HERE'
|
||
AND gps_time >= '2026-04-10 00:00:00+03'
|
||
AND gps_time < '2026-04-11 00:00:00+03'
|
||
ORDER BY gps_time ASC;
|
||
```
|
||
|
||
---
|
||
|
||
### Data density check — poll vs track_list coverage
|
||
|
||
Use this to verify that `poll_track_list` (POLL-01) is ingesting data and to compare row counts between the two sources. After the first 30-minute cycle you should see `track_list` rows outnumber `poll` rows by roughly 4–10×.
|
||
|
||
```sql
|
||
SELECT
|
||
source,
|
||
COUNT(*) AS total_rows,
|
||
COUNT(DISTINCT imei) AS devices_seen,
|
||
MIN(gps_time AT TIME ZONE 'Africa/Nairobi') AS earliest,
|
||
MAX(gps_time AT TIME ZONE 'Africa/Nairobi') AS latest
|
||
FROM tracksolid.position_history
|
||
WHERE gps_time > NOW() - INTERVAL '2 hours'
|
||
GROUP BY source
|
||
ORDER BY source;
|
||
```
|
||
|
||
---
|
||
|
||
### High-resolution speed profile — harsh driving detection
|
||
|
||
Flags 30-second windows where a vehicle's speed changed by more than 30 km/h (sudden acceleration or hard braking). Requires `track_list` data to have meaningful resolution — with only 60-second fleet sweeps this query would miss most events.
|
||
|
||
```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 '24 hours'
|
||
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,
|
||
EXTRACT(EPOCH FROM (gps_time - prev_time))::INT AS interval_s,
|
||
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 delta_kmh DESC;
|
||
```
|
||
|
||
---
|
||
|
||
### Continuous route trace for Grafana map panel
|
||
|
||
Returns ordered waypoints for all vehicles in the last hour suitable for rendering as a continuous path in Grafana's Geomap plugin. The `source` filter includes both ingestion paths to maximise trace density.
|
||
|
||
```sql
|
||
SELECT
|
||
imei,
|
||
gps_time AT TIME ZONE 'Africa/Nairobi' AS gps_nairobi,
|
||
lat,
|
||
lng,
|
||
speed,
|
||
acc_status,
|
||
source
|
||
FROM tracksolid.position_history
|
||
WHERE gps_time > NOW() - INTERVAL '1 hour'
|
||
AND gps_time < NOW()
|
||
ORDER BY imei, gps_time ASC;
|
||
```
|
||
|
||
---
|
||
|
||
## 4. tracksolid.trips
|
||
|
||
The trips table stores auto-detected journey segments. A trip begins when the vehicle's ignition turns on and ends when it turns off again (or after a prolonged stationary period). Each row summarises one journey: start and end times, start and end coordinates, total distance, average and maximum speed, driving time, idle time, and estimated fuel consumption.
|
||
|
||
**Note on `distance_km`:** Stores trip distance directly in kilometres. Prior to migration 04 this column was named `distance_m` and incorrectly held millimetres due to an erroneous `× 1000` in the ingestion code. Migration 04 corrected all historical rows (`÷ 1,000,000`) and renamed the column. Use the raw `distance_km` value in queries — no further division is needed.
|
||
|
||
At the time of audit, `max_speed_kmh` was null on all trips (not yet computed by the trip detection logic) and `fuel_consumed_l` was null because `fuel_100km` is not set on the devices.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d tracksolid.trips
|
||
```
|
||
|
||
### Recent trips with full detail (Nairobi time)
|
||
|
||
Lists the 20 most recent trip records joined with vehicle metadata. Replace `LIMIT 20` with a larger number or remove it to see more history. This is the primary query for reviewing daily driving activity.
|
||
|
||
```sql
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
t.imei,
|
||
t.start_time AT TIME ZONE 'Africa/Nairobi' AS start_nairobi,
|
||
t.end_time AT TIME ZONE 'Africa/Nairobi' AS end_nairobi,
|
||
ROUND(t.distance_km, 3) AS distance_km,
|
||
t.avg_speed_kmh,
|
||
t.max_speed_kmh,
|
||
ROUND(t.driving_time_s / 60.0, 1) AS drive_min,
|
||
ROUND(t.idle_time_s / 60.0, 1) AS idle_min,
|
||
t.fuel_consumed_l
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
ORDER BY t.start_time DESC
|
||
LIMIT 20;
|
||
```
|
||
|
||
### Verify distance accuracy (cross-check via speed × time)
|
||
|
||
Sanity-check query confirming that `distance_km` values are reasonable by comparing them against the expected distance derived from `avg_speed_kmh × driving_time_s`. After migration 04 the two columns should match closely (within a few percent). Run this after deploying updated ingestion containers or after any data correction.
|
||
|
||
```sql
|
||
SELECT
|
||
t.imei,
|
||
t.start_time AT TIME ZONE 'Africa/Nairobi' AS start_nbi,
|
||
t.end_time AT TIME ZONE 'Africa/Nairobi' AS end_nbi,
|
||
ROUND(t.distance_km::numeric, 3) AS distance_km,
|
||
t.avg_speed_kmh,
|
||
t.driving_time_s,
|
||
ROUND((t.avg_speed_kmh * t.driving_time_s / 3600.0), 3) AS expected_km_from_speed,
|
||
ROUND(
|
||
ABS(t.distance_km - (t.avg_speed_kmh * t.driving_time_s / 3600.0))
|
||
/ NULLIF(t.avg_speed_kmh * t.driving_time_s / 3600.0, 0) * 100
|
||
, 1) AS variance_pct
|
||
FROM tracksolid.trips t
|
||
WHERE t.avg_speed_kmh IS NOT NULL
|
||
AND t.driving_time_s IS NOT NULL
|
||
AND t.distance_km > 0
|
||
ORDER BY t.start_time DESC
|
||
LIMIT 10;
|
||
```
|
||
|
||
### Speed band distribution across all trips
|
||
|
||
Categorises trips by average speed to understand whether the fleet primarily operates in congested urban conditions, normal urban flow, or open highway. The result helps benchmark expected journey times and assess whether vehicles are being used efficiently.
|
||
|
||
```sql
|
||
SELECT
|
||
CASE
|
||
WHEN avg_speed_kmh < 20 THEN '0–20 km/h (slow / heavy traffic)'
|
||
WHEN avg_speed_kmh < 40 THEN '20–40 km/h (normal urban)'
|
||
WHEN avg_speed_kmh < 60 THEN '40–60 km/h (highway)'
|
||
WHEN avg_speed_kmh < 80 THEN '60–80 km/h (fast highway)'
|
||
ELSE '80+ km/h (very fast)'
|
||
END AS speed_band,
|
||
COUNT(*) AS trip_count
|
||
FROM tracksolid.trips
|
||
WHERE avg_speed_kmh IS NOT NULL
|
||
GROUP BY 1
|
||
ORDER BY trip_count DESC;
|
||
```
|
||
|
||
---
|
||
|
||
## 5. tracksolid.live_positions
|
||
|
||
This table holds one row per device and is continuously upserted with the latest known position every time the ingestion service polls the API. It is the equivalent of a "last known state" snapshot and is designed for real-time dashboard displays — you never need to scan the full `position_history` table just to find out where a vehicle currently is.
|
||
|
||
Beyond basic GPS coordinates, `live_positions` captures richer status fields than `position_history`: battery level (`elec_quantity`), external power voltage (`power_value`), device operational status (`device_status`), expiry and activation flags, and a human-readable address description (`loc_desc`). The `tracker_oil` field reflects whether the relay/immobiliser output is active.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d tracksolid.live_positions
|
||
```
|
||
|
||
### Current status of all vehicles (Nairobi time)
|
||
|
||
Returns the latest known position and status for every device. Use this as the live fleet map feed. Vehicles with `acc_status = '1'` are currently running; `'0'` means engine off. Check `expire_flag` to catch devices whose subscriptions are about to lapse.
|
||
|
||
```sql
|
||
SELECT
|
||
lp.imei,
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
lp.lat,
|
||
lp.lng,
|
||
lp.speed,
|
||
lp.acc_status,
|
||
lp.device_status,
|
||
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS gps_nairobi,
|
||
lp.updated_at AT TIME ZONE 'Africa/Nairobi' AS last_updated_nairobi,
|
||
lp.elec_quantity,
|
||
lp.power_value,
|
||
lp.expire_flag,
|
||
lp.loc_desc
|
||
FROM tracksolid.live_positions lp
|
||
LEFT JOIN tracksolid.devices d ON d.imei = lp.imei
|
||
ORDER BY lp.updated_at DESC;
|
||
```
|
||
|
||
---
|
||
|
||
## 6. tracksolid.alarms
|
||
|
||
The alarms table records every alarm event reported by the Tracksolid API for any device in the fleet. Alarm types can include overspeed, harsh braking, geofence violations, power disconnection, low battery, tampering, and more — the exact set depends on the tracker model and account configuration. Each alarm record captures the device IMEI, alarm type and name, the timestamp and GPS coordinates at which the alarm fired, the vehicle's speed at that moment, and ignition status.
|
||
|
||
**Fixed [FIX-E06]:** At the time of the initial audit all 1,054 records had `alarm_type` and `alarm_name` null because the polling code was reading webhook field names (`alarmType`, `alarmName`, `alarmTime`) instead of the poll API field names (`alertTypeId`, `alarmTypeName`, `alertTime`). This has been corrected in `ingest_events_rev.py`. New alarm records ingested after the fix will contain classified alarm types. Migration 05 also added `severity`, `geofence_id`, `geofence_name`, `acknowledged_at`, and `acknowledged_by` columns for richer alarm management.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d tracksolid.alarms
|
||
```
|
||
|
||
### Recent alarms with Nairobi timestamp
|
||
|
||
Returns the most recent alarm events. Once `alarm_type` and `alarm_name` are being populated correctly, this query becomes the primary tool for investigating safety incidents, policy violations, and device health events.
|
||
|
||
```sql
|
||
SELECT
|
||
a.imei,
|
||
d.vehicle_name,
|
||
d.driver_name,
|
||
a.alarm_time AT TIME ZONE 'Africa/Nairobi' AS alarm_nairobi,
|
||
a.alarm_type,
|
||
a.alarm_name,
|
||
a.speed,
|
||
a.acc_status,
|
||
a.lat,
|
||
a.lng,
|
||
a.source
|
||
FROM tracksolid.alarms a
|
||
LEFT JOIN tracksolid.devices d ON d.imei = a.imei
|
||
ORDER BY a.alarm_time DESC
|
||
LIMIT 50;
|
||
```
|
||
|
||
### Alarm frequency by type
|
||
|
||
Once alarm types are populated, this aggregation shows which alarm categories are most common across the fleet. High overspeed counts flag driver behaviour issues; high power-disconnect counts may indicate tampering or device faults.
|
||
|
||
```sql
|
||
SELECT
|
||
alarm_type,
|
||
alarm_name,
|
||
COUNT(*) AS total_alarms,
|
||
COUNT(DISTINCT imei) AS vehicles_affected,
|
||
MIN(alarm_time AT TIME ZONE 'Africa/Nairobi') AS first_seen,
|
||
MAX(alarm_time AT TIME ZONE 'Africa/Nairobi') AS last_seen
|
||
FROM tracksolid.alarms
|
||
GROUP BY alarm_type, alarm_name
|
||
ORDER BY total_alarms DESC;
|
||
```
|
||
|
||
---
|
||
|
||
## 7. tracksolid.ingestion_log
|
||
|
||
The ingestion log is the pipeline health ledger. Every time the ingestion service calls a Tracksolid API endpoint, it writes one row here recording: which endpoint was called, how many device IMEIs were processed, how many rows were inserted or upserted into the database, how long the call took (in milliseconds), and whether it succeeded. If a call fails, `success` is set to false and the error details are captured in `error_code` and `error_message`.
|
||
|
||
This table is the first place to check when troubleshooting missing data or unexpected gaps in position history. It tells you definitively whether the pipeline ran and what it ingested, as opposed to whether the GPS device itself was transmitting.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d tracksolid.ingestion_log
|
||
```
|
||
|
||
### Pipeline health summary by endpoint
|
||
|
||
Aggregates the ingestion log by endpoint to show total run counts, data volumes, average call duration, and failure rate. A non-zero `failure_count` needs investigation. A sudden drop in `avg_rows_per_run` compared to historical baseline may indicate the API rate limit was hit or devices went offline.
|
||
|
||
```sql
|
||
SELECT
|
||
endpoint,
|
||
COUNT(*) AS runs,
|
||
SUM(rows_inserted) AS total_inserted,
|
||
SUM(rows_upserted) AS total_upserted,
|
||
ROUND(AVG(rows_inserted)) AS avg_rows_per_run,
|
||
ROUND(AVG(duration_ms)) AS avg_duration_ms,
|
||
MAX(duration_ms) AS max_duration_ms,
|
||
SUM(CASE WHEN NOT success THEN 1 ELSE 0 END) AS failure_count,
|
||
MIN(run_at AT TIME ZONE 'Africa/Nairobi') AS first_run,
|
||
MAX(run_at AT TIME ZONE 'Africa/Nairobi') AS last_run
|
||
FROM tracksolid.ingestion_log
|
||
GROUP BY endpoint
|
||
ORDER BY runs DESC;
|
||
```
|
||
|
||
### Recent pipeline failures
|
||
|
||
Filters the log to show only failed ingestion runs. Run this immediately when investigating data gaps or alert emails from the pipeline. The `error_message` column usually contains the HTTP status code and API error body.
|
||
|
||
```sql
|
||
SELECT
|
||
run_at AT TIME ZONE 'Africa/Nairobi' AS run_nairobi,
|
||
endpoint,
|
||
imei_count,
|
||
rows_inserted,
|
||
duration_ms,
|
||
error_code,
|
||
error_message
|
||
FROM tracksolid.ingestion_log
|
||
WHERE success = false
|
||
ORDER BY run_at DESC
|
||
LIMIT 50;
|
||
```
|
||
|
||
---
|
||
|
||
## 8. tracksolid.heartbeats
|
||
|
||
The heartbeats table is designed to record periodic keep-alive signals sent by GPS devices when they are stationary for extended periods. Rather than inserting a full position record every minute even when nothing has changed, some tracker firmware sends a lightweight heartbeat ping to confirm the device is still powered on and connected. This can be used to distinguish "vehicle parked with tracker alive" from "tracker lost power or signal".
|
||
|
||
At the time of audit this table contained **0 rows**. Either the tracker models currently deployed do not send heartbeat packets, or the heartbeat ingestion endpoint has not yet been implemented.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d tracksolid.heartbeats
|
||
```
|
||
|
||
---
|
||
|
||
## 9. tracksolid.obd_readings
|
||
|
||
The OBD (On-Board Diagnostics) readings table is intended to store data pulled from a vehicle's OBD-II port via a compatible tracker. OBD data can include engine RPM, coolant temperature, throttle position, fuel level, battery voltage, and diagnostic trouble codes. This information is valuable for predictive maintenance and driver behaviour scoring beyond what GPS alone provides.
|
||
|
||
At the time of audit this table contained **0 rows**. The JC400P and X3 tracker models in the fleet may support OBD connectivity, but either the OBD cables are not installed or the OBD data ingestion pipeline has not been built yet.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d tracksolid.obd_readings
|
||
```
|
||
|
||
---
|
||
|
||
## 10. tracksolid.parking_events
|
||
|
||
This table is designed to store discrete parking events — records of when a vehicle stopped and for how long. Rather than deriving parking from position history (which requires scanning many rows), the Tracksolid API's `jimi.open.platform.report.parking` endpoint provides pre-computed parking events. These are useful for calculating driver wait times, identifying vehicles left idle in locations for long periods, and auditing whether vehicles are parked at authorised locations overnight.
|
||
|
||
**Fixed [FIX-M13]:** At the time of the initial audit this table contained 0 rows despite 358 successful API calls. The root cause was two missing parameters in the request — `account` (required to scope results to the fleet) and `acc_type=0` (required to include all stop types). Additionally, the response field `durSecond` was being read as `seconds`, causing duration to always be null. All three issues are corrected in `ingest_movement_rev.py`. Parking events should now populate on the next poll cycle.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d tracksolid.parking_events
|
||
```
|
||
|
||
---
|
||
|
||
## 11. tracksolid.fault_codes
|
||
|
||
The fault codes table stores vehicle diagnostic trouble codes (DTCs) received from OBD-connected trackers. When a vehicle's engine management system logs a fault (e.g. P0300 for a misfire, P0171 for a lean fuel mixture), the tracker reads it via the OBD port and transmits it to the Tracksolid platform, from where it is ingested into this table. This enables remote fleet health monitoring without requiring drivers to visit a workshop.
|
||
|
||
At the time of audit this table contained **0 rows**, which is consistent with the OBD readings table also being empty. Fault code ingestion depends on OBD connectivity being established first.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d tracksolid.fault_codes
|
||
```
|
||
|
||
---
|
||
|
||
## 12. dwh_gold.dim_vehicles
|
||
|
||
This is the vehicles dimension table in the data warehouse gold layer. It is intended to be a clean, enriched, business-friendly view of the fleet — combining device metadata from `tracksolid.devices` with any additional attributes needed for reporting (cost centre, vehicle category, assigned route, etc.). Dimension tables in a star schema are typically populated by an ETL job that joins and transforms raw operational tables.
|
||
|
||
At the time of audit this table contained **0 rows**. The ETL pipeline that populates the gold layer has not yet been run.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d dwh_gold.dim_vehicles
|
||
```
|
||
|
||
---
|
||
|
||
## 13. dwh_gold.fact_daily_fleet_metrics
|
||
|
||
This is the central fact table of the data warehouse. It is designed to hold one pre-aggregated row per vehicle per day, summarising distance driven, fuel consumed, driving time, idle time, trip count, first departure time, last return time, and alarm counts. Pre-aggregating at this level makes dashboards and management reports extremely fast — a full month's fleet summary requires scanning at most 63 vehicles × 31 days = ~2,000 rows rather than hundreds of thousands of raw trip and position records.
|
||
|
||
At the time of audit this table contained **0 rows**. Once the ETL job is running, this should be the primary data source for all Grafana dashboards and business reports.
|
||
|
||
### Describe table structure
|
||
|
||
```sql
|
||
\d dwh_gold.fact_daily_fleet_metrics
|
||
```
|
||
|
||
---
|
||
|
||
## 14. Business Intelligence Queries
|
||
|
||
### Daily work start/end times and driving summary per vehicle (Nairobi time)
|
||
|
||
This is the primary operational report query. For each vehicle on each working day, it computes: the time the first trip of the day began (proxy for "driver started work"), the time the last trip ended (proxy for "driver finished work"), total distance driven, number of trips made, total driving time, and total idle time. Join with `tracksolid.devices` to add vehicle names and driver names once those fields are populated.
|
||
|
||
The query groups by IMEI and calendar date in Nairobi time, so a trip that starts at 23:58 EAT correctly belongs to that day rather than rolling over to the next UTC day.
|
||
|
||
```sql
|
||
WITH daily AS (
|
||
SELECT
|
||
t.imei,
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS work_date,
|
||
MIN(t.start_time AT TIME ZONE 'Africa/Nairobi') AS first_trip_start,
|
||
MAX(t.end_time AT TIME ZONE 'Africa/Nairobi') AS last_trip_end,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
|
||
COUNT(*) AS trip_count,
|
||
ROUND(SUM(t.driving_time_s) / 60.0, 1) AS total_drive_min,
|
||
ROUND(SUM(t.idle_time_s) / 60.0, 1) AS total_idle_min,
|
||
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS avg_speed_kmh,
|
||
MAX(t.max_speed_kmh) AS peak_speed_kmh
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
WHERE t.end_time IS NOT NULL
|
||
GROUP BY
|
||
t.imei, d.vehicle_name, d.vehicle_number, d.driver_name,
|
||
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')
|
||
)
|
||
SELECT
|
||
work_date,
|
||
imei,
|
||
vehicle_name,
|
||
vehicle_number,
|
||
driver_name,
|
||
TO_CHAR(first_trip_start, 'HH24:MI') AS day_start,
|
||
TO_CHAR(last_trip_end, 'HH24:MI') AS day_end,
|
||
EXTRACT(EPOCH FROM (last_trip_end - first_trip_start)) / 3600.0 AS operational_hours,
|
||
total_km,
|
||
trip_count,
|
||
total_drive_min,
|
||
total_idle_min,
|
||
avg_speed_kmh,
|
||
peak_speed_kmh
|
||
FROM daily
|
||
ORDER BY work_date DESC, imei;
|
||
```
|
||
|
||
### Fleet summary for a date range
|
||
|
||
Aggregates across all vehicles and all days within a date range to give a high-level fleet utilisation report. Useful for weekly or monthly management summaries. Adjust the date range in the `WHERE` clause as needed.
|
||
|
||
```sql
|
||
SELECT
|
||
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS work_date,
|
||
COUNT(DISTINCT t.imei) AS active_vehicles,
|
||
COUNT(*) AS total_trips,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS total_fleet_km,
|
||
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS fleet_avg_speed_kmh,
|
||
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS total_drive_hours,
|
||
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS total_idle_hours
|
||
FROM tracksolid.trips t
|
||
WHERE t.start_time >= '2026-04-09 00:00:00+03'
|
||
AND t.start_time < '2026-04-11 00:00:00+03'
|
||
AND t.end_time IS NOT NULL
|
||
GROUP BY DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')
|
||
ORDER BY work_date;
|
||
```
|
||
|
||
### Idle time analysis — vehicles spending excessive time idling
|
||
|
||
Identifies trips where the vehicle spent more than 20% of its time stationary with the engine running. Excessive idling wastes fuel and is often a sign of drivers waiting in traffic, sitting at customer sites, or running the air conditioning while parked. The threshold (20%) can be adjusted.
|
||
|
||
```sql
|
||
SELECT
|
||
t.imei,
|
||
d.vehicle_name,
|
||
d.driver_name,
|
||
t.start_time AT TIME ZONE 'Africa/Nairobi' AS trip_start,
|
||
ROUND(t.driving_time_s / 60.0, 1) AS drive_min,
|
||
ROUND(t.idle_time_s / 60.0, 1) AS idle_min,
|
||
ROUND(
|
||
100.0 * t.idle_time_s /
|
||
NULLIF(t.driving_time_s + t.idle_time_s, 0)
|
||
, 1) AS idle_pct,
|
||
ROUND(t.distance_km, 3) AS distance_km
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
WHERE t.idle_time_s IS NOT NULL
|
||
AND t.driving_time_s IS NOT NULL
|
||
AND (t.idle_time_s + t.driving_time_s) > 0
|
||
AND (100.0 * t.idle_time_s / (t.driving_time_s + t.idle_time_s)) > 20
|
||
ORDER BY idle_pct DESC;
|
||
```
|
||
|
||
### Vehicles with no activity today (Nairobi time)
|
||
|
||
Returns devices that are registered and active but have not started a trip today. Useful for daily fleet readiness checks — if a vehicle was expected to be operational but appears here, investigate whether the device is offline, the vehicle is in the workshop, or the driver has not started work.
|
||
|
||
```sql
|
||
SELECT
|
||
d.imei,
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
d.current_mileage_km,
|
||
d.last_synced_at AT TIME ZONE 'Africa/Nairobi' AS last_synced_nairobi
|
||
FROM tracksolid.devices d
|
||
WHERE d.enabled_flag = 1
|
||
AND d.imei NOT IN (
|
||
SELECT DISTINCT imei
|
||
FROM tracksolid.trips
|
||
WHERE DATE(start_time AT TIME ZONE 'Africa/Nairobi') = CURRENT_DATE AT TIME ZONE 'Africa/Nairobi'
|
||
)
|
||
ORDER BY d.vehicle_name NULLS LAST;
|
||
```
|
||
|
||
---
|
||
|
||
## 15. Today's Metrics — From 00:00 Nairobi Time to Now
|
||
|
||
All queries in this section use a dynamic time window that opens at midnight Nairobi time on the current calendar day and closes at `NOW()`. This means they are safe to run at any point during the day and will always reflect activity from the start of the working day up to the present moment. The anchor expression used throughout is:
|
||
|
||
```sql
|
||
DATE_TRUNC('day', NOW() AT TIME ZONE 'Africa/Nairobi') AT TIME ZONE 'Africa/Nairobi'
|
||
```
|
||
|
||
This evaluates to today's `00:00:00 +03:00` in UTC, regardless of when the query is run. Pairing it with `< NOW()` ensures only records that have actually arrived are included.
|
||
|
||
### GPS pings since midnight (position_history)
|
||
|
||
Returns every position breadcrumb recorded today for all vehicles, from the first ping after midnight Nairobi time up to the current moment. Useful for replaying today's movement on a map or confirming that all devices are actively reporting.
|
||
|
||
```sql
|
||
SELECT
|
||
ph.imei,
|
||
ph.gps_time AT TIME ZONE 'Africa/Nairobi' AS gps_nairobi,
|
||
ph.lat,
|
||
ph.lng,
|
||
ph.speed,
|
||
ph.direction,
|
||
ph.acc_status,
|
||
ph.current_mileage,
|
||
ph.satellite
|
||
FROM tracksolid.position_history ph
|
||
WHERE ph.gps_time >= DATE_TRUNC('day', NOW() AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi'
|
||
AND ph.gps_time < NOW()
|
||
ORDER BY ph.gps_time DESC;
|
||
```
|
||
|
||
### Today's trips per vehicle (trips)
|
||
|
||
Lists every completed trip recorded today, joined with device metadata. Because the day is still in progress, some vehicles may have open trips (where `end_time IS NULL`) — the `WHERE` clause includes both completed and in-progress trips so nothing is missed. The `end_time IS NULL` check in the display makes it easy to spot which trips are still open.
|
||
|
||
```sql
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
t.imei,
|
||
t.start_time AT TIME ZONE 'Africa/Nairobi' AS start_nairobi,
|
||
t.end_time AT TIME ZONE 'Africa/Nairobi' AS end_nairobi,
|
||
CASE WHEN t.end_time IS NULL THEN 'IN PROGRESS' ELSE 'COMPLETE' END AS status,
|
||
ROUND(t.distance_km, 3) AS distance_km,
|
||
t.avg_speed_kmh,
|
||
ROUND(t.driving_time_s / 60.0, 1) AS drive_min,
|
||
ROUND(t.idle_time_s / 60.0, 1) AS idle_min
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
WHERE t.start_time >= DATE_TRUNC('day', NOW() AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi'
|
||
AND t.start_time < NOW()
|
||
ORDER BY t.start_time DESC;
|
||
```
|
||
|
||
### Today's driving summary per vehicle (aggregated)
|
||
|
||
Rolls up all of today's trips into one row per vehicle. Shows the time the driver first moved (day start), the most recent trip end time (last known activity), total kilometres driven so far, total trips made, and cumulative driving and idle minutes. The `MAX(t.end_time)` will reflect the end of the last completed trip — if the vehicle is currently mid-trip that trip's distance and time will not yet appear here until the trip closes.
|
||
|
||
```sql
|
||
WITH today_start AS (
|
||
SELECT DATE_TRUNC('day', NOW() AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' AS ts
|
||
)
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
t.imei,
|
||
TO_CHAR(MIN(t.start_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS day_start,
|
||
TO_CHAR(MAX(t.end_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS last_activity,
|
||
COUNT(*) AS trips_so_far,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
|
||
ROUND(SUM(t.driving_time_s) / 60.0, 1) AS total_drive_min,
|
||
ROUND(SUM(t.idle_time_s) / 60.0, 1) AS total_idle_min,
|
||
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS avg_speed_kmh
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
CROSS JOIN today_start ts
|
||
WHERE t.start_time >= ts.ts
|
||
AND t.start_time < NOW()
|
||
GROUP BY t.imei, d.vehicle_name, d.vehicle_number, d.driver_name
|
||
ORDER BY total_km DESC;
|
||
```
|
||
|
||
### Today's alarms so far
|
||
|
||
Returns all alarm events triggered from midnight Nairobi time to the present moment. Once `alarm_type` is being populated correctly this becomes the real-time safety dashboard — a high count of overspeed or harsh-braking alarms early in the day is an early warning signal.
|
||
|
||
```sql
|
||
SELECT
|
||
a.imei,
|
||
d.vehicle_name,
|
||
d.driver_name,
|
||
a.alarm_time AT TIME ZONE 'Africa/Nairobi' AS alarm_nairobi,
|
||
a.alarm_type,
|
||
a.alarm_name,
|
||
a.speed,
|
||
a.lat,
|
||
a.lng
|
||
FROM tracksolid.alarms a
|
||
LEFT JOIN tracksolid.devices d ON d.imei = a.imei
|
||
WHERE a.alarm_time >= DATE_TRUNC('day', NOW() AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi'
|
||
AND a.alarm_time < NOW()
|
||
ORDER BY a.alarm_time DESC;
|
||
```
|
||
|
||
### Fleet totals for today at a glance
|
||
|
||
A single-row summary of the entire fleet's activity since midnight. Designed for a top-of-dashboard KPI card: how many vehicles have moved today, how many trips have been completed, total fleet kilometres, and total driving hours so far.
|
||
|
||
```sql
|
||
WITH today_start AS (
|
||
SELECT DATE_TRUNC('day', NOW() AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' AS ts
|
||
)
|
||
SELECT
|
||
COUNT(DISTINCT t.imei) AS active_vehicles_today,
|
||
COUNT(*) AS total_trips_today,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS total_fleet_km,
|
||
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS total_drive_hours,
|
||
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS total_idle_hours,
|
||
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS fleet_avg_speed_kmh,
|
||
TO_CHAR(NOW() AT TIME ZONE 'Africa/Nairobi', 'HH24:MI') AS as_at_nairobi
|
||
FROM tracksolid.trips t
|
||
CROSS JOIN today_start ts
|
||
WHERE t.start_time >= ts.ts
|
||
AND t.start_time < NOW();
|
||
```
|
||
|
||
---
|
||
|
||
## 20. New Tables from Migration 05
|
||
|
||
Migration 05 adds the following tables to support expanded webhook ingestion and enriched reporting. All tables have corresponding handlers in `webhook_receiver_rev.py` or are populated via ETL.
|
||
|
||
### tracksolid.device_events
|
||
|
||
Records device network login and logout events received via the `/pushevent` webhook. Distinguishes "vehicle parked, tracker alive" from "tracker lost power or was disconnected". Each row captures the device IMEI, whether it connected (LOGIN) or disconnected (LOGOUT), the event timestamp, and the device's reported timezone. Use this table to calculate true uptime per device and to detect potential tampering (unexpected LOGOUT during operational hours).
|
||
|
||
```sql
|
||
SELECT
|
||
de.imei,
|
||
d.vehicle_name,
|
||
de.event_type,
|
||
de.event_time AT TIME ZONE 'Africa/Nairobi' AS event_nairobi,
|
||
de.timezone
|
||
FROM tracksolid.device_events de
|
||
LEFT JOIN tracksolid.devices d ON d.imei = de.imei
|
||
ORDER BY de.event_time DESC
|
||
LIMIT 50;
|
||
```
|
||
|
||
### tracksolid.fuel_readings
|
||
|
||
Stores fuel and oil sensor readings from the `/pushoil` webhook. Relevant for vehicles equipped with a direct fuel tank sensor — these report tank level (in cm depth, percentage, voltage, or litres depending on sensor type). Sudden unexplained drops in fuel level between readings indicate possible fuel theft. Each reading includes the sensor channel ID (`sensor_path`), the measured value, the unit, and the GPS position at time of reading.
|
||
|
||
```sql
|
||
SELECT
|
||
fr.imei,
|
||
d.vehicle_name,
|
||
fr.reading_time AT TIME ZONE 'Africa/Nairobi' AS reading_nairobi,
|
||
fr.sensor_path,
|
||
fr.value,
|
||
fr.unit,
|
||
fr.lat,
|
||
fr.lng
|
||
FROM tracksolid.fuel_readings fr
|
||
LEFT JOIN tracksolid.devices d ON d.imei = fr.imei
|
||
ORDER BY fr.reading_time DESC
|
||
LIMIT 50;
|
||
```
|
||
|
||
### tracksolid.temperature_readings
|
||
|
||
Stores cabin or cargo temperature and humidity readings from the `/pushtem` webhook. Critical for cold-chain logistics where temperature excursions during transit can compromise cargo. Each row holds the sensor reading at a point in time per device. Pair with `position_history` to map temperature along a route.
|
||
|
||
```sql
|
||
SELECT
|
||
tr.imei,
|
||
d.vehicle_name,
|
||
tr.reading_time AT TIME ZONE 'Africa/Nairobi' AS reading_nairobi,
|
||
tr.temperature,
|
||
tr.humidity_pct
|
||
FROM tracksolid.temperature_readings tr
|
||
LEFT JOIN tracksolid.devices d ON d.imei = tr.imei
|
||
ORDER BY tr.reading_time DESC
|
||
LIMIT 50;
|
||
```
|
||
|
||
### tracksolid.lbs_readings
|
||
|
||
Stores cell tower and WiFi-based positioning data from the `/pushlbs` webhook. Used as a fallback when GPS signal is unavailable (tunnels, underground parking, dense urban areas). The raw cell data is stored as JSONB in `lbs_data` containing MCC, MNC, and cell tower list which can be forwarded to a geocell API (e.g. Google Geolocation, HERE) for approximate coordinate resolution.
|
||
|
||
```sql
|
||
SELECT
|
||
lr.imei,
|
||
d.vehicle_name,
|
||
lr.gate_time AT TIME ZONE 'Africa/Nairobi' AS gate_nairobi,
|
||
lr.post_type,
|
||
lr.lbs_data
|
||
FROM tracksolid.lbs_readings lr
|
||
LEFT JOIN tracksolid.devices d ON d.imei = lr.imei
|
||
ORDER BY lr.gate_time DESC
|
||
LIMIT 50;
|
||
```
|
||
|
||
### tracksolid.geofences
|
||
|
||
Stores geofence boundary definitions synced from the Tracksolid platform. Each row represents a named geographic boundary (circle or polygon) used for arrival/departure alerting. Once populated, this table enables joining alarm events to geofence names so that `tracksolid.alarms.geofence_name` is human-readable rather than just an ID.
|
||
|
||
```sql
|
||
SELECT
|
||
fence_id,
|
||
fence_name,
|
||
fence_type,
|
||
radius_m,
|
||
description,
|
||
created_at AT TIME ZONE 'Africa/Nairobi' AS created_nairobi
|
||
FROM tracksolid.geofences
|
||
ORDER BY created_at DESC;
|
||
```
|
||
|
||
---
|
||
|
||
## 16. Daily Analytics
|
||
|
||
Daily analytics answer the question: **"What happened across the fleet today?"** All queries use `Africa/Nairobi` timezone so that a working day (typically 08:00–20:00 EAT) maps cleanly to a single calendar date. The `dwh_gold.refresh_daily_metrics()` function pre-aggregates these into `fact_daily_fleet_metrics` for fast dashboard queries — run it nightly for the previous day.
|
||
|
||
### Day summary — all vehicles, single date
|
||
|
||
Replace the date literal with any past date or use `CURRENT_DATE - 1` for yesterday. This is the primary end-of-day report: who drove, how far, when they started and finished, and how much time was wasted idling.
|
||
|
||
```sql
|
||
SELECT
|
||
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS work_date,
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
t.imei,
|
||
TO_CHAR(MIN(t.start_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS day_start,
|
||
TO_CHAR(MAX(t.end_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS day_end,
|
||
ROUND(
|
||
EXTRACT(EPOCH FROM (MAX(t.end_time) - MIN(t.start_time))) / 3600.0
|
||
, 2) AS operational_hours,
|
||
COUNT(*) AS trips,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
|
||
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(
|
||
100.0 * SUM(t.idle_time_s) /
|
||
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
|
||
, 1) AS idle_pct,
|
||
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS avg_speed_kmh,
|
||
SUM(t.fuel_consumed_l) AS fuel_l
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
WHERE DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') = '2026-04-10'
|
||
AND t.end_time IS NOT NULL
|
||
GROUP BY 1, d.vehicle_name, d.vehicle_number, d.driver_name, t.imei
|
||
ORDER BY total_km DESC;
|
||
```
|
||
|
||
### Daily alarm summary
|
||
|
||
Counts alarm events per vehicle per type for a single day. Used in end-of-day driver behaviour reporting. Sort by total alarms to surface the most at-risk drivers.
|
||
|
||
```sql
|
||
SELECT
|
||
DATE(a.alarm_time AT TIME ZONE 'Africa/Nairobi') AS alarm_date,
|
||
d.vehicle_name,
|
||
d.driver_name,
|
||
a.imei,
|
||
a.alarm_type,
|
||
a.alarm_name,
|
||
COUNT(*) AS alarm_count,
|
||
MAX(a.speed) AS max_speed_at_alarm
|
||
FROM tracksolid.alarms a
|
||
LEFT JOIN tracksolid.devices d ON d.imei = a.imei
|
||
WHERE DATE(a.alarm_time AT TIME ZONE 'Africa/Nairobi') = '2026-04-10'
|
||
GROUP BY 1, d.vehicle_name, d.driver_name, a.imei, a.alarm_type, a.alarm_name
|
||
ORDER BY alarm_count DESC;
|
||
```
|
||
|
||
### Vehicles not used today
|
||
|
||
Flags registered vehicles that had zero trips on a given day. Cross-reference with `heartbeats` and `live_positions` to determine if the device was online (vehicle was parked) or offline (device problem).
|
||
|
||
```sql
|
||
SELECT
|
||
d.imei,
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
d.current_mileage_km,
|
||
lp.device_status,
|
||
lp.updated_at AT TIME ZONE 'Africa/Nairobi' AS last_seen_nairobi
|
||
FROM tracksolid.devices d
|
||
LEFT JOIN tracksolid.live_positions lp ON lp.imei = d.imei
|
||
WHERE d.enabled_flag = 1
|
||
AND d.imei NOT IN (
|
||
SELECT DISTINCT imei FROM tracksolid.trips
|
||
WHERE DATE(start_time AT TIME ZONE 'Africa/Nairobi') = '2026-04-10'
|
||
)
|
||
ORDER BY d.vehicle_name NULLS LAST;
|
||
```
|
||
|
||
### Daily parking events summary
|
||
|
||
Lists all parking stops recorded in a day with duration and location. Highlights vehicles parked for extended periods at unplanned locations. A stop longer than 2 hours away from the depot during working hours warrants investigation.
|
||
|
||
```sql
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.driver_name,
|
||
p.imei,
|
||
p.start_time AT TIME ZONE 'Africa/Nairobi' AS parked_at,
|
||
p.end_time AT TIME ZONE 'Africa/Nairobi' AS departed_at,
|
||
ROUND(p.duration_seconds / 60.0, 1) AS duration_min,
|
||
p.address
|
||
FROM tracksolid.parking_events p
|
||
LEFT JOIN tracksolid.devices d ON d.imei = p.imei
|
||
WHERE DATE(p.start_time AT TIME ZONE 'Africa/Nairobi') = '2026-04-10'
|
||
ORDER BY p.duration_seconds DESC;
|
||
```
|
||
|
||
### First departure and last return — all vehicles
|
||
|
||
Gives the operational window for each vehicle: when the first trip began (proxy for driver start time) and when the last trip ended (proxy for sign-off time). Used to enforce working-hours policy and detect unauthorised after-hours use.
|
||
|
||
```sql
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
t.imei,
|
||
TO_CHAR(MIN(t.start_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS first_departure,
|
||
TO_CHAR(MAX(t.end_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS last_return,
|
||
ROUND(
|
||
EXTRACT(EPOCH FROM (MAX(t.end_time) - MIN(t.start_time))) / 3600.0
|
||
, 2) AS operational_hours
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
WHERE DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') = '2026-04-10'
|
||
AND t.end_time IS NOT NULL
|
||
GROUP BY d.vehicle_name, d.vehicle_number, d.driver_name, t.imei
|
||
ORDER BY first_departure;
|
||
```
|
||
|
||
---
|
||
|
||
## 17. Weekly Analytics
|
||
|
||
Weekly analytics answer: **"How did the fleet perform over the past 7 days?"** Replace the date range with the desired ISO week or rolling window. For Monday–Sunday weeks use `DATE_TRUNC('week', ...)`.
|
||
|
||
### Weekly driving summary per vehicle
|
||
|
||
One row per vehicle per week. Shows total km, trips, drive hours, idle ratio, and average speed. Useful for identifying vehicles consistently underperforming or overloading drivers.
|
||
|
||
```sql
|
||
WITH week_range AS (
|
||
SELECT
|
||
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' AS week_start,
|
||
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '7 days' AS week_end
|
||
)
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
t.imei,
|
||
COUNT(*) AS total_trips,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
|
||
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(
|
||
100.0 * SUM(t.idle_time_s) /
|
||
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
|
||
, 1) AS idle_pct,
|
||
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS avg_speed_kmh,
|
||
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS active_days,
|
||
ROUND(SUM(t.fuel_consumed_l)::numeric, 2) AS fuel_l
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
CROSS JOIN week_range wr
|
||
WHERE t.start_time >= wr.week_start
|
||
AND t.start_time < wr.week_end
|
||
AND t.end_time IS NOT NULL
|
||
GROUP BY d.vehicle_name, d.vehicle_number, d.driver_name, t.imei
|
||
ORDER BY total_km DESC;
|
||
```
|
||
|
||
### Weekly alarm league table — by driver
|
||
|
||
Ranks drivers by total alarm count for the week. Helps safety managers identify drivers needing coaching. Break down by `alarm_type` to differentiate overspeed from geofence violations vs. device health alerts.
|
||
|
||
```sql
|
||
WITH week_range AS (
|
||
SELECT
|
||
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' AS week_start,
|
||
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '7 days' AS week_end
|
||
)
|
||
SELECT
|
||
d.driver_name,
|
||
d.vehicle_name,
|
||
a.imei,
|
||
COUNT(*) AS total_alarms,
|
||
COUNT(*) FILTER (WHERE a.alarm_type ILIKE '%speed%') AS overspeed,
|
||
COUNT(*) FILTER (WHERE a.alarm_type ILIKE '%geofence%'
|
||
OR a.alarm_type ILIKE '%fence%') AS geofence,
|
||
COUNT(*) FILTER (WHERE a.alarm_type ILIKE '%power%'
|
||
OR a.alarm_type ILIKE '%battery%') AS device_health
|
||
FROM tracksolid.alarms a
|
||
LEFT JOIN tracksolid.devices d ON d.imei = a.imei
|
||
CROSS JOIN week_range wr
|
||
WHERE a.alarm_time >= wr.week_start
|
||
AND a.alarm_time < wr.week_end
|
||
GROUP BY d.driver_name, d.vehicle_name, a.imei
|
||
ORDER BY total_alarms DESC;
|
||
```
|
||
|
||
### Weekly idle time analysis — worst offenders
|
||
|
||
Surfaces vehicles where idle time exceeded 25% of total engine-on time across the week. Sustained high idle ratios point to route inefficiencies, long customer wait times, or drivers keeping engines running while stationary.
|
||
|
||
```sql
|
||
WITH week_range AS (
|
||
SELECT
|
||
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' AS week_start,
|
||
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '7 days' AS week_end
|
||
)
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.driver_name,
|
||
t.imei,
|
||
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(
|
||
100.0 * SUM(t.idle_time_s) /
|
||
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
|
||
, 1) AS idle_pct,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
CROSS JOIN week_range wr
|
||
WHERE t.start_time >= wr.week_start
|
||
AND t.start_time < wr.week_end
|
||
AND t.end_time IS NOT NULL
|
||
GROUP BY d.vehicle_name, d.driver_name, t.imei
|
||
HAVING
|
||
100.0 * SUM(t.idle_time_s) /
|
||
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0) > 25
|
||
ORDER BY idle_pct DESC;
|
||
```
|
||
|
||
### Weekly day-by-day fleet activity
|
||
|
||
Shows fleet-wide totals for each day of the current week. Good for identifying quiet days (public holidays, weather events) or usage patterns (e.g. Saturdays consistently low).
|
||
|
||
```sql
|
||
WITH week_range AS (
|
||
SELECT
|
||
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' AS week_start,
|
||
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '7 days' AS week_end
|
||
)
|
||
SELECT
|
||
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS work_date,
|
||
TO_CHAR(DATE(t.start_time AT TIME ZONE 'Africa/Nairobi'), 'Day') AS day_name,
|
||
COUNT(DISTINCT t.imei) AS active_vehicles,
|
||
COUNT(*) AS trips,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS fleet_km,
|
||
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
|
||
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours
|
||
FROM tracksolid.trips t
|
||
CROSS JOIN week_range wr
|
||
WHERE t.start_time >= wr.week_start
|
||
AND t.start_time < wr.week_end
|
||
AND t.end_time IS NOT NULL
|
||
GROUP BY DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')
|
||
ORDER BY work_date;
|
||
```
|
||
|
||
---
|
||
|
||
## 18. Monthly Analytics
|
||
|
||
Monthly analytics answer: **"How did the fleet perform this month?"** Queries use `DATE_TRUNC('month', ...)` to anchor to the first of the current month. For historical months replace with a specific date literal. When `dwh_gold.fact_daily_fleet_metrics` is fully populated these queries can be rewritten against that table for much faster execution.
|
||
|
||
### Monthly summary per vehicle
|
||
|
||
The primary monthly management report. One row per vehicle for the selected month, showing utilisation, distance, fuel, and alarm counts side by side. Replace the date with the first day of the target month.
|
||
|
||
```sql
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
t.imei,
|
||
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS active_days,
|
||
COUNT(*) AS total_trips,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
|
||
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(
|
||
100.0 * SUM(t.idle_time_s) /
|
||
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
|
||
, 1) AS idle_pct,
|
||
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS avg_speed_kmh,
|
||
ROUND(SUM(t.fuel_consumed_l)::numeric, 2) AS fuel_l,
|
||
ROUND(
|
||
SUM(t.fuel_consumed_l) / NULLIF(SUM(t.distance_km), 0) * 100
|
||
, 2) AS fuel_per_100km
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
WHERE t.start_time >= DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi'
|
||
AND t.start_time < DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '1 month'
|
||
AND t.end_time IS NOT NULL
|
||
GROUP BY d.vehicle_name, d.vehicle_number, d.driver_name, t.imei
|
||
ORDER BY total_km DESC;
|
||
```
|
||
|
||
### Monthly odometer progression
|
||
|
||
Shows how much each vehicle's cumulative mileage grew during the month, derived from the difference between the earliest and latest `current_mileage` in `position_history`. Cross-check against `trips.distance_km` totals to validate data consistency.
|
||
|
||
```sql
|
||
SELECT
|
||
ph.imei,
|
||
d.vehicle_name,
|
||
MIN(ph.current_mileage) AS mileage_start,
|
||
MAX(ph.current_mileage) AS mileage_end,
|
||
ROUND((MAX(ph.current_mileage) - MIN(ph.current_mileage))::numeric, 2) AS km_added
|
||
FROM tracksolid.position_history ph
|
||
JOIN tracksolid.devices d ON d.imei = ph.imei
|
||
WHERE ph.gps_time >= DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi'
|
||
AND ph.gps_time < DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '1 month'
|
||
GROUP BY ph.imei, d.vehicle_name
|
||
ORDER BY km_added DESC;
|
||
```
|
||
|
||
### Monthly alarm trends — week by week
|
||
|
||
Breaks the month into ISO weeks to show whether alarm frequency is increasing or decreasing. A rising trend requires management intervention; a falling trend after coaching sessions confirms improvement.
|
||
|
||
```sql
|
||
SELECT
|
||
DATE_TRUNC('week', a.alarm_time AT TIME ZONE 'Africa/Nairobi') AS week_start,
|
||
a.alarm_type,
|
||
COUNT(*) AS alarm_count,
|
||
COUNT(DISTINCT a.imei) AS vehicles_affected
|
||
FROM tracksolid.alarms a
|
||
WHERE a.alarm_time >= DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi'
|
||
AND a.alarm_time < DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '1 month'
|
||
AND a.alarm_type IS NOT NULL
|
||
GROUP BY 1, a.alarm_type
|
||
ORDER BY week_start, alarm_count DESC;
|
||
```
|
||
|
||
### Monthly utilisation rate — fleet availability vs. usage
|
||
|
||
Compares the number of days each vehicle was used against the number of working days in the month. A utilisation rate below 60% may indicate excess fleet capacity; above 95% may indicate a vehicle at risk of missing maintenance.
|
||
|
||
```sql
|
||
WITH month_days AS (
|
||
SELECT
|
||
DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' AS month_start,
|
||
DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '1 month' AS month_end,
|
||
-- Approximate working days: calendar days minus weekends
|
||
(EXTRACT(DAY FROM
|
||
(DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
+ INTERVAL '1 month'
|
||
- DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi'))
|
||
) * 5 / 7)::int AS working_days
|
||
)
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
t.imei,
|
||
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS days_used,
|
||
md.working_days,
|
||
ROUND(
|
||
100.0 * COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi'))
|
||
/ NULLIF(md.working_days, 0)
|
||
, 1) AS utilisation_pct
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
CROSS JOIN month_days md
|
||
WHERE t.start_time >= md.month_start
|
||
AND t.start_time < md.month_end
|
||
GROUP BY d.vehicle_name, d.vehicle_number, t.imei, md.working_days
|
||
ORDER BY utilisation_pct DESC;
|
||
```
|
||
|
||
### Monthly speed compliance summary
|
||
|
||
Calculates the share of trips within each speed band for the month. Feeds directly into a safety compliance report. A healthy fleet should have the majority of trips below 60 km/h average for urban operations.
|
||
|
||
```sql
|
||
SELECT
|
||
CASE
|
||
WHEN avg_speed_kmh < 20 THEN '0–20 km/h (slow / congested)'
|
||
WHEN avg_speed_kmh < 40 THEN '20–40 km/h (normal urban)'
|
||
WHEN avg_speed_kmh < 60 THEN '40–60 km/h (arterial / highway)'
|
||
WHEN avg_speed_kmh < 80 THEN '60–80 km/h (fast highway)'
|
||
ELSE '80+ km/h (excessive)'
|
||
END AS speed_band,
|
||
COUNT(*) AS trips,
|
||
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct_of_total,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS km_in_band
|
||
FROM tracksolid.trips t
|
||
WHERE t.start_time >= DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi'
|
||
AND t.end_time IS NOT NULL
|
||
AND t.avg_speed_kmh IS NOT NULL
|
||
GROUP BY 1
|
||
ORDER BY MIN(avg_speed_kmh);
|
||
```
|
||
|
||
---
|
||
|
||
## 19. Quarterly Analytics
|
||
|
||
Quarterly analytics answer: **"What are the trends over the past three months?"** These queries are designed to surface patterns that are invisible at the daily or weekly level — vehicle degradation, seasonal demand shifts, cumulative fuel costs, and driver behaviour trajectories. Use `DATE_TRUNC('quarter', ...)` for clean calendar quarters or specify explicit date ranges for financial quarters.
|
||
|
||
### Quarterly fleet KPI dashboard row
|
||
|
||
A single aggregated row per quarter giving headline KPIs: total km, fuel, drive hours, idle ratio, and alarm rate per 1,000 km. Feed this into a year-over-year comparison table.
|
||
|
||
```sql
|
||
SELECT
|
||
DATE_TRUNC('quarter', t.start_time AT TIME ZONE 'Africa/Nairobi') AS quarter,
|
||
COUNT(DISTINCT t.imei) AS avg_active_vehicles,
|
||
COUNT(*) AS total_trips,
|
||
ROUND(SUM(t.distance_km)::numeric, 0) AS total_km,
|
||
ROUND(SUM(t.driving_time_s) / 3600.0, 0) AS drive_hours,
|
||
ROUND(SUM(t.idle_time_s) / 3600.0, 0) AS idle_hours,
|
||
ROUND(
|
||
100.0 * SUM(t.idle_time_s) /
|
||
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
|
||
, 1) AS idle_pct,
|
||
ROUND(SUM(t.fuel_consumed_l)::numeric, 0) AS fuel_l,
|
||
ROUND(
|
||
SUM(t.fuel_consumed_l) / NULLIF(SUM(t.distance_km), 0) * 100
|
||
, 2) AS fuel_per_100km
|
||
FROM tracksolid.trips t
|
||
WHERE t.end_time IS NOT NULL
|
||
GROUP BY DATE_TRUNC('quarter', t.start_time AT TIME ZONE 'Africa/Nairobi')
|
||
ORDER BY quarter;
|
||
```
|
||
|
||
### Quarterly vehicle performance ranking
|
||
|
||
Ranks each vehicle across the quarter by distance, drive hours, and idle ratio. Highlights top performers and outliers. Vehicles consistently ranking at the bottom may need reassignment, maintenance, or driver change.
|
||
|
||
```sql
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.driver_name,
|
||
t.imei,
|
||
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS active_days,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
|
||
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
|
||
ROUND(
|
||
100.0 * SUM(t.idle_time_s) /
|
||
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
|
||
, 1) AS idle_pct,
|
||
ROUND(SUM(t.fuel_consumed_l)::numeric, 2) AS fuel_l,
|
||
ROUND(
|
||
SUM(t.fuel_consumed_l) / NULLIF(SUM(t.distance_km), 0) * 100
|
||
, 2) AS fuel_per_100km,
|
||
RANK() OVER (ORDER BY SUM(t.distance_km) DESC) AS km_rank
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
WHERE t.start_time >= DATE_TRUNC('quarter', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi'
|
||
AND t.end_time IS NOT NULL
|
||
GROUP BY d.vehicle_name, d.vehicle_number, d.driver_name, t.imei
|
||
ORDER BY total_km DESC;
|
||
```
|
||
|
||
### Quarterly alarm frequency trend — month over month
|
||
|
||
Shows whether safety events are improving or worsening over the quarter. Break down by `alarm_type` to distinguish overspeed trends from geofence or mechanical alarms.
|
||
|
||
```sql
|
||
SELECT
|
||
TO_CHAR(DATE_TRUNC('month', a.alarm_time AT TIME ZONE 'Africa/Nairobi'), 'YYYY-MM') AS month,
|
||
a.alarm_type,
|
||
COUNT(*) AS alarm_count,
|
||
COUNT(DISTINCT a.imei) AS vehicles_affected,
|
||
ROUND(
|
||
COUNT(*) * 1000.0 /
|
||
NULLIF((
|
||
SELECT SUM(t2.distance_km) FROM tracksolid.trips t2
|
||
WHERE DATE_TRUNC('month', t2.start_time AT TIME ZONE 'Africa/Nairobi')
|
||
= DATE_TRUNC('month', a.alarm_time AT TIME ZONE 'Africa/Nairobi')
|
||
), 0)
|
||
, 2) AS alarms_per_1000km
|
||
FROM tracksolid.alarms a
|
||
WHERE a.alarm_time >= DATE_TRUNC('quarter', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi'
|
||
AND a.alarm_type IS NOT NULL
|
||
GROUP BY 1, a.alarm_type
|
||
ORDER BY month, alarm_count DESC;
|
||
```
|
||
|
||
### Quarterly high-mileage vehicles — service interval flag
|
||
|
||
Flags vehicles that have accumulated more than a configurable kilometre threshold in the quarter. At 5,000 km/quarter a vehicle has driven ~20,000 km/year. Adjust the `HAVING` threshold to match your service interval policy (e.g. every 5,000 km or every 10,000 km).
|
||
|
||
```sql
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.vehicle_number,
|
||
d.imei,
|
||
d.current_mileage_km AS current_odometer,
|
||
ROUND(SUM(t.distance_km)::numeric, 2) AS km_this_quarter,
|
||
CASE
|
||
WHEN SUM(t.distance_km) > 10000 THEN 'URGENT SERVICE'
|
||
WHEN SUM(t.distance_km) > 5000 THEN 'SERVICE DUE'
|
||
ELSE 'OK'
|
||
END AS service_flag
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
WHERE t.start_time >= DATE_TRUNC('quarter', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi'
|
||
AND t.end_time IS NOT NULL
|
||
GROUP BY d.vehicle_name, d.vehicle_number, d.imei, d.current_mileage_km
|
||
HAVING SUM(t.distance_km) > 5000
|
||
ORDER BY km_this_quarter DESC;
|
||
```
|
||
|
||
### Quarterly work-hours pattern — average start and finish times
|
||
|
||
Calculates the average first-departure and last-return time per vehicle across the quarter. Surfaces systematic early-starters, late-finishers, and vehicles being used outside contracted hours. Shows the standard deviation to identify inconsistent schedules.
|
||
|
||
```sql
|
||
SELECT
|
||
d.vehicle_name,
|
||
d.driver_name,
|
||
t.imei,
|
||
TO_CHAR(
|
||
TO_TIMESTAMP(AVG(EXTRACT(EPOCH FROM (MIN(t.start_time AT TIME ZONE 'Africa/Nairobi')::time))))
|
||
, 'HH24:MI') AS avg_day_start,
|
||
TO_CHAR(
|
||
TO_TIMESTAMP(AVG(EXTRACT(EPOCH FROM (MAX(t.end_time AT TIME ZONE 'Africa/Nairobi')::time))))
|
||
, 'HH24:MI') AS avg_day_end,
|
||
ROUND(STDDEV(EXTRACT(EPOCH FROM (MIN(t.start_time AT TIME ZONE 'Africa/Nairobi')::time))) / 60, 0)
|
||
AS start_time_std_min,
|
||
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS days_worked
|
||
FROM tracksolid.trips t
|
||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||
WHERE t.start_time >= DATE_TRUNC('quarter', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
|
||
AT TIME ZONE 'Africa/Nairobi'
|
||
AND t.end_time IS NOT NULL
|
||
GROUP BY d.vehicle_name, d.driver_name, t.imei,
|
||
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')
|
||
-- Outer aggregation over the per-day rows
|
||
-- Wrap in a CTE if additional aggregation by vehicle is needed
|
||
ORDER BY avg_day_start;
|
||
```
|
||
|
||
---
|
||
|
||
## 21. Known Data Issues
|
||
|
||
The following issues were identified during the April 2026 audit. Each represents either a data pipeline gap or a missing enrichment step.
|
||
|
||
| # | Table | Issue | Impact | Status |
|
||
|---|---|---|---|---|
|
||
| 1 | `devices` | `vehicle_name`, `vehicle_number`, `driver_name` all null for all 63 devices | All reports show blank vehicle identity | **Open** — manually populate or sync from fleet management source |
|
||
| 2 | `devices` | `fuel_100km` not set for any device | `fuel_consumed_l` in trips will remain null | **Open** — set fuel consumption rate per vehicle type |
|
||
| 3 | `alarms` | `alarm_type` and `alarm_name` were null for all 1,054 records | Alarm events could not be categorised | **Fixed** in `ingest_events_rev.py` [FIX-E06] — poll API uses `alertTypeId`/`alarmTypeName`, not webhook field names |
|
||
| 4 | `trips` | `max_speed_kmh` is null on all records | Cannot identify speeding from trip summaries | **Open** — `jimi.device.track.mileage` may not return this field; verify API response |
|
||
| 5 | `trips` | `distance_km` was stored in millimetres | All distance queries returned values 1,000,000× too large | **Fixed** — migration 04 divides historical data by 1,000,000 and renames column; ingestion code corrected [FIX-M11/M12] |
|
||
| 6 | `heartbeats` | 0 rows | Cannot distinguish parked-alive from powered-off | **Open** — verify tracker firmware supports heartbeat push |
|
||
| 7 | `obd_readings` | 0 rows | No engine health data | **Open** — requires OBD cable installation + `/pushobd` webhook registration in Tracksolid account |
|
||
| 8 | `parking_events` | 0 rows despite 358 successful API calls | No parking dwell-time reporting | **Fixed** in `ingest_movement_rev.py` [FIX-M13] — added missing `account` and `acc_type=0` params; fixed `durSecond` field mapping |
|
||
| 9 | `dwh_gold.*` | Both tables empty | Grafana dashboards have no data | **Fixed** — migration 05 adds `refresh_daily_metrics()` ETL function; run nightly via cron or n8n |
|
||
| 10 | `position_history` | Only 1 fix/min per vehicle from fleet sweep — route traces incomplete | Grafana map paths had gaps; speed profiles too coarse for harsh-driving detection | **Fixed** — `poll_track_list()` added [FIX-M14]; captures every device waypoint every 30 min; density increases to 2–6 fixes/min per vehicle |
|
||
| 11 | `live_positions` | No on-demand refresh mechanism for specific vehicles | Alarm enrichment and stale-device recovery required waiting up to 60s | **Fixed** — `get_device_locations()` utility added [FIX-M15]; call with specific IMEIs for instant precision refresh |
|