Compare commits
2 commits
8867be9d3d
...
274473c544
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
274473c544 | ||
|
|
cebcf74ba2 |
4 changed files with 1382 additions and 54 deletions
|
|
@ -6,20 +6,67 @@
|
|||
|
||||
## Table of Contents
|
||||
|
||||
0. [How to Use This Document](#0-how-to-use-this-document)
|
||||
1. [Data Foundation Summary](#1-data-foundation-summary)
|
||||
2. [Fleet Utilisation](#2-fleet-utilisation)
|
||||
3. [Driver Behaviour](#3-driver-behaviour)
|
||||
4. [Real-Time Dispatch — Nearest Vehicle to Job](#4-real-time-dispatch--nearest-vehicle-to-job)
|
||||
4. [Real-Time Dispatch & Field-Service SLAs](#4-real-time-dispatch--field-service-slas)
|
||||
5. [Distance per Driver per Day](#5-distance-per-driver-per-day)
|
||||
6. [Business Questions Now Answerable](#6-business-questions-now-answerable)
|
||||
7. [Grafana Dashboard Blueprint](#7-grafana-dashboard-blueprint)
|
||||
8. [What Unlocks the Remaining 30%](#8-what-unlocks-the-remaining-30)
|
||||
9. [Fleet Readiness Scorecard](#9-fleet-readiness-scorecard)
|
||||
10. [Service-Interval Forecaster](#10-service-interval-forecaster)
|
||||
|
||||
---
|
||||
|
||||
## 0. How to Use This Document
|
||||
|
||||
Every query in this document is tagged by intended consumption cadence. Build Grafana panels, alert rules, and scheduled reports against the tag — not the SQL text — so that moving a metric between dashboard and alert is a one-line change.
|
||||
|
||||
| Tag | Meaning | Typical cadence | Owner |
|
||||
|---|---|---|---|
|
||||
| `[DASHBOARD]` | Live or near-live panel | Refresh 30 s – 5 min | Ops / Dispatch |
|
||||
| `[ALERT]` | Trigger a page or ticket | Evaluate 1 – 15 min | On-call / Fleet Manager |
|
||||
| `[MONTHLY]` | Management / exec reporting | Run on 1st of month | Finance / Ops Lead |
|
||||
| `[AD-HOC]` | Investigation, audit, one-off | On demand | Analyst / Ops |
|
||||
|
||||
**Reading a query block**: each section lead-in states the tag(s). If a query has no tag it is reference material (schema, benchmark tables, appendix).
|
||||
|
||||
**Thresholds are starting points, not gospel**. Every red/amber/green band in this document must be re-calibrated against your own 30-day distribution once data matures. See [Appendix B — Threshold Calibration Guide](#appendix-b--threshold-calibration-guide).
|
||||
|
||||
**City-cohort cuts**. Fireside operates in Nairobi, Mombasa, and Kampala. Traffic, fuel prices, and shift norms differ materially between them. Any fleet-level metric should be sliceable by `devices.assigned_city` once that column is populated (see §3.7).
|
||||
|
||||
---
|
||||
|
||||
## 1. Data Foundation Summary
|
||||
|
||||
The ingestion stack currently populates the following data sources, each feeding the analytics layer:
|
||||
### 1.1 Current Deployment State *(as of 18 Apr 2026)*
|
||||
|
||||
> **⚠ New stack not yet live.** The refactored ingestion pipeline (`ingest_movement_rev.py` v2.2) targets the `tracksolid` schema, which is currently empty. All live data sits in the legacy `tracksolid_2` schema populated by the prior codebase. The queries in this document are written for the target schema (`tracksolid`) and will produce results once the new stack is deployed and the device sync has run.
|
||||
|
||||
| Metric | Observed value | Source |
|
||||
|---|---|---|
|
||||
| Devices registered | **63** (AT4-series, `353549*` IMEIs) | `tracksolid_2.devices` |
|
||||
| Driver names populated | **0 / 63** | `tracksolid_2.devices` |
|
||||
| Vehicle numbers populated | **0 / 63** | `tracksolid_2.devices` |
|
||||
| SIM numbers populated | **14 / 63** | `tracksolid_2.devices` |
|
||||
| Live positions (stale) | **19** | `tracksolid_2.live_positions` |
|
||||
| Position history rows | **208** | `tracksolid_2.position_history` |
|
||||
| Trips recorded | **5** (12.8 km total) | `tracksolid_2.trips` |
|
||||
| Parking / alarms / OBD | **0** each | `tracksolid_2.*` |
|
||||
| Last pipeline run | **6 Apr 2026 13:20 EAT** | `tracksolid_2.ingestion_log` |
|
||||
| Pipeline failure rate | **41%** (277/668 runs, all 401 auth errors) | `tracksolid_2.ingestion_log` |
|
||||
|
||||
**Why the pipeline stopped (6 Apr):** 276 consecutive `401 Unauthorized` errors against `eu-open.tracksolidpro.com`. The API token expired and was not refreshed — the prior codebase lacked the auto-refresh logic that `ts_shared_rev.py` now includes. Deploying the new stack resolves this permanently.
|
||||
|
||||
**CSV fleet (144 devices, X3/JC400P series):** The `20260414_FS__Logistics - final_fixed.csv` file contains a separate, newer batch of devices (`865135*`, `862798*` IMEIs) with full driver names and plates. **These 144 devices are not yet registered in the DB at all** — they will be synced by `sync_driver_audit.py` after the new stack is deployed, then enriched by `import_drivers_csv.py`.
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Target Data Architecture
|
||||
|
||||
Once deployed, the ingestion stack populates the following data sources:
|
||||
|
||||
| Table | Content | Frequency |
|
||||
|---|---|---|
|
||||
|
|
@ -32,7 +79,7 @@ The ingestion stack currently populates the following data sources, each feeding
|
|||
| `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):
|
||||
**Position history density** improvement with `poll_track_list` (POLL-01):
|
||||
|
||||
| Before | After |
|
||||
|---|---|
|
||||
|
|
@ -119,6 +166,8 @@ WHERE day >= DATE_TRUNC('month', CURRENT_DATE);
|
|||
|
||||
### 2.3 Vehicles That Did Not Move Today
|
||||
|
||||
`[DASHBOARD]` `[ALERT]` — alert if a vehicle has not moved for ≥ 2 consecutive working days.
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
d.imei,
|
||||
|
|
@ -139,6 +188,73 @@ ORDER BY d.imei;
|
|||
|
||||
---
|
||||
|
||||
### 2.4 Cost-per-Ticket and Cost-per-Km
|
||||
|
||||
`[MONTHLY]` — the single most actionable finance metric: *what does one completed field-service job actually cost in fuel?* Pairs the trip table with the ticketing system (replace `ops.tickets` with the actual source — Zoho Desk, Freshdesk, or the Fireside job-management export).
|
||||
|
||||
Requires `devices.fuel_100km` (see §8 Step 2). Diesel price is parameterised so this query works across Nairobi / Mombasa / Kampala without editing.
|
||||
|
||||
```sql
|
||||
WITH fuel_rates AS (
|
||||
SELECT
|
||||
'NBO'::TEXT AS city, 180.0::NUMERIC AS price_per_litre -- Nairobi diesel KES
|
||||
UNION ALL SELECT 'MBA', 175.0
|
||||
UNION ALL SELECT 'KLA', 5200.0 -- Kampala UGX → convert in BI layer
|
||||
),
|
||||
daily_cost AS (
|
||||
SELECT
|
||||
t.imei,
|
||||
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS working_day,
|
||||
SUM(t.distance_km) AS km,
|
||||
SUM(t.distance_km) * (d.fuel_100km / 100.0) AS litres,
|
||||
SUM(t.distance_km) * (d.fuel_100km / 100.0) * f.price_per_litre AS fuel_cost
|
||||
FROM tracksolid.trips t
|
||||
JOIN tracksolid.devices d ON d.imei = t.imei
|
||||
LEFT JOIN fuel_rates f ON f.city = d.assigned_city
|
||||
WHERE t.start_time >= DATE_TRUNC('month', CURRENT_DATE)
|
||||
AND t.end_time IS NOT NULL
|
||||
GROUP BY t.imei, working_day, d.fuel_100km, f.price_per_litre
|
||||
),
|
||||
tickets AS (
|
||||
SELECT
|
||||
assigned_imei AS imei,
|
||||
DATE(closed_at AT TIME ZONE 'Africa/Nairobi') AS working_day,
|
||||
COUNT(*) FILTER (WHERE status = 'resolved') AS tickets_closed
|
||||
FROM ops.tickets
|
||||
WHERE closed_at >= DATE_TRUNC('month', CURRENT_DATE)
|
||||
GROUP BY assigned_imei, working_day
|
||||
)
|
||||
SELECT
|
||||
dc.imei,
|
||||
d.driver_name,
|
||||
d.vehicle_number,
|
||||
SUM(dc.km) AS km_month,
|
||||
ROUND(SUM(dc.fuel_cost), 0) AS fuel_cost_kes_month,
|
||||
COALESCE(SUM(tk.tickets_closed), 0) AS tickets_closed,
|
||||
ROUND(SUM(dc.fuel_cost) / NULLIF(SUM(tk.tickets_closed), 0), 0) AS cost_per_ticket_kes,
|
||||
ROUND(SUM(dc.fuel_cost) / NULLIF(SUM(dc.km), 0), 2) AS cost_per_km_kes
|
||||
FROM daily_cost dc
|
||||
JOIN tracksolid.devices d ON d.imei = dc.imei
|
||||
LEFT JOIN tickets tk
|
||||
ON tk.imei = dc.imei
|
||||
AND tk.working_day = dc.working_day
|
||||
GROUP BY dc.imei, d.driver_name, d.vehicle_number
|
||||
ORDER BY cost_per_ticket_kes DESC NULLS LAST;
|
||||
```
|
||||
|
||||
**Interpretation bands** — driver-level cost-per-ticket (van fleet, Nairobi baseline):
|
||||
|
||||
| KES / ticket | Signal | Typical cause |
|
||||
|---|---|---|
|
||||
| < 400 | Efficient | Dense route, minimal backtracking |
|
||||
| 400 – 900 | Normal | Mixed urban route |
|
||||
| 900 – 1500 | Review | Scattered geography or low ticket throughput |
|
||||
| > 1500 | Investigate | Idle time, off-route driving, or single-ticket days |
|
||||
|
||||
> **Dependency:** requires ticket data joined on IMEI or driver ID. If only driver-level data is available, swap `assigned_imei` for a driver→imei lookup.
|
||||
|
||||
---
|
||||
|
||||
## 3. Driver Behaviour
|
||||
|
||||
### 3.1 Speeding
|
||||
|
|
@ -421,7 +537,169 @@ ORDER BY t.imei, week_start;
|
|||
|
||||
---
|
||||
|
||||
## 4. Real-Time Dispatch — Nearest Vehicle to Job
|
||||
### 3.6 Alarm-While-Parked — Tamper and Theft Signal
|
||||
|
||||
`[ALERT]` — an alarm event on a vehicle that has been stationary for > 10 minutes is qualitatively different from an alarm mid-drive. Stationary alarms are the strongest signal for tamper, battery disconnect, unauthorised ignition, or geofence breach by a *parked* vehicle being loaded. Fires highest-priority page.
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
a.imei,
|
||||
d.driver_name,
|
||||
d.vehicle_number,
|
||||
a.alarm_name,
|
||||
a.alarm_time AT TIME ZONE 'Africa/Nairobi' AS event_time,
|
||||
ROUND(
|
||||
EXTRACT(EPOCH FROM (a.alarm_time - p.end_time)) / 60.0, 1
|
||||
) AS minutes_parked_before_alarm,
|
||||
p.address AS park_location,
|
||||
a.lat, a.lng
|
||||
FROM tracksolid.alarms a
|
||||
JOIN tracksolid.devices d ON d.imei = a.imei
|
||||
JOIN LATERAL (
|
||||
SELECT end_time, address
|
||||
FROM tracksolid.parking_events p
|
||||
WHERE p.imei = a.imei
|
||||
AND p.start_time <= a.alarm_time
|
||||
AND (p.end_time IS NULL OR p.end_time >= a.alarm_time)
|
||||
ORDER BY p.start_time DESC
|
||||
LIMIT 1
|
||||
) p ON TRUE
|
||||
WHERE a.alarm_time > NOW() - INTERVAL '24 hours'
|
||||
AND a.alarm_type IN ('vibration', 'power_cut', 'geofence_enter', 'geofence_exit', 'unauthorized_ignition')
|
||||
ORDER BY a.alarm_time DESC;
|
||||
```
|
||||
|
||||
> **Page rule:** any row where `alarm_type IN ('power_cut', 'unauthorized_ignition')` AND vehicle has been parked > 10 min pages the on-call operations lead immediately. Other stationary alarms ticket to the fleet manager for next-day review.
|
||||
|
||||
---
|
||||
|
||||
### 3.7 Geographic Drift — Vehicles Operating Outside Assigned City
|
||||
|
||||
`[MONTHLY]` `[ALERT]` — detects vehicles running outside their assigned operating territory. Protects against unauthorised inter-city trips, fuel tourism, and route fraud.
|
||||
|
||||
**Prerequisite** — add an `assigned_city` column to the devices table:
|
||||
|
||||
```sql
|
||||
ALTER TABLE tracksolid.devices ADD COLUMN IF NOT EXISTS assigned_city TEXT;
|
||||
-- Example back-fill:
|
||||
UPDATE tracksolid.devices SET assigned_city = 'NBO' WHERE imei IN (...);
|
||||
UPDATE tracksolid.devices SET assigned_city = 'MBA' WHERE imei IN (...);
|
||||
UPDATE tracksolid.devices SET assigned_city = 'KLA' WHERE imei IN (...);
|
||||
```
|
||||
|
||||
City bounding boxes (approximate; widen as needed for suburban coverage):
|
||||
|
||||
| City | Code | min lat | max lat | min lng | max lng |
|
||||
|---|---|---|---|---|---|
|
||||
| Nairobi metro | NBO | -1.45 | -1.15 | 36.65 | 37.05 |
|
||||
| Mombasa metro | MBA | -4.15 | -3.90 | 39.55 | 39.80 |
|
||||
| Kampala metro | KLA | 0.20 | 0.45 | 32.50 | 32.75 |
|
||||
|
||||
```sql
|
||||
WITH city_box AS (
|
||||
SELECT * FROM (VALUES
|
||||
('NBO', -1.45, -1.15, 36.65, 37.05),
|
||||
('MBA', -4.15, -3.90, 39.55, 39.80),
|
||||
('KLA', 0.20, 0.45, 32.50, 32.75)
|
||||
) AS c(code, min_lat, max_lat, min_lng, max_lng)
|
||||
),
|
||||
out_of_zone AS (
|
||||
SELECT
|
||||
ph.imei,
|
||||
d.assigned_city,
|
||||
DATE(ph.gps_time AT TIME ZONE 'Africa/Nairobi') AS day,
|
||||
COUNT(*) AS fixes_outside_zone
|
||||
FROM tracksolid.position_history ph
|
||||
JOIN tracksolid.devices d ON d.imei = ph.imei
|
||||
JOIN city_box c ON c.code = d.assigned_city
|
||||
WHERE ph.gps_time > NOW() - INTERVAL '30 days'
|
||||
AND (
|
||||
ph.lat < c.min_lat OR ph.lat > c.max_lat
|
||||
OR ph.lng < c.min_lng OR ph.lng > c.max_lng
|
||||
)
|
||||
GROUP BY ph.imei, d.assigned_city, day
|
||||
)
|
||||
SELECT
|
||||
o.imei,
|
||||
d.driver_name,
|
||||
d.vehicle_number,
|
||||
o.assigned_city,
|
||||
o.day,
|
||||
o.fixes_outside_zone
|
||||
FROM out_of_zone o
|
||||
JOIN tracksolid.devices d ON d.imei = o.imei
|
||||
WHERE o.fixes_outside_zone > 20 -- ~10 minutes of continuous out-of-zone driving
|
||||
ORDER BY o.day DESC, o.fixes_outside_zone DESC;
|
||||
```
|
||||
|
||||
> **Alert threshold:** > 50 fixes outside zone in a single day = escalate. Expected legitimate cases: cross-city service trips, driver taking vehicle home across a city boundary (policy decision).
|
||||
|
||||
---
|
||||
|
||||
### 3.8 Odometer Divergence — Tracker vs Physical Reading
|
||||
|
||||
`[MONTHLY]` — compares cumulative distance recorded by the tracker against the vehicle's physical odometer (captured at service or fuel card events). Divergence > 10% suggests sensor drift, GPS gaps, or unauthorised driving with the tracker disabled.
|
||||
|
||||
```sql
|
||||
WITH tracker_km AS (
|
||||
SELECT
|
||||
imei,
|
||||
SUM(distance_km) AS trips_km_30d
|
||||
FROM tracksolid.trips
|
||||
WHERE start_time > NOW() - INTERVAL '30 days'
|
||||
AND end_time IS NOT NULL
|
||||
GROUP BY imei
|
||||
),
|
||||
physical_readings AS (
|
||||
-- Replace with actual odometer log source (service records, fuel card, manual entry)
|
||||
SELECT
|
||||
imei,
|
||||
reading_km,
|
||||
reading_date,
|
||||
LAG(reading_km) OVER (PARTITION BY imei ORDER BY reading_date) AS prev_reading_km,
|
||||
LAG(reading_date) OVER (PARTITION BY imei ORDER BY reading_date) AS prev_reading_date
|
||||
FROM ops.odometer_readings
|
||||
WHERE reading_date > NOW() - INTERVAL '60 days'
|
||||
),
|
||||
physical_delta AS (
|
||||
SELECT
|
||||
imei,
|
||||
reading_km - prev_reading_km AS physical_km,
|
||||
EXTRACT(DAY FROM (reading_date - prev_reading_date)) AS period_days
|
||||
FROM physical_readings
|
||||
WHERE prev_reading_km IS NOT NULL
|
||||
AND period_days BETWEEN 20 AND 40
|
||||
)
|
||||
SELECT
|
||||
p.imei,
|
||||
d.driver_name,
|
||||
d.vehicle_number,
|
||||
ROUND(p.physical_km, 0) AS odometer_km_period,
|
||||
ROUND(tk.trips_km_30d, 0) AS tracker_km_30d,
|
||||
ROUND(
|
||||
(p.physical_km - tk.trips_km_30d) / NULLIF(p.physical_km, 0) * 100,
|
||||
1
|
||||
) AS divergence_pct
|
||||
FROM physical_delta p
|
||||
JOIN tracker_km tk ON tk.imei = p.imei
|
||||
JOIN tracksolid.devices d ON d.imei = p.imei
|
||||
WHERE ABS(
|
||||
(p.physical_km - tk.trips_km_30d) / NULLIF(p.physical_km, 0)
|
||||
) > 0.10
|
||||
ORDER BY ABS(p.physical_km - tk.trips_km_30d) DESC;
|
||||
```
|
||||
|
||||
**Interpretation:**
|
||||
|
||||
| Divergence | Likely cause | Action |
|
||||
|---|---|---|
|
||||
| Tracker < physical (> 10%) | GPS outage, tracker powered off, engine driven with no fix | Audit device uptime; inspect for tamper |
|
||||
| Tracker > physical (> 10%) | Duplicate trip records, distance-correction bug | Run migration check; review `trips.distance_km` distribution |
|
||||
| Divergence growing month-over-month | Sensor drift, antenna degradation | Replace device or antenna |
|
||||
|
||||
---
|
||||
|
||||
## 4. Real-Time Dispatch & Field-Service SLAs
|
||||
|
||||
### 4.1 Find the 5 Closest Available Vehicles
|
||||
|
||||
|
|
@ -512,6 +790,148 @@ ORDER BY lp.imei;
|
|||
|
||||
---
|
||||
|
||||
### 4.4 Dispatch Log Schema
|
||||
|
||||
A persistent record of every dispatch decision, needed for every SLA and cost metric that follows. Create once:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS tracksolid.dispatch_log (
|
||||
dispatch_id BIGSERIAL PRIMARY KEY,
|
||||
ticket_id TEXT NOT NULL,
|
||||
imei TEXT NOT NULL REFERENCES tracksolid.devices(imei),
|
||||
driver_name TEXT,
|
||||
job_lat DOUBLE PRECISION NOT NULL,
|
||||
job_lng DOUBLE PRECISION NOT NULL,
|
||||
job_geom GEOMETRY(POINT, 4326),
|
||||
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
first_movement_at TIMESTAMPTZ, -- populated when vehicle leaves depot
|
||||
on_site_at TIMESTAMPTZ, -- vehicle enters 150 m radius of job
|
||||
resolved_at TIMESTAMPTZ, -- ticket closed in ops system
|
||||
cancelled_at TIMESTAMPTZ,
|
||||
distance_km NUMERIC(8, 2),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dispatch_log_ticket ON tracksolid.dispatch_log(ticket_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_dispatch_log_imei_assigned
|
||||
ON tracksolid.dispatch_log(imei, assigned_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_dispatch_log_assigned_at
|
||||
ON tracksolid.dispatch_log(assigned_at DESC);
|
||||
```
|
||||
|
||||
**Population plan:** n8n or the ops integration layer writes one row per dispatch at assignment. A nightly job back-fills `first_movement_at` / `on_site_at` by joining `trips` and `live_positions` against `job_geom`.
|
||||
|
||||
---
|
||||
|
||||
### 4.5 Field-Service SLA Metrics
|
||||
|
||||
`[DASHBOARD]` `[ALERT]` `[MONTHLY]` — the operational heartbeat of a field-services business. Four timings per ticket, each a discrete SLA with its own band.
|
||||
|
||||
```
|
||||
ticket_created ─► assigned ─► first_movement ─► on_site ─► resolved
|
||||
(dispatch (depot depart (vehicle (job done)
|
||||
latency) latency) arrived)
|
||||
```
|
||||
|
||||
**(a) Dispatch latency** — from ticket creation to vehicle assignment:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
t.ticket_id,
|
||||
EXTRACT(EPOCH FROM (dl.assigned_at - t.created_at)) / 60 AS dispatch_latency_min
|
||||
FROM ops.tickets t
|
||||
JOIN tracksolid.dispatch_log dl ON dl.ticket_id = t.ticket_id
|
||||
WHERE t.created_at > NOW() - INTERVAL '7 days';
|
||||
```
|
||||
|
||||
**(b) Dispatch-to-depart** — from assignment to vehicle actually leaving the depot:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
dl.ticket_id,
|
||||
dl.imei,
|
||||
d.driver_name,
|
||||
EXTRACT(EPOCH FROM (dl.first_movement_at - dl.assigned_at)) / 60 AS depart_delay_min
|
||||
FROM tracksolid.dispatch_log dl
|
||||
JOIN tracksolid.devices d ON d.imei = dl.imei
|
||||
WHERE dl.assigned_at > NOW() - INTERVAL '7 days'
|
||||
AND dl.first_movement_at IS NOT NULL
|
||||
ORDER BY depart_delay_min DESC;
|
||||
```
|
||||
|
||||
**(c) Time-to-site** — from assignment to arrival at the job location (vehicle within 150 m):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
dl.ticket_id,
|
||||
dl.imei,
|
||||
ROUND(dl.distance_km, 1) AS distance_km,
|
||||
EXTRACT(EPOCH FROM (dl.on_site_at - dl.assigned_at)) / 60 AS time_to_site_min,
|
||||
ROUND(
|
||||
dl.distance_km /
|
||||
NULLIF(EXTRACT(EPOCH FROM (dl.on_site_at - dl.assigned_at)) / 3600, 0),
|
||||
1
|
||||
) AS avg_transit_kmh
|
||||
FROM tracksolid.dispatch_log dl
|
||||
WHERE dl.assigned_at > NOW() - INTERVAL '7 days'
|
||||
AND dl.on_site_at IS NOT NULL;
|
||||
```
|
||||
|
||||
**(d) On-site to resolution** — wrench time at the job:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
dl.ticket_id,
|
||||
dl.imei,
|
||||
EXTRACT(EPOCH FROM (dl.resolved_at - dl.on_site_at)) / 60 AS wrench_time_min
|
||||
FROM tracksolid.dispatch_log dl
|
||||
WHERE dl.on_site_at IS NOT NULL
|
||||
AND dl.resolved_at IS NOT NULL
|
||||
AND dl.assigned_at > NOW() - INTERVAL '30 days';
|
||||
```
|
||||
|
||||
**Monthly SLA attainment per driver:**
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
dl.imei,
|
||||
d.driver_name,
|
||||
COUNT(*) AS tickets,
|
||||
ROUND(AVG(
|
||||
EXTRACT(EPOCH FROM (dl.first_movement_at - dl.assigned_at))
|
||||
) / 60, 1) AS avg_depart_min,
|
||||
ROUND(AVG(
|
||||
EXTRACT(EPOCH FROM (dl.on_site_at - dl.assigned_at))
|
||||
) / 60, 1) AS avg_time_to_site_min,
|
||||
ROUND(AVG(
|
||||
EXTRACT(EPOCH FROM (dl.resolved_at - dl.on_site_at))
|
||||
) / 60, 1) AS avg_wrench_min,
|
||||
ROUND(
|
||||
100.0 * COUNT(*) FILTER (
|
||||
WHERE EXTRACT(EPOCH FROM (dl.on_site_at - dl.assigned_at)) / 60 <= 90
|
||||
) / NULLIF(COUNT(*), 0),
|
||||
1
|
||||
) AS pct_on_site_within_90min
|
||||
FROM tracksolid.dispatch_log dl
|
||||
JOIN tracksolid.devices d ON d.imei = dl.imei
|
||||
WHERE dl.assigned_at >= DATE_TRUNC('month', CURRENT_DATE)
|
||||
AND dl.on_site_at IS NOT NULL
|
||||
GROUP BY dl.imei, d.driver_name
|
||||
ORDER BY pct_on_site_within_90min DESC;
|
||||
```
|
||||
|
||||
**Target bands** (baseline — recalibrate after 90 days of data):
|
||||
|
||||
| SLA | Green | Amber | Red |
|
||||
|---|---|---|---|
|
||||
| Dispatch latency (ops → driver) | < 10 min | 10 – 25 min | > 25 min |
|
||||
| Depart delay (assigned → moving) | < 15 min | 15 – 35 min | > 35 min |
|
||||
| Time-to-site (assigned → on-site) | < 60 min | 60 – 120 min | > 120 min |
|
||||
| Wrench time (on-site → resolved) | < 90 min | 90 – 180 min | > 180 min |
|
||||
| % on-site within 90 min (monthly) | ≥ 85% | 70 – 85% | < 70% |
|
||||
|
||||
---
|
||||
|
||||
## 5. Distance per Driver per Day
|
||||
|
||||
### 5.1 Today's Summary
|
||||
|
|
@ -586,28 +1006,32 @@ ORDER BY k.total_km DESC;
|
|||
|
||||
## 6. Business Questions Now Answerable
|
||||
|
||||
| Business Question | Primary Data Source | Confidence |
|
||||
Status key: **✅ Ready** = answerable once new stack deployed | **⚙ Needs data** = additional setup required | **🔴 Blocked** = pending action before any data
|
||||
|
||||
| Business Question | Primary Data Source | Status |
|
||||
|---|---|---|
|
||||
| Which vehicles are moving right now? | `live_positions` | High |
|
||||
| Who started work latest today? | `fact_daily_fleet_metrics.day_start_time` | High |
|
||||
| Who drove the most km this week? | `trips` + `devices` | High |
|
||||
| Which vehicle spent the most time idling? | `trips.idle_time_s` | High |
|
||||
| How much fuel was wasted on idle today? | `trips.idle_time_s` × est. rate | Medium (needs `fuel_100km` set) |
|
||||
| Which driver triggered the most alarms this month? | `alarms` + `devices` | High |
|
||||
| What is total fleet distance this month? | `trips` | High |
|
||||
| Which vehicles did not move at all today? | `trips` LEFT JOIN `devices` | High |
|
||||
| Who is nearest to a new job right now? | `live_positions` + PostGIS | High |
|
||||
| Did any vehicle leave depot after hours? | `trips` time filter | High |
|
||||
| What is the speeding rate per driver per week? | `position_history` speed filter | High |
|
||||
| Which driver has the harshest driving style? | `position_history` delta query | High (needs 1–2 weeks of `track_list` data to accumulate) |
|
||||
| Are vehicles on approved routes? | `position_history` + `geofences` | Low (pending geofence population) |
|
||||
| Is cold chain in temperature range? | `temperature_readings` | Low (pending webhook registration) |
|
||||
| How much fuel is consumed per route? | `fuel_readings` + `trips` | Low (pending fuel sensor webhook) |
|
||||
| What is the real odometer per vehicle? | `live_positions.current_mileage` | Medium (depends on tracker calibration) |
|
||||
| How many km to next service interval? | `live_positions.current_mileage` - last service | Open (requires service log) |
|
||||
| Did any vehicle enter a restricted zone? | `alarms` (geofence type) + `geofences` | Low (pending geofence setup) |
|
||||
| Which drivers are consistently late on Mondays? | `fact_daily_fleet_metrics` day-of-week filter | High |
|
||||
| What percentage of the fleet was utilised today? | `trips` + `devices` count | High |
|
||||
| Which vehicles are moving right now? | `live_positions` | ✅ Ready (deploy stack) |
|
||||
| Who started work latest today? | `fact_daily_fleet_metrics.day_start_time` | ✅ Ready (deploy stack) |
|
||||
| Who drove the most km this week? | `trips` + `devices` | ✅ Ready (deploy + CSV import) |
|
||||
| Which vehicle spent the most time idling? | `trips.idle_time_s` | ✅ Ready (deploy stack) |
|
||||
| How much fuel was wasted on idle today? | `trips.idle_time_s` × rate | ⚙ Needs `fuel_100km` set per vehicle |
|
||||
| Which driver triggered the most alarms this month? | `alarms` + `devices` | ✅ Ready (deploy stack) |
|
||||
| What is total fleet distance this month? | `trips` | ✅ Ready (deploy stack) |
|
||||
| Which vehicles did not move at all today? | `trips` LEFT JOIN `devices` | ✅ Ready (deploy stack) |
|
||||
| Who is nearest to a new job right now? | `live_positions` + PostGIS | ✅ Ready (deploy + CSV import for names) |
|
||||
| Did any vehicle leave depot after hours? | `trips` time filter | ✅ Ready (deploy stack) |
|
||||
| What is the speeding rate per driver per week? | `position_history` speed filter | ✅ Ready (needs 1 week data) |
|
||||
| Which driver has the harshest driving style? | `position_history` delta query | ✅ Ready (needs 2 weeks `track_list`) |
|
||||
| What does one field ticket cost in fuel? | `trips` + `ops.tickets` + `fuel_100km` | ⚙ Needs `fuel_100km` + ticket feed wired |
|
||||
| Which vehicles are running outside assigned city? | `position_history` + `assigned_city` | ⚙ Needs `assigned_city` set (CSV import) |
|
||||
| How many km to next service interval? | `devices.current_mileage` + `ops.service_log` | ⚙ Needs first service-log entry per vehicle |
|
||||
| Are vehicles on approved routes? | `position_history` + `geofences` | ⚙ Pending geofence population (Step 4) |
|
||||
| Is cold chain in temperature range? | `temperature_readings` | 🔴 Pending webhook registration (Step 1) |
|
||||
| How much fuel is consumed per route? | `fuel_readings` + `trips` | 🔴 Pending fuel sensor webhook (Step 1) |
|
||||
| Did any vehicle enter a restricted zone? | `alarms` + `geofences` | 🔴 Pending geofence setup (Step 4) |
|
||||
| What percentage of the fleet was utilised today? | `trips` + `devices` count | ✅ Ready (deploy stack) |
|
||||
| Alarm while parked — tamper / theft signal | `alarms` + `parking_events` | ✅ Ready (deploy stack) |
|
||||
| Odometer divergence — tracker vs physical | `trips` + `ops.odometer_readings` | ⚙ Needs first odometer reading entry |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -659,7 +1083,48 @@ Ranked by aggression index (harsh events per 100 km), speeding events, and late
|
|||
|
||||
## 8. What Unlocks the Remaining 30%
|
||||
|
||||
The data foundation is in place. The following five steps activate the remaining analytics capabilities:
|
||||
The data foundation is in place. The following steps activate the remaining analytics capabilities, in priority order.
|
||||
|
||||
### Step 0 — Deploy New Ingestion Stack *(Current Blocker — do first)*
|
||||
|
||||
All analytics in this document are blocked until the new stack is live. The legacy pipeline stopped on **6 Apr 2026** due to 401 token expiry errors. The refactored code fixes this permanently.
|
||||
|
||||
```bash
|
||||
# On the Coolify server / inside the repo directory:
|
||||
|
||||
# 1. Pull latest code (includes all revisions through cebcf74)
|
||||
git pull
|
||||
|
||||
# 2. Apply schema migrations (01 through 06 in order)
|
||||
TS_DB=$(docker ps --filter "name=timescale_db" --format "{{.Names}}" | head -1)
|
||||
for f in 01_tracksolid_base.sql 02_tracksolid_full_schema_rev.sql \
|
||||
03_webhook_schema_migration.sql 04_bug_fix_migration.sql \
|
||||
05_enhancement_migration.sql 06_business_analytics_migration.sql; do
|
||||
echo "Applying $f..."
|
||||
docker exec -i "$TS_DB" psql -U postgres -d tracksolid_db < "$f"
|
||||
done
|
||||
|
||||
# 3. Rebuild and start new ingestion containers
|
||||
docker compose up -d --build ingest_movement ingest_events webhook_receiver
|
||||
|
||||
# 4. Run initial device sync (populates tracksolid.devices from API)
|
||||
docker exec -it ingest_movement python sync_driver_audit.py
|
||||
|
||||
# 5. Import driver/vehicle details from CSV
|
||||
docker exec -it ingest_movement python import_drivers_csv.py # dry-run
|
||||
docker exec -it ingest_movement python import_drivers_csv.py --apply # commit
|
||||
|
||||
# 6. Schedule nightly ETL
|
||||
# Add to cron or n8n: SELECT dwh_gold.refresh_daily_metrics(CURRENT_DATE - 1);
|
||||
```
|
||||
|
||||
**Expected state after Step 0:**
|
||||
- `tracksolid.devices`: 144+ rows with driver names, plates, departments, assigned_city
|
||||
- `tracksolid.live_positions`: positions refreshing every 60 seconds
|
||||
- `tracksolid.trips` / `position_history`: accumulating from first pipeline run
|
||||
- All analytics in this document begin producing results within 15 minutes of container start
|
||||
|
||||
---
|
||||
|
||||
### Step 1 — Register Webhooks in Tracksolid Pro Account *(Blocker)*
|
||||
Without registration, the following tables remain empty regardless of code:
|
||||
|
|
@ -693,16 +1158,24 @@ 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.
|
||||
**Automated:** `import_drivers_csv.py` (committed to the repo) reads `20260414_FS__Logistics - final_fixed.csv` (144 devices) and sets `driver_name`, `vehicle_number`, `vehicle_models`, `cost_centre`, `assigned_city`, `sim`, `iccid`, `imsi` in a single pass. Run after Step 0 device sync.
|
||||
|
||||
```bash
|
||||
docker exec -it ingest_movement python import_drivers_csv.py --apply
|
||||
```
|
||||
|
||||
CSV coverage after import: 140 vehicles with plates, 144 with driver names, 138 with SIM, `assigned_city` inferred (NBO=136, KLA=4). The 4 "Identification" spare units are skipped automatically.
|
||||
|
||||
**Manual top-up** for any device not in the CSV:
|
||||
|
||||
```sql
|
||||
-- Update individually or import from CSV via COPY
|
||||
UPDATE tracksolid.devices
|
||||
SET vehicle_name = 'KBZ 123A',
|
||||
vehicle_number = 'KBZ 123A',
|
||||
driver_name = 'John Kamau',
|
||||
driver_phone = '+254700000001',
|
||||
vehicle_category = 'van'
|
||||
vehicle_category = 'van',
|
||||
assigned_city = 'NBO'
|
||||
WHERE imei = '352093080000001';
|
||||
```
|
||||
|
||||
|
|
@ -731,29 +1204,233 @@ VALUES (
|
|||
|
||||
### Step 5 — Run Migrations and Deploy Updated Containers
|
||||
|
||||
```bash
|
||||
# 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);
|
||||
```
|
||||
See **Step 0** above for the full deployment sequence. All six migrations (01–06) must be applied in order before starting the new containers. Step 0 includes the complete command block.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — Key Metric Thresholds Reference
|
||||
## 9. Fleet Readiness Scorecard
|
||||
|
||||
`[DASHBOARD]` `[MONTHLY]` — a single composite number per vehicle, useful as a morning briefing and a monthly fleet health report. Runs against only the tables you already have — no new DDL required — so this is the fastest concrete win in this document.
|
||||
|
||||
Five sub-scores (0 – 100), averaged with weights:
|
||||
|
||||
| Sub-score | Weight | Signal |
|
||||
|---|---|---|
|
||||
| **Freshness** | 25% | GPS fix age vs. a 5-minute target |
|
||||
| **Coverage** | 20% | Active days in the last 7 |
|
||||
| **Silence** | 15% | Tracker went dark > 30 min during working hours |
|
||||
| **Alarm pressure** | 20% | Alarms per 100 km over 30 days |
|
||||
| **Driver behaviour** | 20% | Aggression + speeding index |
|
||||
|
||||
```sql
|
||||
WITH freshness AS (
|
||||
SELECT
|
||||
imei,
|
||||
EXTRACT(EPOCH FROM (NOW() - gps_time)) / 60 AS minutes_since_fix
|
||||
FROM tracksolid.live_positions
|
||||
),
|
||||
coverage AS (
|
||||
SELECT
|
||||
imei,
|
||||
COUNT(DISTINCT DATE(start_time AT TIME ZONE 'Africa/Nairobi')) AS days_active_7d
|
||||
FROM tracksolid.trips
|
||||
WHERE start_time > NOW() - INTERVAL '7 days'
|
||||
GROUP BY imei
|
||||
),
|
||||
silence AS (
|
||||
-- Gaps > 30 min during 07:00 – 19:00 EAT in the last 7 days
|
||||
SELECT
|
||||
imei,
|
||||
COUNT(*) AS silence_events_7d
|
||||
FROM (
|
||||
SELECT
|
||||
imei,
|
||||
gps_time,
|
||||
LAG(gps_time) OVER (PARTITION BY imei ORDER BY gps_time) AS prev_time
|
||||
FROM tracksolid.position_history
|
||||
WHERE gps_time > NOW() - INTERVAL '7 days'
|
||||
AND EXTRACT(HOUR FROM gps_time AT TIME ZONE 'Africa/Nairobi') BETWEEN 7 AND 19
|
||||
) gaps
|
||||
WHERE EXTRACT(EPOCH FROM (gps_time - prev_time)) > 1800
|
||||
GROUP BY imei
|
||||
),
|
||||
alarm_pressure AS (
|
||||
SELECT
|
||||
a.imei,
|
||||
COUNT(*) AS alarms_30d,
|
||||
SUM(t.distance_km) AS km_30d
|
||||
FROM tracksolid.alarms a
|
||||
LEFT JOIN tracksolid.trips t
|
||||
ON t.imei = a.imei
|
||||
AND t.start_time > NOW() - INTERVAL '30 days'
|
||||
WHERE a.alarm_time > NOW() - INTERVAL '30 days'
|
||||
GROUP BY a.imei
|
||||
),
|
||||
behaviour AS (
|
||||
SELECT
|
||||
ph.imei,
|
||||
COUNT(*) FILTER (WHERE ph.speed > 100) AS over_100,
|
||||
COUNT(*) FILTER (
|
||||
WHERE ABS(ph.speed - LAG(ph.speed) OVER (
|
||||
PARTITION BY ph.imei ORDER BY ph.gps_time
|
||||
)) > 30
|
||||
) AS harsh_events
|
||||
FROM tracksolid.position_history ph
|
||||
WHERE ph.gps_time > NOW() - INTERVAL '30 days'
|
||||
AND ph.source = 'track_list'
|
||||
GROUP BY ph.imei
|
||||
)
|
||||
SELECT
|
||||
d.imei,
|
||||
d.driver_name,
|
||||
d.vehicle_number,
|
||||
ROUND(
|
||||
GREATEST(0, 100 - COALESCE(f.minutes_since_fix, 999) / 5.0 * 20)
|
||||
) AS freshness_score,
|
||||
ROUND(
|
||||
LEAST(100, COALESCE(c.days_active_7d, 0) / 5.0 * 100)
|
||||
) AS coverage_score,
|
||||
ROUND(
|
||||
GREATEST(0, 100 - COALESCE(s.silence_events_7d, 0) * 10)
|
||||
) AS silence_score,
|
||||
ROUND(
|
||||
GREATEST(0, 100 - COALESCE(
|
||||
ap.alarms_30d::NUMERIC / NULLIF(ap.km_30d, 0) * 100 * 20, 0
|
||||
))
|
||||
) AS alarm_score,
|
||||
ROUND(
|
||||
GREATEST(0, 100 - COALESCE(b.over_100, 0) * 2 - COALESCE(b.harsh_events, 0) * 3)
|
||||
) AS behaviour_score,
|
||||
ROUND(
|
||||
GREATEST(0, 100 - COALESCE(f.minutes_since_fix, 999) / 5.0 * 20) * 0.25
|
||||
+ LEAST(100, COALESCE(c.days_active_7d, 0) / 5.0 * 100) * 0.20
|
||||
+ GREATEST(0, 100 - COALESCE(s.silence_events_7d, 0) * 10) * 0.15
|
||||
+ GREATEST(0, 100 - COALESCE(
|
||||
ap.alarms_30d::NUMERIC / NULLIF(ap.km_30d, 0) * 100 * 20, 0
|
||||
)) * 0.20
|
||||
+ GREATEST(0, 100 - COALESCE(b.over_100, 0) * 2 - COALESCE(b.harsh_events, 0) * 3) * 0.20
|
||||
) AS readiness_score
|
||||
FROM tracksolid.devices d
|
||||
LEFT JOIN freshness f ON f.imei = d.imei
|
||||
LEFT JOIN coverage c ON c.imei = d.imei
|
||||
LEFT JOIN silence s ON s.imei = d.imei
|
||||
LEFT JOIN alarm_pressure ap ON ap.imei = d.imei
|
||||
LEFT JOIN behaviour b ON b.imei = d.imei
|
||||
WHERE d.enabled_flag = 1
|
||||
ORDER BY readiness_score ASC NULLS FIRST;
|
||||
```
|
||||
|
||||
**Interpretation:**
|
||||
|
||||
| Score | Band | Action |
|
||||
|---|---|---|
|
||||
| 85 – 100 | Green — ready | Dispatch freely |
|
||||
| 60 – 84 | Amber — monitor | Review the lowest sub-score; fix trackers or coach driver |
|
||||
| < 60 | Red — unreliable | Do not dispatch for priority jobs; service or replace |
|
||||
| NULL | Silent | Vehicle never reported — investigate install / commission |
|
||||
|
||||
The scorecard is also the cleanest Panel 2 replacement for the Grafana Fleet Status Summary.
|
||||
|
||||
---
|
||||
|
||||
## 10. Service-Interval Forecaster
|
||||
|
||||
`[MONTHLY]` `[ALERT]` — predicts when each vehicle will hit its next service interval (default 10,000 km), based on its trailing 30-day km rate. Lets ops pre-book workshop slots and avoid fleet-wide conflicts.
|
||||
|
||||
Requires a service-log table (create once):
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS ops.service_log (
|
||||
service_id BIGSERIAL PRIMARY KEY,
|
||||
imei TEXT NOT NULL REFERENCES tracksolid.devices(imei),
|
||||
service_date DATE NOT NULL,
|
||||
odometer_km INTEGER NOT NULL,
|
||||
service_type TEXT, -- 'scheduled', 'repair', 'tyre', etc.
|
||||
cost_kes INTEGER,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_service_log_imei_date
|
||||
ON ops.service_log(imei, service_date DESC);
|
||||
```
|
||||
|
||||
**Forecaster query** — km until next service, projected service date:
|
||||
|
||||
```sql
|
||||
WITH last_service AS (
|
||||
SELECT DISTINCT ON (imei)
|
||||
imei,
|
||||
service_date,
|
||||
odometer_km
|
||||
FROM ops.service_log
|
||||
WHERE service_type = 'scheduled'
|
||||
ORDER BY imei, service_date DESC
|
||||
),
|
||||
current_odometer AS (
|
||||
SELECT imei, current_mileage_km
|
||||
FROM tracksolid.devices
|
||||
),
|
||||
trailing_rate AS (
|
||||
SELECT
|
||||
imei,
|
||||
SUM(distance_km) / 30.0 AS km_per_day_30d
|
||||
FROM tracksolid.trips
|
||||
WHERE start_time > NOW() - INTERVAL '30 days'
|
||||
AND end_time IS NOT NULL
|
||||
GROUP BY imei
|
||||
)
|
||||
SELECT
|
||||
d.imei,
|
||||
d.driver_name,
|
||||
d.vehicle_number,
|
||||
ls.service_date AS last_service_date,
|
||||
ls.odometer_km AS last_service_odo,
|
||||
co.current_mileage_km AS current_odo,
|
||||
(co.current_mileage_km - COALESCE(ls.odometer_km, 0)) AS km_since_service,
|
||||
GREATEST(
|
||||
0,
|
||||
10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0))
|
||||
) AS km_to_next_service,
|
||||
ROUND(tr.km_per_day_30d, 1) AS km_per_day_30d,
|
||||
CASE
|
||||
WHEN tr.km_per_day_30d > 0 THEN
|
||||
CURRENT_DATE + (
|
||||
GREATEST(0, 10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0)))
|
||||
/ tr.km_per_day_30d
|
||||
)::INT
|
||||
ELSE NULL
|
||||
END AS projected_service_date
|
||||
FROM tracksolid.devices d
|
||||
LEFT JOIN last_service ls ON ls.imei = d.imei
|
||||
LEFT JOIN current_odometer co ON co.imei = d.imei
|
||||
LEFT JOIN trailing_rate tr ON tr.imei = d.imei
|
||||
WHERE d.enabled_flag = 1
|
||||
ORDER BY projected_service_date NULLS LAST;
|
||||
```
|
||||
|
||||
**Weekly booking view** — how many vehicles need service in each of the next 8 weeks:
|
||||
|
||||
```sql
|
||||
WITH forecast AS (
|
||||
-- (same CTE body as above; wrap as subquery or view `ops.vw_service_forecast`)
|
||||
SELECT imei, projected_service_date
|
||||
FROM ops.vw_service_forecast
|
||||
WHERE projected_service_date IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
DATE_TRUNC('week', projected_service_date)::DATE AS week_start,
|
||||
COUNT(*) AS vehicles_due
|
||||
FROM forecast
|
||||
WHERE projected_service_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '8 weeks'
|
||||
GROUP BY week_start
|
||||
ORDER BY week_start;
|
||||
```
|
||||
|
||||
> **Alert:** any vehicle with `km_to_next_service < (7 × km_per_day_30d)` fires an amber ticket to the fleet manager. Any vehicle already overdue (`km_to_next_service = 0`) fires red.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A — Key Metric Thresholds Reference
|
||||
|
||||
| Metric | Green | Amber | Red |
|
||||
|---|---|---|---|
|
||||
|
|
@ -765,8 +1442,57 @@ docker compose up -d --build ingest_movement ingest_events webhook_receiver
|
|||
| Days vehicle not used (per month) | 0–2 | 3–5 | > 5 |
|
||||
| GPS fix age (live_positions) | < 2 min | 2–10 min | > 10 min |
|
||||
| Alarm rate per vehicle per week | 0–2 | 3–7 | > 7 |
|
||||
| Readiness score (§9) | ≥ 85 | 60–84 | < 60 |
|
||||
| Cost per ticket (van, NBO baseline) | < 400 KES | 400–900 KES | > 900 KES |
|
||||
| On-site within 90 min (§4.5) | ≥ 85% | 70–85% | < 70% |
|
||||
|
||||
---
|
||||
|
||||
*Document generated: 2026-04-10 · Stack: TimescaleDB 2.15 + PostGIS + Tracksolid Pro Open Platform API*
|
||||
## Appendix B — Threshold Calibration Guide
|
||||
|
||||
Every threshold in Appendix A is a **starting point**. They are drawn from general field-services norms and three Fireside incident reviews — not from Fireside's own distribution. After ~30 days of clean data, recalibrate each one against your own observed p50 / p90 / p99.
|
||||
|
||||
**The principle:** green should catch ≥ 50% of vehicle-days, amber ≥ 30%, red ≤ 20%. If red is firing on more than 25% of the fleet every day, the alert is noise and will be ignored.
|
||||
|
||||
**Calibration recipe** — run monthly for each threshold-backed metric:
|
||||
|
||||
```sql
|
||||
-- Example: utilisation % — recompute green/amber/red cut-points from the live distribution
|
||||
WITH daily AS (
|
||||
SELECT
|
||||
t.imei,
|
||||
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS day,
|
||||
SUM(t.driving_time_s) / (10.0 * 3600) * 100 AS utilisation_pct
|
||||
FROM tracksolid.trips t
|
||||
WHERE t.start_time > NOW() - INTERVAL '30 days'
|
||||
AND t.end_time IS NOT NULL
|
||||
GROUP BY t.imei, day
|
||||
)
|
||||
SELECT
|
||||
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY utilisation_pct) AS p25_red_cut,
|
||||
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY utilisation_pct) AS p50_amber_cut,
|
||||
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY utilisation_pct) AS p75_green_cut,
|
||||
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY utilisation_pct) AS p90_stretch
|
||||
FROM daily;
|
||||
```
|
||||
|
||||
Replace the Appendix A band edges with the returned percentiles. Repeat for idle %, speeding rate, harsh driving index, alarms per week. Document the recalibration date and the previous values in a changelog so band drift is visible.
|
||||
|
||||
**City-cohort cuts.** Nairobi traffic, Mombasa port runs, and Kampala cross-border routes produce genuinely different distributions. Group the recalibration by `devices.assigned_city` so you end up with three threshold sets, not one fleet-average compromise:
|
||||
|
||||
```sql
|
||||
-- Apply the same percentile function grouped by city
|
||||
SELECT
|
||||
d.assigned_city,
|
||||
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY utilisation_pct) AS p50,
|
||||
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY utilisation_pct) AS p75
|
||||
FROM daily
|
||||
JOIN tracksolid.devices d ON d.imei = daily.imei
|
||||
GROUP BY d.assigned_city;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Document updated: 2026-04-18 · 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`*
|
||||
*DB state verified: 18 Apr 2026 — live data in `tracksolid_2` (63 devices, pipeline stopped 6 Apr). New stack targets `tracksolid` schema — pending deployment.*
|
||||
|
|
|
|||
225
06_business_analytics_migration.sql
Normal file
225
06_business_analytics_migration.sql
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
-- Migration 06 — Business Analytics Schema Support
|
||||
-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
-- Adds the schema objects referenced by 01_BusinessAnalytics.md:
|
||||
-- • tracksolid.devices.assigned_city (§3.7 Geographic Drift)
|
||||
-- • tracksolid.dispatch_log (§4.4, §4.5 Field-Service SLAs)
|
||||
-- • ops schema (external ops integration namespace)
|
||||
-- • ops.service_log (§10 Service-Interval Forecaster)
|
||||
-- • ops.odometer_readings (§3.8 Odometer Divergence)
|
||||
-- • ops.tickets (§2.4 Cost-per-Ticket — skeleton)
|
||||
-- • ops.vw_service_forecast (§10 weekly booking view)
|
||||
--
|
||||
-- Run after migration 05. Safe to re-run (uses IF NOT EXISTS / DO NOTHING /
|
||||
-- CREATE OR REPLACE).
|
||||
-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ── 1. City cohort column (§3.7) ─────────────────────────────────────────────
|
||||
|
||||
ALTER TABLE tracksolid.devices
|
||||
ADD COLUMN IF NOT EXISTS assigned_city TEXT;
|
||||
|
||||
COMMENT ON COLUMN tracksolid.devices.assigned_city
|
||||
IS 'Operating territory code: NBO (Nairobi) | MBA (Mombasa) | KLA (Kampala). '
|
||||
'Used for city-cohort analytics and geographic drift detection.';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_devices_assigned_city
|
||||
ON tracksolid.devices (assigned_city)
|
||||
WHERE assigned_city IS NOT NULL;
|
||||
|
||||
-- ── 2. Dispatch log (§4.4, §4.5) ──────────────────────────────────────────────
|
||||
-- One row per ticket dispatch. Populated by n8n / ops integration at
|
||||
-- assignment; back-filled by nightly job using trips + live_positions.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tracksolid.dispatch_log (
|
||||
dispatch_id BIGSERIAL PRIMARY KEY,
|
||||
ticket_id TEXT NOT NULL,
|
||||
imei TEXT NOT NULL REFERENCES tracksolid.devices(imei),
|
||||
driver_name TEXT,
|
||||
job_lat DOUBLE PRECISION NOT NULL,
|
||||
job_lng DOUBLE PRECISION NOT NULL,
|
||||
job_geom geometry(Point, 4326),
|
||||
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
first_movement_at TIMESTAMPTZ,
|
||||
on_site_at TIMESTAMPTZ,
|
||||
resolved_at TIMESTAMPTZ,
|
||||
cancelled_at TIMESTAMPTZ,
|
||||
distance_km NUMERIC(8, 2),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dispatch_log_ticket
|
||||
ON tracksolid.dispatch_log (ticket_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_dispatch_log_imei_assigned
|
||||
ON tracksolid.dispatch_log (imei, assigned_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_dispatch_log_assigned_at
|
||||
ON tracksolid.dispatch_log (assigned_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_dispatch_log_job_geom
|
||||
ON tracksolid.dispatch_log USING GIST (job_geom);
|
||||
|
||||
COMMENT ON TABLE tracksolid.dispatch_log
|
||||
IS 'Persistent record of every dispatch decision. Powers SLA metrics: '
|
||||
'dispatch latency, depart delay, time-to-site, wrench time.';
|
||||
COMMENT ON COLUMN tracksolid.dispatch_log.first_movement_at
|
||||
IS 'First trip start after assigned_at. Back-filled nightly from trips.';
|
||||
COMMENT ON COLUMN tracksolid.dispatch_log.on_site_at
|
||||
IS 'Time vehicle entered 150 m radius of job_geom. Back-filled nightly.';
|
||||
COMMENT ON COLUMN tracksolid.dispatch_log.resolved_at
|
||||
IS 'Ticket close time from the ops system (ops.tickets.closed_at).';
|
||||
|
||||
-- ── 3. ops schema namespace ───────────────────────────────────────────────────
|
||||
-- Separates Fireside operations domain (tickets, services, odometers) from
|
||||
-- the tracksolid telematics namespace so ownership / grants can diverge.
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS ops;
|
||||
|
||||
COMMENT ON SCHEMA ops
|
||||
IS 'Fireside operations domain: tickets, service logs, odometer readings. '
|
||||
'Distinct from tracksolid.* which holds telematics data.';
|
||||
|
||||
-- ── 4. Service log (§10) ──────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ops.service_log (
|
||||
service_id BIGSERIAL PRIMARY KEY,
|
||||
imei TEXT NOT NULL REFERENCES tracksolid.devices(imei),
|
||||
service_date DATE NOT NULL,
|
||||
odometer_km INTEGER NOT NULL,
|
||||
service_type TEXT,
|
||||
cost_kes INTEGER,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_service_log_imei_date
|
||||
ON ops.service_log (imei, service_date DESC);
|
||||
|
||||
COMMENT ON TABLE ops.service_log
|
||||
IS 'Workshop service history. Powers §10 Service-Interval Forecaster.';
|
||||
COMMENT ON COLUMN ops.service_log.service_type
|
||||
IS 'scheduled | repair | tyre | bodywork | inspection | other';
|
||||
COMMENT ON COLUMN ops.service_log.odometer_km
|
||||
IS 'Physical odometer reading at service time (integer km).';
|
||||
|
||||
-- ── 5. Odometer readings (§3.8) ───────────────────────────────────────────────
|
||||
-- Periodic physical odometer captures from service events, fuel card receipts,
|
||||
-- or manual driver entry. Divergence vs tracker-computed distance flags
|
||||
-- sensor drift or tamper.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ops.odometer_readings (
|
||||
reading_id BIGSERIAL PRIMARY KEY,
|
||||
imei TEXT NOT NULL REFERENCES tracksolid.devices(imei),
|
||||
reading_date DATE NOT NULL,
|
||||
reading_km INTEGER NOT NULL,
|
||||
source TEXT,
|
||||
recorded_by TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (imei, reading_date)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_odometer_readings_imei_date
|
||||
ON ops.odometer_readings (imei, reading_date DESC);
|
||||
|
||||
COMMENT ON TABLE ops.odometer_readings
|
||||
IS 'Physical odometer captures from service, fuel card, or manual entry. '
|
||||
'Powers §3.8 Odometer Divergence audit.';
|
||||
COMMENT ON COLUMN ops.odometer_readings.source
|
||||
IS 'service | fuel_card | driver_manual | workshop_form';
|
||||
|
||||
-- ── 6. Tickets skeleton (§2.4) ───────────────────────────────────────────────
|
||||
-- MINIMAL skeleton so the Cost-per-Ticket query is runnable. In production,
|
||||
-- this table is expected to be populated by the Fireside ticketing system
|
||||
-- (Zoho/Freshdesk/job-management export) via n8n or a direct feed. Schema
|
||||
-- is intentionally narrow — extend with columns specific to your source.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ops.tickets (
|
||||
ticket_id TEXT PRIMARY KEY,
|
||||
assigned_imei TEXT REFERENCES tracksolid.devices(imei),
|
||||
driver_name TEXT,
|
||||
customer TEXT,
|
||||
job_type TEXT,
|
||||
priority TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
assigned_at TIMESTAMPTZ,
|
||||
closed_at TIMESTAMPTZ,
|
||||
job_lat DOUBLE PRECISION,
|
||||
job_lng DOUBLE PRECISION,
|
||||
job_geom geometry(Point, 4326),
|
||||
ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_status_created
|
||||
ON ops.tickets (status, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_assigned_imei
|
||||
ON ops.tickets (assigned_imei)
|
||||
WHERE assigned_imei IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_closed_at
|
||||
ON ops.tickets (closed_at DESC NULLS LAST);
|
||||
|
||||
COMMENT ON TABLE ops.tickets
|
||||
IS 'Skeleton for ticket data sourced from the Fireside ops system. '
|
||||
'Replace or extend to match the actual feed (Zoho Desk, Freshdesk, etc).';
|
||||
COMMENT ON COLUMN ops.tickets.status
|
||||
IS 'open | assigned | in_progress | resolved | cancelled';
|
||||
|
||||
-- ── 7. Service forecast view (§10) ────────────────────────────────────────────
|
||||
-- Wraps the §10 forecaster CTE so the weekly booking query in
|
||||
-- 01_BusinessAnalytics.md references a stable object.
|
||||
|
||||
CREATE OR REPLACE VIEW ops.vw_service_forecast AS
|
||||
WITH last_service AS (
|
||||
SELECT DISTINCT ON (imei)
|
||||
imei,
|
||||
service_date,
|
||||
odometer_km
|
||||
FROM ops.service_log
|
||||
WHERE service_type = 'scheduled'
|
||||
ORDER BY imei, service_date DESC
|
||||
),
|
||||
current_odometer AS (
|
||||
SELECT imei, current_mileage_km
|
||||
FROM tracksolid.devices
|
||||
),
|
||||
trailing_rate AS (
|
||||
SELECT
|
||||
imei,
|
||||
SUM(distance_km) / 30.0 AS km_per_day_30d
|
||||
FROM tracksolid.trips
|
||||
WHERE start_time > NOW() - INTERVAL '30 days'
|
||||
AND end_time IS NOT NULL
|
||||
GROUP BY imei
|
||||
)
|
||||
SELECT
|
||||
d.imei,
|
||||
d.driver_name,
|
||||
d.vehicle_number,
|
||||
ls.service_date AS last_service_date,
|
||||
ls.odometer_km AS last_service_odo,
|
||||
co.current_mileage_km AS current_odo,
|
||||
(co.current_mileage_km - COALESCE(ls.odometer_km, 0)) AS km_since_service,
|
||||
GREATEST(
|
||||
0,
|
||||
10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0))
|
||||
) AS km_to_next_service,
|
||||
ROUND(tr.km_per_day_30d, 1) AS km_per_day_30d,
|
||||
CASE
|
||||
WHEN tr.km_per_day_30d > 0 THEN
|
||||
CURRENT_DATE + (
|
||||
GREATEST(0, 10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0)))
|
||||
/ tr.km_per_day_30d
|
||||
)::INT
|
||||
ELSE NULL
|
||||
END AS projected_service_date
|
||||
FROM tracksolid.devices d
|
||||
LEFT JOIN last_service ls ON ls.imei = d.imei
|
||||
LEFT JOIN current_odometer co ON co.imei = d.imei
|
||||
LEFT JOIN trailing_rate tr ON tr.imei = d.imei
|
||||
WHERE d.enabled_flag = 1;
|
||||
|
||||
COMMENT ON VIEW ops.vw_service_forecast
|
||||
IS 'Projected next-service date per vehicle based on 30-day km rate. '
|
||||
'Service interval default 10,000 km — override at query time if needed.';
|
||||
|
||||
COMMIT;
|
||||
145
20260414_FS__Logistics - final_fixed.csv
Normal file
145
20260414_FS__Logistics - final_fixed.csv
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
Account,Customer Name,Device Name,IMEI,Model,Activated Date,Sales Time,SIM,MAC,Subscription Expiration,User Expiration Date,Battery replacement date,Group,ICCID,IMSI,Driver Name,Telephone,License Plate No.,ID Number,Department,VIN,Engine Number,Vehicle Brand,Vehicle Model,Fuel/100km,Installation Time
|
||||
fireside,Fireside Group HQ,UMA 382EK_UG,865135061569479,X3,2026-02-26,2025-09-08,+256792997079,,2036-02-27,2036-02-27,,Default Group,8925610001837573419F,641101970467667,UG,,UMA 382EK,,MTN,,,,,,
|
||||
fireside,Fireside Group HQ,UMA 418EK_UG,865135061569131,X3,2026-02-26,2025-09-08,+256792997053,,2036-02-27,2036-02-27,,Default Group,8925610001837573385F,641101970467664,UG,,UMA 418EK,,MTN,,,,,,
|
||||
fireside,Fireside Group HQ,John Mbugua/OSP-KDW 573B_CAM,862798052707896,JC400P,2026-01-30,2025-06-11,,,2036-01-31,2036-01-31,,Default Group,89254021414206816725,639021410681672,John Mbugua,,KDW 573B,,OSP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,JOEL NTUMBA/ISP-UMA 826AB_UG,865135061563423,X3,2026-01-28,2025-09-08,0119051036,,2036-01-29,2036-01-29,,Default Group,89254021414206652690,639021410665269,Joel Ntumba,,UMA 826AB,,MTN,,,,Motorbike,,
|
||||
fireside,Fireside Group HQ,RODIN KIBERU/ISP-UMA 011EK_UG,865135061564280,X3,2026-01-28,2025-09-08,0118081642,,2036-01-29,2036-01-29,,Default Group,89254021414206817244,639021410681724,Rodin Kiberu,,UMA 011EK,,MTN,,,,Motorbike,,
|
||||
fireside,Fireside Group HQ,Wambua/ROLLOUT-KDV 683Z_CAM,862798052708068,JC400P,2026-01-24,2025-06-11,0758048043,,2036-01-25,2036-01-25,,Default Group,89254021414206816964,639021410681696,Dominic Wambua,,KDV 683Z,,ROLLOUT,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Levine/OSP-KDV 439_CAM,862798052708167,JC400P,2025-12-13,2025-06-11,0758046738,,2035-12-14,2035-12-14,,Default Group,89254021414206816741,639021410681674,Levine Wasike,,KDV 439W,,FDS,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Benjamin/PLAN-KDV 438W_Track,865135061563639,X3,2025-12-13,2025-09-08,0758047065,,2035-12-14,2035-12-14,,Default Group,89254021414206816683,639021410681668,Benjamin Ananda,,KDV 438W,,PLANNING,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Albert/FDS-KDV 437W_Track,865135061569123,X3,2025-12-13,2025-09-08,0758047101,,2035-12-14,2035-12-14,,Default Group,89254021414206816881,639021410681688,Albert Mutwiri,,KDV 437W,,FDS,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Silvanus/FDS-KDV 064S_Track,865135061564470,X3,2025-11-21,2025-09-08,0113669866,,2035-11-22,2035-11-22,,Default Group,89254021414206378718,639021410637871,Silvanus Kipkorir,,KDV 064S,,AIRTEL,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Robbert/FDS-KDV 072L_Track,865135061581904,X3,2025-11-21,2025-09-08,0114149576,,2035-11-22,2035-11-22,,Default Group,89254021264261503993,639021266150399,Robert Kipruto,,KDV 072L,,FDS,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Benard Kimutai/KDN 759G_CAM,862798052713779,JC400P,2025-08-23,2025-06-11,0752143258,,2035-08-24,2035-08-24,,Default Group,89254035061001753860,639035060175386,Benard Kimutai,,KDN 759G,,OSP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Geoffrey/Rider-KMGS 239H,865135061043426,X3,2025-08-22,2025-06-11,0768696658,,2035-08-23,2035-08-23,,Default Group,89254021394274518926,639021397451892,Geoffrey Karanja,,KMGS 239H,,OSP-PATROL,,,,Motorbike,,
|
||||
fireside,Fireside Group HQ,Samuel Kihara/Rider_KMEL 225X,865135061053714,X3,2025-08-02,2025-06-11,0768696832,,2035-08-03,2035-08-03,,Default Group,89254021394274518934,639021397451893,Samuel Kihara,,KMEL 225X,,OSP-PATROL,,,,Motorbike,,
|
||||
fireside,Fireside Group HQ,Brian Njenga/Rider-KMFF 113Z,865135061036164,X3,2025-07-31,2025-06-11,0768696705,,2035-08-01,2035-08-01,,Default Group,89254021394274518850,639021397451885,Brian Njenga,,KMFF 113Z,,OSP-PATROL,,,,Motorbike,,
|
||||
fireside,Fireside Group HQ,KMGK 596V,865135061049001,X3,2025-07-31,2025-06-11,0768697064,,2035-08-01,2035-08-01,,Default Group,89254021394274518884,639021397451888,Parked,,KMGK 596V,,DELIVERIES,,,,Motorbike,,
|
||||
fireside,Fireside Group HQ,Rofas/General-KDT 728R_CAM,862798052715220,JC400P,2025-07-16,2025-06-11,0704573658,,2035-07-17,2035-07-17,,Default Group,89254021334258495873,639021335849587,Rofas Njagi,,KDT 728R,,REGIONAL,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Emmanuel/Gen-KDS 453Y_Track,865135061037980,X3,2025-07-15,2025-06-11,0790176734,,,2035-07-15,,Default Group,89254021394215205856,639021391520585,Emmanuel Luseno,,KDS 453Y,,GENERAL,,,,Pick-Up,,
|
||||
fireside,Fireside Group HQ,Kimeria/Crane-KDS 525D_Track,865135061035778,X3,2025-07-11,2025-06-11,0790176738,,2035-07-12,2035-07-12,,Default Group,89254021394215205922,639021391520592,John Kimeria,,KDS 525D,,GENERAL,,,,Crane,,
|
||||
fireside,Fireside Group HQ,Rashid/ISP-KDM 840V_Track,865135061053748,X3,2025-07-10,2025-06-11,0768445963,,2035-07-11,2035-07-11,,Default Group,89254021334212352574,639021331235257,Rashid Hassan,,KDM 840V,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Wambugu/FDS-KDR 592N_Track,865135061042261,X3,2025-07-10,2025-06-11,0797680464,,2035-07-11,2035-07-11,,Default Group,89254021334258159693,639021335815969,Kelvin Wambugu,,KDR 592N,,FDS,,,,Probox,,
|
||||
fireside,Fireside Group HQ,James Onyango-KDU 613B__CAM,862798052713811,JC400P,2025-07-09,2025-06-11,0790176542,,2035-07-10,2035-07-10,,Default Group,89254021394215205880,639021391520588,James Onyango,,KDU 613B,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Mazda-KDU 613A_Track,865135061047435,X3,2025-07-09,2025-06-11,0790175971,,2035-07-10,2035-07-10,,Default Group,89254021394215205971,639021391520597,Management_Mazda,,KDU 613A,,MGT,,,,Mazda,,
|
||||
fireside,Fireside Group HQ,Charles Nyambane/ISP-KCB 711C_CAM,862798050522743,JC400P,2023-12-22,2024-11-08,0768657106,,2033-12-23,2033-12-23,,Default Group,,,Charles Nyambane,,KCB 711C,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Sadique/GEN-KDC 490Q_CAM,862798050525225,JC400P,2023-12-22,2024-11-08,0768652386,,2043-12-22,2043-12-22,,Default Group,,,Sadique Wakayula,,KDC 490Q,,GENERAL,,,,Crane,,
|
||||
fireside,Fireside Group HQ,Samuel Nganga/ISP-KDE 264M_CAM,862798050525068,JC400P,2023-12-22,2024-11-08,0768658564,,2033-12-23,2033-12-23,,Default Group,,,Samuel Ng'ang'a,,KDE 264M,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Kennedy Ondieki/ISP-KCU 237Z_CAM,862798050525837,JC400P,2023-12-21,,0113669852,,2033-12-22,2033-12-22,,Default Group,,,Kennedy Ondieki,,KCU 237Z,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Geoffrey Too/OSP-KDM 308S_CAM,862798050523618,JC400P,2023-08-15,2023-08-22,0701211625,,2033-08-16,2033-08-16,,Default Group,,,Geoffrey Too,,KDM 308S,,OSP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Job Ngare/ISP Coast-KDM309S_CAM,862798050523816,JC400P,2023-08-15,2023-08-22,0707936781,,2033-08-16,2033-08-16,,Default Group,,,Job Ngare,,KDM 309S,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Daudi Jaoko/OSP-KDK 815R_Track,359857082912239,GT06E,2023-06-21,2023-07-27,0706392117,,2033-06-22,2033-06-22,,Default Group,89254021234296021287,639021239602128,Dickson Jaoko,,KDK 815R,,OSP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Peter Mbugua/ISP-KDK 728K_Track,359857082897091,GT06E,2022-12-14,2022-12-16,0790262984,,2042-12-15,2042-12-15,,Default Group,89254021234222500396,639021232250039,Peter Mbugua,,KDK 728K,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Peter Mbugua/KDK 728K_CAM,862798050524608,JC400P,2022-12-03,2022-12-15,0706742413,,2042-12-04,2042-12-04,,Default Group,,,Peter Mbugua,,KDK 728K,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,JC400P-24368,862798050524368,JC400P,2022-10-29,2022-12-17,,,2042-10-30,2042-10-30,,Default Group,,,Identification,,,,,,,,,,
|
||||
fireside,Fireside Group HQ,Mutuku/FDS-KDC 739F_CAM,862798050524558,JC400P,2022-01-22,2022-01-25,0100858817,,2042-01-23,2042-01-23,,Default Group,,,Mutuku Joseph,,KDC 739F,,FDS,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Cornelius/FDS-KCU 938R_CAM,862798050524897,JC400P,2022-01-22,2022-01-25,0114924404,,2042-01-23,2042-01-23,,Default Group,,,Cornelius Kimutai,,KCU 938R,,FDS,,,,Van,,
|
||||
fireside,Fireside Group HQ,Cassius/OSP-KDB 323M_CAM,862798050522107,JC400P,2022-01-22,2022-01-25,0114149576,,2042-01-23,2042-01-23,,Default Group,,,Cassius Wakiyo,,KDB 323M,,OSP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Richardson/ISP/Coast-KDC 207R _CAM,862798050524657,JC400P,2022-01-22,2022-01-25,0758689195,,2042-01-23,2042-01-23,,Default Group,,,Felix Andole,,KDC 207R,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,George/OSP KDD 684Y-CAM,862798050523386,JC400P,2022-01-22,2022-01-27,0785586834,,2042-01-23,2042-01-23,,Default Group,,,George Ochieng',,KDD 684Y,,OSP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Hamis Pande/ISP-KDD 689Y_CAM,862798050524384,JC400P,2022-01-22,2022-01-27,0701211744,,2042-01-23,2042-01-23,,Default Group,,,Hamisi Pande,,KDD 689Y,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Simon Kamau/ISP-KCE 090R_CAM,862798050525589,JC400P,2022-01-19,2022-01-17,0796276387,,2042-01-20,2042-01-20,,Default Group,,,Simon Kamau,,KCE 090R,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Makori John/PLAN-KDB 585E_CAM,862798050525423,JC400P,2022-01-15,2022-01-17,0701211724,,2042-01-16,2042-01-16,,Default Group,,,Makori John,,KDB 585E,,PLANNING,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Oseko/OSP-KCG 668W_CAM,862798050525951,JC400P,2022-01-15,2022-01-17,0741943212,,2042-01-16,2042-01-16,,Default Group,,,Wright Oseko,,KCG 668W,,OSP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Garage/OSP-KCH 167M_CAM,862798050522859,JC400P,2022-01-15,2022-01-17,0706740252,,2042-01-16,2042-01-16,,Default Group,,,Garage,,KCH 167M,,OSP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Garage/ROLL-KCE 699F_CAM,862798050524707,JC400P,2022-01-15,2022-01-17,0110525751,,2042-01-16,2042-01-16,,Default Group,,,Garage,,KCE 699F,,ROLLOUT,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Dan Watila/ISP-KDE 638J_CAM,862798050522883,JC400P,2022-01-15,2022-01-17,0112615393,,2042-01-16,2042-01-16,,Default Group,,,Dan Watila,,KDE 638J,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ, Samuel Kamau/ROLL-KCA 542Q_CAM,862798050525605,JC400P,2022-01-15,2022-01-17,0110526783,,2042-01-16,2042-01-16,,Default Group,,,John Ondego,,KCA 542Q,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Brian Ngetich/ISP-KDA 717B_CAM,862798050288360,JC400P,2021-11-05,2021-11-08,0717867861,,2041-11-06,2041-11-06,,Default Group,,,Brian Ngetich,,KDA 717B,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Patric Bet/OSP-KDA 609E_CAM,862798050288261,JC400P,2021-10-23,2021-10-25,0790176509,,2041-10-24,2041-10-24,,Default Group,,,Patric Bett,0112693340,KDA 609E,,OSP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Gabriel/ROLL-KCE 690F_Track,359857082042052,GT06E,2020-04-03,2020-04-16,0110094466,,2040-04-04,2040-04-04,,Default Group,89254021164215938024,639021161593802,Gabriel Musumba,,KCE 690F,,OSP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,Allan Owana/ISP-KDK780K_Track,359857081885410,GT06E,2019-06-19,2019-07-01,0703616117,,2039-06-20,2039-06-20,,Default Group,89254021234222499854,639021232249985,Allan Owana,,KDK 780K,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ, Garage/OSP-KCH 167M,359857081891798,GT06E,2019-06-16,2019-07-01,0746760102,,2039-06-17,2039-06-17,,Default Group,89254021084186499493,639021088649949,Garage,,KCH 167M,,OSP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,John Ondego/ISP-KCA 542Q_Track,359857081891632,GT06E,2019-06-15,2019-07-01,0746760038,,2039-06-16,2039-06-16,,Default Group,89254021084186499485,639021088649948,John Ondego,,KCA 542Q,,ISP,,,,Probox,,
|
||||
fireside,Fireside Group HQ,JC400P-08035,862798052708035,JC400P,Inactive,2025-06-11,,,120Month,——,,Default Group,,,Identification,,,,,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Wambua/ROLLOUT-KDV 683Z_Track,865135061563597,X3,2026-01-30,2026-02-24,0758052405,,2036-01-31,2036-01-31,,Default Group,89254021414206816733,639021410681673,Dominic Wambua,,KDV 683Z,,ROLLOUT,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,John Mbugua/OSP-KDW 573B_Track,865135061562722,X3,2026-01-30,2026-02-24,0758052508,,2036-01-31,2036-01-31,,Default Group,89254021414206816832,639021410681683,John Mbugua,,KDW 573B,,OSP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Godffrey Nandwa/ISP-KCN 496A_CAM,862798052708282,JC400P,2026-01-25,2026-02-20,0758047934,,2036-01-26,2036-01-26,,Default Group,89254021414206816865,639021410681686,Godffrey Nandwa,,KCN 496A,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Benjamin/PLAN-KDV 438W_CAM,862798052707888,JC400P,2025-12-15,2026-02-20,0758047312,,2035-12-16,2035-12-16,,Default Group,89254021414206816980,639021410681698,Benjamin Ananda,,KDV 438W,,PLANNING,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Albert/FDS-KDV 437W_CAM,862798052708076,JC400P,2025-12-13,2026-02-20,0758047094,,2035-12-14,2035-12-14,,Default Group,89254021414206816782,639021410681678,Albert Mutwiri,,KDV 437W,,FDS,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Levine/OSP-KDV 439W_Track,865135061562847,X3,2025-12-13,2026-02-24,0758047032,,2035-12-14,2035-12-14,,Default Group,89254021414206816840,639021410681684,Levine Wasike,,KDV 439W,,OSP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,JC400P-14066,862798052714066,JC400P,2025-11-21,2025-06-11,,,2035-11-22,2035-11-22,,Default Group,89254021414206378684,639021410637868,Identification,,,,,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Kennedy Ondieki/ISP-KCU 237Z_CAM,862798052713837,JC400P,2025-10-08,2026-02-20,0113669852,,2035-10-09,2035-10-09,,Default Group,89254021414206327855,639021410632785,Kennedy Ondieki,,KCU 237Z,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,JC400P-13696,862798052713696,JC400P,2025-09-02,2025-06-11,,,2035-09-03,2035-09-03,,Default Group,89254021394215205906,639021391520590,Identification,,,,,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Gitau/Regional-KDT 916R_CAM,862798052713985,JC400P,2025-08-02,2026-02-20,0768696668,,2035-08-03,2035-08-03,,Default Group,89254021394274518892,639021397451889,Timothy Gitau,,KDT 916R,,REGIONAL,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Richardson Komu-KDT 923R_Track,865135061035653,X3,2025-08-02,2026-02-24,0768697292,,2035-08-03,2035-08-03,,Default Group,89254021394274518942,639021397451894,Richardson Komu,,KDT 923R,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Muriithi/Huawei-KDR 594N_Track,865135061048466,X3,2025-07-24,2026-02-24,0797680395,,2035-07-25,2035-07-25,,Default Group,89254021334258159628,639021335815962,Samuel Muriithy,,KDR 594N,,ROLLOUT,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Rofas/General-KDT 728R_Track,865135061054555,X3,2025-07-16,2026-02-24,0790176726,,2035-07-17,2035-07-17,,Default Group,89254021394215205823,639021391520582,Rofas Njagi,,KDT 728R,,REGIONAL,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Mazda-KDU 613A_CAM,862798052713761,JC400P,2025-07-09,2026-02-20,0790176786,,2035-07-10,2035-07-10,,Default Group,89254021394215205955,639021391520595,Management_Mazda,,KDU 613A,,MGT,,,,Mazda,,
|
||||
Fireside@HQ,Fireside Telematics ,James Onyango-KDU 613B_Track,865135061054548,X3,2025-07-09,2026-02-24,0790175997,,2035-07-10,2035-07-10,,Default Group,89254021394215205948,639021391520594,James Onyango,,KDU 613B,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Rashid/ISP-KDM 840V_CAM,862798050526231,JC400P,2023-12-22,2026-02-20,0790175526,,2043-12-23,2043-12-23,,Default Group,,,Rashid Hassan,,KDM 840V,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Mike Wanaswa/FDS-KDT 724R_CAM,862798050523139,JC400P,2023-12-22,2026-02-20,0790175045,,2043-12-23,2043-12-23,,Default Group,,,Mike Wanaswa,,KDT 724R,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Wambugu/FDS-KDR 592N_CAM,862798050523063,JC400P,2023-12-22,2026-02-20,0701211876,,2043-12-22,2043-12-22,,Default Group,,,Kelvin Wambugu,,KDR 594N,,FDS,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Major Simiyu/FDS-KDS949Y_CAM,862798050523626,JC400P,2023-12-22,2026-02-20,0701211892,,2033-12-23,2033-12-23,,Default Group,,,Major Simiyu,,KDS 949Y,,FDS,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics , VICTOR/OSP-KDS919Y_CAM ,862798050523337,JC400P,2023-12-22,2026-02-20,0700242527,,2043-12-22,2043-12-22,,Default Group,,,Victor Kimutai,,KDS 919Y,,OSP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Emmanuel/Gen-KDS 453Y_CAM,862798050523295,JC400P,2023-12-22,2026-02-20,0700242474,,2033-12-23,2033-12-23,,Default Group,,,Emmanuel Luseno,,KDS 453 Y,,GENERAL,,,,Pick-Up,,
|
||||
Fireside@HQ,Fireside Telematics ,Muriithi/Huawei-KDR 594N_CAM,862798050523014,JC400P,2023-12-21,2026-02-20,0790175423,,2033-12-22,2033-12-22,,Default Group,,,Samuel Muriithy,,KDR 594N,,ROLLOUT,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Kimeria-General-KDS 525D_CAM,862798050521521,JC400P,2023-11-26,2026-02-20,0752958416,,2033-11-27,2033-11-27,,Default Group,,,John Kimeria,,KDS 525D,,GENERAL,,,,Crane,,
|
||||
Fireside@HQ,Fireside Telematics ,Leonard/ISP-KDM 306S _CAM,862798050524533,JC400P,2023-08-21,2026-02-20,0703487162,,2033-08-22,2033-08-22,,Default Group,,,Leonard Nzai,,KDM 306S,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Job Ngare/ISP Coast-KDM309S_Track,359857082898016,GT06E,2023-08-15,2026-02-24,0706895756,,2033-08-16,2033-08-16,,Default Group,89254021324273007563,639021327300756,Job Ngare,,KDM 309S,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Dickson Jaoko/OSP-KDK 815R_CAM,862798050525266,JC400P,2023-06-21,2026-02-20,0706665867,,2033-06-22,2033-06-22,,Default Group,,,Dickson Jaoko,,KDK 815R,,OSP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Alan Owana/ISP-KDK 780K_CAM,862798050523527,JC400P,2022-12-03,2026-02-20,0792375024,,2042-12-04,2042-12-04,,Default Group,,,Allan Owana,,KDK 780K,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Amani Sulubu/ISP-KCY 090X_CAM,862798050524426,JC400P,2022-01-16,2026-02-20,0113823350,,2042-01-17,2042-01-17,,Default Group,,,Amani Sulubu,,KCY 090X,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Gideon/ISP-KCQ 215F_CAM,862798050522065,JC400P,2022-01-16,2026-02-20,0113343715,,2042-01-17,2042-01-17,,Default Group,,,Gideon Kiprono,,KCQ 215F,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Gabriel/OSP-KCE 690F_CAM,862798050525670,JC400P,2022-01-15,2026-02-20,0701211996,,2042-01-16,2042-01-16,,Default Group,,,Gabriel Musumba,,KCE 690F,,OSP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Santoes/OSP-KCZ 181P_CAM D-Max,862798050288345,JC400P,2021-11-06,2026-02-20,0768446105,,2041-11-07,2041-11-07,,Default Group,,,Santoes Omondi,,KCZ 181P,,OSP,,,,Pick-Up,,
|
||||
Fireside@HQ,Fireside Telematics ,Elias Baya/FDS-KCZ 476E_CAM,862798050288303,JC400P,2021-11-06,2026-02-20,0115870439,,2041-11-07,2041-11-07,,Default Group,,,Elias Baya,,KCZ 476E,,FDS,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Nicholas Erastus /ISP-KCQ 581M_CAM,862798050288212,JC400P,2021-11-02,2026-02-20,0746979531,,2041-11-03,2041-11-03,,Default Group,,,Nicholas Erastus,,KCQ 581M,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Samuel Ng'ang'a/ISP-KDE 264M_Track,359857082898008,GT06E,2021-10-28,2026-02-24,0711731539,,2041-10-29,2041-10-29,,Default Group,89254021264260342245,639021266034224,Samuel Ng'ang'a,,KDE 264M,,ISP ,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Dan Watila/ISP-KDE 638J,359857082898487,GT06E,2021-10-21,2026-02-24,0116242996,,2041-10-22,2041-10-22,,Default Group,89254021334258404214,639021335840421,Dan Watila,,KDE 638J,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Geoffrey Too/ISP-KDM 308S,359857082900358,GT06E,2021-10-21,2026-02-24,0796527601,,2041-10-22,2041-10-22,,Default Group,89254021264260126572,639021266012657,Geoffrey Too,,KDM 308S,,OSP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Hamisi/ISP-KDD 689Y,359857082896911,GT06E,2021-09-17,2026-02-24,0112714612,,2041-09-18,2041-09-18,,Default Group,89254021214211314660,639021211131466,Hamisi Pande,,KDD 689Y,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,George/OSP-KDD 684Y_Track,359857082900697,GT06E,2021-09-17,2026-02-24,0114879518,,2041-09-18,2041-09-18,,Default Group,89254021214211314678,639021211131467,George Ochieng',,KDD 684Y,,OSP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Cassius/OSP-KDB 323M_Track,359857082897257,GT06E,2021-08-29,2026-02-24,0746428882,,2041-08-29,2041-08-29,,Default Group,89254021234222500818,639021232250081,Cassius Wakiyo,,KDB 323M,,OSP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,John Makori/PLAN-KDB 585E,359857082897737,GT06E,2021-08-29,2026-02-24,0114596734,,2041-08-29,2041-08-29,,Default Group,89254021214211145262,639021211114526,John Makori,,KDB 585E,,PLANNING,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Kelvin Gichea/ISP-KDA 717B,359857082911983,GT06E,2021-08-29,2026-02-24,0795188807,,2041-08-29,2041-08-29,,Default Group,89254021214211145288,639021211114528,Brian Ngetich,0795188807,KDA 717B,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Sadique/GEN-KDC 490Q_Track,359857082902461,GT06E,2021-05-22,2026-02-24,0757556468,,2041-05-22,2041-05-22,,Default Group,89254021154296722488,639021159672248,Sadique Wakayula,,KDC 490Q,,GENERAL,,,,Crane,,
|
||||
Fireside@HQ,Fireside Telematics ,Andrew Makanda/ISP/Coast-KDC 207R ,359857082902503,GT06E,2021-05-15,2026-02-24,0794820817,,2041-05-15,2041-05-15,,Default Group,89254021224270993254,639021227099325,Felix Andole,,KDC 207R,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Mutuku Joseph/FDS-KDC 739F ,359857082897794,GT06E,2021-04-10,2026-02-24,0115019037,,2041-04-10,2041-04-10,,Default Group,89254021224222632356,639021222263235,Mutuku Joseph,0115019037,KDC 739F,,FDS,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics , Patric Bet/OSP-KDA 609E_Track,359857082910589,GT06E,2020-10-26,2026-02-24,0797622637,,2040-10-27,2040-10-27,,Default Group,89254021154296722496,639021159672249,Patric Bett,,KDA 609E,,OSP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Charles Nyambane/ISP-KCB 711C_Track,359857082918012,GT06E,2020-09-21,2026-02-24,0793704231,,2040-09-22,2040-09-22,,Default Group,89254021154287138363,639021158713836,Charles Nyambane,,KCB 711C,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Oseko Wright/OSP-KCG 668W_Track,359857081887069,GT06E,2019-06-30,2026-02-24,0746763106,,2039-07-01,2039-07-01,,Default Group,89254021084186499915,639021088649991,Wright Oseko,,KCG 668W,,OSP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,KCE 699F,359857081891590,GT06E,2019-06-16,2026-02-24,0746760215,,2039-06-17,2039-06-17,,Default Group,89254021084186499519,639021088649951,Garage,,KCE 699F,,ROLLOUT,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Simon Kamau/ISP-KCE 090R,359857081891566,GT06E,2019-06-16,2026-02-24,0746760404,,2039-06-17,2039-06-17,,Default Group,89254021084186499527,639021088649952,Simon Kamau,,KCE 090R,,ISP,,,,Probox,,
|
||||
Fireside@HQ,Fireside Telematics ,Cornelius/FDS-KCU 938R VAN,359857081892101,GT06E,2019-06-12,2026-02-24,0746759919,,2039-06-13,2039-06-13,,Default Group,89254021084186499451,639021088649945,Cornelius Kimutai,,KCU 938R,,FDS,,,,Van,,2019-06-12
|
||||
Fireside@HQ,Fireside Telematics ,Nicholas Erastus/ISP-KCQ581M,359857081892309,GT06E,2019-06-09,2026-02-24,0700023776,,2039-06-10,2039-06-10,,Default Group,89254021084178504672,639021087850467,Nicholas Erastus,,KCQ 581M,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Barack_Personal-KDW 781E,865135061563415,X3,2026-01-13,2025-09-08,0758052541,,2036-01-14,2036-01-14,,Default Group,89254021414206816931,639021410681693,Barack Orwa,,KDW 781E,,MGT,,,,Vazel,,
|
||||
Fireside_MSA,Fireside Group MSA,Major Simiyu-KDS 949Y_Track,865135061035133,X3,2025-08-02,2025-06-11,0768696642,,2035-08-03,2035-08-03,,Default Group,89254021394274518918,639021397451891,Major Simiyu,,KDS 949Y,,FDS,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Harisson/KDT 724R_Track,865135061043079,X3,2025-08-02,2025-06-11,0768696664,,2035-08-03,2035-08-03,,Default Group,89254021394274518959,639021397451895,Mike Wanaswa,,KDT 724R,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Gitau/Regional-KDT 916R_Track,865135061048953,X3,2025-08-02,2025-06-11,0768697056,,2035-08-03,2035-08-03,,Default Group,89254021394274518967,639021397451896,Timothy Gitau,,KDT 916R,,REGIONAL,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Victor/OSP-KDS 919Y_Track,865135061048276,X3,2025-08-02,2025-06-11,0768696755,,2035-08-03,2035-08-03,,Default Group,89254021394274518900,639021397451890,Victor Kimutai,,KDS 919Y,,OSP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Ian Dancan-KDT 923R_CAM,862798050526256,JC400P,2023-12-22,,0794873610,,2043-12-22,2043-12-22,,Default Group,,,Ian Dancun,,KDT 923R,,QEHS,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Wilfred/Gen-KCU 729C_CAM,862798050526165,JC400P,2023-11-26,2024-11-08,0790564929,,2033-11-27,2033-11-27,,Default Group,,,Wilfred Kinyanjui,,KCU 729C,,GENERAL,,,,Crane,,
|
||||
Fireside_MSA,Fireside Group MSA,Denis Kazungu/KDM 794R_Track,359857082916826,GT06E,2023-08-21,2023-08-22,0705700971,,2033-08-22,2033-08-22,,Default Group,89254021324273006854,639021327300685,Denis Kazungu,,KDM 794R,,FDS,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Mutuku Anthony-KDK 732K_Track,359857082898073,GT06E,2022-12-20,2022-12-20,0793026954,,2042-12-21,2042-12-21,,Default Group,89254021234222387539,639021232238753,Mutuku Antony,,KDK 732K,,FDS,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Anthon/KDK 732K_CAM,862798050524681,JC400P,2022-12-06,2022-12-16,0796275746,,2042-12-07,2042-12-07,,Default Group,,,Mutuku Antony,,KDK 732K,,FDS,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Makanda-KCZ 155P_CAM,862798050524566,JC400P,2022-01-22,2025-02-24,0758781444,,2042-01-23,2042-01-23,,Default Group,,,Makanda Andrew,,KCZ 155P,,OSP,,,,Pick-Up,,
|
||||
Fireside_MSA,Fireside Group MSA,Dennis Kazungu/-KDM 794R_CAM,862798050521612,JC400P,2022-01-22,2024-11-19,0704113731,,2042-01-23,2042-01-23,,Default Group,,,Denis Kazungu,,KDM 794R,,FDS,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Mbuvi Kioko/OSP-KCZ 199P_CAM,862798050522719,JC400P,2022-01-16,2022-12-16,0768218655,,2042-01-17,2042-01-17,,Default Group,,,Mbuvi Kioko,,KCZ 199P,,OSP,,,,Pick-Up,,
|
||||
Fireside_MSA,Fireside Group MSA,Felix Muema-KCZ 223P_CAM D-Max,862798050524087,JC400P,2022-01-16,2024-12-30,0113973875,,2042-01-17,2042-01-17,,Default Group,,,Felix Muema,,KCZ 223P,,OSP,,,,Pick-Up,,
|
||||
Fireside_MSA,Fireside Group MSA,Lawrence Kijogi/ROLL-KCY 080X_CAM,862798050522891,JC400P,2022-01-16,2022-12-16,0113287191,,2042-01-17,2042-01-17,,Default Group,,,Lawrence Kijogi,,KCY 080X,,ROLLOUT,,,,Pick-Up,,
|
||||
Fireside_MSA,Fireside Group MSA,Ndegwa Duncan/PM-KCG 669W_CAM,862798050524392,JC400P,2022-01-16,2022-12-16, 0113799173,,2042-01-17,2042-01-17,,Default Group,,,Ndegwa Dancun,,KCG 669W,,OSP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Simon Munda-KCZ 154S_CAM,862798050521752,JC400P,2022-01-16,2022-12-16,0113805921,,2042-01-17,2042-01-17,,Default Group,,,Simon Munda,,KCZ 154S,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Moses Wambua-KCZ 751V_CAM,862798050524012,JC400P,2022-01-16,2022-12-16,0113313797,,2042-01-17,2042-01-17,,Default Group,,,Moses Wambua,,KCZ 751V,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Amani Kazungu-KCY 084X_CAM,862798050523204,JC400P,2022-01-16,2022-12-16,0707892547,,2042-01-17,2042-01-17,,Default Group,,,Amani Kazungu,,KCY 084X,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Joseph Kabandi-KCY 076X_CAM,862798050523949,JC400P,2022-01-16,2022-12-16, 0113288492,,2042-01-17,2042-01-17,,Default Group,,,Joseph Kabandi,,KCY 076X,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Kennedy Chege-KCQ 618K_CAM,862798050525613,JC400P,2022-01-16,2022-12-19,0729994247,,2042-01-17,2042-01-17,,Default Group,,,Kennedy Chege,,KCQ 618K,,OSP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Noel/FDS/VOI-KCY 838X_CAM,862798050525753,JC400P,2022-01-15,2023-08-23,,,2042-01-16,2042-01-16,,Default Group,,,Noel Merengeni,,KCY 838X,,FDS,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Noel/VOI-KCY 838X_Track,359857082925330,GT06E,2020-10-26,2023-08-22,0794873610,,2040-10-27,2040-10-27,,Default Group,89254021154296723429,639021159672342,Noel Merengeni,,KCY 838X,,FDS,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Simon Munda-KCZ 154S_Track,359857082900341,GT06E,2020-09-23,2022-12-16,0757236135,,2040-09-24,2040-09-24,,Default Group,89254021154296723312,639021159672331,Simon Munda,,KCZ 154S,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA, Michael Odongo-KCZ 751V,359857082912486,GT06E,2020-09-23,2022-12-16,0792756503,,2040-09-24,2040-09-24,,Default Group,89254021154296723437,639021159672343,Moses Wambua,,KCZ 751V,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Daniel Omondi/Rider_KMFF 099Z,353549090553685,AT4,2020-09-23,2022-12-16,0759336150,,2040-09-24,2040-09-24,,Default Group,89254021334258404099,639021335840409,Daniel Omondi,0112794067,KMFF 099Z,,OSP-PATROL,,,,Motorbike,,
|
||||
Fireside_MSA,Fireside Group MSA,Daniel Kipkirui/Rider-KMFF 162Z,353549090567685,AT4,2020-09-23,2022-12-16,0742532058,,2040-09-24,2040-09-24,,Default Group,89254021264260388966,639021266038896,Daniel Kipkirui,0112795498,KMFF 162Z,,OSP-PATROL,,,,Motorbike,,
|
||||
Fireside_MSA,Fireside Group MSA,Makanda/OSP-KCZ155P D-Max,359857082910886,GT06E,2020-08-23,2025-02-24,0745067338,,2040-08-24,2040-08-24,,Default Group,89254021154287138397,639021158713839,Makanda Andrew,,KCZ 155P,,OSP,,,,Pick-Up,,
|
||||
Fireside_MSA,Fireside Group MSA,Santos/OSP-KCZ 181P D-Max,359857082908500,GT06E,2020-08-23,2022-12-16,0701211974,,2040-08-24,2040-08-24,,Default Group,89254021374215155087,639021371515508,Santoes Omondi,,KCZ 181P,,OSP,,,,Pick-Up,,
|
||||
Fireside_MSA,Fireside Group MSA,Mbuvi Kioko-KCZ 199P D-Max,359857082918038,GT06E,2020-08-22,2022-12-16,0797318126,,2040-08-23,2040-08-23,,Default Group,89254021154287138389,639021158713838,Mbuvi Kioko,,KCC 199P,,OSP,,,,Pick-Up,,
|
||||
Fireside_MSA,Fireside Group MSA,Felix Muema-KCZ 223P D-Max,359857082907973,GT06E,2020-08-22,2024-12-30,0757843826,,2040-08-23,2040-08-23,,Default Group,89254021154287138371,639021158713837,Felix Muema,,KCZ 223P,,OSP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Elias KCZ 476E,359857082042854,GT06E,2020-08-09,2022-12-16,0110941187,,2040-08-10,2040-08-10,,Default Group,89254021164224352993,639021162435299,Elias Baya,,KCZ 476E,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Lawrence Kijogi/ROLL-KCY 080X,359857082044280,GT06E,2020-07-13,2022-12-16,0708155933,,2040-07-13,2040-07-13,,Default Group,89254029851005131222,639029850513122,Lawrence Kijogi,,KCY 080X,,ROLLOUT,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Amani Kazungu/ISP-KCY 084X,359857082037185,GT06E,2020-07-13,2022-12-16,0757338522,,2040-07-14,2040-07-14,,Default Group,89254021154287000597,639021158700059,Amani Kazungu,,KCY 084X,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Joseph kabandi-KCY 076X,359857082046145,GT06E,2020-07-13,2022-12-16,0110850007,,2040-07-14,2040-07-14,,Default Group,89254021164223447158,639021162344715,Joseph Kabandi,,KCY 076X,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Rashid Musa-KCY 090X,359857082040981,GT06E,2020-07-13,2022-12-16,0793375853,,2040-07-14,2040-07-14,,Default Group,89254021064168004164,639021066800416,Amani Sulubu,,KCY 090X,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Wilfred/Gen-KCU 729C_Track,359857082038977,GT06E,2020-04-05,2022-12-16,0110094469,,2040-04-06,2040-04-06,,Default Group,89254021164215938057,639021161593805,Wilfred Kinyanjui,,KCU 729C,,GENERAL,,,,Crane,,
|
||||
Fireside_MSA,Fireside Group MSA,Amani Kazungu/ISP-KCQ 215F_Track,359857081886467,GT06E,2019-06-30,2022-12-16,0746763076,,2039-07-01,2039-07-01,,Default Group,89254021084186499865,639021088649986,Gideon Kiprono,,KCQ 215F,,ISP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA, Kennedy Chege/OSP-KCQ 618K,359857081886905,GT06E,2019-06-30,2022-12-16,0746763132,,2039-07-01,2039-07-01,,Default Group,89254021084186499923,639021088649992,Kennedy Chege,,KCQ 618K,,OSP,,,,Probox,,
|
||||
Fireside_MSA,Fireside Group MSA,Ndegwa Duncan/PM-KCG 669W_Track,359857081887192,GT06E,2019-06-15,2022-12-16,0746760191,,2039-06-16,2039-06-16,,Default Group,89254021084186499501,639021088649950,Ndegwa Dancun,,KCG 669W,,OSP,,,,Probox,,
|
||||
|
232
import_drivers_csv.py
Normal file
232
import_drivers_csv.py
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
"""
|
||||
import_drivers_csv.py — Fireside Communications · Driver & Vehicle CSV Import
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
One-shot script: reads 20260414_FS__Logistics - final_fixed.csv, compares
|
||||
each row against the current tracksolid.devices values, and updates the DB.
|
||||
|
||||
Usage:
|
||||
# Dry-run — shows diff, writes nothing
|
||||
python import_drivers_csv.py
|
||||
|
||||
# Filter to a single IMEI (dry-run)
|
||||
python import_drivers_csv.py --imei 862798052707896
|
||||
|
||||
# Apply all changes to DB
|
||||
python import_drivers_csv.py --apply
|
||||
|
||||
# Only fill fields that are currently NULL in the DB (never overwrite)
|
||||
python import_drivers_csv.py --only-null --apply
|
||||
|
||||
Pre-requisite:
|
||||
Migration 06 must be applied first (adds assigned_city / cost_centre columns).
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from ts_shared_rev import clean, clean_num, clean_ts, get_conn, get_logger
|
||||
|
||||
log = get_logger("csv_import")
|
||||
|
||||
CSV_PATH = Path(__file__).parent / "20260414_FS__Logistics - final_fixed.csv"
|
||||
|
||||
# Columns fetched from DB for comparison
|
||||
DB_COLS = [
|
||||
"imei", "driver_name", "driver_phone", "vehicle_number", "vehicle_name",
|
||||
"vehicle_models", "cost_centre", "sim", "iccid", "imsi", "mc_type",
|
||||
"activation_time", "expiration", "device_name", "assigned_city",
|
||||
]
|
||||
|
||||
# Driver Name values that are placeholders — skip writing driver_name for these
|
||||
_DRIVER_SKIP = {"identification", "ug"}
|
||||
|
||||
|
||||
def _infer_city(plate: str) -> str | None:
|
||||
"""Derive assigned_city from license plate prefix."""
|
||||
p = (plate or "").strip().upper()
|
||||
if p.startswith("UMA") or p.startswith("UAG"):
|
||||
return "KLA"
|
||||
if p.startswith("K"):
|
||||
return "NBO"
|
||||
return None
|
||||
|
||||
|
||||
def _clean_date(v: str) -> str | None:
|
||||
"""Accept YYYY-MM-DD and return as ISO string suitable for TIMESTAMPTZ cast."""
|
||||
s = (v or "").strip()
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
date.fromisoformat(s)
|
||||
return s
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def load_csv() -> dict[str, dict]:
|
||||
"""Load CSV into a dict keyed by IMEI."""
|
||||
rows: dict[str, dict] = {}
|
||||
with open(CSV_PATH, encoding="utf-8-sig", newline="") as f:
|
||||
for row in csv.DictReader(f):
|
||||
imei = (row.get("IMEI") or "").strip()
|
||||
if not imei:
|
||||
continue
|
||||
rows[imei] = row
|
||||
log.info("CSV loaded: %d rows from %s", len(rows), CSV_PATH.name)
|
||||
return rows
|
||||
|
||||
|
||||
def load_db_devices() -> dict[str, dict]:
|
||||
"""Fetch current device rows from DB, keyed by IMEI."""
|
||||
devices: dict[str, dict] = {}
|
||||
with get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(f"SELECT {', '.join(DB_COLS)} FROM tracksolid.devices")
|
||||
col_names = [d[0] for d in cur.description]
|
||||
for row in cur.fetchall():
|
||||
rec = dict(zip(col_names, row))
|
||||
devices[rec["imei"]] = rec
|
||||
log.info("DB loaded: %d devices", len(devices))
|
||||
return devices
|
||||
|
||||
|
||||
def build_update(csv_row: dict, db_row: dict | None, only_null: bool) -> dict[str, object]:
|
||||
"""
|
||||
Return a dict of column→new_value for fields that need updating.
|
||||
When only_null=True, skip any DB column that already has a value.
|
||||
The driver_name column is skipped for placeholder-labelled devices.
|
||||
"""
|
||||
driver_raw = clean(csv_row.get("Driver Name")) or ""
|
||||
plate = clean(csv_row.get("License Plate No.")) or ""
|
||||
is_placeholder = driver_raw.lower() in _DRIVER_SKIP
|
||||
skip_row = driver_raw.lower() == "identification"
|
||||
|
||||
if skip_row:
|
||||
return {}
|
||||
|
||||
proposed: dict[str, object] = {
|
||||
"vehicle_number": clean(plate),
|
||||
"vehicle_name": clean(plate),
|
||||
"vehicle_models": clean(csv_row.get("Vehicle Model")),
|
||||
"cost_centre": clean(csv_row.get("Department")),
|
||||
"sim": clean(csv_row.get("SIM")),
|
||||
"iccid": clean(csv_row.get("ICCID")),
|
||||
"imsi": clean(csv_row.get("IMSI")),
|
||||
"mc_type": clean(csv_row.get("Model")),
|
||||
"activation_time": _clean_date(csv_row.get("Activated Date", "")),
|
||||
"expiration": _clean_date(csv_row.get("Subscription Expiration", "")),
|
||||
"driver_phone": clean(csv_row.get("Telephone")),
|
||||
"assigned_city": _infer_city(plate),
|
||||
}
|
||||
if not is_placeholder:
|
||||
proposed["driver_name"] = driver_raw or None
|
||||
|
||||
# Drop None values — no point sending a NULL to overwrite another NULL
|
||||
proposed = {k: v for k, v in proposed.items() if v is not None}
|
||||
|
||||
if not only_null or db_row is None:
|
||||
return proposed
|
||||
|
||||
# only_null: drop any column that already has a non-null value in the DB
|
||||
return {
|
||||
k: v for k, v in proposed.items()
|
||||
if db_row.get(k) is None
|
||||
}
|
||||
|
||||
|
||||
def print_diff(imei: str, updates: dict[str, object], db_row: dict | None) -> None:
|
||||
"""Pretty-print what will change for one device."""
|
||||
if not updates:
|
||||
return
|
||||
db = db_row or {}
|
||||
print(f"\n IMEI {imei}:")
|
||||
for col, new_val in sorted(updates.items()):
|
||||
old_val = db.get(col)
|
||||
if old_val != new_val:
|
||||
print(f" {col:<20} {str(old_val):<30} → {new_val}")
|
||||
|
||||
|
||||
def run(apply: bool, only_null: bool, filter_imei: str | None) -> None:
|
||||
csv_rows = load_csv()
|
||||
db_rows = load_db_devices()
|
||||
|
||||
if filter_imei:
|
||||
csv_rows = {k: v for k, v in csv_rows.items() if k == filter_imei}
|
||||
if not csv_rows:
|
||||
print(f"IMEI {filter_imei} not found in CSV.")
|
||||
return
|
||||
|
||||
updated = skipped = no_change = not_in_db = 0
|
||||
|
||||
with get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
for imei, csv_row in csv_rows.items():
|
||||
db_row = db_rows.get(imei)
|
||||
|
||||
updates = build_update(csv_row, db_row, only_null)
|
||||
|
||||
if not updates:
|
||||
# Either an "Identification" placeholder or nothing to change
|
||||
driver_raw = (csv_row.get("Driver Name") or "").strip().lower()
|
||||
if driver_raw == "identification":
|
||||
skipped += 1
|
||||
else:
|
||||
no_change += 1
|
||||
continue
|
||||
|
||||
if db_row is None:
|
||||
not_in_db += 1
|
||||
log.warning("IMEI %s in CSV but NOT in DB — skipping.", imei)
|
||||
continue
|
||||
|
||||
print_diff(imei, updates, db_row)
|
||||
|
||||
if apply:
|
||||
set_clauses = []
|
||||
params = []
|
||||
for col, val in updates.items():
|
||||
if col in ("activation_time", "expiration"):
|
||||
set_clauses.append(f"{col} = COALESCE(%s::TIMESTAMPTZ, {col})")
|
||||
else:
|
||||
set_clauses.append(
|
||||
f"{col} = COALESCE(NULLIF(%s, ''), {col})"
|
||||
)
|
||||
params.append(str(val) if val is not None else None)
|
||||
|
||||
set_clauses.append("updated_at = NOW()")
|
||||
params.append(imei)
|
||||
|
||||
cur.execute(
|
||||
f"UPDATE tracksolid.devices SET {', '.join(set_clauses)} WHERE imei = %s",
|
||||
params,
|
||||
)
|
||||
updated += 1
|
||||
else:
|
||||
updated += 1 # count as "would update" in dry-run
|
||||
|
||||
mode = "APPLIED" if apply else "DRY-RUN"
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {mode} COMPLETE")
|
||||
print(f"{'='*60}")
|
||||
print(f" Would update / updated : {updated}")
|
||||
print(f" No change needed : {no_change}")
|
||||
print(f" Skipped (Identification): {skipped}")
|
||||
print(f" IMEI not in DB : {not_in_db}")
|
||||
if not apply:
|
||||
print("\n Run with --apply to commit changes.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Import driver/vehicle details from CSV into tracksolid.devices")
|
||||
parser.add_argument("--apply", action="store_true", help="Write changes to DB (default: dry-run)")
|
||||
parser.add_argument("--only-null", action="store_true", help="Only update fields currently NULL in the DB")
|
||||
parser.add_argument("--imei", default=None, help="Limit to a single IMEI")
|
||||
args = parser.parse_args()
|
||||
|
||||
run(apply=args.apply, only_null=args.only_null, filter_imei=args.imei)
|
||||
Loading…
Reference in a new issue