Compare commits
No commits in common. "80c0e6510f56bc7c77a19558b45222093409b4c7" and "85cb408deaa8601331ef96b7a2e67cc9b43c21be" have entirely different histories.
80c0e6510f
...
85cb408dea
32 changed files with 14 additions and 12005 deletions
|
|
@ -1 +0,0 @@
|
||||||
{"sessionId":"c108db62-4596-4e11-a4d2-c706487747bf","pid":15863,"acquiredAt":1776761841807}
|
|
||||||
|
|
@ -432,27 +432,6 @@ WHERE d.device_name = 'FRED KMGW 538W HULETI'
|
||||||
ORDER BY ph.gps_time ASC;
|
ORDER BY ph.gps_time ASC;
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- distinct routes per vehicle -->
|
|
||||||
``` sql
|
|
||||||
SELECT
|
|
||||||
ph.gps_time AT TIME ZONE 'Africa/Nairobi' AS gps_time_eat,
|
|
||||||
ROUND(ph.lat::numeric, 5) AS lat,
|
|
||||||
ROUND(ph.lng::numeric, 5) AS lng,
|
|
||||||
ph.speed,
|
|
||||||
ph.direction,
|
|
||||||
ph.acc_status,
|
|
||||||
ph.source
|
|
||||||
FROM tracksolid.position_history ph
|
|
||||||
JOIN tracksolid.devices d ON d.imei = ph.imei
|
|
||||||
WHERE d.device_name = 'FRED KMGW 538W HULETI'
|
|
||||||
AND ph.gps_time > now() - interval '24 hours'
|
|
||||||
ORDER BY ph.gps_time ASC;
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Fix density per device — last 24 hours
|
### Fix density per device — last 24 hours
|
||||||
```sql
|
```sql
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"path": "."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../60_karuracc-test-app"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../62_ts_postman"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {}
|
|
||||||
}
|
|
||||||
31
CLAUDE.md
31
CLAUDE.md
|
|
@ -58,7 +58,6 @@ See `docs/CONNECTIONS.md` for the full shape. Summary:
|
||||||
- **DB name:** `tracksolid_db` · **DB user:** `postgres` (internal) · `tracksolid_owner` (app) · `grafana_ro` (read-only)
|
- **DB name:** `tracksolid_db` · **DB user:** `postgres` (internal) · `tracksolid_owner` (app) · `grafana_ro` (read-only)
|
||||||
- **DB schemas:** `tracksolid` (live, single source of truth) · `dwh_gold` (aggregates) · `ops` (workshop / tickets / odometer) · `infrastructure`. The legacy `tracksolid_2` schema no longer exists — migrations 02–06 applied 2026-04-18.
|
- **DB schemas:** `tracksolid` (live, single source of truth) · `dwh_gold` (aggregates) · `ops` (workshop / tickets / odometer) · `infrastructure`. The legacy `tracksolid_2` schema no longer exists — migrations 02–06 applied 2026-04-18.
|
||||||
- **DB access:** `DATABASE_URL` points to `timescale_db:5432` (internal Docker network — not reachable locally). Use `docker exec` pattern above. See `docs/CONNECTIONS.md` for full reference.
|
- **DB access:** `DATABASE_URL` points to `timescale_db:5432` (internal Docker network — not reachable locally). Use `docker exec` pattern above. See `docs/CONNECTIONS.md` for full reference.
|
||||||
- **DWH target DB:** `tracksolid_dwh` at `31.97.44.246:5888` (separate PostGIS instance, public IP). Users: `dwh_owner` (bronze writes + `dwh_control`), `grafana_ro` (reads bronze/silver/gold/`dwh_control`). Always connect with `sslmode=require`. Fed by the n8n `dwh_extract` + `dwh_load_bronze` workflows — see `docs/DWH_PIPELINE.md`.
|
|
||||||
- **Container naming:** Coolify appends a random suffix. Always resolve with:
|
- **Container naming:** Coolify appends a random suffix. Always resolve with:
|
||||||
```bash
|
```bash
|
||||||
docker ps --filter name=<service_name> --format "{{.Names}}" | head -1
|
docker ps --filter name=<service_name> --format "{{.Names}}" | head -1
|
||||||
|
|
@ -81,16 +80,9 @@ run_migrations.py # Applies SQL migrations in order at container start
|
||||||
docker-compose.yaml # Services: timescale_db, ingest_movement, ingest_events,
|
docker-compose.yaml # Services: timescale_db, ingest_movement, ingest_events,
|
||||||
# webhook_receiver, grafana
|
# webhook_receiver, grafana
|
||||||
grafana/ # Grafana provisioning (baked into image)
|
grafana/ # Grafana provisioning (baked into image)
|
||||||
n8n-workflows/ # n8n workflow exports (incl. dwh_extract, dwh_load_bronze)
|
n8n-workflows/ # n8n workflow exports
|
||||||
docs/ # Reference docs (connections, API, KPIs, project context)
|
docs/ # Reference docs (connections, API, KPIs, project context)
|
||||||
docs/DWH_PIPELINE.md # DWH pipeline operations runbook (setup, troubleshooting)
|
|
||||||
docs/superpowers/ # Pitch specs and implementation plans (not deployed code)
|
docs/superpowers/ # Pitch specs and implementation plans (not deployed code)
|
||||||
dwh/ # DWH migrations for tracksolid_dwh@31.97.44.246:5888
|
|
||||||
# 260423_dwh_ddl_v1.sql — bronze/silver/gold schemas + roles
|
|
||||||
# 261001_dwh_control.sql — watermarks + run log
|
|
||||||
# 261002_bronze_constraints_audit.sql — ON CONFLICT key assertion
|
|
||||||
# 261003_dwh_roles.sql — role contract assertion
|
|
||||||
# 261004_dwh_observability_views.sql — freshness/failure views
|
|
||||||
02_tracksolid_full_schema_rev.sql # Full schema bootstrap
|
02_tracksolid_full_schema_rev.sql # Full schema bootstrap
|
||||||
03..06_*.sql # Incremental migrations (06 adds assigned_city, dispatch_log, ops.*)
|
03..06_*.sql # Incremental migrations (06 adds assigned_city, dispatch_log, ops.*)
|
||||||
07_analytics_views.sql # Analytics views migration (applied 2026-04-21)
|
07_analytics_views.sql # Analytics views migration (applied 2026-04-21)
|
||||||
|
|
@ -148,25 +140,6 @@ tracksolid.v_sla_inflight -- §4.5 SLA panels (gated on ops.tickets)
|
||||||
|
|
||||||
All views carry a `COMMENT ON VIEW` referencing their spec — `\d+ tracksolid.v_*` shows the provenance.
|
All views carry a `COMMENT ON VIEW` referencing their spec — `\d+ tracksolid.v_*` shows the provenance.
|
||||||
|
|
||||||
**DWH bronze layer (separate DB `tracksolid_dwh`)** — populated by the n8n `dwh_extract` + `dwh_load_bronze` workflows. Operational details in `docs/DWH_PIPELINE.md`.
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- bronze schema mirrors tracksolid.* (16 tables, DDL in dwh/260423_dwh_ddl_v1.sql)
|
|
||||||
bronze.devices, bronze.live_positions -- snapshot tables (TRUNCATE + reload)
|
|
||||||
bronze.position_history, bronze.trips,
|
|
||||||
bronze.alarms, bronze.parking_events,
|
|
||||||
bronze.device_events, bronze.ingestion_log -- incremental (watermark + ON CONFLICT DO NOTHING)
|
|
||||||
-- Schema drift: bronze.trips.distance_km vs source tracksolid.trips.distance_m
|
|
||||||
-- Extract SQL divides by 1000. Cross-ref FIX-M16.
|
|
||||||
|
|
||||||
-- dwh_control schema tracks pipeline state + observability
|
|
||||||
dwh_control.extract_watermarks -- one row per incremental table
|
|
||||||
dwh_control.extract_runs -- per-run audit log (status lifecycle)
|
|
||||||
dwh_control.v_table_freshness -- Grafana: load lag per table
|
|
||||||
dwh_control.v_recent_failures -- Grafana: failures in last 24h
|
|
||||||
dwh_control.v_watermark_lag -- Grafana: extract vs. load lag per table
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. API Critical Facts
|
## 6. API Critical Facts
|
||||||
|
|
@ -251,5 +224,3 @@ Latest full snapshot: `260412_baseline_report.md`
|
||||||
| MEDIUM | Co-develop client KPI framework (see `docs/KPI_FRAMEWORK.md`) |
|
| MEDIUM | Co-develop client KPI framework (see `docs/KPI_FRAMEWORK.md`) |
|
||||||
| LOW | Populate geofences — depot boundaries, city zones |
|
| LOW | Populate geofences — depot boundaries, city zones |
|
||||||
| LOW | Schedule nightly ETL: `SELECT dwh_gold.refresh_daily_metrics(CURRENT_DATE - 1)` (cron or n8n) |
|
| LOW | Schedule nightly ETL: `SELECT dwh_gold.refresh_daily_metrics(CURRENT_DATE - 1)` (cron or n8n) |
|
||||||
| HIGH | Deploy DWH bronze pipeline: apply `dwh/26100{1,2,3,4}.sql` to `tracksolid_dwh`, import + wire the two n8n workflows, verify first run via `dwh_control.v_table_freshness`. Runbook: `docs/DWH_PIPELINE.md` |
|
|
||||||
| MEDIUM | Rotate `dwh_owner` / `grafana_ro` passwords on `tracksolid_dwh` — plaintext in `dwh/260423_dwh_ddl_v1.sql` is a pre-existing flaw to clean up separately |
|
|
||||||
|
|
|
||||||
|
|
@ -1,385 +0,0 @@
|
||||||
# DWH Execution Manual
|
|
||||||
|
|
||||||
> **Purpose:** A reusable playbook for building an `extract → blob → load` data warehouse bronze layer using n8n (or any equivalent orchestrator) + object storage + PostgreSQL/PostGIS. Generalised from the Fireside Tracksolid DWH pipeline (2026-04-24). Apply this pattern to future data projects to skip re-deriving the same decisions.
|
|
||||||
>
|
|
||||||
> **Reference implementation:** `dwh/26100{1..4}.sql` + `docs/DWH_PIPELINE.md` + the two `n8n-workflows/dwh_*.json` files. Treat those as the copy-paste template for the next project.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. When to Use This Pattern
|
|
||||||
|
|
||||||
**Use it when all of these are true:**
|
|
||||||
- You need an analytical replica of a production OLTP DB without letting analytical load hit the OLTP.
|
|
||||||
- Source and target are separate PostgreSQL instances (possibly separate networks).
|
|
||||||
- Data volumes are moderate: millions of rows per day, not billions.
|
|
||||||
- You already have an orchestrator (n8n, Airflow, Prefect) and object storage (rustfs, S3, MinIO) in the stack.
|
|
||||||
- Latency tolerance is hours, not minutes.
|
|
||||||
|
|
||||||
**Don't use it when:**
|
|
||||||
- Sub-minute latency is required → use logical replication or CDC (Debezium, pg_logical, AWS DMS).
|
|
||||||
- Volumes exceed ~100 GB/day → step up to Spark/Flink + columnar store (Iceberg, Delta).
|
|
||||||
- Source and target are the same DB → just use materialized views or scheduled SQL.
|
|
||||||
- You need exactly-once streaming semantics → this pattern is at-least-once + idempotent load.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. The Core Pattern
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
||||||
│ Source DB │──(a)──▶ Orchestr. │──(b)──▶ Object Store │──(c)──▶ Target DB │
|
|
||||||
│ (OLTP) │ │ (extract) │ │ (CSVs) │ │ (bronze) │
|
|
||||||
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
|
||||||
│ ▲
|
|
||||||
└──────────────(d)─────────────────────────────┘
|
|
||||||
(load workflow, per CSV)
|
|
||||||
|
|
||||||
(a) Watermarked SELECT, closed upper bound
|
|
||||||
(b) Atomic CSV upload with timestamped path
|
|
||||||
(c) CSV stays until load confirms success
|
|
||||||
(d) Load = BEGIN → INSERT ON CONFLICT → UPDATE watermark → UPDATE run log → COMMIT → move CSV
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why three stages and not two:**
|
|
||||||
- **Audit trail** — every extracted CSV is a point-in-time snapshot you can replay.
|
|
||||||
- **Decoupling** — target DB downtime doesn't lose data; the CSV waits.
|
|
||||||
- **Observability** — blob listings are a second source of truth independent of the DB.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Pre-flight Checklist
|
|
||||||
|
|
||||||
Before writing any SQL or workflow JSON, confirm in writing:
|
|
||||||
|
|
||||||
- [ ] Source DB reachable from orchestrator (internal network preferred, VPN/public IP with `sslmode=require` otherwise).
|
|
||||||
- [ ] Target DB reachable; you hold a superuser credential for one-time DDL.
|
|
||||||
- [ ] Object storage bucket exists; credentials are configured in the orchestrator.
|
|
||||||
- [ ] For each source table to extract, you have identified:
|
|
||||||
- A **DB-insertion timestamp column** (not device/user-reported time), or "it's a snapshot table".
|
|
||||||
- A **natural unique key** that already has a `PRIMARY KEY` or `UNIQUE` constraint on source (for the `ON CONFLICT` target on bronze).
|
|
||||||
- Any **unit/column drift** between source and target (e.g., `distance_m` vs. `distance_km`).
|
|
||||||
- [ ] Acceptable end-to-end latency (to calibrate cron cadence).
|
|
||||||
- [ ] Security baseline: who writes bronze, who reads it, SSL requirement, password rotation cadence.
|
|
||||||
|
|
||||||
If any row is unchecked, pause and resolve it. Skipping this step is the #1 cause of pipelines that "worked in test but lose data in prod."
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Phase-by-Phase Execution
|
|
||||||
|
|
||||||
Execute in order. Phases are independent of each other within their phase, but phases have strict dependencies.
|
|
||||||
|
|
||||||
### Phase A — Target DB preparation
|
|
||||||
|
|
||||||
Apply three types of migrations, in numeric order:
|
|
||||||
|
|
||||||
1. **Bronze DDL** — one table per source table. Use `IF NOT EXISTS`; make it idempotent.
|
|
||||||
2. **Control schema** — `dwh_control.extract_watermarks` + `dwh_control.extract_runs`.
|
|
||||||
3. **Assertion migrations** — verify roles exist, verify every `ON CONFLICT` target is backed by a PK/UNIQUE (fail loudly if not).
|
|
||||||
|
|
||||||
Template files: `dwh/260423_dwh_ddl_v1.sql`, `dwh/261001_dwh_control.sql`, `dwh/261002_bronze_constraints_audit.sql`, `dwh/261003_dwh_roles.sql`.
|
|
||||||
|
|
||||||
**Role model:**
|
|
||||||
- `<proj>_owner` — owns schemas, writes bronze + control tables.
|
|
||||||
- `<proj>_ro` (or `grafana_ro`) — reads everything, writes nothing.
|
|
||||||
- Never use `postgres` or another superuser from the orchestrator.
|
|
||||||
|
|
||||||
**Watermark seed:** set `last_extracted_at` to a date before any real data (`'2000-01-01T00:00:00Z'` is safe) so the first run back-fills all history in a single CSV per table.
|
|
||||||
|
|
||||||
### Phase B — Object storage
|
|
||||||
|
|
||||||
Create two prefixes per table:
|
|
||||||
|
|
||||||
```
|
|
||||||
s3://<bucket>/<project>/exports/{table}/ # active CSVs, in-flight
|
|
||||||
s3://<bucket>/<project>/processed/{table}/ # loaded CSVs, never deleted (audit)
|
|
||||||
```
|
|
||||||
|
|
||||||
Naming convention: `{YYYYMMDD_HHMM}_{TZ}.csv` (e.g., `20260424_1400_EAT.csv`). Timezone in the filename because "08:00" means nothing a year from now without it.
|
|
||||||
|
|
||||||
Retention: match whatever backup retention is already in the stack (e.g., 30 days). `processed/` should outlive `exports/`.
|
|
||||||
|
|
||||||
### Phase C — Orchestrator credentials
|
|
||||||
|
|
||||||
Three credentials:
|
|
||||||
|
|
||||||
| Credential | Role | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `<proj>_source` | Read-only role on source DB | Extract queries |
|
|
||||||
| `<proj>_dwh_target` | `<proj>_owner` on target DB | Bronze writes + control updates |
|
|
||||||
| `<proj>_s3` | IAM user with `s3:PutObject`, `s3:GetObject`, `s3:ListBucket`, `s3:DeleteObject` on the prefix | CSV upload/download/move |
|
|
||||||
|
|
||||||
**Always** `sslmode=require` on any public-IP DB connection. Test each credential with the orchestrator's "Test connection" button before proceeding.
|
|
||||||
|
|
||||||
### Phase D — Load workflow (build this BEFORE the extract workflow)
|
|
||||||
|
|
||||||
Building load first lets you iterate with hand-crafted CSVs in blob storage before wiring up extract. Much faster feedback loop.
|
|
||||||
|
|
||||||
Load workflow input (parameters):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"table": "position_history",
|
|
||||||
"csv_path": "s3://bucket/project/exports/position_history/20260424_1400_EAT.csv",
|
|
||||||
"run_id": 12345,
|
|
||||||
"run_started_at": "2026-04-24T11:00:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Load workflow steps:
|
|
||||||
1. Download CSV from blob storage.
|
|
||||||
2. Parse CSV into rows.
|
|
||||||
3. **Open transaction.**
|
|
||||||
4. `INSERT INTO bronze.<table> (...) VALUES (...) ON CONFLICT (<natural_key>) DO NOTHING;`
|
|
||||||
5. `UPDATE dwh_control.extract_watermarks SET last_extracted_at = :run_started_at, last_loaded_at = NOW(), rows_loaded_last_run = <count> WHERE table_name = :table;`
|
|
||||||
6. `UPDATE dwh_control.extract_runs SET status = 'loaded', run_finished_at = NOW(), rows_loaded = <count> WHERE run_id = :run_id;`
|
|
||||||
7. **Commit.**
|
|
||||||
8. Move CSV from `exports/` to `processed/` (copy-then-delete; never delete before copy confirms).
|
|
||||||
|
|
||||||
**Non-negotiable invariants:**
|
|
||||||
- Steps 3–7 are one transaction. If any fails, all rollback.
|
|
||||||
- Step 8 only runs after commit. If step 8 fails, the next run will re-load the CSV (idempotent via ON CONFLICT) — not a data loss event.
|
|
||||||
|
|
||||||
### Phase E — Extract workflow
|
|
||||||
|
|
||||||
Extract workflow steps, per table:
|
|
||||||
|
|
||||||
1. Read current watermark: `SELECT last_extracted_at FROM dwh_control.extract_watermarks WHERE table_name = :table;`
|
|
||||||
2. Capture `run_started_at = NOW()` (in the target DB's clock, not the orchestrator's — reduces clock-skew bugs).
|
|
||||||
3. `INSERT INTO dwh_control.extract_runs (table_name, run_started_at, status) VALUES (:table, :run_started_at, 'extracting') RETURNING run_id;`
|
|
||||||
4. Query source with closed upper bound:
|
|
||||||
```sql
|
|
||||||
SELECT <cols>
|
|
||||||
FROM <source_schema>.<table>
|
|
||||||
WHERE <watermark_col> > :last_extracted_at
|
|
||||||
AND <watermark_col> <= :run_started_at
|
|
||||||
ORDER BY <watermark_col>;
|
|
||||||
```
|
|
||||||
5. Render rows as CSV. For geometry columns: `CASE WHEN geom IS NULL THEN NULL ELSE ST_AsEWKT(geom) END`.
|
|
||||||
6. Upload CSV to `s3://bucket/project/exports/{table}/{YYYYMMDD_HHMM}_{TZ}.csv`.
|
|
||||||
7. `UPDATE dwh_control.extract_runs SET status = 'uploaded', rows_extracted = <count>, csv_path = :path WHERE run_id = :run_id;`
|
|
||||||
8. Call load workflow with `{table, csv_path, run_id, run_started_at}`.
|
|
||||||
|
|
||||||
### Phase F — Schedule + observability
|
|
||||||
|
|
||||||
**Cron cadence:** start with 6–8 runs/day during active hours. Fold the overnight gap where traffic is low. Example: `0 5,8,11,14,17,20,23 * * *` TZ `Africa/Nairobi`.
|
|
||||||
|
|
||||||
**Three observability views** (readable by the RO role):
|
|
||||||
|
|
||||||
- `v_table_freshness` — per-table lag from last successful load. Drives the freshness alert.
|
|
||||||
- `v_recent_failures` — failed runs in last 24h. Zero rows = healthy.
|
|
||||||
- `v_watermark_lag` — extract vs. load lag per table. Distinguishes "nothing to extract" from "stuck".
|
|
||||||
|
|
||||||
Template file: `dwh/261004_dwh_observability_views.sql`.
|
|
||||||
|
|
||||||
**Grafana panels** (add at minimum):
|
|
||||||
1. Freshness panel — red if any row in `v_table_freshness` has `lag > 4h`.
|
|
||||||
2. Failures panel — red if `v_recent_failures` has any row.
|
|
||||||
3. Row counts panel — daily bar chart from `extract_runs`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Design Principles (Do Not Skip)
|
|
||||||
|
|
||||||
### 5.1 Watermark on DB insertion time, not source-reported time
|
|
||||||
|
|
||||||
The watermark column must be "when the target DB got the row", not "when the device/user said it happened". Device clocks skew, webhooks arrive late, and batch imports backdate records. A source-reported watermark will silently drop rows that arrive out of order. Use `recorded_at`, `created_at`, `updated_at` (with `DEFAULT NOW()`), or `ingested_at` — never `gps_time` / `event_time` / `timestamp`.
|
|
||||||
|
|
||||||
### 5.2 Closed upper bound
|
|
||||||
|
|
||||||
Extract uses `> last_extracted_at AND <= run_started_at`. The closed upper bound prevents "row committed at `NOW()` during the extract query" from appearing in two adjacent runs. Without it, some rows are double-extracted (wasteful) or missed (data loss).
|
|
||||||
|
|
||||||
### 5.3 Idempotent load via natural unique keys
|
|
||||||
|
|
||||||
Every incremental bronze table needs a PRIMARY KEY or UNIQUE that matches the source's natural unique key. `ON CONFLICT DO NOTHING` makes re-running a CSV harmless. **Do not invent surrogate keys on bronze** — they defeat the ON CONFLICT guarantee. If the source has no natural key, fix the source or accept the table as a snapshot.
|
|
||||||
|
|
||||||
### 5.4 Transactional load boundary
|
|
||||||
|
|
||||||
Insert + watermark update + run-log update are one transaction. Splitting them creates "ghost" states where watermark advanced but rows didn't load, causing silent holes.
|
|
||||||
|
|
||||||
### 5.5 CSV audit trail — never delete
|
|
||||||
|
|
||||||
Moved-to-`processed/` CSVs are cheap ($0.023/GB/month on S3-class storage). They pay for themselves the first time you need to replay a window or debug a row-count mismatch.
|
|
||||||
|
|
||||||
### 5.6 PostGIS round-trip via EWKT
|
|
||||||
|
|
||||||
`ST_AsEWKT(geom)` on extract, `ST_GeomFromEWKT(ewkt)` on load. Preserves SRID inline. Do NOT store `ST_AsText` + separate SRID column — it doubles the chance of mismatch. Guard NULLs: `CASE WHEN geom IS NULL THEN NULL ELSE ST_AsEWKT(geom) END`.
|
|
||||||
|
|
||||||
### 5.7 Fail loud, fail early
|
|
||||||
|
|
||||||
Audit migrations (roles, constraints) should `RAISE EXCEPTION` with a bullet list of what's missing. Silent success is worse than noisy failure — a missing PK surfaces three months later as "why are there duplicate trips?".
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Snapshot vs. Incremental Decision Matrix
|
|
||||||
|
|
||||||
| Signal | Snapshot (TRUNCATE + reload) | Incremental (watermark + append) |
|
|
||||||
|---|---|---|
|
|
||||||
| Row count | < ~10k | > ~10k |
|
|
||||||
| Meaning of "current state" | Matters | Doesn't matter; history matters |
|
|
||||||
| Deletes in source | Common | Rare |
|
|
||||||
| Update frequency per row | High | Low (append-mostly) |
|
|
||||||
| Natural unique key | May not exist | Must exist |
|
|
||||||
| Example | `devices`, `live_positions`, `geofences` | `position_history`, `trips`, `alarms`, event logs |
|
|
||||||
|
|
||||||
When in doubt: **snapshot is simpler**. Only escalate to incremental when the snapshot CSV would exceed a few MB per run.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Observability Contract
|
|
||||||
|
|
||||||
Every pipeline adds these three views to its control schema — no exceptions:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE OR REPLACE VIEW <control>.v_table_freshness AS
|
|
||||||
SELECT table_name,
|
|
||||||
MAX(run_finished_at) AS last_loaded_at,
|
|
||||||
NOW() - MAX(run_finished_at) AS lag,
|
|
||||||
COUNT(*) FILTER (WHERE run_started_at > NOW() - INTERVAL '24 hours') AS loads_last_24h
|
|
||||||
FROM <control>.extract_runs
|
|
||||||
WHERE status = 'loaded'
|
|
||||||
GROUP BY table_name;
|
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW <control>.v_recent_failures AS
|
|
||||||
SELECT run_id, table_name, run_started_at, run_finished_at, csv_path, error_message
|
|
||||||
FROM <control>.extract_runs
|
|
||||||
WHERE status = 'failed' AND run_started_at > NOW() - INTERVAL '24 hours'
|
|
||||||
ORDER BY run_started_at DESC;
|
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW <control>.v_watermark_lag AS
|
|
||||||
SELECT table_name, last_extracted_at, last_loaded_at, rows_loaded_last_run,
|
|
||||||
NOW() - last_loaded_at AS load_lag,
|
|
||||||
NOW() - last_extracted_at AS extract_lag
|
|
||||||
FROM <control>.extract_watermarks;
|
|
||||||
```
|
|
||||||
|
|
||||||
Wire a Grafana alert on each view. Test the alert by manually failing a run before go-live.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Schema Drift Handling
|
|
||||||
|
|
||||||
Schema drift between source and bronze is inevitable. Two rules:
|
|
||||||
|
|
||||||
1. **Detect at design time.** Diff source DDL against bronze DDL before writing any extract SQL. Unit changes (metres vs. km), renamed columns, and added nullable columns are the usual suspects.
|
|
||||||
2. **Fix in the extract query, not the load.** Put all transformations in the SELECT so the CSV on disk already matches the bronze column names and units. The load workflow should be dumb — CSV column N goes to bronze column N.
|
|
||||||
|
|
||||||
Document every drift in the runbook (§5 of the operations runbook). Future developers WILL hit them.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Verification Gates
|
|
||||||
|
|
||||||
### Pre-deploy (before first cron tick)
|
|
||||||
|
|
||||||
- [ ] Every migration applied successfully.
|
|
||||||
- [ ] Control tables seeded (one watermark row per incremental table).
|
|
||||||
- [ ] Every credential's "Test connection" passes.
|
|
||||||
- [ ] Blob storage prefixes exist.
|
|
||||||
- [ ] Manual workflow trigger succeeds end-to-end for one table.
|
|
||||||
|
|
||||||
### First run (manual trigger of extract workflow)
|
|
||||||
|
|
||||||
- [ ] Every processed table has a row in `extract_runs` with `status='loaded'`.
|
|
||||||
- [ ] Row-count parity with source (± in-flight writes): `SELECT COUNT(*) FROM <source>` vs. `SELECT COUNT(*) FROM bronze.<table>`.
|
|
||||||
- [ ] Geometry columns round-trip cleanly: `SELECT ST_AsText(geom) FROM bronze.<table> LIMIT 5` returns valid POINTs.
|
|
||||||
- [ ] All CSVs moved from `exports/` to `processed/`.
|
|
||||||
|
|
||||||
### Steady-state (after 24h / first full schedule cycle)
|
|
||||||
|
|
||||||
- [ ] `v_table_freshness` shows lag < cadence × 2 for every table.
|
|
||||||
- [ ] `v_recent_failures` is empty.
|
|
||||||
- [ ] Row counts in bronze growing at expected rate.
|
|
||||||
|
|
||||||
Only declare "done" after all three gates pass.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Scheduling Calibration
|
|
||||||
|
|
||||||
Tradeoffs:
|
|
||||||
|
|
||||||
| Cadence | Pros | Cons |
|
|
||||||
|---|---|---|
|
|
||||||
| Every 15 min | Low lag, small CSVs | High orchestrator churn, noisy alerts |
|
|
||||||
| Every 3 h (recommended) | Predictable, fits ops windows, tolerable lag | Overnight backlog carries to morning |
|
|
||||||
| Nightly (once/day) | Cheap, simple | Unacceptable for real-time panels |
|
|
||||||
|
|
||||||
Rule of thumb: cadence = 25–50% of your latency tolerance. 4h latency budget → 1-2h cadence.
|
|
||||||
|
|
||||||
Fold cadence around traffic patterns. Don't run 24× at 1-hour intervals if the source generates zero rows between midnight and 05:00.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Common Failure Modes & Recovery
|
|
||||||
|
|
||||||
| Failure | Symptom | Fix |
|
|
||||||
|---|---|---|
|
|
||||||
| CSV stuck in `exports/` | `v_recent_failures` has a row; CSV never moved | Next scheduled run retries automatically (idempotent). If persistent, open orchestrator logs by `run_id`. |
|
|
||||||
| Table marked `loading` for >1 cadence | n8n executor crashed mid-transaction | DB rolled back. Next run retries. If stuck >2 cadences, manually re-trigger the extract. |
|
|
||||||
| Row counts diverge > 1% | CSV parse error silently dropped rows | `rows_extracted != rows_loaded` in `extract_runs` — inspect the CSV for malformed rows. |
|
|
||||||
| Geometry loads as NULL | EWKT serialisation broke | Check for missing `CASE WHEN geom IS NULL` guard in extract SQL. |
|
|
||||||
| Distance/units 1000× wrong | Schema drift not caught | Check extract SQL for the unit conversion (see §8). |
|
|
||||||
|
|
||||||
**Back-fill a window:**
|
|
||||||
```sql
|
|
||||||
UPDATE <control>.extract_watermarks
|
|
||||||
SET last_extracted_at = NOW() - INTERVAL '24 hours'
|
|
||||||
WHERE table_name = '<table>';
|
|
||||||
```
|
|
||||||
Next run re-extracts the gap. `ON CONFLICT DO NOTHING` filters duplicates.
|
|
||||||
|
|
||||||
**Full reseed (nuclear):**
|
|
||||||
```sql
|
|
||||||
UPDATE <control>.extract_watermarks
|
|
||||||
SET last_extracted_at = '2000-01-01T00:00:00Z'
|
|
||||||
WHERE table_name = '<table>';
|
|
||||||
```
|
|
||||||
Next run back-fills all history in one very large CSV. Expected; it moves to `processed/` on success.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Security Baseline
|
|
||||||
|
|
||||||
- Two roles minimum: owner (writes) and RO (reads). Never use superuser from the orchestrator.
|
|
||||||
- `sslmode=require` on every public-IP DB connection.
|
|
||||||
- Passwords never in committed SQL — use placeholder tokens (`CHANGE_ME_BEFORE_APPLY`) and swap in-session during apply. Document rotation in the runbook.
|
|
||||||
- Blob storage credentials scoped to the project's prefix, not the whole bucket.
|
|
||||||
- Rotate all credentials before go-live (don't reuse the ones that were flying around in design conversations).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. Reusability Checklist (Applying to a New Project)
|
|
||||||
|
|
||||||
When starting a new data project, copy the Tracksolid DWH layout and edit these points:
|
|
||||||
|
|
||||||
- [ ] Rename schemas: `<proj>_control` instead of `dwh_control` if multiple DWHs share a DB.
|
|
||||||
- [ ] Adjust `<proj>_owner` / `<proj>_ro` role names.
|
|
||||||
- [ ] Update bucket prefix: `s3://<bucket>/<project>/exports|processed/`.
|
|
||||||
- [ ] Re-do the snapshot/incremental decision for every source table (§6).
|
|
||||||
- [ ] Identify watermark columns and natural unique keys for every incremental table (§5.1, §5.3).
|
|
||||||
- [ ] Map schema drift before writing extract SQL (§8).
|
|
||||||
- [ ] Calibrate cadence to the new project's latency budget (§10).
|
|
||||||
- [ ] Ship the three observability views (§7) — even if nobody will look at them in week one.
|
|
||||||
- [ ] Write the runbook from the template: follow `docs/DWH_PIPELINE.md` section-for-section.
|
|
||||||
- [ ] Run the verification gates (§9) before declaring done.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 14. Reference Implementation (Tracksolid DWH)
|
|
||||||
|
|
||||||
These files are the copy-paste template:
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `dwh/260423_dwh_ddl_v1.sql` | Bronze DDL + roles + schemas |
|
|
||||||
| `dwh/261001_dwh_control.sql` | Control schema (watermarks + run log) |
|
|
||||||
| `dwh/261002_bronze_constraints_audit.sql` | ON CONFLICT key assertion |
|
|
||||||
| `dwh/261003_dwh_roles.sql` | Role contract assertion |
|
|
||||||
| `dwh/261004_dwh_observability_views.sql` | Freshness/failure/watermark views |
|
|
||||||
| `docs/DWH_PIPELINE.md` | Operations runbook (troubleshooting, manual re-run, rotation) |
|
|
||||||
| `docs/superpowers/specs/2026-04-24-n8n-dwh-bronze-pipeline-design.md` | Design spec (why each decision) |
|
|
||||||
| `docs/superpowers/plans/2026-04-24-n8n-dwh-bronze-pipeline.md` | Task-by-task implementation plan |
|
|
||||||
| `n8n-workflows/dwh_extract.json` | Extract workflow (reference) |
|
|
||||||
| `n8n-workflows/dwh_load_bronze.json` | Load workflow (reference) |
|
|
||||||
|
|
||||||
**For the next project, fork this manual first, then adapt.** Do not re-design from scratch — the seven design principles in §5 are the parts people keep getting wrong.
|
|
||||||
|
|
@ -1,252 +0,0 @@
|
||||||
# DWH Pipeline — Operations Runbook
|
|
||||||
|
|
||||||
**Pipeline:** n8n extract + load into `tracksolid_dwh` bronze schema
|
|
||||||
**Design spec:** `docs/superpowers/specs/2026-04-24-n8n-dwh-bronze-pipeline-design.md`
|
|
||||||
**Implementation plan:** `docs/superpowers/plans/2026-04-24-n8n-dwh-bronze-pipeline.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. What This Pipeline Does
|
|
||||||
|
|
||||||
Every ~3 hours during active hours (7 runs/day, 05:00–23:00 EAT), n8n extracts 8 tables from the production `tracksolid_db` (Coolify internal network), writes each as a timestamped CSV to rustfs, then loads each CSV into the `bronze` schema on `tracksolid_dwh` (31.97.44.246:5888). Rustfs CSVs are moved to `dwh/processed/` after a successful load — never deleted.
|
|
||||||
|
|
||||||
Two n8n workflows:
|
|
||||||
- **`dwh_extract`** — cron-triggered, iterates tables in sequence, writes CSVs, calls `dwh_load_bronze` per table.
|
|
||||||
- **`dwh_load_bronze`** — triggered per-table by `dwh_extract`, loads one CSV inside a single transaction (insert → update watermark → update run log → move CSV).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Key Locations
|
|
||||||
|
|
||||||
| What | Where |
|
|
||||||
|---|---|
|
|
||||||
| Source DB | `tracksolid_db` on Coolify (internal Docker network, `timescale_db:5432`) |
|
|
||||||
| Target DB | `tracksolid_dwh` at `31.97.44.246:5888` |
|
|
||||||
| Rustfs bucket | `fleet-db` (same bucket used by pg_dump backups) |
|
|
||||||
| Active CSVs | `s3://fleet-db/dwh/exports/{table}/{YYYYMMDD_HHMM}_EAT.csv` |
|
|
||||||
| Processed CSVs | `s3://fleet-db/dwh/processed/{table}/{YYYYMMDD_HHMM}_EAT.csv` |
|
|
||||||
| Control schema | `dwh_control` in `tracksolid_dwh` |
|
|
||||||
| Migrations | `dwh/26*.sql` applied in numeric order |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. First-Time Setup
|
|
||||||
|
|
||||||
Apply migrations to `tracksolid_dwh` in numeric order, as a superuser (not `dwh_owner`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PSQL="psql 'postgres://postgres:***@31.97.44.246:5888/tracksolid_dwh?sslmode=require'"
|
|
||||||
|
|
||||||
$PSQL -f dwh/260423_dwh_ddl_v1.sql # Bronze DDL, roles, schemas
|
|
||||||
$PSQL -f dwh/261001_dwh_control.sql # Watermarks + run log
|
|
||||||
$PSQL -f dwh/261002_bronze_constraints_audit.sql # Assertion: ON CONFLICT keys exist
|
|
||||||
$PSQL -f dwh/261003_dwh_roles.sql # Assertion: roles + grants present
|
|
||||||
$PSQL -f dwh/261004_dwh_observability_views.sql # Freshness/failure views
|
|
||||||
```
|
|
||||||
|
|
||||||
Each migration is idempotent. Audit files (261002, 261003) raise an exception with a bullet list of what is missing if the contract is broken — re-apply the relevant predecessor file and try again.
|
|
||||||
|
|
||||||
### Rustfs prefixes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aws --endpoint ${RUSTFS_ENDPOINT} s3api put-object \
|
|
||||||
--bucket fleet-db --key dwh/exports/
|
|
||||||
aws --endpoint ${RUSTFS_ENDPOINT} s3api put-object \
|
|
||||||
--bucket fleet-db --key dwh/processed/
|
|
||||||
```
|
|
||||||
|
|
||||||
### n8n credentials
|
|
||||||
|
|
||||||
Three credentials, all configured in the n8n UI before importing workflows:
|
|
||||||
|
|
||||||
| Credential | Target | User | Notes |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `tracksolid_source` | Coolify internal → `tracksolid_db` | `grafana_ro` | Read-only; no `sslmode` needed on internal network |
|
|
||||||
| `tracksolid_dwh_target` | `31.97.44.246:5888` → `tracksolid_dwh` | `dwh_owner` | **Must set `sslmode=require`** — public IP |
|
|
||||||
| `rustfs_s3` | `${RUSTFS_ENDPOINT}` | `${RUSTFS_ACCESS_KEY}` | Same creds as pg_dump backup sidecar |
|
|
||||||
|
|
||||||
Test each credential via the n8n "Test connection" button before enabling the cron schedule.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Schedule
|
|
||||||
|
|
||||||
n8n Schedule node, Africa/Nairobi TZ: `0 5,8,11,14,17,20,23 * * *`
|
|
||||||
|
|
||||||
- 7 runs/day at 05:00, 08:00, 11:00, 14:00, 17:00, 20:00, 23:00 EAT
|
|
||||||
- Overnight gap (23:00 → 05:00 = 6h) by design — device traffic minimal
|
|
||||||
- First-of-day run carries the overnight backlog (watermark picks up where 23:00 left off)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. What Each Table Does on Every Run
|
|
||||||
|
|
||||||
### Snapshot tables (TRUNCATE + full reload)
|
|
||||||
|
|
||||||
`bronze.devices`, `bronze.live_positions` — small state tables, "current state" semantics. Full replace every run.
|
|
||||||
|
|
||||||
### Incremental tables (watermark + append-with-dedup)
|
|
||||||
|
|
||||||
| Bronze table | Source watermark column | ON CONFLICT target |
|
|
||||||
|---|---|---|
|
|
||||||
| `position_history` | `recorded_at` (DB insertion time) | `(imei, gps_time)` |
|
|
||||||
| `trips` | `updated_at` | `id` |
|
|
||||||
| `alarms` | `updated_at` | `id` |
|
|
||||||
| `parking_events` | `updated_at` | `id` |
|
|
||||||
| `device_events` | `created_at` | `id` |
|
|
||||||
| `ingestion_log` | `run_at` | `id` |
|
|
||||||
|
|
||||||
Watermark bounds are closed upper: `WHERE <col> > last_extracted_at AND <col> <= :run_started_at`.
|
|
||||||
|
|
||||||
### Schema drift to handle in extract SQL
|
|
||||||
|
|
||||||
- **`trips.distance_m` → `bronze.trips.distance_km`**: source stores metres, bronze expects km. Extract SQL: `SELECT ..., distance_m/1000.0 AS distance_km, ...`. Cross-reference: FIX-M16 in `CLAUDE.md`.
|
|
||||||
|
|
||||||
### PostGIS geometry round-trip
|
|
||||||
|
|
||||||
All six geometry columns (`live_positions`, `position_history`, `trips.start_geom`, `trips.end_geom`, `parking_events`, `alarms`) use EWKT serialisation:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Extract
|
|
||||||
SELECT ..., CASE WHEN geom IS NULL THEN NULL ELSE ST_AsEWKT(geom) END AS geom_ewkt FROM ...;
|
|
||||||
|
|
||||||
-- Load
|
|
||||||
INSERT INTO bronze... (..., geom) VALUES (..., ST_GeomFromEWKT(:geom_ewkt));
|
|
||||||
```
|
|
||||||
|
|
||||||
SRID 4326 is preserved inline; no separate SRID step required.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Verifying a Healthy Run
|
|
||||||
|
|
||||||
### Immediate sanity checks (after any scheduled run)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Any failures in the last hour?
|
|
||||||
SELECT * FROM dwh_control.v_recent_failures WHERE run_started_at > NOW() - INTERVAL '1 hour';
|
|
||||||
|
|
||||||
-- All tables loaded in last 4h?
|
|
||||||
SELECT * FROM dwh_control.v_table_freshness WHERE lag > INTERVAL '4 hours';
|
|
||||||
|
|
||||||
-- Watermarks advancing?
|
|
||||||
SELECT * FROM dwh_control.v_watermark_lag ORDER BY extract_lag DESC;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Row-count parity (spot check weekly)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Source
|
|
||||||
SELECT COUNT(*) FROM tracksolid.position_history;
|
|
||||||
-- Target
|
|
||||||
SELECT COUNT(*) FROM bronze.position_history;
|
|
||||||
```
|
|
||||||
|
|
||||||
Numbers should match ± rows inserted between the two queries. Persistent gap > 1% → investigate CSV parse errors or a dropped batch.
|
|
||||||
|
|
||||||
### Geometry round-trip
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT ST_AsText(geom) FROM bronze.position_history WHERE geom IS NOT NULL LIMIT 5;
|
|
||||||
-- Should return valid POINT(lng lat), not NULL or garbage.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Troubleshooting
|
|
||||||
|
|
||||||
### "A table is stale (`v_table_freshness` shows lag > 4h)"
|
|
||||||
|
|
||||||
1. Check `v_recent_failures` for that table. If a row exists, read `error_message`.
|
|
||||||
2. If `status='loading'` in `extract_runs` for that table, a load is in progress — wait for the next cron tick. If it stays `loading` across two ticks, the n8n executor crashed mid-transaction; the DB rolled back, and the next run will retry naturally.
|
|
||||||
3. If no failure row and no in-progress row, the extract workflow never fired — check n8n execution logs for the cron trigger.
|
|
||||||
|
|
||||||
### "A CSV is stuck in `dwh/exports/`"
|
|
||||||
|
|
||||||
The load failed after upload. The next scheduled run will re-process it (the watermark did not advance, so the extract SQL returns the same window). Safe to leave. If multiple days of CSVs pile up, the load workflow has a persistent bug — open n8n execution logs for the specific `run_id` in `extract_runs`.
|
|
||||||
|
|
||||||
### "Row counts diverge more than ~1%"
|
|
||||||
|
|
||||||
Usually one of:
|
|
||||||
- A retry window overlapped the PK range and some rows lost the race with a concurrent source-side write. Re-trigger the extract for that window manually (see §8).
|
|
||||||
- CSV parse error silently dropped a row. Check `extract_runs.rows_extracted` vs. `rows_loaded` — if they differ, the loader found malformed CSV.
|
|
||||||
|
|
||||||
### "Geometry loaded as NULL"
|
|
||||||
|
|
||||||
EWKT serialisation broke on the extract side. Verify the source query has the `CASE WHEN geom IS NULL` guard — without it, `ST_AsEWKT(NULL)` returns `NULL` correctly but an empty geometry returns `'GEOMETRYCOLLECTION EMPTY'` which `ST_GeomFromEWKT` rejects.
|
|
||||||
|
|
||||||
### "`bronze.trips.distance_km` values are 1000× too large"
|
|
||||||
|
|
||||||
The extract query is missing `/1000.0` on the source `distance_m` column. See §5 "Schema drift".
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Manual Re-Run
|
|
||||||
|
|
||||||
### Re-run a single table for the current window
|
|
||||||
|
|
||||||
1. Open n8n, go to `dwh_extract` workflow.
|
|
||||||
2. Locate the branch for that table.
|
|
||||||
3. Click **Execute Workflow** → selects that table only.
|
|
||||||
4. Confirm a new row appears in `dwh_control.extract_runs` with `status='loaded'`.
|
|
||||||
|
|
||||||
### Back-fill a historical window
|
|
||||||
|
|
||||||
The extract workflow respects the watermark; to re-extract a window, rewind the watermark:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Rewind position_history to 24h ago
|
|
||||||
UPDATE dwh_control.extract_watermarks
|
|
||||||
SET last_extracted_at = NOW() - INTERVAL '24 hours'
|
|
||||||
WHERE table_name = 'position_history';
|
|
||||||
```
|
|
||||||
|
|
||||||
Next scheduled run will re-extract the gap. Loads are idempotent (`ON CONFLICT DO NOTHING` on the PK), so duplicate rows are filtered at the bronze boundary.
|
|
||||||
|
|
||||||
### Full reseed (nuclear option)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Restart position_history from the beginning
|
|
||||||
UPDATE dwh_control.extract_watermarks
|
|
||||||
SET last_extracted_at = '2026-01-01T00:00:00Z'
|
|
||||||
WHERE table_name = 'position_history';
|
|
||||||
```
|
|
||||||
|
|
||||||
The first post-reseed run will produce one very large CSV (~all history). The rustfs `exports/` prefix will briefly hold a multi-GB object. Expected; it moves to `processed/` on success.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Credential Rotation
|
|
||||||
|
|
||||||
`260423_dwh_ddl_v1.sql` commits role passwords in plaintext — a pre-existing flaw to be cleaned up separately.
|
|
||||||
|
|
||||||
To rotate:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- As superuser on tracksolid_dwh:
|
|
||||||
ALTER ROLE dwh_owner PASSWORD '<new secret>';
|
|
||||||
ALTER ROLE grafana_ro PASSWORD '<new secret>';
|
|
||||||
```
|
|
||||||
|
|
||||||
Then update the matching n8n credential and Grafana datasource. Never commit the new password — store in `.env` if needed for scripts, or keep exclusively inside n8n/Grafana credential stores.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Known Quirks
|
|
||||||
|
|
||||||
| Quirk | Source | Handling |
|
|
||||||
|---|---|---|
|
|
||||||
| `trips.distance_m` → `bronze.trips.distance_km` | Schema drift between source and bronze | Divide in extract SQL (§5) |
|
|
||||||
| Hypertable row counts read 0 in `pg_stat_user_tables` | TimescaleDB quirk | Always `SELECT COUNT(*)` directly |
|
|
||||||
| `parking_events` can be empty for days | Endpoint returns empty; not a failure | Zero rows loaded is a valid run outcome |
|
|
||||||
| First run of each day larger | Overnight backlog | Expected; plan watermark design |
|
|
||||||
| `last_extracted_at` default `2026-01-01` | Seed value from 261001 | First run on a new table back-fills all history |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Out of Scope (follow-up)
|
|
||||||
|
|
||||||
- Silver/gold transformations — `silver` and `gold` schemas exist but contain no views.
|
|
||||||
- Grafana dashboard panels — these views are the data source; panels TBD.
|
|
||||||
- OBD / fault codes / fuel / temperature / LBS / heartbeats — webhooks not yet registered; add a watermark row + extract branch when they start reporting.
|
|
||||||
- Bronze schema evolution tooling — additive changes via numbered migrations is fine for one pipeline; revisit if scope grows.
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,320 +0,0 @@
|
||||||
# n8n DWH Bronze Layer Pipeline — Design & Plan
|
|
||||||
|
|
||||||
**Date:** 2026-04-24
|
|
||||||
**Status:** Awaiting approval
|
|
||||||
**Repo:** `/Users/davidkiania/Downloads/55_ts_coolify_gemini_prod`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
Fireside's Tracksolid fleet pipeline currently ingests telemetry into a single production DB (`tracksolid_db`, TimescaleDB/PostGIS on Coolify at `stage.rahamafresh.com`). There is no downstream data warehouse, so every analytical query hits the live operational DB — risking contention as Grafana panels and ad-hoc analysis scale. A full medallion-architecture bronze DDL exists on disk (`dwh/260423_dwh_ddl_v1.sql`) but has never been populated.
|
|
||||||
|
|
||||||
The user wants to build the **first layer of that DWH** using n8n (already running on the same Coolify instance, already connected to both source and target DBs). The design has two n8n workflows:
|
|
||||||
|
|
||||||
1. **Workflow 1 — Extract**: pull tables from the source `tracksolid_db` (Coolify-hosted TimescaleDB, reached via the same internal Docker network n8n is on), write CSVs to rustfs blob storage.
|
|
||||||
2. **Workflow 2 — Load**: pick up those CSVs and upsert into the bronze schema inside `tracksolid_dwh` (PostGIS) on the separate server `31.97.44.246:5888`.
|
|
||||||
|
|
||||||
**Confirmed connection targets:**
|
|
||||||
- **Source:** `tracksolid_db` on the Coolify stack — n8n connects via internal Docker network (trial confirmed working).
|
|
||||||
- **Target:** `tracksolid_dwh` at `31.97.44.246:5888` — a separate PostGIS instance. Schemas `bronze`, `silver`, `gold`, plus `dwh_control` all live in this one database.
|
|
||||||
|
|
||||||
The intermediate rustfs CSV layer (a) gives a durable audit trail of every extract, (b) decouples source-DB availability from target-DB availability (a remote-DB outage doesn't lose data — the CSV waits in `exports/`), and (c) matches how rustfs is already used in the stack (pg_dump backups).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────────────────────────────────────┐
|
|
||||||
│ n8n (Coolify instance) │
|
|
||||||
│ │
|
|
||||||
│ Workflow 1: dwh_extract │
|
|
||||||
│ Schedule: cron 0 5,8,11,14,17,20,23 * * * │
|
|
||||||
│ (Africa/Nairobi, 7 runs/day) │
|
|
||||||
│ Steps per table: │
|
|
||||||
│ 1. Read watermark from target control table │
|
|
||||||
│ 2. Query source with watermark bounds │
|
|
||||||
│ 3. Render rows as CSV │
|
|
||||||
│ 4. Upload CSV to rustfs │
|
|
||||||
│ 5. Insert row into dwh_control.extract_runs │
|
|
||||||
│ (status='uploaded') │
|
|
||||||
│ 6. Execute Workflow 2 for this CSV │
|
|
||||||
│ │
|
|
||||||
│ Workflow 2: dwh_load_bronze │
|
|
||||||
│ Trigger: Execute Workflow (from Workflow 1) │
|
|
||||||
│ Input: { table, csv_path, run_id, │
|
|
||||||
│ run_started_at } │
|
|
||||||
│ Steps: │
|
|
||||||
│ 1. Download CSV from rustfs │
|
|
||||||
│ 2. Parse CSV │
|
|
||||||
│ 3. BEGIN │
|
|
||||||
│ INSERT ... ON CONFLICT DO NOTHING │
|
|
||||||
│ UPDATE extract_watermarks │
|
|
||||||
│ UPDATE extract_runs SET status='loaded' │
|
|
||||||
│ COMMIT │
|
|
||||||
│ 4. Move CSV: dwh/exports/ → dwh/processed/ │
|
|
||||||
└──────────────────────────────────────────────────┘
|
|
||||||
│ │ │
|
|
||||||
▼ ▼ ▼
|
|
||||||
tracksolid_db rustfs (fleet-db) tracksolid_dwh (PostGIS)
|
|
||||||
(Coolify internal) /dwh/exports/ 31.97.44.246:5888
|
|
||||||
/dwh/processed/ dwh_control.extract_watermarks
|
|
||||||
dwh_control.extract_runs
|
|
||||||
bronze.devices
|
|
||||||
bronze.position_history
|
|
||||||
bronze.trips
|
|
||||||
bronze.alarms
|
|
||||||
bronze.parking_events
|
|
||||||
bronze.device_events
|
|
||||||
bronze.live_positions
|
|
||||||
bronze.ingestion_log
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rustfs path convention:**
|
|
||||||
- Active export: `s3://fleet-db/dwh/exports/{table}/{YYYYMMDD_HHMM}_EAT.csv`
|
|
||||||
- After successful load: moved to `s3://fleet-db/dwh/processed/{table}/{YYYYMMDD_HHMM}_EAT.csv`
|
|
||||||
- Never deleted — this is the audit trail.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Table-by-Table Extraction Strategy
|
|
||||||
|
|
||||||
### Snapshot tables (TRUNCATE + full reload every run)
|
|
||||||
|
|
||||||
Small state-based tables where "current state" matters, not history.
|
|
||||||
|
|
||||||
| Source table | Rows | Bronze target |
|
|
||||||
|---|---|---|
|
|
||||||
| `tracksolid.devices` | 63 | `bronze.devices` |
|
|
||||||
| `tracksolid.live_positions` | 19 | `bronze.live_positions` |
|
|
||||||
|
|
||||||
**Load pattern:**
|
|
||||||
```sql
|
|
||||||
BEGIN;
|
|
||||||
TRUNCATE bronze.devices;
|
|
||||||
INSERT INTO bronze.devices (...) VALUES (...);
|
|
||||||
UPDATE dwh_control.extract_watermarks SET last_loaded_at = NOW() WHERE table_name='devices';
|
|
||||||
COMMIT;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Incremental tables (watermark + append-with-dedup)
|
|
||||||
|
|
||||||
Append-only event/history tables. Watermark is the **DB insertion timestamp**, not the device-reported timestamp, so out-of-order device clocks / delayed pushes can't cause silent data loss.
|
|
||||||
|
|
||||||
| Source table | Watermark column | Natural unique key (exists in source) | Bronze conflict target |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `tracksolid.position_history` | `recorded_at` | `(imei, gps_time)` | `(imei, gps_time)` |
|
|
||||||
| `tracksolid.trips` | `updated_at` | `(imei, start_time)` | `id` |
|
|
||||||
| `tracksolid.alarms` | `updated_at` | `(imei, alarm_type, alarm_time)` | `id` |
|
|
||||||
| `tracksolid.parking_events` | `updated_at` | `(imei, start_time, event_type)` | `id` |
|
|
||||||
| `tracksolid.device_events` | `created_at` | `(imei, event_type, event_time)` | `id` |
|
|
||||||
| `tracksolid.ingestion_log` | `run_at` | PK `id` | `id` |
|
|
||||||
|
|
||||||
**Extract pattern (closed upper bound to avoid boundary drift):**
|
|
||||||
```sql
|
|
||||||
SELECT <cols>, ST_AsEWKT(geom) AS geom_ewkt
|
|
||||||
FROM tracksolid.position_history
|
|
||||||
WHERE recorded_at > :last_extracted_at
|
|
||||||
AND recorded_at <= :run_started_at
|
|
||||||
ORDER BY recorded_at;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Load pattern (idempotent):**
|
|
||||||
```sql
|
|
||||||
BEGIN;
|
|
||||||
INSERT INTO bronze.position_history (imei, gps_time, geom, lat, lng, ...)
|
|
||||||
SELECT imei, gps_time, ST_GeomFromEWKT(geom_ewkt), lat, lng, ...
|
|
||||||
FROM csv_stage
|
|
||||||
ON CONFLICT (imei, gps_time) DO NOTHING;
|
|
||||||
|
|
||||||
UPDATE dwh_control.extract_watermarks
|
|
||||||
SET last_extracted_at = :run_started_at,
|
|
||||||
last_loaded_at = NOW(),
|
|
||||||
rows_loaded_last_run = <count>
|
|
||||||
WHERE table_name = 'position_history';
|
|
||||||
|
|
||||||
UPDATE dwh_control.extract_runs
|
|
||||||
SET status = 'loaded', run_finished_at = NOW(), rows_loaded = <count>
|
|
||||||
WHERE run_id = :run_id;
|
|
||||||
COMMIT;
|
|
||||||
```
|
|
||||||
|
|
||||||
### First-run behaviour
|
|
||||||
|
|
||||||
`extract_watermarks` seeded with `last_extracted_at = '2026-01-01T00:00:00Z'` so the first run back-fills all historical data in a single CSV per table.
|
|
||||||
|
|
||||||
### Skipped for now (no data, webhooks pending)
|
|
||||||
|
|
||||||
`obd_readings`, `fault_codes`, `fuel_readings`, `temperature_readings`, `lbs_readings`, `heartbeats` — add later by copying the incremental pattern and seeding a watermark row.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PostGIS Geometry Handling
|
|
||||||
|
|
||||||
Six source tables have `geometry(Point, 4326)` columns: `live_positions`, `position_history`, `trips` (start+end), `parking_events`, `alarms`.
|
|
||||||
|
|
||||||
- **Extract:** `ST_AsEWKT(geom) AS geom_ewkt` — preserves SRID inline (`SRID=4326;POINT(...)`)
|
|
||||||
- **Load:** `ST_GeomFromEWKT(csv.geom_ewkt)` — no separate SRID step, no loss on round-trip
|
|
||||||
- **NULL safety:** `CASE WHEN geom IS NULL THEN NULL ELSE ST_AsEWKT(geom) END`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Control Tables (to add to `tracksolid_dwh`)
|
|
||||||
|
|
||||||
New migration file: `dwh/261001_dwh_control.sql` — applied once to `tracksolid_dwh@31.97.44.246:5888`.
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE SCHEMA IF NOT EXISTS dwh_control;
|
|
||||||
|
|
||||||
CREATE TABLE dwh_control.extract_watermarks (
|
|
||||||
table_name TEXT PRIMARY KEY,
|
|
||||||
last_extracted_at TIMESTAMPTZ NOT NULL DEFAULT '2026-01-01T00:00:00Z',
|
|
||||||
last_loaded_at TIMESTAMPTZ,
|
|
||||||
rows_loaded_last_run INT,
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE dwh_control.extract_runs (
|
|
||||||
run_id BIGSERIAL PRIMARY KEY,
|
|
||||||
table_name TEXT NOT NULL,
|
|
||||||
run_started_at TIMESTAMPTZ NOT NULL,
|
|
||||||
run_finished_at TIMESTAMPTZ,
|
|
||||||
rows_extracted INT,
|
|
||||||
rows_loaded INT,
|
|
||||||
csv_path TEXT,
|
|
||||||
status TEXT CHECK (status IN ('extracting','uploaded','loading','loaded','failed')),
|
|
||||||
error_message TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_extract_runs_table_time ON dwh_control.extract_runs (table_name, run_started_at DESC);
|
|
||||||
CREATE INDEX idx_extract_runs_status_time ON dwh_control.extract_runs (status, run_finished_at DESC);
|
|
||||||
|
|
||||||
-- Seed one row per incremental table
|
|
||||||
INSERT INTO dwh_control.extract_watermarks (table_name) VALUES
|
|
||||||
('position_history'), ('trips'), ('alarms'),
|
|
||||||
('parking_events'), ('device_events'), ('ingestion_log');
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Scheduling
|
|
||||||
|
|
||||||
- **Cron:** `0 5,8,11,14,17,20,23 * * *` with TZ `Africa/Nairobi` (set in n8n schedule node).
|
|
||||||
- **7 runs/day:** 05:00, 08:00, 11:00, 14:00, 17:00, 20:00, 23:00 EAT.
|
|
||||||
- **Fits the 6–8/day requirement** with even 3-hour gaps in daytime and a silent overnight window (23:00 → 05:00 = 6h) which is fine because device traffic is minimal after hours.
|
|
||||||
- First run of each day (05:00) will carry the overnight backlog — this is the expected behaviour of the watermark design.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling & Observability
|
|
||||||
|
|
||||||
### Per-table isolation
|
|
||||||
Workflow 1 iterates tables in sequence; a failure on one table does not block others. Every table's result (success or failure) is logged to `dwh_control.extract_runs`.
|
|
||||||
|
|
||||||
### Retryable failures
|
|
||||||
If Workflow 2 fails mid-load: transaction rolls back → watermark stays → CSV stays in `exports/` → next scheduled run re-processes it (natural retry).
|
|
||||||
|
|
||||||
### Alerting (Grafana panels on `tracksolid_dwh`, read via `dwh_ro` role — see below)
|
|
||||||
- **Freshness:** `SELECT table_name, NOW() - MAX(run_finished_at) AS lag FROM dwh_control.extract_runs WHERE status='loaded' GROUP BY 1 HAVING NOW() - MAX(run_finished_at) > INTERVAL '4 hours';`
|
|
||||||
- **Failures in last hour:** `SELECT * FROM dwh_control.extract_runs WHERE status='failed' AND run_started_at > NOW() - INTERVAL '1 hour';`
|
|
||||||
- **Row count sanity:** `rows_extracted != rows_loaded` flags CSV parse or load issues.
|
|
||||||
|
|
||||||
### n8n-level error workflow
|
|
||||||
Attach an "Error Workflow" in both n8n workflows that posts to a webhook (existing pattern in `n8n-workflows/`) for immediate notification.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security & Credentials
|
|
||||||
|
|
||||||
Both DB credentials already exist in n8n (connections trialled and working). The required credential shapes are:
|
|
||||||
|
|
||||||
| n8n credential | Host / Port / DB | Recommended user | Usage |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `tracksolid_source` | Coolify internal `timescale_db:5432` → DB `tracksolid_db` | `grafana_ro` (read-only) | Source extract queries |
|
|
||||||
| `tracksolid_dwh_target` | `31.97.44.246:5888` → DB `tracksolid_dwh` | `dwh_owner` (scoped) | Bronze writes + control-table updates |
|
|
||||||
| `rustfs_s3` | `${RUSTFS_ENDPOINT}` | `${RUSTFS_ACCESS_KEY}` | CSV upload/download/move |
|
|
||||||
|
|
||||||
### Credential-hardening recommendations (current state vs target state)
|
|
||||||
|
|
||||||
The trial connection string uses `postgres` (superuser) over a public IP. Two hardening steps to take before production:
|
|
||||||
|
|
||||||
1. **Create a scoped `dwh_owner` role** on `tracksolid_dwh` — owns only `bronze` + `dwh_control` schemas, cannot touch other DBs or cluster roles. n8n's `tracksolid_dwh_target` credential switches to this user.
|
|
||||||
2. **Create a `dwh_ro` role** for Grafana panels — read-only across `bronze` + `dwh_control`. This is what the freshness/failure dashboards in §Error Handling use.
|
|
||||||
3. **Enforce `sslmode=require`** on the `tracksolid_dwh_target` connection string (public-IP hop, cleartext otherwise).
|
|
||||||
4. **Rotate the `postgres` password** that was shared in chat history — one-off cleanup, not a plan blocker.
|
|
||||||
|
|
||||||
All four are one-migration-file tasks and fit naturally into the `dwh/261001_dwh_control.sql` setup step.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files to Create / Modify
|
|
||||||
|
|
||||||
| Path | Action | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `dwh/261001_dwh_control.sql` | **new** | Control-schema migration (watermarks + run log) |
|
|
||||||
| `dwh/260423_dwh_ddl_v1.sql` | **review** | Confirm bronze tables have matching unique constraints; patch if missing |
|
|
||||||
| `n8n-workflows/dwh_extract.json` | **new** | Workflow 1 export |
|
|
||||||
| `n8n-workflows/dwh_load_bronze.json` | **new** | Workflow 2 export |
|
|
||||||
| `docs/DWH_PIPELINE.md` | **new** | Operations runbook (see verification section) |
|
|
||||||
| `CLAUDE.md` §3, §4, §5, §10 | **update** | Add `tracksolid_dwh@31.97.44.246:5888` to §3 Connection Params; add bronze schema + n8n DWH workflows to codebase map; remove DWH item from Open Items |
|
|
||||||
|
|
||||||
**Existing utilities to reuse (do NOT reinvent):**
|
|
||||||
- Rustfs env vars already wired in `docker-compose.yaml` (`RUSTFS_ENDPOINT`, `RUSTFS_ACCESS_KEY`, `RUSTFS_SECRET_KEY`, `RUSTFS_BUCKET`) — Workflow nodes read from the same `.env`.
|
|
||||||
- Backup rustfs client logic in `backup/backup_db.sh` is the reference pattern for S3 auth shape.
|
|
||||||
- Existing n8n workflow pattern in `n8n-workflows/jimi_pushgps.json` et al. for webhook trigger + HTTP-forward shape.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
### Pre-deployment checks (before first cron trigger)
|
|
||||||
1. **Bronze DDL applied:** `psql -h 31.97.44.246 -p 5888 -U dwh_owner -d tracksolid_dwh -c "\dt bronze.*"` lists 16 tables.
|
|
||||||
2. **Control schema applied:** same connection, `\dt dwh_control.*` lists `extract_watermarks`, `extract_runs`.
|
|
||||||
3. **Watermarks seeded:** `SELECT * FROM dwh_control.extract_watermarks;` returns 6 rows, all with `last_extracted_at = 2026-01-01`.
|
|
||||||
4. **Roles created:** `\du` lists `dwh_owner` and `dwh_ro`; `postgres` superuser no longer used for n8n.
|
|
||||||
5. **n8n credentials:** Test each credential individually in n8n UI — all three connect successfully (source via internal network, target via `31.97.44.246:5888` with `sslmode=require`).
|
|
||||||
6. **Rustfs path exists:** `aws --endpoint ${RUSTFS_ENDPOINT} s3 ls s3://fleet-db/dwh/` — if missing, create `exports/` and `processed/` prefixes.
|
|
||||||
|
|
||||||
### First-run verification (manually trigger Workflow 1)
|
|
||||||
1. `SELECT * FROM dwh_control.extract_runs ORDER BY run_id DESC LIMIT 20;` — 8 rows (one per table processed), all `status='loaded'`.
|
|
||||||
2. `SELECT table_name, rows_loaded_last_run FROM dwh_control.extract_watermarks;` — non-zero for all incremental tables that have source data.
|
|
||||||
3. Row-count parity:
|
|
||||||
```sql
|
|
||||||
-- on source (tracksolid_db, Coolify internal)
|
|
||||||
SELECT COUNT(*) FROM tracksolid.position_history;
|
|
||||||
-- on target (tracksolid_dwh @ 31.97.44.246:5888)
|
|
||||||
SELECT COUNT(*) FROM bronze.position_history;
|
|
||||||
```
|
|
||||||
Numbers should match ± rows inserted in the narrow window between the two queries.
|
|
||||||
4. **Geometry round-trip check:**
|
|
||||||
```sql
|
|
||||||
SELECT ST_AsText(geom) FROM bronze.position_history LIMIT 5;
|
|
||||||
-- should return valid POINT(lng lat) values, not NULL or garbage
|
|
||||||
```
|
|
||||||
5. **Rustfs audit:** `aws s3 ls s3://fleet-db/dwh/processed/` — 8 CSV files present (one per table), originals no longer in `exports/`.
|
|
||||||
|
|
||||||
### Steady-state verification (after 24h / 7 runs)
|
|
||||||
1. `SELECT table_name, NOW() - MAX(run_finished_at) FROM dwh_control.extract_runs WHERE status='loaded' GROUP BY 1;` — max lag < 3h 15min for every table.
|
|
||||||
2. `SELECT COUNT(*) FROM dwh_control.extract_runs WHERE status='failed';` — zero.
|
|
||||||
3. Grafana dashboard (to be added in a follow-up plan) shows freshness and row counts per table.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Out of Scope (follow-up work)
|
|
||||||
|
|
||||||
- Silver/gold layer transformations (the DWH DDL defines schemas but no queries yet).
|
|
||||||
- Bronze schema evolution tooling (manual migrations are acceptable for one pipeline).
|
|
||||||
- Backfill of tables where webhooks aren't yet registered (OBD, fuel, temperature, LBS).
|
|
||||||
- Grafana dashboard panels for the DWH — worth its own spec once we have a week of data to design around.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Open Questions (none blocking)
|
|
||||||
|
|
||||||
All design decisions resolved in the brainstorming session. Confirmed:
|
|
||||||
- Source: `tracksolid_db` on Coolify, reached via internal Docker network.
|
|
||||||
- Target: `tracksolid_dwh` at `31.97.44.246:5888` (public IP), schemas `bronze`/`silver`/`gold` + `dwh_control`.
|
|
||||||
- Trial connections already working in n8n.
|
|
||||||
|
|
||||||
If any endpoint/credential changes during implementation, those are n8n-credential updates only — no design change.
|
|
||||||
|
|
@ -1,367 +0,0 @@
|
||||||
-- =============================================================
|
|
||||||
-- TRACKSOLID DWH SETUP & PERMISSIONS
|
|
||||||
-- Target Database: tracksolid_dwh
|
|
||||||
-- =============================================================
|
|
||||||
|
|
||||||
-- 1. EXTENSIONS
|
|
||||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
|
||||||
CREATE EXTENSION IF NOT EXISTS btree_gist;
|
|
||||||
CREATE EXTENSION IF NOT EXISTS postgis; -- REQUIRED for geometry(Point,4326) columns
|
|
||||||
|
|
||||||
-- 2. ROLE CREATION (Idempotent)
|
|
||||||
-- SECURITY: Passwords below are placeholders. Before applying this file:
|
|
||||||
-- 1. Generate two strong secrets (e.g. `openssl rand -hex 24`)
|
|
||||||
-- 2. Replace both CHANGE_ME_BEFORE_APPLY tokens in-session (do NOT commit real values)
|
|
||||||
-- 3. Store the generated secrets in the n8n / Grafana credential stores only
|
|
||||||
-- Rotation: `ALTER ROLE <role> PASSWORD '<new secret>'` as a superuser.
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'dwh_owner') THEN
|
|
||||||
CREATE ROLE dwh_owner WITH LOGIN PASSWORD 'CHANGE_ME_BEFORE_APPLY';
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'grafana_ro') THEN
|
|
||||||
CREATE ROLE grafana_ro WITH LOGIN PASSWORD 'CHANGE_ME_BEFORE_APPLY';
|
|
||||||
END IF;
|
|
||||||
END$$;
|
|
||||||
|
|
||||||
-- Grant database connection
|
|
||||||
GRANT CONNECT ON DATABASE tracksolid_dwh TO dwh_owner;
|
|
||||||
GRANT CONNECT ON DATABASE tracksolid_dwh TO grafana_ro;
|
|
||||||
|
|
||||||
-- 3. SCHEMAS
|
|
||||||
CREATE SCHEMA IF NOT EXISTS bronze AUTHORIZATION dwh_owner;
|
|
||||||
CREATE SCHEMA IF NOT EXISTS silver AUTHORIZATION dwh_owner;
|
|
||||||
CREATE SCHEMA IF NOT EXISTS gold AUTHORIZATION dwh_owner;
|
|
||||||
|
|
||||||
ALTER DATABASE tracksolid_dwh SET search_path TO bronze, silver, gold, public;
|
|
||||||
|
|
||||||
-- 4. PERMISSIONS & DEFAULT PRIVILEGES (Critical for Security & Automation)
|
|
||||||
-- Schema access
|
|
||||||
GRANT USAGE, CREATE ON SCHEMA bronze TO dwh_owner;
|
|
||||||
GRANT USAGE, CREATE ON SCHEMA silver TO dwh_owner;
|
|
||||||
GRANT USAGE, CREATE ON SCHEMA gold TO dwh_owner;
|
|
||||||
GRANT USAGE ON SCHEMA bronze TO grafana_ro;
|
|
||||||
GRANT USAGE ON SCHEMA silver TO grafana_ro;
|
|
||||||
GRANT USAGE ON SCHEMA gold TO grafana_ro;
|
|
||||||
GRANT USAGE ON SCHEMA public TO dwh_owner, grafana_ro;
|
|
||||||
|
|
||||||
-- Existing table access for Grafana
|
|
||||||
GRANT SELECT ON ALL TABLES IN SCHEMA bronze TO grafana_ro;
|
|
||||||
GRANT SELECT ON ALL TABLES IN SCHEMA silver TO grafana_ro;
|
|
||||||
GRANT SELECT ON ALL TABLES IN SCHEMA gold TO grafana_ro;
|
|
||||||
|
|
||||||
-- FUTURE table access: Any table created by dwh_owner will automatically be readable by grafana_ro
|
|
||||||
ALTER DEFAULT PRIVILEGES FOR ROLE dwh_owner IN SCHEMA bronze GRANT SELECT ON TABLES TO grafana_ro;
|
|
||||||
ALTER DEFAULT PRIVILEGES FOR ROLE dwh_owner IN SCHEMA silver GRANT SELECT ON TABLES TO grafana_ro;
|
|
||||||
ALTER DEFAULT PRIVILEGES FOR ROLE dwh_owner IN SCHEMA gold GRANT SELECT ON TABLES TO grafana_ro;
|
|
||||||
|
|
||||||
-- 5. BRONZE SCHEMA TABLES
|
|
||||||
-- Run as dwh_owner to ensure correct ownership & default privileges apply
|
|
||||||
SET ROLE dwh_owner;
|
|
||||||
SET search_path TO bronze, public;
|
|
||||||
|
|
||||||
-- 5.1 DEVICES (Slowly Changing Dimension - Type 2 handled in Silver)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.devices (
|
|
||||||
imei TEXT PRIMARY KEY,
|
|
||||||
device_name TEXT,
|
|
||||||
mc_type TEXT,
|
|
||||||
mc_type_use_scope TEXT,
|
|
||||||
vehicle_name TEXT,
|
|
||||||
vehicle_number TEXT,
|
|
||||||
vehicle_models TEXT,
|
|
||||||
vehicle_icon TEXT,
|
|
||||||
vin TEXT,
|
|
||||||
engine_number TEXT,
|
|
||||||
vehicle_brand TEXT,
|
|
||||||
fuel_100km NUMERIC(6,2),
|
|
||||||
driver_name TEXT,
|
|
||||||
driver_phone TEXT,
|
|
||||||
sim TEXT,
|
|
||||||
iccid TEXT,
|
|
||||||
imsi TEXT,
|
|
||||||
account TEXT,
|
|
||||||
customer_name TEXT,
|
|
||||||
device_group_id TEXT,
|
|
||||||
device_group TEXT,
|
|
||||||
activation_time TIMESTAMPTZ,
|
|
||||||
expiration TIMESTAMPTZ,
|
|
||||||
enabled_flag SMALLINT DEFAULT 1 NOT NULL,
|
|
||||||
status TEXT DEFAULT 'active'::text,
|
|
||||||
city TEXT,
|
|
||||||
current_mileage_km NUMERIC(12,2),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
last_synced_at TIMESTAMPTZ,
|
|
||||||
vehicle_category TEXT,
|
|
||||||
cost_centre TEXT,
|
|
||||||
assigned_route TEXT,
|
|
||||||
depot_geom geometry(Point,4326),
|
|
||||||
depot_address TEXT,
|
|
||||||
assigned_city TEXT,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.2 POSITION HISTORY (High-volume fact table)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.position_history (
|
|
||||||
imei TEXT NOT NULL,
|
|
||||||
gps_time TIMESTAMPTZ NOT NULL,
|
|
||||||
geom geometry(Point,4326),
|
|
||||||
lat DOUBLE PRECISION,
|
|
||||||
lng DOUBLE PRECISION,
|
|
||||||
speed NUMERIC(7,2),
|
|
||||||
direction NUMERIC(6,2),
|
|
||||||
acc_status TEXT,
|
|
||||||
satellite SMALLINT,
|
|
||||||
current_mileage NUMERIC(12,2),
|
|
||||||
recorded_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
altitude NUMERIC(8,2),
|
|
||||||
post_type SMALLINT,
|
|
||||||
source TEXT DEFAULT 'poll'::text,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
PRIMARY KEY (imei, gps_time)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.3 TRIPS (Aggregated fact table)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.trips (
|
|
||||||
id BIGINT NOT NULL,
|
|
||||||
imei TEXT NOT NULL,
|
|
||||||
start_time TIMESTAMPTZ NOT NULL,
|
|
||||||
end_time TIMESTAMPTZ,
|
|
||||||
start_geom geometry(Point,4326),
|
|
||||||
end_geom geometry(Point,4326),
|
|
||||||
distance_km NUMERIC(12,2),
|
|
||||||
avg_speed_kmh NUMERIC(7,2),
|
|
||||||
max_speed_kmh NUMERIC(7,2),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
fuel_consumed_l NUMERIC(8,2),
|
|
||||||
idle_time_s INTEGER,
|
|
||||||
driving_time_s INTEGER,
|
|
||||||
trip_seq INTEGER,
|
|
||||||
source TEXT DEFAULT 'poll'::text,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
PRIMARY KEY (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.4 ALARMS (Event log fact table)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.alarms (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
imei TEXT,
|
|
||||||
alarm_type TEXT,
|
|
||||||
alarm_time TIMESTAMPTZ,
|
|
||||||
geom geometry(Point,4326),
|
|
||||||
lat DOUBLE PRECISION,
|
|
||||||
lng DOUBLE PRECISION,
|
|
||||||
speed NUMERIC(7,2),
|
|
||||||
acc_status TEXT,
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
alarm_name TEXT,
|
|
||||||
source TEXT DEFAULT 'poll'::text,
|
|
||||||
severity TEXT,
|
|
||||||
geofence_id TEXT,
|
|
||||||
geofence_name TEXT,
|
|
||||||
acknowledged_at TIMESTAMPTZ,
|
|
||||||
acknowledged_by TEXT,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.5 DEVICE EVENTS (Connection lifecycle)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.device_events (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
imei TEXT NOT NULL,
|
|
||||||
event_type TEXT NOT NULL,
|
|
||||||
event_time TIMESTAMPTZ NOT NULL,
|
|
||||||
timezone TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.6 DISPATCH LOG (Operational/SLA tracking)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.dispatch_log (
|
|
||||||
dispatch_id BIGINT PRIMARY KEY,
|
|
||||||
ticket_id TEXT NOT NULL,
|
|
||||||
imei TEXT NOT NULL,
|
|
||||||
driver_name TEXT,
|
|
||||||
job_lat DOUBLE PRECISION NOT NULL,
|
|
||||||
job_lng DOUBLE PRECISION NOT NULL,
|
|
||||||
job_geom geometry(Point,4326),
|
|
||||||
assigned_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
first_movement_at TIMESTAMPTZ,
|
|
||||||
on_site_at TIMESTAMPTZ,
|
|
||||||
resolved_at TIMESTAMPTZ,
|
|
||||||
cancelled_at TIMESTAMPTZ,
|
|
||||||
distance_km NUMERIC(8,2),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.7 FAULT CODES (OBD/DTC diagnostics)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.fault_codes (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
imei TEXT NOT NULL,
|
|
||||||
reported_at TIMESTAMPTZ NOT NULL,
|
|
||||||
fault_code TEXT NOT NULL,
|
|
||||||
status_flags INTEGER,
|
|
||||||
lat DOUBLE PRECISION,
|
|
||||||
lng DOUBLE PRECISION,
|
|
||||||
geom geometry(Point,4326),
|
|
||||||
event_time TIMESTAMPTZ,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.8 FUEL READINGS
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.fuel_readings (
|
|
||||||
imei TEXT NOT NULL,
|
|
||||||
reading_time TIMESTAMPTZ NOT NULL,
|
|
||||||
sensor_path TEXT,
|
|
||||||
value NUMERIC(10,3),
|
|
||||||
unit TEXT,
|
|
||||||
lat DOUBLE PRECISION,
|
|
||||||
lng DOUBLE PRECISION,
|
|
||||||
geom geometry(Point,4326),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
PRIMARY KEY (imei, reading_time)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.9 GEOFENCES (Dimension/Reference)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.geofences (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
fence_id TEXT,
|
|
||||||
fence_name TEXT NOT NULL,
|
|
||||||
fence_type TEXT,
|
|
||||||
geom geometry(Geometry,4326),
|
|
||||||
radius_m NUMERIC(10,2),
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.10 HEARTBEATS (Device health/ping)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.heartbeats (
|
|
||||||
imei TEXT NOT NULL,
|
|
||||||
gate_time TIMESTAMPTZ NOT NULL,
|
|
||||||
power_level SMALLINT,
|
|
||||||
gsm_signal SMALLINT,
|
|
||||||
acc_status SMALLINT,
|
|
||||||
power_status SMALLINT,
|
|
||||||
fortify SMALLINT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
PRIMARY KEY (imei, gate_time)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.11 INGESTION LOG (Metadata for tracking loads)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.ingestion_log (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
run_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
endpoint TEXT NOT NULL,
|
|
||||||
imei_count INTEGER DEFAULT 0 NOT NULL,
|
|
||||||
rows_upserted INTEGER DEFAULT 0 NOT NULL,
|
|
||||||
rows_inserted INTEGER DEFAULT 0 NOT NULL,
|
|
||||||
duration_ms INTEGER DEFAULT 0 NOT NULL,
|
|
||||||
success BOOLEAN DEFAULT true NOT NULL,
|
|
||||||
error_code TEXT,
|
|
||||||
error_message TEXT,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.12 LBS READINGS (Fallback positioning)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.lbs_readings (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
imei TEXT NOT NULL,
|
|
||||||
gate_time TIMESTAMPTZ NOT NULL,
|
|
||||||
post_type TEXT,
|
|
||||||
lbs_data JSONB,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.13 LIVE POSITIONS (Current state snapshot)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.live_positions (
|
|
||||||
imei TEXT PRIMARY KEY,
|
|
||||||
geom geometry(Point,4326),
|
|
||||||
lat DOUBLE PRECISION,
|
|
||||||
lng DOUBLE PRECISION,
|
|
||||||
pos_type TEXT,
|
|
||||||
confidence SMALLINT,
|
|
||||||
gps_time TIMESTAMPTZ,
|
|
||||||
hb_time TIMESTAMPTZ,
|
|
||||||
speed NUMERIC(7,2),
|
|
||||||
direction NUMERIC(6,2),
|
|
||||||
acc_status TEXT,
|
|
||||||
gps_signal SMALLINT,
|
|
||||||
gps_num SMALLINT,
|
|
||||||
elec_quantity NUMERIC(5,2),
|
|
||||||
power_value NUMERIC(5,2),
|
|
||||||
battery_power_val NUMERIC(5,2),
|
|
||||||
tracker_oil TEXT,
|
|
||||||
temperature NUMERIC(8,2),
|
|
||||||
current_mileage NUMERIC(12,2),
|
|
||||||
device_status TEXT,
|
|
||||||
expire_flag TEXT,
|
|
||||||
activation_flag TEXT,
|
|
||||||
loc_desc TEXT,
|
|
||||||
recorded_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.14 OBD READINGS (Vehicle diagnostics)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.obd_readings (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
imei TEXT,
|
|
||||||
reading_time TIMESTAMPTZ,
|
|
||||||
engine_rpm INTEGER,
|
|
||||||
fuel_level_pct NUMERIC(5,2),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
car_type SMALLINT,
|
|
||||||
acc_state SMALLINT,
|
|
||||||
status_flags INTEGER,
|
|
||||||
lat DOUBLE PRECISION,
|
|
||||||
lng DOUBLE PRECISION,
|
|
||||||
geom geometry(Point,4326),
|
|
||||||
obd_data JSONB,
|
|
||||||
coolant_temp_c NUMERIC(6,2),
|
|
||||||
battery_voltage NUMERIC(5,2),
|
|
||||||
intake_pressure NUMERIC(6,2),
|
|
||||||
throttle_pct NUMERIC(5,2),
|
|
||||||
vehicle_speed NUMERIC(7,2),
|
|
||||||
engine_load_pct NUMERIC(5,2),
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.15 PARKING EVENTS
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.parking_events (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
imei TEXT NOT NULL,
|
|
||||||
event_type TEXT,
|
|
||||||
start_time TIMESTAMPTZ NOT NULL,
|
|
||||||
end_time TIMESTAMPTZ,
|
|
||||||
duration_seconds INTEGER,
|
|
||||||
geom geometry(Point,4326),
|
|
||||||
address TEXT,
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5.16 TEMPERATURE READINGS (Cold chain sensors)
|
|
||||||
CREATE TABLE IF NOT EXISTS bronze.temperature_readings (
|
|
||||||
imei TEXT NOT NULL,
|
|
||||||
reading_time TIMESTAMPTZ NOT NULL,
|
|
||||||
temperature NUMERIC(6,2),
|
|
||||||
humidity_pct NUMERIC(5,2),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
ingested_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
PRIMARY KEY (imei, reading_time)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Reset role back to superuser
|
|
||||||
RESET ROLE;
|
|
||||||
RESET search_path;
|
|
||||||
|
|
||||||
-- 6. VERIFICATION GRANTS (Ensure Grafana can query immediately)
|
|
||||||
GRANT SELECT ON ALL TABLES IN SCHEMA bronze TO grafana_ro;
|
|
||||||
GRANT USAGE ON SCHEMA bronze TO grafana_ro;
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
"imei","device_name","mc_type","mc_type_use_scope","vehicle_name","vehicle_number","vehicle_models","vehicle_icon","vin","engine_number","vehicle_brand","fuel_100km","driver_name","driver_phone","sim","iccid","imsi","account","customer_name","device_group_id","device_group","activation_time","expiration","enabled_flag","status","city","current_mileage_km","created_at","updated_at","last_synced_at","vehicle_category","cost_centre","assigned_route","depot_geom","depot_address","assigned_city"
|
|
||||||
"353549090553685","Daniel Omondi - KMFF 099Z","AT4","personal","KMFF 099Z","KMFF 099Z","Motorbike","mtc",NULL,NULL,NULL,NULL,"Robert","0112794067","759336150","89254021334258404099","639021335840409","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-09-23 05:50:30+00","2040-09-23 23:59:59+00",1,"1",NULL,"2354.70","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"353549090561720","Wireless_Git","AT4","personal",NULL,NULL,NULL,"bus",NULL,NULL,NULL,NULL,NULL,NULL,"0701211913","89254021374215155053","639021371515505","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2025-06-09 09:12:50+00","2035-06-09 23:59:59+00",1,"1",NULL,"5992.43","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"353549090566281","KDR 592N","AT4","personal",NULL,NULL,NULL,"bus",NULL,NULL,NULL,NULL,NULL,NULL,"0797680464","89254021334258159693","639021335815969","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2024-11-08 04:01:30+00","2034-11-08 23:59:59+00",1,"1",NULL,"7771.90","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"353549090566885","Wireless GPS","AT4","personal",NULL,NULL,NULL,"bus",NULL,NULL,NULL,NULL,NULL,NULL,"0768445963","89254021334212352574","639021331235257","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2024-10-15 13:16:57+00","2034-10-15 23:59:59+00",1,"1",NULL,"17036.41","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"353549090567685","Daniel Kipkirui - KMFF 162Z","AT4","personal","KMFF 162Z","KMFF 162Z","Motorbike","mtc",NULL,NULL,NULL,NULL,"edwine","0112795498","742532058","89254021264260388966","639021266038896","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-09-23 05:09:39+00","2040-09-23 23:59:59+00",1,"1",NULL,"462.33","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"353549090567701","Wireless","AT4","personal",NULL,NULL,NULL,"bus",NULL,NULL,NULL,NULL,NULL,NULL,"0790176094","89254021394215205906","639021391520590","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2024-11-08 04:04:44+00","2034-11-08 23:59:59+00",1,"1",NULL,"16896.20","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081885410","Allan Owana - KDK 780K","GT06E","aotomobile","KDK 780K","KDK 780K","Probox","automobile",NULL,NULL,NULL,NULL,"Allan Owana",NULL,"703616117","89254021234222499854","639021232249985","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2019-06-19 09:32:22+00","2039-06-19 23:59:59+00",1,"1",NULL,"128853.11","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081886467","Gideon Kiprono - KCQ 215F","GT06E","aotomobile","KCQ 215F","OHS","Probox","automobile",NULL,NULL,NULL,"0.00","Gideon Kiprono",NULL,"746763076","89254021084186499865","639021088649986","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2019-06-30 09:30:00+00","2039-06-30 23:59:59+00",1,"1",NULL,"141057.46","2026-04-23 10:56:37.983314+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081886871","Kamonde KBA 467S","GT06E","aotomobile",NULL,NULL,NULL,"bus",NULL,NULL,NULL,NULL,NULL,NULL,"0746763083","89254021084186499873","639021088649987","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2019-06-30 09:09:14+00","2039-06-30 23:59:59+00",1,"1",NULL,"74183.36","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081886905","Kennedy Chege - KCQ 618K","GT06E","aotomobile","KCQ 618K","KCQ 618K","Probox","automobile",NULL,NULL,NULL,NULL,"Kennedy Chege",NULL,"746763132","89254021084186499923","639021088649992","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2019-06-30 07:08:35+00","2039-06-30 23:59:59+00",1,"1",NULL,"215608.19","2026-04-23 10:35:37.678371+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081887069","Wright Oseko - KCG 668W","GT06E","aotomobile","KCG 668W","KCG 668W","Probox","automobile",NULL,NULL,NULL,NULL,"Wright Oseko",NULL,"746763106","89254021084186499915","639021088649991","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2019-06-30 06:17:43+00","2039-06-30 23:59:59+00",1,"1",NULL,"239001.19","2026-04-23 11:00:08.769463+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081887192","Ndegwa Dancun - KCG 669W","GT06E","aotomobile","KCG 669W","KCG 669W","Probox","automobile",NULL,NULL,NULL,NULL,"Ndegwa Dancun",NULL,"746760191","89254021084186499501","639021088649950","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2019-06-15 10:26:15+00","2039-06-15 23:59:59+00",1,"1",NULL,"199191.85","2026-04-23 10:34:29.074112+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081891566","Simon Kamau - KCE 090R","GT06E","aotomobile","KCE 090R","KCE 090R","Probox","automobile",NULL,NULL,NULL,NULL,"Simon Kamau",NULL,"746760404","89254021084186499527","639021088649952","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2019-06-16 07:06:15+00","2039-06-16 23:59:59+00",1,"1",NULL,"215592.36","2026-04-23 10:30:55.739184+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081891590","Garage - KCE 699F","GT06E","aotomobile","KCE 699F","KCE 699F","Probox","automobile",NULL,NULL,NULL,NULL,"Garage",NULL,"746760215","89254021084186499519","639021088649951","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2019-06-16 11:11:24+00","2039-06-16 23:59:59+00",1,"1",NULL,"207814.05","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081891632","John Ondego - KCA 542Q","GT06E","aotomobile","KCA 542Q","KCA 542Q","Probox","automobile",NULL,NULL,NULL,NULL,"John Ondego",NULL,"746760038","89254021084186499485","639021088649948","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2019-06-15 09:17:53+00","2039-06-15 23:59:59+00",1,"1",NULL,"178914.47","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081891798","Garage - KCH 167M","GT06E","aotomobile","KCH 167M","KCH 167M","Probox","automobile",NULL,NULL,NULL,NULL,"Garage",NULL,"746760102","89254021084186499493","639021088649949","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2019-06-16 10:18:57+00","2039-06-16 23:59:59+00",1,"1",NULL,"168840.95","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081892101","Cornelius Kimutai - KCU 938R","GT06E","aotomobile","KCU 938R","KCU 938R","Van","automobile",NULL,NULL,NULL,NULL,"Cornelius Kimutai",NULL,"746759919","89254021084186499451","639021088649945","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2019-06-12 08:13:48+00","2039-06-12 23:59:59+00",1,"1",NULL,"149558.50","2026-04-23 10:29:21.507861+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081892309","Nicholas Erastus - KCQ 581M","GT06E","aotomobile","KCQ 581M","KCQ 581M","Probox","automobile",NULL,NULL,NULL,NULL,"Nicholas Erastus",NULL,"700023776","89254021084178504672","639021087850467","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2019-06-09 09:39:40+00","2039-06-09 23:59:59+00",1,"1",NULL,"209105.89","2026-04-23 10:40:40.169684+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081892440","KAZ 489Z","GT06E","aotomobile",NULL,NULL,NULL,"bus",NULL,NULL,NULL,NULL,NULL,NULL,"0700023806","89254021084178504698","639021087850469","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2019-06-09 10:04:39+00","2039-06-09 23:59:59+00",1,"1",NULL,"38197.20","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857081892762","Nicholas","GT06E","aotomobile",NULL,NULL,"Station Wagon","bus",NULL,NULL,"Toyota",NULL,NULL,NULL,"0746760503","89254021274233125361","639021273312536","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2019-06-16 08:31:46+00","2039-06-16 23:59:59+00",1,"1",NULL,"51048.97","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082037185","Amani Kazungu - KCY 084X","GT06E","aotomobile","KCY 084X","KCY 084X","Probox","automobile",NULL,NULL,NULL,NULL,"Amani Kazungu",NULL,"757338522","89254021154287000597","639021158700059","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-07-13 09:42:28+00","2040-07-13 23:59:59+00",1,"1",NULL,"172298.81","2026-04-23 10:51:08.665273+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082038977","Wilfred Kinyanjui - KCU 729C","GT06E","aotomobile","KCU 729C","KCU 729C","Crane","truck",NULL,NULL,NULL,NULL,"Wilfred Kinyanjui",NULL,"110094469","89254021164215938057","639021161593805","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-04-05 09:26:00+00","2040-04-05 23:59:59+00",1,"1",NULL,"172487.09","2026-04-23 10:24:33.914628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082040981","Amani Sulubu - KCY 090X","GT06E","aotomobile","KCY 090X","KCY 090X","Probox","automobile",NULL,NULL,NULL,NULL,"Amani Sulubu",NULL,"793375853","89254021064168004164","639021066800416","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-07-13 07:25:16+00","2040-07-13 23:59:59+00",1,"1",NULL,"166028.15","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082042052","Gabriel Musumba - KCE 690F","GT06E","aotomobile","KCE 690F","KCE 690F","Probox","automobile",NULL,NULL,NULL,NULL,"Gabriel Musumba",NULL,"110094466","89254021164215938024","639021161593802","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2020-04-03 17:30:13+00","2040-04-03 23:59:59+00",1,"1",NULL,"192693.23","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082042854","Elias Baya - KCZ 476E","GT06E","aotomobile","KCZ 476E","KCZ 476E","Probox","automobile",NULL,NULL,NULL,NULL,"Elias Baya",NULL,"110941187","89254021164224352993","639021162435299","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-08-09 05:06:42+00","2040-08-09 23:59:59+00",1,"1",NULL,"217595.68","2026-04-23 10:33:56.216621+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082042953","KCU 865Q Vanguard",NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,"unknown",NULL,NULL,"2026-04-23 13:24:33.293453+00","2026-04-23 13:24:33.293453+00",NULL,NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082044280","Lawrence Kijogi - KCY 080X","GT06E","aotomobile","KCY 080X","KCY 080X","Probox","automobile",NULL,NULL,NULL,NULL,"Lawrence Kijogi",NULL,"708155933","89254029851005131222","639029850513122","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-07-13 11:05:02+00","2040-07-13 11:05:02+00",1,"1",NULL,"169740.37","2026-04-23 14:52:58.983571+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082046145","Joseph Kabandi - KCY 076X","GT06E","aotomobile","KCY 076X","KCY 076X","Probox","automobile",NULL,NULL,NULL,NULL,"Joseph Kabandi",NULL,"110850007","89254021164223447158","639021162344715","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-07-13 08:31:26+00","2040-07-13 23:59:59+00",1,"1",NULL,"122254.48","2026-04-23 10:47:40.895504+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082896911","Hamisi Pande - KDD 689Y","GT06E","aotomobile","KDD 689Y","KDD 689Y","Probox","automobile",NULL,NULL,NULL,NULL,"Hamisi Pande",NULL,"112714612","89254021214211314660","639021211131466","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-09-17 11:50:53+00","2041-09-17 23:59:59+00",1,"1",NULL,"163435.74","2026-04-23 10:26:09.922447+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082897091","Peter Mbugua - KDK 728K","GT06E","aotomobile","KDK 728K","KDK 728K","Probox","automobile",NULL,NULL,NULL,NULL,"Peter Mbugua",NULL,"790262984","89254021234222500396","639021232250039","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-12-14 11:31:57+00","2042-12-14 23:59:59+00",1,"1",NULL,"131109.26","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082897257","Cassius Wakiyo - KDB 323M","GT06E","aotomobile","KDB 323M","KDB 323M","Probox","automobile",NULL,NULL,NULL,NULL,"Cassius Wakiyo",NULL,"746428882","89254021234222500818","639021232250081","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-08-29 15:07:26+00","2041-08-29 15:07:26+00",1,"1",NULL,"121688.92","2026-04-23 10:28:26.388654+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082897737","John Makori - KDB 585E","GT06E","aotomobile","KDB 585E","KDB 585E","Probox","automobile",NULL,NULL,NULL,NULL,"John Makori",NULL,"114596734","89254021214211145262","639021211114526","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-08-29 14:29:28+00","2041-08-29 14:29:28+00",1,"1",NULL,"156765.03","2026-04-23 10:38:57.445964+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082897794","Mutuku Joseph - KDC 739F","GT06E","aotomobile","KDC 739F","KDC 739F","Probox","automobile",NULL,NULL,NULL,NULL,"Mutuku Joseph","115019037","115019037","89254021224222632356","639021222263235","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-04-10 14:55:32+00","2041-04-10 14:55:32+00",1,"1",NULL,"205169.79","2026-04-23 10:30:22.530563+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082898008","Samuel Ng'ang'a - KDE 264M","GT06E","aotomobile","KDE 264M","KDE 264M","Probox","automobile",NULL,NULL,NULL,NULL,"Samuel Ng'ang'a",NULL,"711731539","89254021264260342245","639021266034224","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-10-28 09:43:11+00","2041-10-28 23:59:59+00",1,"1",NULL,"126584.24","2026-04-23 11:35:59.816581+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082898016","Job Ngare - KDM 309S","GT06E","aotomobile","KDM 309S","KDM 309S","Probox","automobile",NULL,NULL,NULL,NULL,"Job Ngare",NULL,"706895756","89254021324273007563","639021327300756","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2023-08-15 13:45:26+00","2033-08-15 23:59:59+00",1,"1",NULL,"107726.56","2026-04-23 11:20:25.939244+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082898073","Mutuku Antony - KDK 732K","GT06E","aotomobile","KDK 732K","KDK 732K","Probox","automobile",NULL,NULL,NULL,NULL,"Mutuku Antony",NULL,"793026954","89254021234222387539","639021232238753","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-12-20 06:33:12+00","2042-12-20 23:59:59+00",1,"1",NULL,"82096.79","2026-04-23 14:52:07.094447+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082898487","Dan Watila - KDE 638J","GT06E","aotomobile","KDE 638J","KDE 638J","Probox","automobile",NULL,NULL,NULL,NULL,"Dan Watila",NULL,"116242996","89254021334258404214","639021335840421","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-10-21 15:50:06+00","2041-10-21 23:59:59+00",1,"1",NULL,"123872.36","2026-04-23 10:31:45.186653+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082900168","KDD 913G_Ruth Mazda",NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,"unknown",NULL,NULL,"2026-04-23 15:09:48.575568+00","2026-04-23 15:09:48.575568+00",NULL,NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082900341","Simon Munda - KCZ 154S","GT06E","aotomobile","KCZ 154S","KCZ 154S","Probox","automobile",NULL,NULL,NULL,NULL,"Simon Munda",NULL,"757236135","89254021154296723312","639021159672331","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-09-23 17:12:51+00","2040-09-23 23:59:59+00",1,"1",NULL,"186504.10","2026-04-23 10:45:21.454595+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082900358","Geoffrey Too - KDM 308S","GT06E","aotomobile","KDM 308S","KDM 308S","Probox","automobile",NULL,NULL,NULL,NULL,"Geoffrey Too",NULL,"796527601","89254021264260126572","639021266012657","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-10-21 15:25:28+00","2041-10-21 23:59:59+00",1,"1",NULL,"142216.91","2026-04-23 12:35:06.661934+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082900697","George Ochieng' - KDD 684Y","GT06E","aotomobile","KDD 684Y","KDD 684Y","Probox","automobile",NULL,NULL,NULL,NULL,"George Ochieng'",NULL,"114879518","89254021214211314678","639021211131467","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-09-17 10:53:11+00","2041-09-17 23:59:59+00",1,"1",NULL,"152820.07","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082902461","Sadique Wakayula - KDC 490Q","GT06E","aotomobile","KDC 490Q","KDC 490Q","Crane","truck",NULL,NULL,NULL,NULL,"Sadique Wakayula",NULL,"757556468","89254021154296722488","639021159672248","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-05-22 11:27:30+00","2041-05-22 11:27:30+00",1,"1",NULL,"183009.52","2026-04-23 11:16:03.730519+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082902503","Felix Andole - KDC 207R","GT06E","aotomobile","KDC 207R","KDC 207R","Probox","automobile",NULL,NULL,NULL,NULL,"Felix Andole",NULL,"794820817","89254021224270993254","639021227099325","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-05-15 11:38:24+00","2041-05-15 11:38:24+00",1,"1",NULL,"208724.46","2026-04-23 15:32:46.935568+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082907973","Felix Muema - KCZ 223P","GT06E","aotomobile","KCZ 223P","KCZ 223P","Probox","automobile",NULL,NULL,NULL,NULL,"Felix Muema",NULL,"757843826","89254021154287138371","639021158713837","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-08-22 14:01:25+00","2040-08-22 23:59:59+00",1,"1",NULL,"222126.36","2026-04-23 10:26:48.220151+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082908500","Santoes Omondi - KCZ 181P","GT06E","aotomobile","KCZ 181P","KCZ 181P","Pick-Up","automobile",NULL,NULL,NULL,NULL,"Santoes Omondi",NULL,"701211974","89254021374215155087","639021371515508","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-08-23 08:58:55+00","2040-08-23 23:59:59+00",1,"1",NULL,"221339.62","2026-04-23 10:48:09.537346+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082910589","Patric Bett - KDA 609E","GT06E","aotomobile","KDA 609E","KDA 609E","Probox","automobile",NULL,NULL,NULL,NULL,"Patric Bett",NULL,"797622637","89254021154296722496","639021159672249","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2020-10-26 15:46:41+00","2040-10-26 23:59:59+00",1,"1",NULL,"194618.69","2026-04-23 10:34:25.350862+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082910886","Makanda Andrew - KCZ 155P","GT06E","aotomobile","KCZ 155P","KCZ 155P","Pick-Up","automobile",NULL,NULL,NULL,NULL,"Makanda Andrew",NULL,"745067338","89254021154287138397","639021158713839","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-08-23 11:52:35+00","2040-08-23 23:59:59+00",1,"1",NULL,"231065.89","2026-04-23 11:36:31.150282+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082911983","Brian Ngetich - KDA 717B","GT06E","aotomobile","KDA 717B","KDA 717B","Probox","automobile",NULL,NULL,NULL,NULL,"Brian Ngetich","795188807","795188807","89254021214211145288","639021211114528","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-08-29 07:21:43+00","2041-08-29 07:21:43+00",1,"1",NULL,"145404.96","2026-04-23 10:36:11.774166+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082912239","Dickson Jaoko - KDK 815R","GT06E","aotomobile","KDK 815R","KDK 815R","Probox","automobile",NULL,NULL,"Probox","0.00","Sammy",NULL,"706392117","89254021234296021287","639021239602128","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2023-06-21 07:14:51+00","2033-06-21 23:59:59+00",1,"1",NULL,"77008.75","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082912486","Moses Wambua - KCZ 751V","GT06E","aotomobile","KCZ 751V","KCZ 751V","Probox","automobile",NULL,NULL,NULL,NULL,"Moses Wambua","0792756503","792756503","89254021154296723437","639021159672343","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-09-23 10:14:28+00","2040-09-23 23:59:59+00",1,"1",NULL,"139762.20","2026-04-23 10:41:00.207177+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082916826","Denis Kazungu - KDM 794R","GT06E","aotomobile","KDM 794R","KDM 794R","Probox","automobile",NULL,NULL,NULL,NULL,"Denis Kazungu",NULL,"705700971","89254021324273006854","639021327300685","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2023-08-21 06:38:00+00","2033-08-21 23:59:59+00",1,"1",NULL,"79639.71","2026-04-23 20:18:46.496567+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082918012","Charles Nyambane - KCB 711C","GT06E","aotomobile","KCB 711C","KCB 711C","Probox","automobile",NULL,NULL,NULL,NULL,"Charles Nyambane",NULL,"793704231","89254021154287138363","639021158713836","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2020-09-21 10:48:35+00","2040-09-21 23:59:59+00",1,"1",NULL,"159597.27","2026-04-23 10:25:52.843474+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082918038","Mbuvi Kioko - KCC 199P","GT06E","aotomobile","KCC 199P","KCC 199P","Pick-Up","automobile",NULL,NULL,NULL,NULL,"Mbuvi Kioko",NULL,"797318126","89254021154287138389","639021158713838","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-08-22 15:26:27+00","2040-08-22 23:59:59+00",1,"1",NULL,"222106.80","2026-04-23 12:09:05.609075+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082918186","KDD 977T Fielder",NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,"unknown",NULL,NULL,"2026-04-23 10:36:25.832836+00","2026-04-23 10:36:25.832836+00",NULL,NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"359857082925330","Noel Merengeni - KCY 838X","GT06E","aotomobile","KCY 838X","KCY 838X","Probox","automobile",NULL,NULL,NULL,NULL,"Noel Merengeni",NULL,"794873610","89254021154296723429","639021159672342","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2020-10-26 16:36:37+00","2040-10-26 23:59:59+00",1,"1",NULL,"194429.24","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050288212","Nicholas Erastus - KCQ 581M","JC400P","aotomobile","KCQ 581M","KCQ 581M","Probox","automobile",NULL,NULL,NULL,NULL,"Nicholas Erastus",NULL,"746979531",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-11-02 09:07:41+00","2041-11-02 23:59:59+00",1,"1",NULL,"40898.98","2026-04-23 13:05:18.326254+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050288261","Patric Bett - KDA 609E","JC400P","aotomobile","KDA 609E","KDA 609E","Probox","automobile",NULL,NULL,NULL,NULL,"Patric Bett","112693340","790176509",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2021-10-23 11:50:11+00","2041-10-23 23:59:59+00",1,"1",NULL,"18538.42","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050288303","Elias Baya - KCZ 476E","JC400P","aotomobile","KCZ 476E","KCZ 476E","Probox","automobile",NULL,NULL,NULL,NULL,"Elias Baya",NULL,"115870439",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-11-06 08:50:28+00","2041-11-06 23:59:59+00",1,"1",NULL,"116091.42","2026-04-23 17:46:09.993791+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050288345","Santoes Omondi - KCZ 181P","JC400P","aotomobile","KCZ 181P","KCZ 181P","Pick-Up","automobile",NULL,NULL,NULL,NULL,"Santoes Omondi",NULL,"768446105",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2021-11-06 10:17:51+00","2041-11-06 23:59:59+00",1,"1",NULL,"107462.79","2026-04-23 10:29:45.563231+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050288360","Brian Ngetich - KDA 717B","JC400P","aotomobile","KDA 717B","KDA 717B","Probox","automobile",NULL,NULL,NULL,NULL,"Brian Ngetich",NULL,"717867861",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2021-11-05 08:47:08+00","2041-11-05 23:59:59+00",1,"1",NULL,"17808.56","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050521521","John Kimeria - KDS 525D","JC400P","aotomobile","KDS 525D","KDS 525D","Crane","truck",NULL,NULL,NULL,NULL,"John Kimeria",NULL,"752958416",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2023-11-26 07:58:13+00","2033-11-26 23:59:59+00",1,"1",NULL,"19354.92","2026-04-23 10:28:34.917147+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050521612","Denis Kazungu - KDM 794R","JC400P","aotomobile","KDM 794R","KDM 794R","Probox","automobile",NULL,NULL,NULL,NULL,"Denis Kazungu",NULL,"704113731",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-22 07:52:12+00","2042-01-22 23:59:59+00",1,"1",NULL,"4350.75","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050521752","Simon Munda - KCZ 154S","JC400P","aotomobile","KCZ 154S","KCZ 154S","Probox","automobile",NULL,NULL,NULL,NULL,"Simon Munda",NULL,"113805921",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-16 08:14:37+00","2042-01-16 23:59:59+00",1,"1",NULL,"4698.02","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050522065","Gideon Kiprono - KCQ 215F","JC400P","aotomobile","KCQ 215F","KCQ 215F","Probox","automobile",NULL,NULL,NULL,NULL,"Gideon Kiprono",NULL,"113343715",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2022-01-16 07:10:16+00","2042-01-16 23:59:59+00",1,"1",NULL,"8111.98","2026-04-23 18:23:51.445608+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050522107","Cassius Wakiyo - KDB 323M","JC400P","aotomobile","KDB 323M","KDB 323M","Probox","automobile",NULL,NULL,NULL,NULL,"Cassius Wakiyo",NULL,"114149576",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-22 08:18:15+00","2042-01-22 23:59:59+00",1,"1",NULL,"23316.09","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050522719","Mbuvi Kioko - KCZ 199P","JC400P","aotomobile","KCZ 199P","KCZ 199P","Pick-Up","automobile",NULL,NULL,NULL,NULL,"Mbuvi Kioko",NULL,"768218655",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-16 22:07:24+00","2042-01-16 23:59:59+00",1,"1",NULL,"16973.89","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050522743","Charles Nyambane - KCB 711C","JC400P","aotomobile","KCB 711C","KCB 711C","Probox","automobile",NULL,NULL,NULL,NULL,"Charles Nyambane",NULL,"768657106",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2023-12-22 21:53:57+00","2033-12-22 23:59:59+00",1,"1",NULL,"12133.75","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050522859","Garage - KCH 167M","JC400P","aotomobile","KCH 167M","KCH 167M","Probox","automobile",NULL,NULL,NULL,NULL,"Garage",NULL,"706740252",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-15 08:23:21+00","2042-01-15 23:59:59+00",1,"1",NULL,"6934.86","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050522883","Dan Watila - KDE 638J","JC400P","aotomobile","KDE 638J","KDE 638J","Probox","automobile",NULL,NULL,NULL,NULL,"Dan Watila",NULL,"112615393",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-15 07:17:21+00","2042-01-15 23:59:59+00",1,"1",NULL,"14483.01","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050522891","Lawrence Kijogi - KCY 080X","JC400P","aotomobile","KCY 080X","KCY 080X","Pick-Up","automobile",NULL,NULL,NULL,NULL,"Lawrence Kijogi",NULL,"113287191",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-16 10:51:30+00","2042-01-16 23:59:59+00",1,"1",NULL,"11585.33","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523014","Samuel Muriithy - KDR 594N","JC400P","aotomobile","KDR 594N","KDR 594N","Probox","automobile",NULL,NULL,NULL,NULL,"Samuel Muriithy",NULL,"790175423",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2023-12-21 15:54:32+00","2033-12-21 23:59:59+00",1,"1",NULL,"27275.43","2026-04-23 10:26:17.747928+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523063","Kelvin Wambugu - KDR 594N","JC400P","aotomobile","KDR 594N","KDR 594N","Probox","automobile",NULL,NULL,NULL,NULL,"Kelvin Wambugu",NULL,"701211876",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2023-12-22 19:24:51+00","2043-12-22 19:24:51+00",1,"1",NULL,"32698.94","2026-04-23 15:31:08.065856+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523139","Mike Wanaswa - KDT 724R","JC400P","aotomobile","KDT 724R","KDT 724R","Probox","automobile",NULL,NULL,NULL,NULL,"Mike Wanaswa",NULL,"790175045",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2023-12-22 22:28:09+00","2043-12-22 22:28:09+00",1,"1",NULL,"29559.82","2026-04-23 11:16:37.277518+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523204","Amani Kazungu - KCY 084X","JC400P","aotomobile","KCY 084X","KCY 084X","Probox","automobile",NULL,NULL,NULL,NULL,"Amani Kazungu",NULL,"707892547",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-16 06:18:20+00","2042-01-16 23:59:59+00",1,"1",NULL,"66955.70","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523295","Emmanuel Luseno - KDS 453 Y","JC400P","aotomobile","KDS 453 Y","KDS 453 Y","Pick-Up","automobile",NULL,NULL,NULL,NULL,"Emmanuel Luseno",NULL,"700242474",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2023-12-22 14:39:50+00","2033-12-22 23:59:59+00",1,"1",NULL,"37098.35","2026-04-23 11:29:48.369147+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523337","Victor Kimutai - KDS 919Y","JC400P","aotomobile","KDS 919Y","KDS 919Y","Probox","automobile",NULL,NULL,NULL,NULL,"Victor Kimutai",NULL,"700242527",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2023-12-22 18:00:49+00","2043-12-22 18:00:49+00",1,"1",NULL,"50756.64","2026-04-23 10:27:13.522675+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523386","George Ochieng' - KDD 684Y","JC400P","aotomobile","KDD 684Y","KDD 684Y","Probox","automobile",NULL,NULL,NULL,NULL,"George Ochieng'",NULL,"785586834",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-22 06:36:08+00","2042-01-22 23:59:59+00",1,"1",NULL,"33979.83","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523527","Allan Owana - KDK 780K","JC400P","aotomobile","KDK 780K","KDK 780K","Probox","automobile",NULL,NULL,NULL,NULL,"Allan Owana",NULL,"792375024",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2022-12-03 10:43:41+00","2042-12-03 23:59:59+00",1,"1",NULL,"109564.95","2026-04-23 10:25:24.360765+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523618","Geoffrey Too - KDM 308S","JC400P","aotomobile","KDM 308S","KDM 308S","Probox","automobile",NULL,NULL,NULL,NULL,"Geoffrey Too",NULL,"701211625",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2023-08-15 15:42:32+00","2033-08-15 23:59:59+00",1,"1",NULL,"26496.50","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523626","Major Simiyu - KDS 949Y","JC400P","aotomobile","KDS 949Y","KDS 949Y","Probox","automobile",NULL,NULL,NULL,NULL,"Major Simiyu",NULL,"701211892",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2023-12-22 18:05:16+00","2033-12-22 23:59:59+00",1,"1",NULL,"37042.97","2026-04-23 10:51:18.245194+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523816","Job Ngare - KDM 309S","JC400P","aotomobile","KDM 309S","KDM 309S","Probox","automobile",NULL,NULL,NULL,NULL,"Job Ngare",NULL,"707936781",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2023-08-15 14:05:52+00","2033-08-15 23:59:59+00",1,"1",NULL,"54320.21","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050523949","Joseph Kabandi - KCY 076X","JC400P","aotomobile","KCY 076X","KCY 076X","Probox","automobile",NULL,NULL,NULL,NULL,"Joseph Kabandi",NULL,"113288492",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-16 05:52:54+00","2042-01-16 23:59:59+00",1,"1",NULL,"14427.50","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524012","Moses Wambua - KCZ 751V","JC400P","aotomobile","KCZ 751V","KCZ 751V","Probox","automobile",NULL,NULL,NULL,NULL,"Moses Wambua",NULL,"113313797",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-16 07:40:10+00","2042-01-16 23:59:59+00",1,"1",NULL,"26551.46","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524087","Felix Muema - KCZ 223P","JC400P","aotomobile","KCZ 223P","KCZ 223P","Pick-Up","automobile",NULL,NULL,NULL,NULL,"Felix Muema",NULL,"113973875",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-16 13:02:24+00","2042-01-16 23:59:59+00",1,"1",NULL,"11543.26","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524368","862798050524368","JC400P","aotomobile",NULL,NULL,NULL,"automobile",NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-10-29 09:24:53+00","2042-10-29 23:59:59+00",1,"1",NULL,"169208.91","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524384","Hamisi Pande - KDD 689Y","JC400P","aotomobile","KDD 689Y","KDD 689Y","Probox","automobile",NULL,NULL,NULL,"0.00","Hamisi Pande",NULL,"701211744",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-22 05:49:19+00","2042-01-22 23:59:59+00",1,"1",NULL,"13685.18","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524392","Ndegwa Dancun - KCG 669W","JC400P","aotomobile","KCG 669W","KCG 669W","Probox","automobile",NULL,NULL,NULL,NULL,"Ndegwa Dancun",NULL,"113799173",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-16 09:43:10+00","2042-01-16 23:59:59+00",1,"1",NULL,"13638.25","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524426","Amani Sulubu - KCY 090X","JC400P","aotomobile","KCY 090X","KCY 090X","Probox","automobile",NULL,NULL,NULL,NULL,"Amani Sulubu",NULL,"113823350",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2022-01-16 08:56:25+00","2042-01-16 23:59:59+00",1,"1",NULL,"14243.83","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524533","Leonard Nzai - KDM 306S","JC400P","aotomobile","KDM 306S","KDM 306S","Probox","automobile",NULL,NULL,NULL,NULL,"Leonard Nzai",NULL,"703487162",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2023-08-21 08:22:12+00","2033-08-21 23:59:59+00",1,"1",NULL,"68942.41","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524558","Mutuku Joseph - KDC 739F","JC400P","aotomobile","KDC 739F","KDC 739F","Probox","automobile",NULL,NULL,NULL,NULL,"Mutuku Joseph",NULL,"100858817",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-22 10:38:25+00","2042-01-22 23:59:59+00",1,"1",NULL,"23711.63","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524566","Makanda Andrew - KCZ 155P","JC400P","aotomobile","KCZ 155P","KCZ 155P","Pick-Up","automobile",NULL,NULL,NULL,NULL,"Makanda Andrew",NULL,"758781444",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-22 09:47:33+00","2042-01-22 23:59:59+00",1,"1",NULL,"31663.30","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524608","Peter Mbugua - KDK 728K","JC400P","aotomobile","KDK 728K","KDK 728K","Probox","automobile",NULL,NULL,NULL,NULL,"Peter Mbugua",NULL,"706742413",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-12-03 12:11:32+00","2042-12-03 23:59:59+00",1,"1",NULL,"7219.31","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524657","Felix Andole - KDC 207R","JC400P","aotomobile","KDC 207R","KDC 207R","Probox","automobile",NULL,NULL,NULL,NULL,"Felix Andole",NULL,"758689195",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-22 07:17:47+00","2042-01-22 23:59:59+00",1,"1",NULL,"46233.99","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524681","Mutuku Antony - KDK 732K","JC400P","aotomobile","KDK 732K","KDK 732K","Probox","automobile",NULL,NULL,NULL,NULL,"Mutuku Antony",NULL,"796275746",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-12-06 13:37:49+00","2042-12-06 23:59:59+00",1,"1",NULL,"14993.36","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524707","Garage - KCE 699F","JC400P","aotomobile","KCE 699F","KCE 699F","Probox","automobile",NULL,NULL,NULL,NULL,"Garage",NULL,"110525751",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-15 07:58:49+00","2042-01-15 23:59:59+00",1,"1",NULL,"34715.97","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050524897","Cornelius Kimutai - KCU 938R","JC400P","aotomobile","KCU 938R","KCU 938R","Van","automobile",NULL,NULL,NULL,NULL,"Cornelius Kimutai",NULL,"114924404",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-22 09:03:40+00","2042-01-22 23:59:59+00",1,"1",NULL,"12668.43","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050525068","Samuel Ng'ang'a - KDE 264M","JC400P","aotomobile","KDE 264M","KDE 264M","Probox","automobile",NULL,NULL,NULL,NULL,"Samuel Ng'ang'a",NULL,"768658564",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2023-12-22 13:33:42+00","2033-12-22 23:59:59+00",1,"1",NULL,"12299.13","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050525225","Sadique Wakayula - KDC 490Q","JC400P","aotomobile","KDC 490Q","KDC 490Q","Crane","truck",NULL,NULL,NULL,NULL,"Sadique Wakayula",NULL,"768652386",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2023-12-22 20:52:08+00","2043-12-22 20:52:08+00",1,"1",NULL,"19138.05","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050525266","Dickson Jaoko - KDK 815R","JC400P","aotomobile","KDK 815R","KDK 815R","Probox","automobile",NULL,NULL,NULL,NULL,"Dickson Jaoko",NULL,"706665867",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2023-06-21 07:50:00+00","2033-06-21 23:59:59+00",1,"1",NULL,"63754.71","2026-04-23 13:50:24.21992+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050525423","Makori John - KDB 585E","JC400P","aotomobile","KDB 585E","KDB 585E","Probox","automobile",NULL,NULL,NULL,NULL,"Makori John",NULL,"701211724",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-15 10:59:19+00","2042-01-15 23:59:59+00",1,"1",NULL,"48804.83","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050525589","Simon Kamau - KCE 090R","JC400P","aotomobile","KCE 090R","KCE 090R","Probox","automobile",NULL,NULL,NULL,NULL,"Simon Kamau",NULL,"796276387",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-19 10:10:04+00","2042-01-19 23:59:59+00",1,"1",NULL,"15874.39","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050525605","John Ondego - KCA 542Q","JC400P","aotomobile","KCA 542Q","KCA 542Q","Probox","automobile",NULL,NULL,NULL,NULL,"John Ondego",NULL,"110526783",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-15 05:56:11+00","2042-01-15 23:59:59+00",1,"1",NULL,"23976.94","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050525613","Kennedy Chege - KCQ 618K","JC400P","aotomobile","KCQ 618K","KCQ 618K","Probox","automobile",NULL,NULL,NULL,NULL,"Kennedy Chege",NULL,"729994247",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-16 05:21:05+00","2042-01-16 23:59:59+00",1,"1",NULL,"12804.24","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050525670","Gabriel Musumba - KCE 690F","JC400P","aotomobile","KCE 690F","KCE 690F","Probox","automobile",NULL,NULL,NULL,NULL,"Gabriel Musumba",NULL,"701211996",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2022-01-15 06:40:01+00","2042-01-15 23:59:59+00",1,"1",NULL,"20110.93","2026-04-24 05:34:23.167312+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050525753","Noel Merengeni - KCY 838X","JC400P","aotomobile","KCY 838X","KCY 838X","Probox","automobile",NULL,NULL,NULL,NULL,"Noel Merengeni",NULL,NULL,NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2022-01-15 05:24:00+00","2042-01-15 23:59:59+00",1,"1",NULL,"14596.59","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050525837","Kennedy Ondieki - KCU 237Z","JC400P","aotomobile","KCU 237Z","KCU 237Z","Probox","automobile",NULL,NULL,NULL,NULL,"Kennedy Ondieki",NULL,"113669852",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2023-12-21 19:32:44+00","2033-12-21 23:59:59+00",1,"1",NULL,NULL,"2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050525951","Wright Oseko - KCG 668W","JC400P","aotomobile","KCG 668W","KCG 668W","Probox","automobile",NULL,NULL,NULL,NULL,"Wright Oseko",NULL,"741943212",NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2022-01-15 09:36:45+00","2042-01-15 23:59:59+00",1,"1",NULL,"13116.00","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050526165","Wilfred Kinyanjui - KCU 729C","JC400P","aotomobile","KCU 729C","KCU 729C","Crane","truck",NULL,NULL,NULL,NULL,"Wilfred Kinyanjui",NULL,"790564929",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2023-11-26 10:17:19+00","2033-11-26 23:59:59+00",1,"1",NULL,"24270.20","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050526231","Rashid Hassan - KDM 840V","JC400P","aotomobile","KDM 840V","KDM 840V","Probox","automobile",NULL,NULL,NULL,NULL,"Rashid Hassan",NULL,"790175526",NULL,NULL,"Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2023-12-22 22:36:15+00","2043-12-22 22:36:15+00",1,"1",NULL,"45418.38","2026-04-23 10:29:41.575467+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798050526256","Ian Dancun - KDT 923R","JC400P","aotomobile","KDT 923R","KDT 923R","Probox","automobile",NULL,NULL,NULL,NULL,"Ian Dancun",NULL,"794873610",NULL,NULL,"Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2023-12-22 19:37:24+00","2043-12-22 19:37:24+00",1,"1",NULL,"11093.11","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052707888","Benjamin Ananda - KDV 438W","JC400P","aotomobile","KDV 438W","KDV 438W","Probox","automobile",NULL,NULL,NULL,NULL,"Benjamin Ananda",NULL,"758047312","89254021414206816980","639021410681698","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-12-15 07:39:23+00","2035-12-15 23:59:59+00",1,"1",NULL,"8720.87","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052707896","John Mbugua - KDW 573B","JC400P","aotomobile","KDW 573B","KDW 573B","Probox","automobile",NULL,NULL,NULL,NULL,"John Mbugua",NULL,NULL,"89254021414206816725","639021410681672","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2026-01-30 14:48:17+00","2036-01-30 23:59:59+00",1,"1",NULL,"515.16","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052707946","Tom Wekesa/OSP-KCY 930Y_CAM","JC400P","aotomobile",NULL,NULL,NULL,"automobile",NULL,NULL,NULL,NULL,NULL,NULL,"0758047806","89254021414206816766","639021410681676","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2026-01-20 21:02:13+00","2036-01-20 23:59:59+00",1,"1",NULL,"10079.17","2026-04-23 10:25:24.363965+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052708035","862798052708035","JC400P","aotomobile",NULL,NULL,"Probox","automobile",NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,"fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group",NULL,NULL,1,"1",NULL,NULL,"2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052708068","Dominic Wambua - KDV 683Z","JC400P","aotomobile","KDV 683Z","KDV 683Z","Probox","automobile",NULL,NULL,NULL,NULL,"Dominic Wambua",NULL,"758048043","89254021414206816964","639021410681696","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2026-01-24 09:20:09+00","2036-01-24 23:59:59+00",1,"1",NULL,"4438.55","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052708076","Albert Mutwiri - KDV 437W","JC400P","aotomobile",NULL,"KDV 437W","Probox","automobile",NULL,NULL,NULL,NULL,"Albert Mutwiri",NULL,"758047094","89254021414206816782","639021410681678","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-12-13 15:03:30+00","2035-12-13 23:59:59+00",1,"1",NULL,"5575.64","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052708167","Levine Wasike - KDV 439W","JC400P","aotomobile","KDV 439W","KDV 439W","Probox","automobile",NULL,NULL,NULL,NULL,"Levine Wasike",NULL,"758046738","89254021414206816741","639021410681674","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-12-13 19:49:29+00","2035-12-13 23:59:59+00",1,"1",NULL,"4601.08","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052708282","Godffrey Nandwa - KCN 496A","JC400P","aotomobile","KCN 496A","KCN 496A","Probox","automobile",NULL,NULL,NULL,NULL,"Godffrey Nandwa",NULL,"758047934","89254021414206816865","639021410681686","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2026-01-25 18:55:54+00","2036-01-25 23:59:59+00",1,"1",NULL,"7040.60","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052713654","Garage/ISP_KCL 502T_CAM","JC400P","aotomobile",NULL,NULL,NULL,"automobile",NULL,NULL,NULL,NULL,NULL,NULL,"0780215879","89254035061001753803","639035060175380","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-09-02 10:09:57+00","2035-09-02 23:59:59+00",1,"1",NULL,"5199.72","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052713696","862798052713696","JC400P","aotomobile",NULL,NULL,"Probox","automobile",NULL,NULL,NULL,NULL,NULL,NULL,NULL,"89254021394215205906","639021391520590","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-09-02 10:20:58+00","2035-09-02 23:59:59+00",1,"1",NULL,"6214.49","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052713761","Management_Mazda - KDU 613A","JC400P","aotomobile","KDU 613A","KDU 613A","Mazda","automobile",NULL,NULL,NULL,NULL,"Management_Mazda",NULL,"790176786","89254021394215205955","639021391520595","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-07-09 15:49:26+00","2035-07-09 23:59:59+00",1,"1",NULL,"9262.78","2026-04-23 16:40:48.879666+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052713779","Benard Kimutai - KDN 759G","JC400P","aotomobile","KDN 759G","KDN 759G","Probox","automobile",NULL,NULL,NULL,NULL,"Benard Kimutai",NULL,"752143258","89254035061001753860","639035060175386","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-08-23 11:15:59+00","2035-08-23 23:59:59+00",1,"1",NULL,"5344.24","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052713811","James Onyango - KDU 613B","JC400P","aotomobile","KDU 613B","KDU 613B","Probox","automobile",NULL,NULL,NULL,NULL,"James Onyango",NULL,"790176542","89254021394215205880","639021391520588","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-07-09 19:24:14+00","2035-07-09 23:59:59+00",1,"1",NULL,"9657.42","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052713837","Kennedy Ondieki - KCU 237Z","JC400P","aotomobile","KCU 237Z","KCU 237Z","Probox","automobile",NULL,NULL,NULL,NULL,"Kennedy Ondieki",NULL,"113669852","89254021414206327855","639021410632785","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-10-08 14:55:23+00","2035-10-08 23:59:59+00",1,"1",NULL,"9346.02","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052713985","Timothy Gitau - KDT 916R","JC400P","aotomobile","KDT 916R","KDT 916R","Probox","automobile",NULL,NULL,NULL,NULL,"Timothy Gitau",NULL,"768696668","89254021394274518892","639021397451889","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-08-02 18:21:23+00","2035-08-02 23:59:59+00",1,"1",NULL,"19998.22","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052714066","862798052714066","JC400P","aotomobile",NULL,NULL,"Probox","automobile",NULL,NULL,NULL,NULL,NULL,NULL,NULL,"89254021414206378684","639021410637868","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-11-21 17:44:44+00","2035-11-21 23:59:59+00",1,"1",NULL,"10755.28","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"862798052715220","Rofas Njagi - KDT 728R","JC400P","aotomobile","KDT 728R","KDT 728R","Probox","automobile",NULL,NULL,NULL,NULL,"Rofas Njagi",NULL,"704573658","89254021334258495873","639021335849587","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-07-16 07:09:25+00","2035-07-16 23:59:59+00",1,"1",NULL,"16385.58","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061035133","Major Simiyu - KDS 949Y","X3","aotomobile","KDS 949Y","KDS 949Y","Probox","automobile",NULL,NULL,NULL,NULL,"Major Simiyu",NULL,"768696642","89254021394274518918","639021397451891","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2025-08-02 13:14:33+00","2035-08-02 23:59:59+00",1,"1",NULL,"25089.98","2026-04-23 12:07:56.044395+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061035653","Richardson Komu - KDT 923R","X3","aotomobile","KDT 923R","KDT 923R","Probox","automobile",NULL,NULL,NULL,NULL,"Richardson Komu",NULL,"768697292","89254021394274518942","639021397451894","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-08-02 08:11:46+00","2035-08-02 23:59:59+00",1,"1",NULL,"23556.65","2026-04-23 10:24:50.340401+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061035778","John Kimeria - KDS 525D","X3","aotomobile","KDS 525D","KDS 525D","Crane","truck",NULL,NULL,NULL,NULL,"John Kimeria",NULL,"790176738","89254021394215205922","639021391520592","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-07-11 05:50:36+00","2035-07-11 23:59:59+00",1,"1",NULL,"17653.96","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061036164","Brian Njenga - KMFF 113Z","X3","aotomobile","KMFF 113Z","KMFF 113Z","Motorbike","mtc",NULL,NULL,NULL,NULL,"Brian Njenga",NULL,"768696705","89254021394274518850","639021397451885","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-07-31 10:06:46+00","2035-07-31 23:59:59+00",1,"1",NULL,"22990.33","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061037980","Emmanuel Luseno - KDS 453Y","X3","aotomobile","KDS 453Y","KDS 453Y","Pick-Up","automobile",NULL,NULL,NULL,NULL,"Emmanuel Luseno",NULL,"790176734","89254021394215205856","639021391520585","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-07-15 06:30:34+00","2035-07-15 23:59:59+00",1,"1",NULL,"42609.03","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061042261","Kelvin Wambugu - KDR 592N","X3","aotomobile","KDR 592N","KDR 592N","Probox","automobile",NULL,NULL,NULL,NULL,"Kelvin Wambugu",NULL,"797680464","89254021334258159693","639021335815969","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-07-10 10:23:44+00","2035-07-10 23:59:59+00",1,"1",NULL,"18755.66","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061043079","Mike Wanaswa - KDT 724R","X3","aotomobile","KDT 724R","KDT 724R","Probox","automobile",NULL,NULL,NULL,NULL,"Mike Wanaswa",NULL,"768696664","89254021394274518959","639021397451895","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2025-08-02 12:16:11+00","2035-08-02 23:59:59+00",1,"1",NULL,"27470.11","2026-04-23 11:16:35.682194+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061043426","Geoffrey Karanja - KMGS 239H","X3","aotomobile","KMGS 239H","KMGS 239H","Motorbike","mtc",NULL,NULL,NULL,NULL,"Geoffrey Karanja",NULL,"768696658","89254021394274518926","639021397451892","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-08-22 13:32:25+00","2035-08-22 23:59:59+00",1,"1",NULL,"21267.01","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061047435","Management_Mazda - KDU 613A","X3","aotomobile","KDU 613A","KDU 613A","Mazda","automobile",NULL,NULL,NULL,NULL,"Management_Mazda",NULL,"790175971","89254021394215205971","639021391520597","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-07-09 08:02:26+00","2035-07-09 23:59:59+00",1,"1",NULL,"9761.38","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061048276","Victor Kimutai - KDS 919Y","X3","aotomobile","KDS 919Y","KDS 919Y","Probox","automobile",NULL,NULL,NULL,NULL,"Victor Kimutai",NULL,"768696755","89254021394274518900","639021397451890","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2025-08-02 07:38:01+00","2035-08-02 23:59:59+00",1,"1",NULL,"23296.79","2026-04-23 10:54:41.63532+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061048300","KMGR 409U HENRY JAZZ",NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,"unknown",NULL,NULL,"2026-04-24 04:30:20.231102+00","2026-04-24 04:30:20.231102+00",NULL,NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061048466","Samuel Muriithy - KDR 594N","X3","aotomobile","KDR 594N","KDR 594N","Probox","automobile",NULL,NULL,NULL,NULL,"Samuel Muriithy",NULL,"797680395","89254021334258159628","639021335815962","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-07-24 09:37:31+00","2035-07-24 23:59:59+00",1,"1",NULL,"27634.10","2026-04-23 11:43:39.178819+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061048615","Office-KMDG 902Z","X3","aotomobile",NULL,NULL,NULL,"automobile",NULL,NULL,NULL,NULL,NULL,NULL,"0768697276","89254021394274518876","639021397451887","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-07-31 09:59:43+00","2035-07-31 23:59:59+00",1,"1",NULL,"5721.21","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061048953","Timothy Gitau - KDT 916R","X3","aotomobile","KDT 916R","KDT 916R","Probox","automobile",NULL,NULL,NULL,NULL,"Timothy Gitau",NULL,"768697056","89254021394274518967","639021397451896","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2025-08-02 08:48:05+00","2035-08-02 23:59:59+00",1,"1",NULL,"28536.23","2026-04-23 10:53:31.102315+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061049001","Parked - KMGK 596V","X3","aotomobile","KMGK 596V","KMGK 596V","Motorbike","mtc",NULL,NULL,NULL,NULL,"Parked",NULL,"768697064","89254021394274518884","639021397451888","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-07-31 08:40:18+00","2035-07-31 23:59:59+00",1,"1",NULL,"20612.89","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061053714","Samuel Kihara - KMEL 225X","X3","aotomobile","KMEL 225X","KMEL 225X","Motorbike","mtc",NULL,NULL,NULL,NULL,"Samuel Kihara",NULL,"768696832","89254021394274518934","639021397451893","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-08-02 13:51:47+00","2035-08-02 23:59:59+00",1,"1",NULL,"26897.18","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061053748","Rashid Hassan - KDM 840V","X3","aotomobile","KDM 840V","KDM 840V","Probox","automobile",NULL,NULL,NULL,NULL,"Rashid Hassan",NULL,"768445963","89254021334212352574","639021331235257","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-07-10 13:54:11+00","2035-07-10 23:59:59+00",1,"1",NULL,"26612.42","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061054548","James Onyango - KDU 613B","X3","aotomobile","KDU 613B","KDU 613B","Probox","automobile",NULL,NULL,NULL,NULL,"James Onyango",NULL,"790175997","89254021394215205948","639021391520594","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-07-09 07:11:15+00","2035-07-09 23:59:59+00",1,"1",NULL,"13446.05","2026-04-23 10:26:24.667167+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061054555","Rofas Njagi - KDT 728R","X3","aotomobile","KDT 728R","KDT 728R","Probox","automobile",NULL,NULL,NULL,NULL,"Rofas Njagi",NULL,"790176726","89254021394215205823","639021391520582","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-07-16 06:44:33+00","2035-07-16 23:59:59+00",1,"1",NULL,"27250.80","2026-04-23 10:25:21.085437+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061559538","FRED KMGW 538W HULETI",NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,"unknown",NULL,NULL,"2026-04-23 10:42:18.5831+00","2026-04-23 10:42:18.5831+00",NULL,NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061562722","John Mbugua - KDW 573B","X3","aotomobile","KDW 573B","KDW 573B","Probox","automobile",NULL,NULL,NULL,NULL,"John Mbugua",NULL,"758052508","89254021414206816832","639021410681683","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2026-01-30 06:53:57+00","2036-01-30 23:59:59+00",1,"1",NULL,"4488.19","2026-04-23 10:25:38.887433+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061562847","Levine Wasike - KDV 439W","X3","aotomobile","KDV 439W","KDV 439W","Probox","automobile",NULL,NULL,NULL,NULL,"Levine Wasike",NULL,"758047032","89254021414206816840","639021410681684","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2025-12-13 11:14:14+00","2035-12-13 23:59:59+00",1,"1",NULL,"7880.92","2026-04-23 10:35:50.779597+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061563282","X3-63282","X3","aotomobile",NULL,NULL,NULL,"automobile",NULL,NULL,NULL,NULL,NULL,NULL,NULL,"8925610001837573427F","641101970467668","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2026-02-14 07:20:10+00","2036-02-14 23:59:59+00",1,"1",NULL,"4758.32","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061563415","Barack Orwa - KDW 781E","X3","aotomobile","KDW 781E","KDW 781E","Vazel","automobile",NULL,NULL,NULL,NULL,"Barack Orwa",NULL,"758052541","89254021414206816931","639021410681693","Fireside_MSA","Fireside Group MSA","9d0927d235e44fe7abf254902fc68921","Default group","2026-01-13 12:37:42+00","2036-01-13 23:59:59+00",1,"1",NULL,"4165.95","2026-04-23 11:22:00.676215+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061563423","Joel Ntumba - UMA 826AB","X3","aotomobile","UMA 826AB","UMA 826AB","Motorbike","mtc",NULL,NULL,NULL,NULL,"Joel Ntumba",NULL,"119051036","89254021414206652690","639021410665269","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2026-01-28 13:55:39+00","2036-01-28 23:59:59+00",1,"1",NULL,"1174.05","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061563597","Dominic Wambua - KDV 683Z","X3","aotomobile","KDV 683Z","KDV 683Z","Probox","automobile",NULL,NULL,NULL,NULL,"Dominic Wambua",NULL,"758052405","89254021414206816733","639021410681673","Fireside@HQ","Fireside Telematics","6ef0b0fc2d964b358b70dc2cfcbc5b7e","Default group","2026-01-30 06:55:35+00","2036-01-30 23:59:59+00",1,"1",NULL,"6790.53","2026-04-23 10:25:40.125927+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061563639","Benjamin Ananda - KDV 438W","X3","aotomobile","KDV 438W","KDV 438W","Probox","automobile",NULL,NULL,NULL,NULL,"Benjamin Ananda",NULL,"758047065","89254021414206816683","639021410681668","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-12-13 16:02:37+00","2035-12-13 23:59:59+00",1,"1",NULL,"14446.33","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061564280","Rodin Kiberu - UMA 011EK","X3","aotomobile","UMA 011EK","UMA 011EK","Motorbike","mtc",NULL,NULL,NULL,NULL,"Rodin Kiberu",NULL,"118081642","89254021414206817244","639021410681724","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2026-01-28 13:13:57+00","2036-01-28 23:59:59+00",1,"1",NULL,"841.39","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061564470","Silvanus Kipkorir - KDV 064S","X3","aotomobile","KDV 064S","KDV 064S","Probox","automobile",NULL,NULL,NULL,NULL,"Silvanus Kipkorir",NULL,"113669866","89254021414206378718","639021410637871","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-11-21 16:49:17+00","2035-11-21 23:59:59+00",1,"1",NULL,"23869.16","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061568968","X3-68968","X3","aotomobile",NULL,NULL,NULL,"automobile",NULL,NULL,NULL,NULL,NULL,NULL,NULL,"89254021414206816915","639021410681691","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2026-03-11 06:19:14+00","2036-03-11 23:59:59+00",1,"1",NULL,"16.23","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061569123","Albert Mutwiri - KDV 437W","X3","aotomobile","KDV 437W","KDV 437W","Probox","automobile",NULL,NULL,NULL,NULL,"Albert Mutwiri",NULL,"758047101","89254021414206816881","639021410681688","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-12-13 14:26:17+00","2035-12-13 23:59:59+00",1,"1",NULL,"13032.60","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061569131","UMA 418EK","X3","aotomobile","UMA 418EK","UMA 418EK",NULL,"automobile",NULL,NULL,NULL,NULL,"UG",NULL,"256792997053","8925610001837573385F","641101970467664","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2026-02-26 08:15:44+00","2036-02-26 23:59:59+00",1,"1",NULL,"2333.45","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061569479","UMA 382EK","X3","aotomobile","UMA 382EK","UMA 382EK",NULL,"automobile",NULL,NULL,NULL,NULL,"UG",NULL,"256792997079","8925610001837573419F","641101970467667","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2026-02-26 08:21:10+00","2036-02-26 23:59:59+00",1,"1",NULL,"1954.86","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061578553","X3-78553",NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,"unknown",NULL,NULL,"2026-04-23 15:30:19.981271+00","2026-04-23 15:30:19.981271+00",NULL,NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
"865135061581904","Robert Kipruto - KDV 072L","X3","aotomobile","KDV 072L","KDV 072L","Probox","automobile",NULL,NULL,NULL,NULL,"Robert Kipruto",NULL,"114149576","89254021264261503993","639021266150399","fireside","Fireside Group HQ","2f1acaef6c884214b4598719180fe68d","Default group","2025-11-21 15:30:29+00","2035-11-21 23:59:59+00",1,"1",NULL,"15252.84","2026-04-23 10:23:56.546784+00","2026-04-24 07:43:45.210628+00","2026-04-24 07:43:45.210628+00",NULL,NULL,NULL,NULL,NULL,NULL
|
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
-- =============================================================
|
|
||||||
-- TRACKSOLID DWH CONTROL SCHEMA
|
|
||||||
-- Target Database: tracksolid_dwh
|
|
||||||
-- Purpose: Watermarks + per-run audit log for the n8n DWH pipeline
|
|
||||||
-- Applies after: 260423_dwh_ddl_v1.sql (requires dwh_owner role + grafana_ro role)
|
|
||||||
-- =============================================================
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
-- 1. CONTROL SCHEMA
|
|
||||||
-- Owned by dwh_owner to match bronze/silver/gold ownership convention from 260423.
|
|
||||||
CREATE SCHEMA IF NOT EXISTS dwh_control AUTHORIZATION dwh_owner;
|
|
||||||
|
|
||||||
GRANT USAGE ON SCHEMA dwh_control TO grafana_ro;
|
|
||||||
|
|
||||||
-- 2. PERMISSIONS (dwh_owner writes, grafana_ro reads)
|
|
||||||
-- Existing default privileges from 260423 only cover bronze/silver/gold; extend to dwh_control.
|
|
||||||
ALTER DEFAULT PRIVILEGES FOR ROLE dwh_owner IN SCHEMA dwh_control
|
|
||||||
GRANT SELECT ON TABLES TO grafana_ro;
|
|
||||||
|
|
||||||
-- 3. EXTRACT WATERMARKS
|
|
||||||
-- One row per incremental table. Updated by Workflow 2 after a successful load commit.
|
|
||||||
-- last_extracted_at is the UPPER bound used in the most recent successful extract,
|
|
||||||
-- so the next run uses `WHERE <ts_col> > last_extracted_at AND <ts_col> <= :run_started_at`.
|
|
||||||
SET ROLE dwh_owner;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS dwh_control.extract_watermarks (
|
|
||||||
table_name TEXT PRIMARY KEY,
|
|
||||||
last_extracted_at TIMESTAMPTZ NOT NULL DEFAULT '2026-01-01T00:00:00Z',
|
|
||||||
last_loaded_at TIMESTAMPTZ,
|
|
||||||
rows_loaded_last_run INT,
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 4. EXTRACT RUN AUDIT LOG
|
|
||||||
-- One row per table per cron tick. Lifecycle: extracting → uploaded → loading → loaded (or failed).
|
|
||||||
-- Failures retain error_message; the CSV stays in dwh/exports/ for the next run to pick up.
|
|
||||||
CREATE TABLE IF NOT EXISTS dwh_control.extract_runs (
|
|
||||||
run_id BIGSERIAL PRIMARY KEY,
|
|
||||||
table_name TEXT NOT NULL,
|
|
||||||
run_started_at TIMESTAMPTZ NOT NULL,
|
|
||||||
run_finished_at TIMESTAMPTZ,
|
|
||||||
rows_extracted INT,
|
|
||||||
rows_loaded INT,
|
|
||||||
csv_path TEXT,
|
|
||||||
status TEXT CHECK (status IN ('extracting','uploaded','loading','loaded','failed')),
|
|
||||||
error_message TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_extract_runs_table_time
|
|
||||||
ON dwh_control.extract_runs (table_name, run_started_at DESC);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_extract_runs_status_time
|
|
||||||
ON dwh_control.extract_runs (status, run_finished_at DESC);
|
|
||||||
|
|
||||||
-- 5. SEED WATERMARKS
|
|
||||||
-- One row per incremental table. Snapshot tables (devices, live_positions) do not need
|
|
||||||
-- watermarks and are intentionally omitted.
|
|
||||||
INSERT INTO dwh_control.extract_watermarks (table_name) VALUES
|
|
||||||
('position_history'),
|
|
||||||
('trips'),
|
|
||||||
('alarms'),
|
|
||||||
('parking_events'),
|
|
||||||
('device_events'),
|
|
||||||
('ingestion_log')
|
|
||||||
ON CONFLICT (table_name) DO NOTHING;
|
|
||||||
|
|
||||||
RESET ROLE;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
-- =============================================================
|
|
||||||
-- BRONZE CONSTRAINTS AUDIT
|
|
||||||
-- Target Database: tracksolid_dwh
|
|
||||||
-- Purpose: Assert that every ON CONFLICT target used by Workflow 2
|
|
||||||
-- (dwh_load_bronze) is backed by a PRIMARY KEY or UNIQUE
|
|
||||||
-- constraint in the bronze schema. Fails loudly if a future
|
|
||||||
-- DDL edit removes a key the ingestion pipeline depends on.
|
|
||||||
-- Applies after: 260423_dwh_ddl_v1.sql
|
|
||||||
-- Idempotent: pure assertion, no DDL changes.
|
|
||||||
-- =============================================================
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
missing TEXT := '';
|
|
||||||
expected RECORD;
|
|
||||||
BEGIN
|
|
||||||
-- Each row asserts: bronze.<table> has a PK/UNIQUE matching <cols>.
|
|
||||||
-- If the pipeline's ON CONFLICT clause ever diverges from this list,
|
|
||||||
-- update both here and the n8n load workflow in lockstep.
|
|
||||||
FOR expected IN
|
|
||||||
SELECT * FROM (VALUES
|
|
||||||
('devices', 'imei'),
|
|
||||||
('live_positions', 'imei'),
|
|
||||||
('position_history', 'imei,gps_time'),
|
|
||||||
('trips', 'id'),
|
|
||||||
('alarms', 'id'),
|
|
||||||
('parking_events', 'id'),
|
|
||||||
('device_events', 'id'),
|
|
||||||
('ingestion_log', 'id')
|
|
||||||
) AS t(table_name, cols)
|
|
||||||
LOOP
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM pg_constraint c
|
|
||||||
JOIN pg_class r ON r.oid = c.conrelid
|
|
||||||
JOIN pg_namespace n ON n.oid = r.relnamespace
|
|
||||||
CROSS JOIN LATERAL (
|
|
||||||
SELECT string_agg(a.attname, ',' ORDER BY k.ord) AS keycols
|
|
||||||
FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
|
|
||||||
JOIN pg_attribute a
|
|
||||||
ON a.attrelid = c.conrelid AND a.attnum = k.attnum
|
|
||||||
) AS cols
|
|
||||||
WHERE n.nspname = 'bronze'
|
|
||||||
AND r.relname = expected.table_name
|
|
||||||
AND c.contype IN ('p','u')
|
|
||||||
AND cols.keycols = expected.cols
|
|
||||||
) THEN
|
|
||||||
missing := missing
|
|
||||||
|| format(E'\n - bronze.%s missing PK/UNIQUE on (%s)',
|
|
||||||
expected.table_name, expected.cols);
|
|
||||||
END IF;
|
|
||||||
END LOOP;
|
|
||||||
|
|
||||||
IF length(missing) > 0 THEN
|
|
||||||
RAISE EXCEPTION E'Bronze constraint audit FAILED:%s', missing;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Bronze constraint audit OK: all 8 ON CONFLICT targets backed by PK/UNIQUE.';
|
|
||||||
END$$;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
-- =============================================================
|
|
||||||
-- DWH ROLES AUDIT
|
|
||||||
-- Target Database: tracksolid_dwh
|
|
||||||
-- Purpose: Assert that the n8n DWH pipeline's role contract holds:
|
|
||||||
-- - dwh_owner exists (writes bronze + dwh_control)
|
|
||||||
-- - grafana_ro exists (reads bronze + silver + gold + dwh_control)
|
|
||||||
-- - grafana_ro has CONNECT on the database
|
|
||||||
-- - grafana_ro has USAGE on every schema it needs
|
|
||||||
-- Applies after: 260423_dwh_ddl_v1.sql, 261001_dwh_control.sql
|
|
||||||
-- Idempotent: pure assertion, no CREATE ROLE or GRANT statements.
|
|
||||||
--
|
|
||||||
-- Why this file exists: 260423 creates both roles and grants bronze/silver/gold;
|
|
||||||
-- 261001 grants dwh_control. This file is a single checkpoint that verifies
|
|
||||||
-- those prior migrations were applied in the right order, and fails loudly
|
|
||||||
-- if anything is missing before the pipeline goes live.
|
|
||||||
--
|
|
||||||
-- Password rotation and sslmode=require enforcement are out-of-band:
|
|
||||||
-- rotate via ALTER ROLE ... PASSWORD ... in a psql superuser session,
|
|
||||||
-- enforce SSL via the n8n credential (sslmode=require) — not SQL-level.
|
|
||||||
-- =============================================================
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
missing TEXT := '';
|
|
||||||
r RECORD;
|
|
||||||
BEGIN
|
|
||||||
-- 1. Roles exist
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dwh_owner') THEN
|
|
||||||
missing := missing || E'\n - role dwh_owner missing (expected from 260423)';
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN
|
|
||||||
missing := missing || E'\n - role grafana_ro missing (expected from 260423)';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- 2. grafana_ro CONNECT on this database
|
|
||||||
IF NOT has_database_privilege('grafana_ro', current_database(), 'CONNECT') THEN
|
|
||||||
missing := missing
|
|
||||||
|| format(E'\n - grafana_ro lacks CONNECT on database %s',
|
|
||||||
current_database());
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- 3. grafana_ro USAGE on every schema the pipeline / dashboards touch
|
|
||||||
FOR r IN
|
|
||||||
SELECT unnest(ARRAY['bronze','silver','gold','dwh_control']) AS schema_name
|
|
||||||
LOOP
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = r.schema_name) THEN
|
|
||||||
missing := missing
|
|
||||||
|| format(E'\n - schema %s missing (expected from 260423/261001)',
|
|
||||||
r.schema_name);
|
|
||||||
ELSIF NOT has_schema_privilege('grafana_ro', r.schema_name, 'USAGE') THEN
|
|
||||||
missing := missing
|
|
||||||
|| format(E'\n - grafana_ro lacks USAGE on schema %s',
|
|
||||||
r.schema_name);
|
|
||||||
END IF;
|
|
||||||
END LOOP;
|
|
||||||
|
|
||||||
IF length(missing) > 0 THEN
|
|
||||||
RAISE EXCEPTION E'DWH roles audit FAILED:%s', missing;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RAISE NOTICE 'DWH roles audit OK: dwh_owner + grafana_ro present with expected grants.';
|
|
||||||
END$$;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
-- =============================================================
|
|
||||||
-- DWH OBSERVABILITY VIEWS
|
|
||||||
-- Target Database: tracksolid_dwh
|
|
||||||
-- Purpose: Surface pipeline health for Grafana dashboards. Three views,
|
|
||||||
-- one concern each:
|
|
||||||
-- v_table_freshness — how long since each table was last loaded
|
|
||||||
-- v_recent_failures — failed runs in the last 24h
|
|
||||||
-- v_watermark_lag — per-table watermark vs. now
|
|
||||||
-- Applies after: 261001_dwh_control.sql
|
|
||||||
-- Readability: owned by dwh_owner → grafana_ro inherits SELECT via the
|
|
||||||
-- ALTER DEFAULT PRIVILEGES set in 261001. Explicit GRANT below
|
|
||||||
-- covers the case where defaults were set AFTER this file runs.
|
|
||||||
-- =============================================================
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
SET ROLE dwh_owner;
|
|
||||||
|
|
||||||
-- 1. FRESHNESS
|
|
||||||
-- One row per table that has ever loaded successfully. `lag` drives the
|
|
||||||
-- freshness panel; `loads_last_24h` sanity-checks the cron cadence.
|
|
||||||
CREATE OR REPLACE VIEW dwh_control.v_table_freshness AS
|
|
||||||
SELECT
|
|
||||||
table_name,
|
|
||||||
MAX(run_finished_at) AS last_loaded_at,
|
|
||||||
NOW() - MAX(run_finished_at) AS lag,
|
|
||||||
COUNT(*) FILTER (WHERE run_started_at > NOW() - INTERVAL '24 hours') AS loads_last_24h
|
|
||||||
FROM dwh_control.extract_runs
|
|
||||||
WHERE status = 'loaded'
|
|
||||||
GROUP BY table_name;
|
|
||||||
|
|
||||||
COMMENT ON VIEW dwh_control.v_table_freshness IS
|
|
||||||
'Per-table load lag. Alert when lag > 4h during active hours (05:00–23:00 EAT).';
|
|
||||||
|
|
||||||
-- 2. RECENT FAILURES
|
|
||||||
-- Failures retain error_message; the CSV stays in dwh/exports/ for the next
|
|
||||||
-- scheduled run to retry. Panel should show run_id so operators can grep logs.
|
|
||||||
CREATE OR REPLACE VIEW dwh_control.v_recent_failures AS
|
|
||||||
SELECT
|
|
||||||
run_id,
|
|
||||||
table_name,
|
|
||||||
run_started_at,
|
|
||||||
run_finished_at,
|
|
||||||
csv_path,
|
|
||||||
error_message
|
|
||||||
FROM dwh_control.extract_runs
|
|
||||||
WHERE status = 'failed'
|
|
||||||
AND run_started_at > NOW() - INTERVAL '24 hours'
|
|
||||||
ORDER BY run_started_at DESC;
|
|
||||||
|
|
||||||
COMMENT ON VIEW dwh_control.v_recent_failures IS
|
|
||||||
'Failed extract/load runs in the last 24h. Alert on any row.';
|
|
||||||
|
|
||||||
-- 3. WATERMARK LAG
|
|
||||||
-- Distinguishes "pipeline ran but found nothing" (load_lag small, extract_lag
|
|
||||||
-- growing) from "pipeline is stuck" (both lags growing). Snapshot tables are
|
|
||||||
-- not in extract_watermarks so they do not appear here — that is intentional.
|
|
||||||
CREATE OR REPLACE VIEW dwh_control.v_watermark_lag AS
|
|
||||||
SELECT
|
|
||||||
table_name,
|
|
||||||
last_extracted_at,
|
|
||||||
last_loaded_at,
|
|
||||||
rows_loaded_last_run,
|
|
||||||
NOW() - last_loaded_at AS load_lag,
|
|
||||||
NOW() - last_extracted_at AS extract_lag
|
|
||||||
FROM dwh_control.extract_watermarks;
|
|
||||||
|
|
||||||
COMMENT ON VIEW dwh_control.v_watermark_lag IS
|
|
||||||
'Per-table watermark position vs. now. Incremental tables only (6 rows).';
|
|
||||||
|
|
||||||
RESET ROLE;
|
|
||||||
|
|
||||||
-- Explicit grants: defensive in case ALTER DEFAULT PRIVILEGES from 261001
|
|
||||||
-- was not in effect when these views were created.
|
|
||||||
GRANT SELECT ON dwh_control.v_table_freshness TO grafana_ro;
|
|
||||||
GRANT SELECT ON dwh_control.v_recent_failures TO grafana_ro;
|
|
||||||
GRANT SELECT ON dwh_control.v_watermark_lag TO grafana_ro;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -1,145 +0,0 @@
|
||||||
IMEI,Device Name,Vehicle Name,Vehicle Icon,License Plate No.,Vehicle Model,Vehicle Brand,Driver Name,Telephone,SIM,Fuel/100km,VIN,Engine Number,Remarks,Group,Department,Account,Customer Name,Model,Activated Date,Sales Time,MAC,Subscription Expiration,User Expiration Date,Battery replacement date,ICCID,IMSI,ID Number,Installation Time
|
|
||||||
865135061569479,UMA 382EK,UMA 382EK,automobile,UMA 382EK,,,UG,,+256792997079,,,,Dept: MTN,Default Group,MTN,fireside,Fireside Group HQ,X3,2026-02-26,2025-09-08,,2036-02-27,2036-02-27,,8925610001837573419F,641101970467667,,
|
|
||||||
865135061569131,UMA 418EK,UMA 418EK,automobile,UMA 418EK,,,UG,,+256792997053,,,,Dept: MTN,Default Group,MTN,fireside,Fireside Group HQ,X3,2026-02-26,2025-09-08,,2036-02-27,2036-02-27,,8925610001837573385F,641101970467664,,
|
|
||||||
862798052707896,John Mbugua - KDW 573B,KDW 573B,automobile,KDW 573B,Probox,,John Mbugua,,,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group HQ,JC400P,2026-01-30,2025-06-11,,2036-01-31,2036-01-31,,89254021414206816725,639021410681672,,
|
|
||||||
865135061563423,Joel Ntumba - UMA 826AB,UMA 826AB,mtc,UMA 826AB,Motorbike,,Joel Ntumba,,0119051036,,,,Dept: MTN,Default Group,MTN,fireside,Fireside Group HQ,X3,2026-01-28,2025-09-08,,2036-01-29,2036-01-29,,89254021414206652690,639021410665269,,
|
|
||||||
865135061564280,Rodin Kiberu - UMA 011EK,UMA 011EK,mtc,UMA 011EK,Motorbike,,Rodin Kiberu,,0118081642,,,,Dept: MTN,Default Group,MTN,fireside,Fireside Group HQ,X3,2026-01-28,2025-09-08,,2036-01-29,2036-01-29,,89254021414206817244,639021410681724,,
|
|
||||||
862798052708068,Dominic Wambua - KDV 683Z,KDV 683Z,automobile,KDV 683Z,Probox,,Dominic Wambua,,0758048043,,,,Dept: ROLLOUT,Default Group,ROLLOUT,fireside,Fireside Group HQ,JC400P,2026-01-24,2025-06-11,,2036-01-25,2036-01-25,,89254021414206816964,639021410681696,,
|
|
||||||
862798052708167,Levine Wasike - KDV 439W,KDV 439W,automobile,KDV 439W,Probox,,Levine Wasike,,0758046738,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group HQ,JC400P,2025-12-13,2025-06-11,,2035-12-14,2035-12-14,,89254021414206816741,639021410681674,,
|
|
||||||
865135061563639,Benjamin Ananda - KDV 438W,KDV 438W,automobile,KDV 438W,Probox,,Benjamin Ananda,,0758047065,,,,Dept: PLANNING,Default Group,PLANNING,fireside,Fireside Group HQ,X3,2025-12-13,2025-09-08,,2035-12-14,2035-12-14,,89254021414206816683,639021410681668,,
|
|
||||||
865135061569123,Albert Mutwiri - KDV 437W,KDV 437W,automobile,KDV 437W,Probox,,Albert Mutwiri,,0758047101,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group HQ,X3,2025-12-13,2025-09-08,,2035-12-14,2035-12-14,,89254021414206816881,639021410681688,,
|
|
||||||
865135061564470,Silvanus Kipkorir - KDV 064S,KDV 064S,automobile,KDV 064S,Probox,,Silvanus Kipkorir,,0113669866,,,,Dept: AIRTEL,Default Group,AIRTEL,fireside,Fireside Group HQ,X3,2025-11-21,2025-09-08,,2035-11-22,2035-11-22,,89254021414206378718,639021410637871,,
|
|
||||||
865135061581904,Robert Kipruto - KDV 072L,KDV 072L,automobile,KDV 072L,Probox,,Robert Kipruto,,0114149576,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group HQ,X3,2025-11-21,2025-09-08,,2035-11-22,2035-11-22,,89254021264261503993,639021266150399,,
|
|
||||||
862798052713779,Benard Kimutai - KDN 759G,KDN 759G,automobile,KDN 759G,Probox,,Benard Kimutai,,0752143258,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group HQ,JC400P,2025-08-23,2025-06-11,,2035-08-24,2035-08-24,,89254035061001753860,639035060175386,,
|
|
||||||
865135061043426,Geoffrey Karanja - KMGS 239H,KMGS 239H,mtc,KMGS 239H,Motorbike,,Geoffrey Karanja,,0768696658,,,,Dept: OSP-PATROL,Default Group,OSP-PATROL,fireside,Fireside Group HQ,X3,2025-08-22,2025-06-11,,2035-08-23,2035-08-23,,89254021394274518926,639021397451892,,
|
|
||||||
865135061053714,Samuel Kihara - KMEL 225X,KMEL 225X,mtc,KMEL 225X,Motorbike,,Samuel Kihara,,0768696832,,,,Dept: OSP-PATROL,Default Group,OSP-PATROL,fireside,Fireside Group HQ,X3,2025-08-02,2025-06-11,,2035-08-03,2035-08-03,,89254021394274518934,639021397451893,,
|
|
||||||
865135061036164,Brian Njenga - KMFF 113Z,KMFF 113Z,mtc,KMFF 113Z,Motorbike,,Brian Njenga,,0768696705,,,,Dept: OSP-PATROL,Default Group,OSP-PATROL,fireside,Fireside Group HQ,X3,2025-07-31,2025-06-11,,2035-08-01,2035-08-01,,89254021394274518850,639021397451885,,
|
|
||||||
865135061049001,Parked - KMGK 596V,KMGK 596V,mtc,KMGK 596V,Motorbike,,Parked,,0768697064,,,,Dept: DELIVERIES,Default Group,DELIVERIES,fireside,Fireside Group HQ,X3,2025-07-31,2025-06-11,,2035-08-01,2035-08-01,,89254021394274518884,639021397451888,,
|
|
||||||
862798052715220,Rofas Njagi - KDT 728R,KDT 728R,automobile,KDT 728R,Probox,,Rofas Njagi,,0704573658,,,,Dept: REGIONAL,Default Group,REGIONAL,fireside,Fireside Group HQ,JC400P,2025-07-16,2025-06-11,,2035-07-17,2035-07-17,,89254021334258495873,639021335849587,,
|
|
||||||
865135061037980,Emmanuel Luseno - KDS 453Y,KDS 453Y,automobile,KDS 453Y,Pick-Up,,Emmanuel Luseno,,0790176734,,,,Dept: GENERAL,Default Group,GENERAL,fireside,Fireside Group HQ,X3,2025-07-15,2025-06-11,,,2035-07-15,,89254021394215205856,639021391520585,,
|
|
||||||
865135061035778,John Kimeria - KDS 525D,KDS 525D,truck,KDS 525D,Crane,,John Kimeria,,0790176738,,,,Dept: GENERAL,Default Group,GENERAL,fireside,Fireside Group HQ,X3,2025-07-11,2025-06-11,,2035-07-12,2035-07-12,,89254021394215205922,639021391520592,,
|
|
||||||
865135061053748,Rashid Hassan - KDM 840V,KDM 840V,automobile,KDM 840V,Probox,,Rashid Hassan,,0768445963,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,X3,2025-07-10,2025-06-11,,2035-07-11,2035-07-11,,89254021334212352574,639021331235257,,
|
|
||||||
865135061042261,Kelvin Wambugu - KDR 592N,KDR 592N,automobile,KDR 592N,Probox,,Kelvin Wambugu,,0797680464,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group HQ,X3,2025-07-10,2025-06-11,,2035-07-11,2035-07-11,,89254021334258159693,639021335815969,,
|
|
||||||
862798052713811,James Onyango - KDU 613B,KDU 613B,automobile,KDU 613B,Probox,,James Onyango,,0790176542,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2025-07-09,2025-06-11,,2035-07-10,2035-07-10,,89254021394215205880,639021391520588,,
|
|
||||||
865135061047435,Management_Mazda - KDU 613A,KDU 613A,automobile,KDU 613A,Mazda,,Management_Mazda,,0790175971,,,,Dept: MGT,Default Group,MGT,fireside,Fireside Group HQ,X3,2025-07-09,2025-06-11,,2035-07-10,2035-07-10,,89254021394215205971,639021391520597,,
|
|
||||||
862798050522743,Charles Nyambane - KCB 711C,KCB 711C,automobile,KCB 711C,Probox,,Charles Nyambane,,0768657106,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2023-12-22,2024-11-08,,2033-12-23,2033-12-23,,,,,
|
|
||||||
862798050525225,Sadique Wakayula - KDC 490Q,KDC 490Q,truck,KDC 490Q,Crane,,Sadique Wakayula,,0768652386,,,,Dept: GENERAL,Default Group,GENERAL,fireside,Fireside Group HQ,JC400P,2023-12-22,2024-11-08,,2043-12-22,2043-12-22,,,,,
|
|
||||||
862798050525068,Samuel Ng'ang'a - KDE 264M,KDE 264M,automobile,KDE 264M,Probox,,Samuel Ng'ang'a,,0768658564,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2023-12-22,2024-11-08,,2033-12-23,2033-12-23,,,,,
|
|
||||||
862798050525837,Kennedy Ondieki - KCU 237Z,KCU 237Z,automobile,KCU 237Z,Probox,,Kennedy Ondieki,,0113669852,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2023-12-21,,,2033-12-22,2033-12-22,,,,,
|
|
||||||
862798050523618,Geoffrey Too - KDM 308S,KDM 308S,automobile,KDM 308S,Probox,,Geoffrey Too,,0701211625,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group HQ,JC400P,2023-08-15,2023-08-22,,2033-08-16,2033-08-16,,,,,
|
|
||||||
862798050523816,Job Ngare - KDM 309S,KDM 309S,automobile,KDM 309S,Probox,,Job Ngare,,0707936781,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2023-08-15,2023-08-22,,2033-08-16,2033-08-16,,,,,
|
|
||||||
359857082912239,Dickson Jaoko - KDK 815R,KDK 815R,automobile,KDK 815R,Probox,,Dickson Jaoko,,0706392117,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group HQ,GT06E,2023-06-21,2023-07-27,,2033-06-22,2033-06-22,,89254021234296021287,639021239602128,,
|
|
||||||
359857082897091,Peter Mbugua - KDK 728K,KDK 728K,automobile,KDK 728K,Probox,,Peter Mbugua,,0790262984,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,GT06E,2022-12-14,2022-12-16,,2042-12-15,2042-12-15,,89254021234222500396,639021232250039,,
|
|
||||||
862798050524608,Peter Mbugua - KDK 728K,KDK 728K,automobile,KDK 728K,Probox,,Peter Mbugua,,0706742413,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2022-12-03,2022-12-15,,2042-12-04,2042-12-04,,,,,
|
|
||||||
862798050524368,862798050524368,,automobile,,,,,,,,,,,Default Group,,fireside,Fireside Group HQ,JC400P,2022-10-29,2022-12-17,,2042-10-30,2042-10-30,,,,,
|
|
||||||
862798050524558,Mutuku Joseph - KDC 739F,KDC 739F,automobile,KDC 739F,Probox,,Mutuku Joseph,,0100858817,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group HQ,JC400P,2022-01-22,2022-01-25,,2042-01-23,2042-01-23,,,,,
|
|
||||||
862798050524897,Cornelius Kimutai - KCU 938R,KCU 938R,automobile,KCU 938R,Van,,Cornelius Kimutai,,0114924404,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group HQ,JC400P,2022-01-22,2022-01-25,,2042-01-23,2042-01-23,,,,,
|
|
||||||
862798050522107,Cassius Wakiyo - KDB 323M,KDB 323M,automobile,KDB 323M,Probox,,Cassius Wakiyo,,0114149576,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group HQ,JC400P,2022-01-22,2022-01-25,,2042-01-23,2042-01-23,,,,,
|
|
||||||
862798050524657,Felix Andole - KDC 207R,KDC 207R,automobile,KDC 207R,Probox,,Felix Andole,,0758689195,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2022-01-22,2022-01-25,,2042-01-23,2042-01-23,,,,,
|
|
||||||
862798050523386,George Ochieng' - KDD 684Y,KDD 684Y,automobile,KDD 684Y,Probox,,George Ochieng',,0785586834,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group HQ,JC400P,2022-01-22,2022-01-27,,2042-01-23,2042-01-23,,,,,
|
|
||||||
862798050524384,Hamisi Pande - KDD 689Y,KDD 689Y,automobile,KDD 689Y,Probox,,Hamisi Pande,,0701211744,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2022-01-22,2022-01-27,,2042-01-23,2042-01-23,,,,,
|
|
||||||
862798050525589,Simon Kamau - KCE 090R,KCE 090R,automobile,KCE 090R,Probox,,Simon Kamau,,0796276387,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2022-01-19,2022-01-17,,2042-01-20,2042-01-20,,,,,
|
|
||||||
862798050525423,Makori John - KDB 585E,KDB 585E,automobile,KDB 585E,Probox,,Makori John,,0701211724,,,,Dept: PLANNING,Default Group,PLANNING,fireside,Fireside Group HQ,JC400P,2022-01-15,2022-01-17,,2042-01-16,2042-01-16,,,,,
|
|
||||||
862798050525951,Wright Oseko - KCG 668W,KCG 668W,automobile,KCG 668W,Probox,,Wright Oseko,,0741943212,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group HQ,JC400P,2022-01-15,2022-01-17,,2042-01-16,2042-01-16,,,,,
|
|
||||||
862798050522859,Garage - KCH 167M,KCH 167M,automobile,KCH 167M,Probox,,Garage,,0706740252,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group HQ,JC400P,2022-01-15,2022-01-17,,2042-01-16,2042-01-16,,,,,
|
|
||||||
862798050524707,Garage - KCE 699F,KCE 699F,automobile,KCE 699F,Probox,,Garage,,0110525751,,,,Dept: ROLLOUT,Default Group,ROLLOUT,fireside,Fireside Group HQ,JC400P,2022-01-15,2022-01-17,,2042-01-16,2042-01-16,,,,,
|
|
||||||
862798050522883,Dan Watila - KDE 638J,KDE 638J,automobile,KDE 638J,Probox,,Dan Watila,,0112615393,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2022-01-15,2022-01-17,,2042-01-16,2042-01-16,,,,,
|
|
||||||
862798050525605,John Ondego - KCA 542Q,KCA 542Q,automobile,KCA 542Q,Probox,,John Ondego,,0110526783,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2022-01-15,2022-01-17,,2042-01-16,2042-01-16,,,,,
|
|
||||||
862798050288360,Brian Ngetich - KDA 717B,KDA 717B,automobile,KDA 717B,Probox,,Brian Ngetich,,0717867861,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,JC400P,2021-11-05,2021-11-08,,2041-11-06,2041-11-06,,,,,
|
|
||||||
862798050288261,Patric Bett - KDA 609E,KDA 609E,automobile,KDA 609E,Probox,,Patric Bett,0112693340,0790176509,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group HQ,JC400P,2021-10-23,2021-10-25,,2041-10-24,2041-10-24,,,,,
|
|
||||||
359857082042052,Gabriel Musumba - KCE 690F,KCE 690F,automobile,KCE 690F,Probox,,Gabriel Musumba,,0110094466,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group HQ,GT06E,2020-04-03,2020-04-16,,2040-04-04,2040-04-04,,89254021164215938024,639021161593802,,
|
|
||||||
359857081885410,Allan Owana - KDK 780K,KDK 780K,automobile,KDK 780K,Probox,,Allan Owana,,0703616117,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,GT06E,2019-06-19,2019-07-01,,2039-06-20,2039-06-20,,89254021234222499854,639021232249985,,
|
|
||||||
359857081891798,Garage - KCH 167M,KCH 167M,automobile,KCH 167M,Probox,,Garage,,0746760102,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group HQ,GT06E,2019-06-16,2019-07-01,,2039-06-17,2039-06-17,,89254021084186499493,639021088649949,,
|
|
||||||
359857081891632,John Ondego - KCA 542Q,KCA 542Q,automobile,KCA 542Q,Probox,,John Ondego,,0746760038,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group HQ,GT06E,2019-06-15,2019-07-01,,2039-06-16,2039-06-16,,89254021084186499485,639021088649948,,
|
|
||||||
862798052708035,862798052708035,,automobile,,Probox,,,,,,,,,Default Group,,fireside,Fireside Group HQ,JC400P,Inactive,2025-06-11,,120Month,——,,,,,
|
|
||||||
865135061563597,Dominic Wambua - KDV 683Z,KDV 683Z,automobile,KDV 683Z,Probox,,Dominic Wambua,,0758052405,,,,Dept: ROLLOUT,Default Group,ROLLOUT,fireside,Fireside Telematics ,X3,2026-01-30,2026-02-24,,2036-01-31,2036-01-31,,89254021414206816733,639021410681673,,
|
|
||||||
865135061562722,John Mbugua - KDW 573B,KDW 573B,automobile,KDW 573B,Probox,,John Mbugua,,0758052508,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Telematics ,X3,2026-01-30,2026-02-24,,2036-01-31,2036-01-31,,89254021414206816832,639021410681683,,
|
|
||||||
862798052708282,Godffrey Nandwa - KCN 496A,KCN 496A,automobile,KCN 496A,Probox,,Godffrey Nandwa,,0758047934,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,JC400P,2026-01-25,2026-02-20,,2036-01-26,2036-01-26,,89254021414206816865,639021410681686,,
|
|
||||||
862798052707888,Benjamin Ananda - KDV 438W,KDV 438W,automobile,KDV 438W,Probox,,Benjamin Ananda,,0758047312,,,,Dept: PLANNING,Default Group,PLANNING,fireside,Fireside Telematics ,JC400P,2025-12-15,2026-02-20,,2035-12-16,2035-12-16,,89254021414206816980,639021410681698,,
|
|
||||||
862798052708076,Albert Mutwiri - KDV 437W,KDV 437W,automobile,KDV 437W,Probox,,Albert Mutwiri,,0758047094,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Telematics ,JC400P,2025-12-13,2026-02-20,,2035-12-14,2035-12-14,,89254021414206816782,639021410681678,,
|
|
||||||
865135061562847,Levine Wasike - KDV 439W,KDV 439W,automobile,KDV 439W,Probox,,Levine Wasike,,0758047032,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Telematics ,X3,2025-12-13,2026-02-24,,2035-12-14,2035-12-14,,89254021414206816840,639021410681684,,
|
|
||||||
862798052714066,862798052714066,,automobile,,Probox,,,,,,,,,Default Group,,fireside,Fireside Telematics ,JC400P,2025-11-21,2025-06-11,,2035-11-22,2035-11-22,,89254021414206378684,639021410637868,,
|
|
||||||
862798052713837,Kennedy Ondieki - KCU 237Z,KCU 237Z,automobile,KCU 237Z,Probox,,Kennedy Ondieki,,0113669852,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,JC400P,2025-10-08,2026-02-20,,2035-10-09,2035-10-09,,89254021414206327855,639021410632785,,
|
|
||||||
862798052713696,862798052713696,,automobile,,Probox,,,,,,,,,Default Group,,fireside,Fireside Telematics ,JC400P,2025-09-02,2025-06-11,,2035-09-03,2035-09-03,,89254021394215205906,639021391520590,,
|
|
||||||
862798052713985,Timothy Gitau - KDT 916R,KDT 916R,automobile,KDT 916R,Probox,,Timothy Gitau,,0768696668,,,,Dept: REGIONAL,Default Group,REGIONAL,fireside,Fireside Telematics ,JC400P,2025-08-02,2026-02-20,,2035-08-03,2035-08-03,,89254021394274518892,639021397451889,,
|
|
||||||
865135061035653,Richardson Komu - KDT 923R,KDT 923R,automobile,KDT 923R,Probox,,Richardson Komu,,0768697292,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,X3,2025-08-02,2026-02-24,,2035-08-03,2035-08-03,,89254021394274518942,639021397451894,,
|
|
||||||
865135061048466,Samuel Muriithy - KDR 594N,KDR 594N,automobile,KDR 594N,Probox,,Samuel Muriithy,,0797680395,,,,Dept: ROLLOUT,Default Group,ROLLOUT,fireside,Fireside Telematics ,X3,2025-07-24,2026-02-24,,2035-07-25,2035-07-25,,89254021334258159628,639021335815962,,
|
|
||||||
865135061054555,Rofas Njagi - KDT 728R,KDT 728R,automobile,KDT 728R,Probox,,Rofas Njagi,,0790176726,,,,Dept: REGIONAL,Default Group,REGIONAL,fireside,Fireside Telematics ,X3,2025-07-16,2026-02-24,,2035-07-17,2035-07-17,,89254021394215205823,639021391520582,,
|
|
||||||
862798052713761,Management_Mazda - KDU 613A,KDU 613A,automobile,KDU 613A,Mazda,,Management_Mazda,,0790176786,,,,Dept: MGT,Default Group,MGT,fireside,Fireside Telematics ,JC400P,2025-07-09,2026-02-20,,2035-07-10,2035-07-10,,89254021394215205955,639021391520595,,
|
|
||||||
865135061054548,James Onyango - KDU 613B,KDU 613B,automobile,KDU 613B,Probox,,James Onyango,,0790175997,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,X3,2025-07-09,2026-02-24,,2035-07-10,2035-07-10,,89254021394215205948,639021391520594,,
|
|
||||||
862798050526231,Rashid Hassan - KDM 840V,KDM 840V,automobile,KDM 840V,Probox,,Rashid Hassan,,0790175526,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,JC400P,2023-12-22,2026-02-20,,2043-12-23,2043-12-23,,,,,
|
|
||||||
862798050523139,Mike Wanaswa - KDT 724R,KDT 724R,automobile,KDT 724R,Probox,,Mike Wanaswa,,0790175045,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,JC400P,2023-12-22,2026-02-20,,2043-12-23,2043-12-23,,,,,
|
|
||||||
862798050523063,Kelvin Wambugu - KDR 594N,KDR 594N,automobile,KDR 594N,Probox,,Kelvin Wambugu,,0701211876,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Telematics ,JC400P,2023-12-22,2026-02-20,,2043-12-22,2043-12-22,,,,,
|
|
||||||
862798050523626,Major Simiyu - KDS 949Y,KDS 949Y,automobile,KDS 949Y,Probox,,Major Simiyu,,0701211892,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Telematics ,JC400P,2023-12-22,2026-02-20,,2033-12-23,2033-12-23,,,,,
|
|
||||||
862798050523337,Victor Kimutai - KDS 919Y,KDS 919Y,automobile,KDS 919Y,Probox,,Victor Kimutai,,0700242527,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Telematics ,JC400P,2023-12-22,2026-02-20,,2043-12-22,2043-12-22,,,,,
|
|
||||||
862798050523295,Emmanuel Luseno - KDS 453 Y,KDS 453 Y,automobile,KDS 453 Y,Pick-Up,,Emmanuel Luseno,,0700242474,,,,Dept: GENERAL,Default Group,GENERAL,fireside,Fireside Telematics ,JC400P,2023-12-22,2026-02-20,,2033-12-23,2033-12-23,,,,,
|
|
||||||
862798050523014,Samuel Muriithy - KDR 594N,KDR 594N,automobile,KDR 594N,Probox,,Samuel Muriithy,,0790175423,,,,Dept: ROLLOUT,Default Group,ROLLOUT,fireside,Fireside Telematics ,JC400P,2023-12-21,2026-02-20,,2033-12-22,2033-12-22,,,,,
|
|
||||||
862798050521521,John Kimeria - KDS 525D,KDS 525D,truck,KDS 525D,Crane,,John Kimeria,,0752958416,,,,Dept: GENERAL,Default Group,GENERAL,fireside,Fireside Telematics ,JC400P,2023-11-26,2026-02-20,,2033-11-27,2033-11-27,,,,,
|
|
||||||
862798050524533,Leonard Nzai - KDM 306S,KDM 306S,automobile,KDM 306S,Probox,,Leonard Nzai,,0703487162,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,JC400P,2023-08-21,2026-02-20,,2033-08-22,2033-08-22,,,,,
|
|
||||||
359857082898016,Job Ngare - KDM 309S,KDM 309S,automobile,KDM 309S,Probox,,Job Ngare,,0706895756,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,GT06E,2023-08-15,2026-02-24,,2033-08-16,2033-08-16,,89254021324273007563,639021327300756,,
|
|
||||||
862798050525266,Dickson Jaoko - KDK 815R,KDK 815R,automobile,KDK 815R,Probox,,Dickson Jaoko,,0706665867,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Telematics ,JC400P,2023-06-21,2026-02-20,,2033-06-22,2033-06-22,,,,,
|
|
||||||
862798050523527,Allan Owana - KDK 780K,KDK 780K,automobile,KDK 780K,Probox,,Allan Owana,,0792375024,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,JC400P,2022-12-03,2026-02-20,,2042-12-04,2042-12-04,,,,,
|
|
||||||
862798050524426,Amani Sulubu - KCY 090X,KCY 090X,automobile,KCY 090X,Probox,,Amani Sulubu,,0113823350,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,JC400P,2022-01-16,2026-02-20,,2042-01-17,2042-01-17,,,,,
|
|
||||||
862798050522065,Gideon Kiprono - KCQ 215F,KCQ 215F,automobile,KCQ 215F,Probox,,Gideon Kiprono,,0113343715,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,JC400P,2022-01-16,2026-02-20,,2042-01-17,2042-01-17,,,,,
|
|
||||||
862798050525670,Gabriel Musumba - KCE 690F,KCE 690F,automobile,KCE 690F,Probox,,Gabriel Musumba,,0701211996,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Telematics ,JC400P,2022-01-15,2026-02-20,,2042-01-16,2042-01-16,,,,,
|
|
||||||
862798050288345,Santoes Omondi - KCZ 181P,KCZ 181P,automobile,KCZ 181P,Pick-Up,,Santoes Omondi,,0768446105,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Telematics ,JC400P,2021-11-06,2026-02-20,,2041-11-07,2041-11-07,,,,,
|
|
||||||
862798050288303,Elias Baya - KCZ 476E,KCZ 476E,automobile,KCZ 476E,Probox,,Elias Baya,,0115870439,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Telematics ,JC400P,2021-11-06,2026-02-20,,2041-11-07,2041-11-07,,,,,
|
|
||||||
862798050288212,Nicholas Erastus - KCQ 581M,KCQ 581M,automobile,KCQ 581M,Probox,,Nicholas Erastus,,0746979531,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,JC400P,2021-11-02,2026-02-20,,2041-11-03,2041-11-03,,,,,
|
|
||||||
359857082898008,Samuel Ng'ang'a - KDE 264M,KDE 264M,automobile,KDE 264M,Probox,,Samuel Ng'ang'a,,0711731539,,,,Dept: ISP,Default Group,ISP ,fireside,Fireside Telematics ,GT06E,2021-10-28,2026-02-24,,2041-10-29,2041-10-29,,89254021264260342245,639021266034224,,
|
|
||||||
359857082898487,Dan Watila - KDE 638J,KDE 638J,automobile,KDE 638J,Probox,,Dan Watila,,0116242996,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,GT06E,2021-10-21,2026-02-24,,2041-10-22,2041-10-22,,89254021334258404214,639021335840421,,
|
|
||||||
359857082900358,Geoffrey Too - KDM 308S,KDM 308S,automobile,KDM 308S,Probox,,Geoffrey Too,,0796527601,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Telematics ,GT06E,2021-10-21,2026-02-24,,2041-10-22,2041-10-22,,89254021264260126572,639021266012657,,
|
|
||||||
359857082896911,Hamisi Pande - KDD 689Y,KDD 689Y,automobile,KDD 689Y,Probox,,Hamisi Pande,,0112714612,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,GT06E,2021-09-17,2026-02-24,,2041-09-18,2041-09-18,,89254021214211314660,639021211131466,,
|
|
||||||
359857082900697,George Ochieng' - KDD 684Y,KDD 684Y,automobile,KDD 684Y,Probox,,George Ochieng',,0114879518,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Telematics ,GT06E,2021-09-17,2026-02-24,,2041-09-18,2041-09-18,,89254021214211314678,639021211131467,,
|
|
||||||
359857082897257,Cassius Wakiyo - KDB 323M,KDB 323M,automobile,KDB 323M,Probox,,Cassius Wakiyo,,0746428882,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Telematics ,GT06E,2021-08-29,2026-02-24,,2041-08-29,2041-08-29,,89254021234222500818,639021232250081,,
|
|
||||||
359857082897737,John Makori - KDB 585E,KDB 585E,automobile,KDB 585E,Probox,,John Makori,,0114596734,,,,Dept: PLANNING,Default Group,PLANNING,fireside,Fireside Telematics ,GT06E,2021-08-29,2026-02-24,,2041-08-29,2041-08-29,,89254021214211145262,639021211114526,,
|
|
||||||
359857082911983,Brian Ngetich - KDA 717B,KDA 717B,automobile,KDA 717B,Probox,,Brian Ngetich,0795188807,0795188807,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,GT06E,2021-08-29,2026-02-24,,2041-08-29,2041-08-29,,89254021214211145288,639021211114528,,
|
|
||||||
359857082902461,Sadique Wakayula - KDC 490Q,KDC 490Q,truck,KDC 490Q,Crane,,Sadique Wakayula,,0757556468,,,,Dept: GENERAL,Default Group,GENERAL,fireside,Fireside Telematics ,GT06E,2021-05-22,2026-02-24,,2041-05-22,2041-05-22,,89254021154296722488,639021159672248,,
|
|
||||||
359857082902503,Felix Andole - KDC 207R,KDC 207R,automobile,KDC 207R,Probox,,Felix Andole,,0794820817,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,GT06E,2021-05-15,2026-02-24,,2041-05-15,2041-05-15,,89254021224270993254,639021227099325,,
|
|
||||||
359857082897794,Mutuku Joseph - KDC 739F,KDC 739F,automobile,KDC 739F,Probox,,Mutuku Joseph,0115019037,0115019037,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Telematics ,GT06E,2021-04-10,2026-02-24,,2041-04-10,2041-04-10,,89254021224222632356,639021222263235,,
|
|
||||||
359857082910589,Patric Bett - KDA 609E,KDA 609E,automobile,KDA 609E,Probox,,Patric Bett,,0797622637,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Telematics ,GT06E,2020-10-26,2026-02-24,,2040-10-27,2040-10-27,,89254021154296722496,639021159672249,,
|
|
||||||
359857082918012,Charles Nyambane - KCB 711C,KCB 711C,automobile,KCB 711C,Probox,,Charles Nyambane,,0793704231,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,GT06E,2020-09-21,2026-02-24,,2040-09-22,2040-09-22,,89254021154287138363,639021158713836,,
|
|
||||||
359857081887069,Wright Oseko - KCG 668W,KCG 668W,automobile,KCG 668W,Probox,,Wright Oseko,,0746763106,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Telematics ,GT06E,2019-06-30,2026-02-24,,2039-07-01,2039-07-01,,89254021084186499915,639021088649991,,
|
|
||||||
359857081891590,Garage - KCE 699F,KCE 699F,automobile,KCE 699F,Probox,,Garage,,0746760215,,,,Dept: ROLLOUT,Default Group,ROLLOUT,fireside,Fireside Telematics ,GT06E,2019-06-16,2026-02-24,,2039-06-17,2039-06-17,,89254021084186499519,639021088649951,,
|
|
||||||
359857081891566,Simon Kamau - KCE 090R,KCE 090R,automobile,KCE 090R,Probox,,Simon Kamau,,0746760404,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,GT06E,2019-06-16,2026-02-24,,2039-06-17,2039-06-17,,89254021084186499527,639021088649952,,
|
|
||||||
359857081892101,Cornelius Kimutai - KCU 938R,KCU 938R,automobile,KCU 938R,Van,,Cornelius Kimutai,,0746759919,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Telematics ,GT06E,2019-06-12,2026-02-24,,2039-06-13,2039-06-13,,89254021084186499451,639021088649945,,2019-06-12
|
|
||||||
359857081892309,Nicholas Erastus - KCQ 581M,KCQ 581M,automobile,KCQ 581M,Probox,,Nicholas Erastus,,0700023776,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Telematics ,GT06E,2019-06-09,2026-02-24,,2039-06-10,2039-06-10,,89254021084178504672,639021087850467,,
|
|
||||||
865135061563415,Barack Orwa - KDW 781E,KDW 781E,automobile,KDW 781E,Vazel,,Barack Orwa,,0758052541,,,,Dept: MGT,Default Group,MGT,fireside,Fireside Group MSA,X3,2026-01-13,2025-09-08,,2036-01-14,2036-01-14,,89254021414206816931,639021410681693,,
|
|
||||||
865135061035133,Major Simiyu - KDS 949Y,KDS 949Y,automobile,KDS 949Y,Probox,,Major Simiyu,,0768696642,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group MSA,X3,2025-08-02,2025-06-11,,2035-08-03,2035-08-03,,89254021394274518918,639021397451891,,
|
|
||||||
865135061043079,Mike Wanaswa - KDT 724R,KDT 724R,automobile,KDT 724R,Probox,,Mike Wanaswa,,0768696664,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,X3,2025-08-02,2025-06-11,,2035-08-03,2035-08-03,,89254021394274518959,639021397451895,,
|
|
||||||
865135061048953,Timothy Gitau - KDT 916R,KDT 916R,automobile,KDT 916R,Probox,,Timothy Gitau,,0768697056,,,,Dept: REGIONAL,Default Group,REGIONAL,fireside,Fireside Group MSA,X3,2025-08-02,2025-06-11,,2035-08-03,2035-08-03,,89254021394274518967,639021397451896,,
|
|
||||||
865135061048276,Victor Kimutai - KDS 919Y,KDS 919Y,automobile,KDS 919Y,Probox,,Victor Kimutai,,0768696755,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,X3,2025-08-02,2025-06-11,,2035-08-03,2035-08-03,,89254021394274518900,639021397451890,,
|
|
||||||
862798050526256,Ian Dancun - KDT 923R,KDT 923R,automobile,KDT 923R,Probox,,Ian Dancun,,0794873610,,,,Dept: QEHS,Default Group,QEHS,fireside,Fireside Group MSA,JC400P,2023-12-22,,,2043-12-22,2043-12-22,,,,,
|
|
||||||
862798050526165,Wilfred Kinyanjui - KCU 729C,KCU 729C,truck,KCU 729C,Crane,,Wilfred Kinyanjui,,0790564929,,,,Dept: GENERAL,Default Group,GENERAL,fireside,Fireside Group MSA,JC400P,2023-11-26,2024-11-08,,2033-11-27,2033-11-27,,,,,
|
|
||||||
359857082916826,Denis Kazungu - KDM 794R,KDM 794R,automobile,KDM 794R,Probox,,Denis Kazungu,,0705700971,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group MSA,GT06E,2023-08-21,2023-08-22,,2033-08-22,2033-08-22,,89254021324273006854,639021327300685,,
|
|
||||||
359857082898073,Mutuku Antony - KDK 732K,KDK 732K,automobile,KDK 732K,Probox,,Mutuku Antony,,0793026954,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group MSA,GT06E,2022-12-20,2022-12-20,,2042-12-21,2042-12-21,,89254021234222387539,639021232238753,,
|
|
||||||
862798050524681,Mutuku Antony - KDK 732K,KDK 732K,automobile,KDK 732K,Probox,,Mutuku Antony,,0796275746,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group MSA,JC400P,2022-12-06,2022-12-16,,2042-12-07,2042-12-07,,,,,
|
|
||||||
862798050524566,Makanda Andrew - KCZ 155P,KCZ 155P,automobile,KCZ 155P,Pick-Up,,Makanda Andrew,,0758781444,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,JC400P,2022-01-22,2025-02-24,,2042-01-23,2042-01-23,,,,,
|
|
||||||
862798050521612,Denis Kazungu - KDM 794R,KDM 794R,automobile,KDM 794R,Probox,,Denis Kazungu,,0704113731,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group MSA,JC400P,2022-01-22,2024-11-19,,2042-01-23,2042-01-23,,,,,
|
|
||||||
862798050522719,Mbuvi Kioko - KCZ 199P,KCZ 199P,automobile,KCZ 199P,Pick-Up,,Mbuvi Kioko,,0768218655,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,JC400P,2022-01-16,2022-12-16,,2042-01-17,2042-01-17,,,,,
|
|
||||||
862798050524087,Felix Muema - KCZ 223P,KCZ 223P,automobile,KCZ 223P,Pick-Up,,Felix Muema,,0113973875,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,JC400P,2022-01-16,2024-12-30,,2042-01-17,2042-01-17,,,,,
|
|
||||||
862798050522891,Lawrence Kijogi - KCY 080X,KCY 080X,automobile,KCY 080X,Pick-Up,,Lawrence Kijogi,,0113287191,,,,Dept: ROLLOUT,Default Group,ROLLOUT,fireside,Fireside Group MSA,JC400P,2022-01-16,2022-12-16,,2042-01-17,2042-01-17,,,,,
|
|
||||||
862798050524392,Ndegwa Dancun - KCG 669W,KCG 669W,automobile,KCG 669W,Probox,,Ndegwa Dancun,,0113799173,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,JC400P,2022-01-16,2022-12-16,,2042-01-17,2042-01-17,,,,,
|
|
||||||
862798050521752,Simon Munda - KCZ 154S,KCZ 154S,automobile,KCZ 154S,Probox,,Simon Munda,,0113805921,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,JC400P,2022-01-16,2022-12-16,,2042-01-17,2042-01-17,,,,,
|
|
||||||
862798050524012,Moses Wambua - KCZ 751V,KCZ 751V,automobile,KCZ 751V,Probox,,Moses Wambua,,0113313797,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,JC400P,2022-01-16,2022-12-16,,2042-01-17,2042-01-17,,,,,
|
|
||||||
862798050523204,Amani Kazungu - KCY 084X,KCY 084X,automobile,KCY 084X,Probox,,Amani Kazungu,,0707892547,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,JC400P,2022-01-16,2022-12-16,,2042-01-17,2042-01-17,,,,,
|
|
||||||
862798050523949,Joseph Kabandi - KCY 076X,KCY 076X,automobile,KCY 076X,Probox,,Joseph Kabandi,,0113288492,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,JC400P,2022-01-16,2022-12-16,,2042-01-17,2042-01-17,,,,,
|
|
||||||
862798050525613,Kennedy Chege - KCQ 618K,KCQ 618K,automobile,KCQ 618K,Probox,,Kennedy Chege,,0729994247,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,JC400P,2022-01-16,2022-12-19,,2042-01-17,2042-01-17,,,,,
|
|
||||||
862798050525753,Noel Merengeni - KCY 838X,KCY 838X,automobile,KCY 838X,Probox,,Noel Merengeni,,,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group MSA,JC400P,2022-01-15,2023-08-23,,2042-01-16,2042-01-16,,,,,
|
|
||||||
359857082925330,Noel Merengeni - KCY 838X,KCY 838X,automobile,KCY 838X,Probox,,Noel Merengeni,,0794873610,,,,Dept: FDS,Default Group,FDS,fireside,Fireside Group MSA,GT06E,2020-10-26,2023-08-22,,2040-10-27,2040-10-27,,89254021154296723429,639021159672342,,
|
|
||||||
359857082900341,Simon Munda - KCZ 154S,KCZ 154S,automobile,KCZ 154S,Probox,,Simon Munda,,0757236135,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,GT06E,2020-09-23,2022-12-16,,2040-09-24,2040-09-24,,89254021154296723312,639021159672331,,
|
|
||||||
359857082912486,Moses Wambua - KCZ 751V,KCZ 751V,automobile,KCZ 751V,Probox,,Moses Wambua,,0792756503,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,GT06E,2020-09-23,2022-12-16,,2040-09-24,2040-09-24,,89254021154296723437,639021159672343,,
|
|
||||||
353549090553685,Daniel Omondi - KMFF 099Z,KMFF 099Z,mtc,KMFF 099Z,Motorbike,,Daniel Omondi,0112794067,0759336150,,,,Dept: OSP-PATROL,Default Group,OSP-PATROL,fireside,Fireside Group MSA,AT4,2020-09-23,2022-12-16,,2040-09-24,2040-09-24,,89254021334258404099,639021335840409,,
|
|
||||||
353549090567685,Daniel Kipkirui - KMFF 162Z,KMFF 162Z,mtc,KMFF 162Z,Motorbike,,Daniel Kipkirui,0112795498,0742532058,,,,Dept: OSP-PATROL,Default Group,OSP-PATROL,fireside,Fireside Group MSA,AT4,2020-09-23,2022-12-16,,2040-09-24,2040-09-24,,89254021264260388966,639021266038896,,
|
|
||||||
359857082910886,Makanda Andrew - KCZ 155P,KCZ 155P,automobile,KCZ 155P,Pick-Up,,Makanda Andrew,,0745067338,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,GT06E,2020-08-23,2025-02-24,,2040-08-24,2040-08-24,,89254021154287138397,639021158713839,,
|
|
||||||
359857082908500,Santoes Omondi - KCZ 181P,KCZ 181P,automobile,KCZ 181P,Pick-Up,,Santoes Omondi,,0701211974,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,GT06E,2020-08-23,2022-12-16,,2040-08-24,2040-08-24,,89254021374215155087,639021371515508,,
|
|
||||||
359857082918038,Mbuvi Kioko - KCC 199P,KCC 199P,automobile,KCC 199P,Pick-Up,,Mbuvi Kioko,,0797318126,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,GT06E,2020-08-22,2022-12-16,,2040-08-23,2040-08-23,,89254021154287138389,639021158713838,,
|
|
||||||
359857082907973,Felix Muema - KCZ 223P,KCZ 223P,automobile,KCZ 223P,Probox,,Felix Muema,,0757843826,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,GT06E,2020-08-22,2024-12-30,,2040-08-23,2040-08-23,,89254021154287138371,639021158713837,,
|
|
||||||
359857082042854,Elias Baya - KCZ 476E,KCZ 476E,automobile,KCZ 476E,Probox,,Elias Baya,,0110941187,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,GT06E,2020-08-09,2022-12-16,,2040-08-10,2040-08-10,,89254021164224352993,639021162435299,,
|
|
||||||
359857082044280,Lawrence Kijogi - KCY 080X,KCY 080X,automobile,KCY 080X,Probox,,Lawrence Kijogi,,0708155933,,,,Dept: ROLLOUT,Default Group,ROLLOUT,fireside,Fireside Group MSA,GT06E,2020-07-13,2022-12-16,,2040-07-13,2040-07-13,,89254029851005131222,639029850513122,,
|
|
||||||
359857082037185,Amani Kazungu - KCY 084X,KCY 084X,automobile,KCY 084X,Probox,,Amani Kazungu,,0757338522,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,GT06E,2020-07-13,2022-12-16,,2040-07-14,2040-07-14,,89254021154287000597,639021158700059,,
|
|
||||||
359857082046145,Joseph Kabandi - KCY 076X,KCY 076X,automobile,KCY 076X,Probox,,Joseph Kabandi,,0110850007,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,GT06E,2020-07-13,2022-12-16,,2040-07-14,2040-07-14,,89254021164223447158,639021162344715,,
|
|
||||||
359857082040981,Amani Sulubu - KCY 090X,KCY 090X,automobile,KCY 090X,Probox,,Amani Sulubu,,0793375853,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,GT06E,2020-07-13,2022-12-16,,2040-07-14,2040-07-14,,89254021064168004164,639021066800416,,
|
|
||||||
359857082038977,Wilfred Kinyanjui - KCU 729C,KCU 729C,truck,KCU 729C,Crane,,Wilfred Kinyanjui,,0110094469,,,,Dept: GENERAL,Default Group,GENERAL,fireside,Fireside Group MSA,GT06E,2020-04-05,2022-12-16,,2040-04-06,2040-04-06,,89254021164215938057,639021161593805,,
|
|
||||||
359857081886467,Gideon Kiprono - KCQ 215F,KCQ 215F,automobile,KCQ 215F,Probox,,Gideon Kiprono,,0746763076,,,,Dept: ISP,Default Group,ISP,fireside,Fireside Group MSA,GT06E,2019-06-30,2022-12-16,,2039-07-01,2039-07-01,,89254021084186499865,639021088649986,,
|
|
||||||
359857081886905,Kennedy Chege - KCQ 618K,KCQ 618K,automobile,KCQ 618K,Probox,,Kennedy Chege,,0746763132,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,GT06E,2019-06-30,2022-12-16,,2039-07-01,2039-07-01,,89254021084186499923,639021088649992,,
|
|
||||||
359857081887192,Ndegwa Dancun - KCG 669W,KCG 669W,automobile,KCG 669W,Probox,,Ndegwa Dancun,,0746760191,,,,Dept: OSP,Default Group,OSP,fireside,Fireside Group MSA,GT06E,2019-06-15,2022-12-16,,2039-06-16,2039-06-16,,89254021084186499501,639021088649950,,
|
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
},
|
},
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"unit": "none",
|
"unit": "lengthkm",
|
||||||
"decimals": 1,
|
"decimals": 1,
|
||||||
"color": { "mode": "fixed", "fixedColor": "blue" }
|
"color": { "mode": "fixed", "fixedColor": "blue" }
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +133,7 @@
|
||||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }
|
||||||
},
|
},
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": { "unit": "none", "decimals": 1, "color": { "mode": "fixed", "fixedColor": "green" } }
|
"defaults": { "unit": "h", "decimals": 1, "color": { "mode": "fixed", "fixedColor": "green" } }
|
||||||
},
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
{ "datasource": { "type": "postgres", "uid": "tracksolid_pg" },
|
||||||
|
|
@ -154,7 +154,7 @@
|
||||||
},
|
},
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"unit": "none", "decimals": 1,
|
"unit": "h", "decimals": 1,
|
||||||
"color": { "mode": "thresholds" },
|
"color": { "mode": "thresholds" },
|
||||||
"thresholds": {
|
"thresholds": {
|
||||||
"mode": "absolute",
|
"mode": "absolute",
|
||||||
|
|
@ -256,7 +256,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tooltip": { "mode": "details" },
|
"tooltip": { "mode": "details" },
|
||||||
"view": { "allLayers": true, "id": "coords", "lat": -2.0, "lon": 35.5, "zoom": 5, "minZoom": 5, "maxZoom": 12 }
|
"view": { "allLayers": true, "id": "coords", "lat": -1.5, "lon": 36.5, "zoom": 6 }
|
||||||
},
|
},
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": { "color": { "mode": "palette-classic-by-name" } },
|
"defaults": { "color": { "mode": "palette-classic-by-name" } },
|
||||||
|
|
@ -335,11 +335,11 @@
|
||||||
"defaults": { "custom": { "align": "auto", "filterable": true } },
|
"defaults": { "custom": { "align": "auto", "filterable": true } },
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{ "matcher": { "id": "byName", "options": "km_today" },
|
{ "matcher": { "id": "byName", "options": "km_today" },
|
||||||
"properties": [{ "id": "unit", "value": "none" }, { "id": "decimals", "value": 1 }, { "id": "displayName", "value": "km today" }] },
|
"properties": [{ "id": "unit", "value": "lengthkm" }, { "id": "decimals", "value": 1 }] },
|
||||||
{ "matcher": { "id": "byName", "options": "drive_hours" },
|
{ "matcher": { "id": "byName", "options": "drive_hours" },
|
||||||
"properties": [{ "id": "unit", "value": "none" }, { "id": "decimals", "value": 1 }, { "id": "displayName", "value": "Drive h" }] },
|
"properties": [{ "id": "unit", "value": "h" }, { "id": "decimals", "value": 1 }] },
|
||||||
{ "matcher": { "id": "byName", "options": "idle_hours" },
|
{ "matcher": { "id": "byName", "options": "idle_hours" },
|
||||||
"properties": [{ "id": "unit", "value": "none" }, { "id": "decimals", "value": 1 }, { "id": "displayName", "value": "Idle h" }] },
|
"properties": [{ "id": "unit", "value": "h" }, { "id": "decimals", "value": 1 }] },
|
||||||
{ "matcher": { "id": "byName", "options": "did_not_move" },
|
{ "matcher": { "id": "byName", "options": "did_not_move" },
|
||||||
"properties": [
|
"properties": [
|
||||||
{ "id": "custom.cellOptions", "value": { "type": "color-background" } },
|
{ "id": "custom.cellOptions", "value": { "type": "color-background" } },
|
||||||
|
|
@ -378,7 +378,7 @@
|
||||||
"defaults": { "custom": { "align": "auto", "filterable": true } },
|
"defaults": { "custom": { "align": "auto", "filterable": true } },
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{ "matcher": { "id": "byName", "options": "km" },
|
{ "matcher": { "id": "byName", "options": "km" },
|
||||||
"properties": [{ "id": "unit", "value": "none" }, { "id": "decimals", "value": 0 }, { "id": "displayName", "value": "km" }] },
|
"properties": [{ "id": "unit", "value": "lengthkm" }, { "id": "decimals", "value": 0 }] },
|
||||||
{ "matcher": { "id": "byName", "options": "speeding_per_100km" },
|
{ "matcher": { "id": "byName", "options": "speeding_per_100km" },
|
||||||
"properties": [
|
"properties": [
|
||||||
{ "id": "custom.cellOptions", "value": { "type": "color-background" } },
|
{ "id": "custom.cellOptions", "value": { "type": "color-background" } },
|
||||||
|
|
@ -424,9 +424,8 @@
|
||||||
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
|
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"unit": "none",
|
"unit": "lengthkm",
|
||||||
"decimals": 1,
|
"custom": { "drawStyle": "bars", "fillOpacity": 60, "lineWidth": 1 }
|
||||||
"custom": { "drawStyle": "bars", "fillOpacity": 60, "lineWidth": 1, "axisLabel": "km" }
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
What I'd skip (for now)
|
|
||||||
|
|
||||||
- ML/forecasting sections until 6+ months of trip data exist — with 3 recorded trips, anything is noise.
|
|
||||||
- Temperature, fuel-theft, OBD-RPM queries — already gated behind webhook registration in §8; duplicating them in §3 just adds clutter.
|
|
||||||
- Route-optimisation queries — that's a separate product, not an analytics-doc addition.
|
|
||||||
|
|
@ -220,9 +220,9 @@ SELECT
|
||||||
current_mileage,
|
current_mileage,
|
||||||
satellite
|
satellite
|
||||||
FROM tracksolid.position_history
|
FROM tracksolid.position_history
|
||||||
WHERE imei = '862798052708167'
|
WHERE imei = 'YOUR_IMEI_HERE'
|
||||||
AND gps_time >= '2026-04-22 00:00:00+03'
|
AND gps_time >= '2026-04-10 00:00:00+03'
|
||||||
AND gps_time < '2026-04-23 00:00:00+03'
|
AND gps_time < '2026-04-11 00:00:00+03'
|
||||||
ORDER BY gps_time ASC;
|
ORDER BY gps_time ASC;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -1,377 +0,0 @@
|
||||||
pgcli "postgres://postgres:U1pm3f5SX34DXkHoW6aKFsBHOlMA9binDPNG4aT0FAcg7AubEvYm0e6kU2dZiYrR@stage.rahamafresh.com:5433/tracksolid_db"
|
|
||||||
|
|
||||||
postgresql://postgres:U1pm3f5SX34DXkHoW6aKFsBHOlMA9binDPNG4aT0FAcg7AubEvYm0e6kU2dZiYrR@timescale_db-bo3nov2ija7g8wn9b1g2paxs-104717464280:5432/tracksolid_db
|
|
||||||
|
|
||||||
---- CURRENT LIVE POSITIONS ------- All devices with a position in the last hour ----
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
d.mc_type,
|
|
||||||
ROUND(lp.lat::numeric, 5) AS lat,
|
|
||||||
ROUND(lp.lng::numeric, 5) AS lng,
|
|
||||||
lp.speed,
|
|
||||||
lp.acc_status,
|
|
||||||
lp.gps_signal,
|
|
||||||
lp.gps_num AS satellites,
|
|
||||||
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_fix_eat,
|
|
||||||
ROUND(EXTRACT(EPOCH FROM (now() - lp.gps_time)) / 60.0, 0) AS mins_ago
|
|
||||||
FROM tracksolid.live_positions lp
|
|
||||||
JOIN tracksolid.devices d ON d.imei = lp.imei
|
|
||||||
WHERE lp.gps_time > now() - interval '1 hour'
|
|
||||||
ORDER BY lp.gps_time DESC;
|
|
||||||
|
|
||||||
----- All 19 live positions — current snapshot ----
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
d.mc_type,
|
|
||||||
ROUND(lp.lat::numeric, 5) AS lat,
|
|
||||||
ROUND(lp.lng::numeric, 5) AS lng,
|
|
||||||
lp.speed,
|
|
||||||
lp.acc_status,
|
|
||||||
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_fix_eat,
|
|
||||||
ROUND(EXTRACT(EPOCH FROM (now() - lp.gps_time)) / 3600.0, 1) AS hours_ago,
|
|
||||||
lp.current_mileage AS odometer_km
|
|
||||||
FROM tracksolid.live_positions lp
|
|
||||||
JOIN tracksolid.devices d ON d.imei = lp.imei
|
|
||||||
ORDER BY lp.gps_time DESC;
|
|
||||||
|
|
||||||
-------- Devices with no position (silent fleet) -----
|
|
||||||
|
|
||||||
SELECT d.imei, d.device_name, d.mc_type, d.sim, d.expiration::date
|
|
||||||
FROM tracksolid.devices d
|
|
||||||
LEFT JOIN tracksolid.live_positions lp ON lp.imei = d.imei
|
|
||||||
WHERE lp.imei IS NULL
|
|
||||||
ORDER BY d.mc_type, d.device_name;
|
|
||||||
|
|
||||||
---- Vehicles currently moving (ACC on OR speed > 0)
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
lp.speed,
|
|
||||||
lp.acc_status,
|
|
||||||
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_fix_eat
|
|
||||||
FROM tracksolid.live_positions lp
|
|
||||||
JOIN tracksolid.devices d ON d.imei = lp.imei
|
|
||||||
WHERE lp.speed > 0 OR lp.acc_status = '1'
|
|
||||||
ORDER BY lp.speed DESC;
|
|
||||||
|
|
||||||
---- Vehicles in Uganda -----
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
ROUND(lp.lat::numeric, 4) AS lat,
|
|
||||||
ROUND(lp.lng::numeric, 4) AS lng,
|
|
||||||
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_fix_eat
|
|
||||||
FROM tracksolid.live_positions lp
|
|
||||||
JOIN tracksolid.devices d ON d.imei = lp.imei
|
|
||||||
WHERE lp.lat NOT BETWEEN -5.0 AND 5.0
|
|
||||||
OR lp.lng NOT BETWEEN 33.9 AND 42.0;
|
|
||||||
|
|
||||||
---- Vehicles in Kenya -----
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
ROUND(lp.lat::numeric, 4) AS lat,
|
|
||||||
ROUND(lp.lng::numeric, 4) AS lng,
|
|
||||||
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_fix_eat
|
|
||||||
FROM tracksolid.live_positions lp
|
|
||||||
JOIN tracksolid.devices d ON d.imei = lp.imei
|
|
||||||
WHERE lp.lat BETWEEN -5.0 AND 5.0
|
|
||||||
OR lp.lng BETWEEN 33.9 AND 42.0;
|
|
||||||
|
|
||||||
|
|
||||||
========== TRIPS AND MOVEMENT ======
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
d.vehicle_number,
|
|
||||||
d.driver_name,
|
|
||||||
COUNT(*) AS trips,
|
|
||||||
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
|
|
||||||
ROUND(AVG(t.avg_speed_kmh)::numeric, 1) AS avg_speed_kmh,
|
|
||||||
MAX(t.max_speed_kmh) AS top_speed_kmh,
|
|
||||||
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
|
|
||||||
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours,
|
|
||||||
MIN(t.start_time AT TIME ZONE 'Africa/Nairobi') AS day_start,
|
|
||||||
MAX(t.end_time AT TIME ZONE 'Africa/Nairobi') AS day_end
|
|
||||||
FROM tracksolid.trips t
|
|
||||||
JOIN tracksolid.devices d ON d.imei = t.imei
|
|
||||||
WHERE t.start_time >= CURRENT_DATE AT TIME ZONE 'Africa/Nairobi'
|
|
||||||
GROUP BY d.device_name, d.vehicle_number, d.driver_name
|
|
||||||
ORDER BY total_km DESC;
|
|
||||||
|
|
||||||
------ trips summary LAST 24 HOURS per vehicle -------
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
t.start_time AT TIME ZONE 'Africa/Nairobi' AS start_eat,
|
|
||||||
t.end_time AT TIME ZONE 'Africa/Nairobi' AS end_eat,
|
|
||||||
ROUND(t.distance_km::numeric / 1000.0, 2) AS distance_km,
|
|
||||||
t.avg_speed_kmh,
|
|
||||||
t.max_speed_kmh,
|
|
||||||
ROUND(t.driving_time_s / 60.0, 0) AS drive_mins,
|
|
||||||
ROUND(t.idle_time_s / 60.0, 0) AS idle_mins,
|
|
||||||
t.source
|
|
||||||
FROM tracksolid.trips t
|
|
||||||
JOIN tracksolid.devices d ON d.imei = t.imei
|
|
||||||
WHERE t.start_time > now() - interval '24 hours'
|
|
||||||
ORDER BY t.start_time DESC;
|
|
||||||
|
|
||||||
------- Fleet utilisation rate per vehicle (today)
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
d.driver_name,
|
|
||||||
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
|
|
||||||
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours,
|
|
||||||
LEAST(ROUND(SUM(t.driving_time_s + COALESCE(t.idle_time_s, 0)) / (12.0 * 3600) * 100, 1), 100) AS utilisation_pct
|
|
||||||
FROM tracksolid.trips t
|
|
||||||
JOIN tracksolid.devices d ON d.imei = t.imei
|
|
||||||
WHERE t.start_time >= CURRENT_DATE AT TIME ZONE 'Africa/Nairobi'
|
|
||||||
GROUP BY d.device_name, d.driver_name
|
|
||||||
ORDER BY utilisation_pct DESC;
|
|
||||||
|
|
||||||
KDK 829A GP | | 1.87 | | 15.5
|
|
||||||
FRED KMGW 538W HULETI | | 1.65 | | 13.7
|
|
||||||
X3-63282 | | 1.25 | | 10.4
|
|
||||||
|
|
||||||
-----Tracking to this point of the day ----
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
d.driver_name,
|
|
||||||
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
|
|
||||||
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours,
|
|
||||||
LEAST(ROUND(
|
|
||||||
SUM(t.driving_time_s + COALESCE(t.idle_time_s, 0))
|
|
||||||
/ (EXTRACT(EPOCH FROM (
|
|
||||||
LEAST(now(), CURRENT_DATE::timestamp AT TIME ZONE 'Africa/Nairobi' + interval '19 hours 30 minutes')
|
|
||||||
- (CURRENT_DATE::timestamp AT TIME ZONE 'Africa/Nairobi' + interval '7 hours 30 minutes')
|
|
||||||
))) * 100
|
|
||||||
, 1), 100) AS utilisation_pct_so_far
|
|
||||||
FROM tracksolid.trips t
|
|
||||||
JOIN tracksolid.devices d ON d.imei = t.imei
|
|
||||||
WHERE t.start_time >= (CURRENT_DATE::timestamp AT TIME ZONE 'Africa/Nairobi' + interval '7 hours 30 minutes')
|
|
||||||
GROUP BY d.device_name, d.driver_name
|
|
||||||
ORDER BY utilisation_pct_so_far DESC;
|
|
||||||
|
|
||||||
|
|
||||||
------ Vehicles that have not moved today -------
|
|
||||||
|
|
||||||
SELECT d.device_name, d.mc_type, d.driver_name
|
|
||||||
FROM tracksolid.devices d
|
|
||||||
LEFT JOIN tracksolid.trips t
|
|
||||||
ON t.imei = d.imei
|
|
||||||
AND t.start_time >= CURRENT_DATE AT TIME ZONE 'Africa/Nairobi'
|
|
||||||
WHERE t.imei IS NULL
|
|
||||||
ORDER BY d.device_name;
|
|
||||||
|
|
||||||
------ Distance per driver — last 30 days
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
d.driver_name,
|
|
||||||
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS active_days,
|
|
||||||
COUNT(*) AS total_trips,
|
|
||||||
ROUND(SUM(t.distance_km / 1000.0)::numeric, 0) AS total_km,
|
|
||||||
ROUND(AVG(t.distance_km / 1000.0)::numeric, 1) AS avg_km_per_trip,
|
|
||||||
MAX(t.max_speed_kmh) AS top_speed_ever
|
|
||||||
FROM tracksolid.trips t
|
|
||||||
JOIN tracksolid.devices d ON d.imei = t.imei
|
|
||||||
WHERE t.start_time > now() - interval '30 days'
|
|
||||||
GROUP BY d.device_name, d.driver_name
|
|
||||||
ORDER BY total_km DESC;
|
|
||||||
|
|
||||||
|
|
||||||
====== ALL ALARMS =====
|
|
||||||
|
|
||||||
----Alarms in 24 hours ----
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
a.alarm_type,
|
|
||||||
a.alarm_name,
|
|
||||||
a.alarm_time AT TIME ZONE 'Africa/Nairobi' AS alarm_time_eat,
|
|
||||||
ROUND(a.lat::numeric, 5) AS lat,
|
|
||||||
ROUND(a.lng::numeric, 5) AS lng,
|
|
||||||
a.speed,
|
|
||||||
a.severity,
|
|
||||||
a.acknowledged_at
|
|
||||||
FROM tracksolid.alarms a
|
|
||||||
JOIN tracksolid.devices d ON d.imei = a.imei
|
|
||||||
WHERE a.alarm_time > now() - interval '24 hours'
|
|
||||||
ORDER BY a.alarm_time DESC;
|
|
||||||
|
|
||||||
----Alarms in 7 Days ----
|
|
||||||
SELECT
|
|
||||||
a.alarm_name,
|
|
||||||
a.alarm_type,
|
|
||||||
COUNT(*) AS occurrences,
|
|
||||||
COUNT(DISTINCT a.imei) AS devices_affected,
|
|
||||||
MAX(a.alarm_time AT TIME ZONE 'Africa/Nairobi') AS last_seen_eat
|
|
||||||
FROM tracksolid.alarms a
|
|
||||||
WHERE a.alarm_time > now() - interval '7 days'
|
|
||||||
GROUP BY a.alarm_name, a.alarm_type
|
|
||||||
ORDER BY occurrences DESC;
|
|
||||||
|
|
||||||
------ unacknowledged alarms ------
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
a.alarm_name,
|
|
||||||
a.alarm_time AT TIME ZONE 'Africa/Nairobi' AS alarm_time_eat,
|
|
||||||
ROUND(EXTRACT(EPOCH FROM (now() - a.alarm_time)) / 3600.0, 1) AS hours_open
|
|
||||||
FROM tracksolid.alarms a
|
|
||||||
JOIN tracksolid.devices d ON d.imei = a.imei
|
|
||||||
WHERE a.acknowledged_at IS NULL
|
|
||||||
ORDER BY a.alarm_time DESC;
|
|
||||||
|
|
||||||
------ acknowledged alarms ------
|
|
||||||
|
|
||||||
UPDATE tracksolid.alarms
|
|
||||||
SET acknowledged_at = now(),
|
|
||||||
acknowledged_by = 'operator_name'
|
|
||||||
WHERE id = <alarm_id>;
|
|
||||||
|
|
||||||
===========
|
|
||||||
|
|
||||||
------ Position history by source — counts ----
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
source,
|
|
||||||
COUNT(*) AS fixes,
|
|
||||||
MIN(gps_time AT TIME ZONE 'Africa/Nairobi') AS earliest,
|
|
||||||
MAX(gps_time AT TIME ZONE 'Africa/Nairobi') AS latest
|
|
||||||
FROM tracksolid.position_history
|
|
||||||
GROUP BY source;
|
|
||||||
|
|
||||||
--------- Route replay for a specific vehicle — last 24 hours
|
|
||||||
|
|
||||||
device = name <'FRED KMGW 538W HULETI'>
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
ph.gps_time AT TIME ZONE 'Africa/Nairobi' AS gps_time_eat,
|
|
||||||
ROUND(ph.lat::numeric, 5) AS lat,
|
|
||||||
ROUND(ph.lng::numeric, 5) AS lng,
|
|
||||||
ph.speed,
|
|
||||||
ph.direction,
|
|
||||||
ph.acc_status,
|
|
||||||
ph.source
|
|
||||||
FROM tracksolid.position_history ph
|
|
||||||
JOIN tracksolid.devices d ON d.imei = ph.imei
|
|
||||||
WHERE d.device_name = <'FRED KMGW 538W HULETI'>
|
|
||||||
AND ph.gps_time > now() - interval '24 hours'
|
|
||||||
ORDER BY ph.gps_time ASC;
|
|
||||||
|
|
||||||
|
|
||||||
-------- Fix density per device — last 24 hours ---------
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
d.device_name,
|
|
||||||
COUNT(*) AS total_fixes,
|
|
||||||
COUNT(*) FILTER (WHERE ph.source = 'poll') AS poll_fixes,
|
|
||||||
COUNT(*) FILTER (WHERE ph.source = 'track_list') AS track_list_fixes,
|
|
||||||
MIN(ph.gps_time AT TIME ZONE 'Africa/Nairobi') AS first_fix,
|
|
||||||
MAX(ph.gps_time AT TIME ZONE 'Africa/Nairobi') AS last_fix
|
|
||||||
FROM tracksolid.position_history ph
|
|
||||||
JOIN tracksolid.devices d ON d.imei = ph.imei
|
|
||||||
WHERE ph.gps_time > now() - interval '24 hours'
|
|
||||||
GROUP BY d.device_name
|
|
||||||
ORDER BY total_fixes DESC;
|
|
||||||
|
|
||||||
### Device & Fleet Registry
|
|
||||||
|
|
||||||
-------- Full fleet — all 63 devices ------
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
device_name,
|
|
||||||
mc_type,
|
|
||||||
vehicle_number,
|
|
||||||
driver_name,
|
|
||||||
sim,
|
|
||||||
ROUND(current_mileage_km::numeric, 0) AS odometer_km,
|
|
||||||
expiration::date AS expires,
|
|
||||||
enabled_flag
|
|
||||||
FROM tracksolid.devices
|
|
||||||
ORDER BY mc_type, device_name;
|
|
||||||
|
|
||||||
-------- Fleet by devices model ------
|
|
||||||
|
|
||||||
SELECT mc_type, COUNT(*) AS devices
|
|
||||||
FROM tracksolid.devices
|
|
||||||
GROUP BY mc_type ORDER BY devices DESC;
|
|
||||||
|
|
||||||
|
|
||||||
------ fleet by odometer readings -----
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
device_name,
|
|
||||||
mc_type,
|
|
||||||
sim,
|
|
||||||
ROUND(current_mileage_km::numeric, 0) AS odometer_km,
|
|
||||||
expiration::date AS expires
|
|
||||||
FROM tracksolid.devices
|
|
||||||
WHERE current_mileage_km IS NOT NULL AND current_mileage_km > 0
|
|
||||||
ORDER BY current_mileage_km DESC
|
|
||||||
LIMIT 15;
|
|
||||||
|
|
||||||
---- fleet blank driver sim vehicle number blanks ----
|
|
||||||
|
|
||||||
SELECT device_name, mc_type, sim
|
|
||||||
FROM tracksolid.devices
|
|
||||||
WHERE vehicle_number IS NULL OR vehicle_number = ''
|
|
||||||
OR driver_name IS NULL OR driver_name = ''
|
|
||||||
ORDER BY mc_type, device_name;
|
|
||||||
|
|
||||||
----- fleet by expiry dates ---
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
COUNT(*) FILTER (WHERE expiration IS NULL) AS no_expiry_set,
|
|
||||||
COUNT(*) FILTER (WHERE expiration < now()) AS already_expired,
|
|
||||||
COUNT(*) FILTER (WHERE expiration BETWEEN now() AND now() + interval '90 days') AS expiring_90days,
|
|
||||||
COUNT(*) FILTER (WHERE expiration > now() + interval '90 days') AS valid_long_term
|
|
||||||
FROM tracksolid.devices;
|
|
||||||
|
|
||||||
|
|
||||||
-----no of rows in db -----
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.devices) AS devices,
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.live_positions) AS live_positions,
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.position_history) AS position_history,
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.trips) AS trips,
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.alarms) AS alarms,
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.parking_events) AS parking_events,
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.obd_readings) AS obd_readings,
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.device_events) AS device_events,
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.fuel_readings) AS fuel_readings,
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.temperature_readings) AS temperature_readings,
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.lbs_readings) AS lbs_readings,
|
|
||||||
(SELECT COUNT(*) FROM tracksolid.ingestion_log) AS ingestion_log;
|
|
||||||
|
|
||||||
=====
|
|
||||||
|
|
||||||
SELECT 'devices' AS table_name, COUNT(*) FROM tracksolid.devices
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'live_positions', COUNT(*) FROM tracksolid.live_positions
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'position_history', COUNT(*) FROM tracksolid.position_history
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'trips', COUNT(*) FROM tracksolid.trips
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'alarms', COUNT(*) FROM tracksolid.alarms
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'parking_events', COUNT(*) FROM tracksolid.parking_events
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'obd_readings', COUNT(*) FROM tracksolid.obd_readings
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'device_events', COUNT(*) FROM tracksolid.device_events
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'fuel_readings', COUNT(*) FROM tracksolid.fuel_readings
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'temperature_readings', COUNT(*) FROM tracksolid.temperature_readings
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'lbs_readings', COUNT(*) FROM tracksolid.lbs_readings
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'ingestion_log', COUNT(*) FROM tracksolid.ingestion_log
|
|
||||||
ORDER BY table_name;
|
|
||||||
|
|
@ -1,297 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Tracksolid Pro - Device List Extractor
|
|
||||||
Calls jimi.user.device.list and saves all vehicle/device data to CSV and JSON.
|
|
||||||
|
|
||||||
Uses the same signing approach as tracksolid_update.py (confirmed working):
|
|
||||||
- POST as x-www-form-urlencoded
|
|
||||||
- All parameter values cast to strings before signing
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python3 tracksolid_extract.py
|
|
||||||
python3 tracksolid_extract.py --target "Fireside Communications"
|
|
||||||
python3 tracksolid_extract.py --format json
|
|
||||||
python3 tracksolid_extract.py --format both
|
|
||||||
|
|
||||||
Environment variables (same .env file as tracksolid_update.py):
|
|
||||||
TS_USER_ID - Your Tracksolid account username
|
|
||||||
TS_USER_PWD_MD5 - MD5 hash of your password (lowercase)
|
|
||||||
TS_APP_KEY - Your appKey from JIMI
|
|
||||||
TS_APP_SECRET - Your appSecret from JIMI
|
|
||||||
TS_API_URL - API base URL (defaults to EU node)
|
|
||||||
TS_TARGET - Account to query (defaults to TS_USER_ID)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import argparse
|
|
||||||
import time
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# CONFIGURATION — reads from environment / same .env as the updater
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
CONFIG = {
|
|
||||||
"user_id": os.getenv("TS_USER_ID", "Fireside Communications"),
|
|
||||||
"user_pwd_md5": os.getenv("TS_USER_PWD_MD5", "81a1b005efd3596073e38efd8a2fd3fd"),
|
|
||||||
"app_key": os.getenv("TS_APP_KEY", "8FB345B8693CCD00BB70D528C0D4019E"),
|
|
||||||
"app_secret": os.getenv("TS_APP_SECRET", "3177c89993b446c6aced0d7c56375d2c"),
|
|
||||||
"api_url": os.getenv("TS_API_URL", "https://eu-open.tracksolidpro.com/route/rest"),
|
|
||||||
"target": os.getenv("TS_TARGET", ""), # account to query; defaults to user_id
|
|
||||||
"expires_in": "7200",
|
|
||||||
}
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# LOGGING
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format="%(asctime)s %(levelname)-8s %(message)s",
|
|
||||||
handlers=[
|
|
||||||
logging.StreamHandler(sys.stdout),
|
|
||||||
logging.FileHandler("tracksolid_extract.log", encoding="utf-8"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# SIGNING UTILITIES (identical to tracksolid_update.py)
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def utc_timestamp() -> str:
|
|
||||||
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
|
|
||||||
def build_sign(params: dict, app_secret: str) -> str:
|
|
||||||
sorted_keys = sorted(
|
|
||||||
k for k in params
|
|
||||||
if k != "sign" and params[k] is not None and str(params[k]).strip() != ""
|
|
||||||
)
|
|
||||||
param_string = "".join(f"{k}{params[k]}" for k in sorted_keys)
|
|
||||||
raw_string = f"{app_secret}{param_string}{app_secret}"
|
|
||||||
return hashlib.md5(raw_string.encode("utf-8")).hexdigest().upper()
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# TRACKSOLID CLIENT
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class TracksolidClient:
|
|
||||||
def __init__(self, cfg: dict):
|
|
||||||
self.cfg = cfg
|
|
||||||
self._token: str | None = None
|
|
||||||
self._token_expires_at: float = 0.0
|
|
||||||
self.session = requests.Session()
|
|
||||||
|
|
||||||
def _post(self, params: dict) -> dict:
|
|
||||||
str_params = {
|
|
||||||
k: str(v)
|
|
||||||
for k, v in params.items()
|
|
||||||
if v is not None and str(v).strip() != ""
|
|
||||||
}
|
|
||||||
str_params["sign"] = build_sign(str_params, self.cfg["app_secret"])
|
|
||||||
|
|
||||||
log.debug("POST %s params=%s", self.cfg["api_url"], str_params)
|
|
||||||
resp = self.session.post(
|
|
||||||
self.cfg["api_url"],
|
|
||||||
data=str_params, # form-encoded — confirmed working
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = resp.json()
|
|
||||||
log.debug("Response: %s", json.dumps(data))
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _common_params(self, method: str) -> dict:
|
|
||||||
return {
|
|
||||||
"method": method,
|
|
||||||
"timestamp": utc_timestamp(),
|
|
||||||
"app_key": self.cfg["app_key"],
|
|
||||||
"sign_method": "md5",
|
|
||||||
"v": "1.0",
|
|
||||||
"format": "json",
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_token(self) -> str:
|
|
||||||
if self._token and time.time() < self._token_expires_at - 60:
|
|
||||||
return self._token
|
|
||||||
|
|
||||||
log.info("Obtaining access token ...")
|
|
||||||
params = self._common_params("jimi.oauth.token.get")
|
|
||||||
params.update({
|
|
||||||
"user_id": self.cfg["user_id"],
|
|
||||||
"user_pwd_md5": self.cfg["user_pwd_md5"],
|
|
||||||
"expires_in": self.cfg["expires_in"],
|
|
||||||
})
|
|
||||||
|
|
||||||
data = self._post(params)
|
|
||||||
if data.get("code") != 0:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Auth failed — code={data.get('code')} message={data.get('message')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._token = data["result"]["accessToken"]
|
|
||||||
self._token_expires_at = time.time() + int(data["result"]["expiresIn"])
|
|
||||||
log.info("Token acquired. Valid for %s seconds.", data["result"]["expiresIn"])
|
|
||||||
return self._token
|
|
||||||
|
|
||||||
def get_device_list(self, target: str) -> list[dict]:
|
|
||||||
"""
|
|
||||||
Call jimi.user.device.list for the given target account.
|
|
||||||
Returns the full list of device/vehicle records.
|
|
||||||
"""
|
|
||||||
log.info("Fetching device list for account: %s", target)
|
|
||||||
token = self.get_token()
|
|
||||||
|
|
||||||
params = self._common_params("jimi.user.device.list")
|
|
||||||
params["access_token"] = token
|
|
||||||
params["target"] = target
|
|
||||||
|
|
||||||
data = self._post(params)
|
|
||||||
|
|
||||||
if data.get("code") != 0:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Device list failed — code={data.get('code')} message={data.get('message')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
devices = data.get("result", [])
|
|
||||||
log.info("Retrieved %d devices.", len(devices))
|
|
||||||
return devices
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# OUTPUT HELPERS
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Friendly column names for the CSV output
|
|
||||||
COLUMN_RENAME = {
|
|
||||||
"imei": "IMEI",
|
|
||||||
"deviceName": "Device Name",
|
|
||||||
"mcType": "Model",
|
|
||||||
"mcTypeUseScope": "Vehicle Type",
|
|
||||||
"sim": "SIM",
|
|
||||||
"expiration": "Platform Expiry",
|
|
||||||
"activationTime": "Activated",
|
|
||||||
"reMark": "Remarks",
|
|
||||||
"vehicleName": "Vehicle Name",
|
|
||||||
"vehicleIcon": "Icon",
|
|
||||||
"vehicleNumber": "License Plate",
|
|
||||||
"vehicleModels": "Vehicle Model",
|
|
||||||
"carFrame": "VIN",
|
|
||||||
"driverName": "Driver Name",
|
|
||||||
"driverPhone": "Driver Phone",
|
|
||||||
"enabledFlag": "Active",
|
|
||||||
"engineNumber": "Engine Number",
|
|
||||||
"deviceGroupId": "Group ID",
|
|
||||||
"deviceGroup": "Group",
|
|
||||||
}
|
|
||||||
|
|
||||||
def save_csv(devices: list[dict], path: str):
|
|
||||||
df = pd.DataFrame(devices)
|
|
||||||
df.rename(columns=COLUMN_RENAME, inplace=True)
|
|
||||||
# Put the most useful columns first
|
|
||||||
priority = ["IMEI", "License Plate", "Driver Name", "Driver Phone",
|
|
||||||
"Device Name", "Vehicle Model", "Vehicle Type", "Group",
|
|
||||||
"SIM", "Platform Expiry", "Activated", "Active",
|
|
||||||
"VIN", "Engine Number", "Remarks"]
|
|
||||||
ordered = [c for c in priority if c in df.columns]
|
|
||||||
rest = [c for c in df.columns if c not in ordered]
|
|
||||||
df = df[ordered + rest]
|
|
||||||
df.to_csv(path, index=False)
|
|
||||||
log.info("CSV saved → %s (%d rows, %d columns)", path, len(df), len(df.columns))
|
|
||||||
|
|
||||||
|
|
||||||
def save_json(devices: list[dict], path: str):
|
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(devices, f, indent=2, ensure_ascii=False)
|
|
||||||
log.info("JSON saved → %s (%d records)", path, len(devices))
|
|
||||||
|
|
||||||
|
|
||||||
def print_summary(devices: list[dict]):
|
|
||||||
df = pd.DataFrame(devices)
|
|
||||||
total = len(df)
|
|
||||||
active = df["enabledFlag"].eq(1).sum() if "enabledFlag" in df.columns else "?"
|
|
||||||
groups = df["deviceGroup"].nunique() if "deviceGroup" in df.columns else "?"
|
|
||||||
with_plate = df["vehicleNumber"].notna().sum() if "vehicleNumber" in df.columns else "?"
|
|
||||||
with_driver = df["driverName"].notna().sum() if "driverName" in df.columns else "?"
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("=" * 50)
|
|
||||||
print(" DEVICE LIST SUMMARY")
|
|
||||||
print("=" * 50)
|
|
||||||
print(f" Total devices : {total}")
|
|
||||||
print(f" Active : {active}")
|
|
||||||
print(f" Device groups : {groups}")
|
|
||||||
print(f" With plate no. : {with_plate}")
|
|
||||||
print(f" With driver name : {with_driver}")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
if "deviceGroup" in df.columns:
|
|
||||||
print("\n Breakdown by group:")
|
|
||||||
for group, count in df["deviceGroup"].value_counts().items():
|
|
||||||
print(f" {group:<25} {count} devices")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# MAIN
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Extract Tracksolid device list to CSV / JSON."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--target", default="",
|
|
||||||
help="Account to query (default: same as TS_USER_ID / user_id in CONFIG)."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--format", choices=["csv", "json", "both"], default="csv",
|
|
||||||
help="Output format (default: csv)."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--out", default="",
|
|
||||||
help="Output filename without extension (default: tracksolid_devices_YYYYMMDD_HHMMSS)."
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Resolve target account
|
|
||||||
target = args.target or CONFIG["target"] or CONFIG["user_id"]
|
|
||||||
|
|
||||||
# Resolve output filename base
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
out_base = args.out or f"tracksolid_devices_{timestamp}"
|
|
||||||
|
|
||||||
# ── Fetch ──────────────────────────────────────────────────────────────────
|
|
||||||
client = TracksolidClient(CONFIG)
|
|
||||||
try:
|
|
||||||
devices = client.get_device_list(target)
|
|
||||||
except Exception as exc:
|
|
||||||
log.error("Failed to fetch device list: %s", exc)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not devices:
|
|
||||||
log.warning("No devices returned for account: %s", target)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# ── Save ───────────────────────────────────────────────────────────────────
|
|
||||||
if args.format in ("csv", "both"):
|
|
||||||
save_csv(devices, f"{out_base}.csv")
|
|
||||||
|
|
||||||
if args.format in ("json", "both"):
|
|
||||||
save_json(devices, f"{out_base}.json")
|
|
||||||
|
|
||||||
# ── Summary ────────────────────────────────────────────────────────────────
|
|
||||||
print_summary(devices)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
### Pipeline health — last hour (key check)
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
endpoint,
|
|
||||||
COUNT(*) AS calls,
|
|
||||||
SUM(rows_upserted) AS upserted,
|
|
||||||
SUM(rows_inserted) AS inserted,
|
|
||||||
ROUND(AVG(duration_ms)::numeric, 0) AS avg_ms,
|
|
||||||
COUNT(*) FILTER (WHERE success = false) AS failures,
|
|
||||||
MAX(run_at AT TIME ZONE 'Africa/Nairobi') AS last_call_eat
|
|
||||||
FROM tracksolid.ingestion_log
|
|
||||||
WHERE run_at > now() - interval '1 hour'
|
|
||||||
GROUP BY endpoint
|
|
||||||
ORDER BY calls DESC;
|
|
||||||
|
|
||||||
-------- Ingestion Pipeline Health
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
endpoint,
|
|
||||||
COUNT(*) AS total_calls,
|
|
||||||
SUM(rows_upserted) AS total_upserted,
|
|
||||||
SUM(rows_inserted) AS total_inserted,
|
|
||||||
ROUND(AVG(duration_ms)::numeric, 0) AS avg_ms,
|
|
||||||
COUNT(*) FILTER (WHERE success = false) AS failures,
|
|
||||||
MIN(run_at AT TIME ZONE 'Africa/Nairobi') AS first_call,
|
|
||||||
MAX(run_at AT TIME ZONE 'Africa/Nairobi') AS last_call
|
|
||||||
FROM tracksolid.ingestion_log
|
|
||||||
GROUP BY endpoint
|
|
||||||
ORDER BY total_calls DESC;
|
|
||||||
|
|
||||||
### Recent calls — last 20 entries
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
run_at AT TIME ZONE 'Africa/Nairobi' AS run_eat,
|
|
||||||
endpoint,
|
|
||||||
imei_count,
|
|
||||||
rows_upserted,
|
|
||||||
rows_inserted,
|
|
||||||
duration_ms,
|
|
||||||
success,
|
|
||||||
error_message
|
|
||||||
FROM tracksolid.ingestion_log
|
|
||||||
ORDER BY run_at DESC
|
|
||||||
LIMIT 20;
|
|
||||||
|
|
||||||
### Recent calls — FAILED CALLS entries
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
run_at AT TIME ZONE 'Africa/Nairobi' AS run_eat,
|
|
||||||
endpoint,
|
|
||||||
error_code,
|
|
||||||
error_message
|
|
||||||
FROM tracksolid.ingestion_log
|
|
||||||
WHERE success = false
|
|
||||||
ORDER BY run_at DESC;
|
|
||||||
|
|
||||||
|
|
@ -1,359 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Tracksolid Pro - Bulk Vehicle Information Updater
|
|
||||||
Updates vehicle details via the jimi.open.device.update API endpoint
|
|
||||||
using data from the Fireside logistics CSV.
|
|
||||||
|
|
||||||
Signing approach taken directly from tspostman.py (confirmed working):
|
|
||||||
- POST as x-www-form-urlencoded (NOT JSON)
|
|
||||||
- All parameter values cast to strings before signing
|
|
||||||
- expires_in passed as string '7200', not integer
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python tracksolid_update.py [--dry-run] [--csv path/to/file.csv] [--limit N]
|
|
||||||
|
|
||||||
Environment variables (or edit CONFIG below):
|
|
||||||
TS_USER_ID - Your Tracksolid account username
|
|
||||||
TS_USER_PWD_MD5 - MD5 hash of your password (lowercase)
|
|
||||||
TS_APP_KEY - Your appKey from JIMI
|
|
||||||
TS_APP_SECRET - Your appSecret from JIMI
|
|
||||||
TS_API_URL - API base URL (defaults to EU node)
|
|
||||||
TS_CSV_PATH - Path to the logistics CSV
|
|
||||||
"""
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# CONFIGURATION — edit here or set environment variables
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
CONFIG = {
|
|
||||||
"user_id": os.getenv("TS_USER_ID", "Fireside Communications"),
|
|
||||||
"user_pwd_md5": os.getenv("TS_USER_PWD_MD5", "81a1b005efd3596073e38efd8a2fd3fd"),
|
|
||||||
"app_key": os.getenv("TS_APP_KEY", "8FB345B8693CCD00BB70D528C0D4019E"),
|
|
||||||
"app_secret": os.getenv("TS_APP_SECRET", "3177c89993b446c6aced0d7c56375d2c"),
|
|
||||||
# EU node confirmed for this account
|
|
||||||
"api_url": os.getenv("TS_API_URL", "https://eu-open.tracksolidpro.com/route/rest"),
|
|
||||||
"expires_in": "7200", # string, not int — matches tspostman.py
|
|
||||||
"request_delay": 0.5, # seconds between API calls
|
|
||||||
}
|
|
||||||
|
|
||||||
CSV_PATH = os.getenv("TS_CSV_PATH", "20260414_FS__Logistics_-_final_fixed.csv")
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# LOGGING
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format="%(asctime)s %(levelname)-8s %(message)s",
|
|
||||||
handlers=[
|
|
||||||
logging.StreamHandler(sys.stdout),
|
|
||||||
logging.FileHandler("tracksolid_update.log", encoding="utf-8"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# SIGNING UTILITIES (ported directly from tspostman.py)
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def utc_timestamp() -> str:
|
|
||||||
"""UTC time formatted as yyyy-MM-dd HH:mm:ss."""
|
|
||||||
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
|
|
||||||
def build_sign(params: dict, app_secret: str) -> str:
|
|
||||||
"""
|
|
||||||
Tracksolid signing algorithm (matches tspostman.py exactly):
|
|
||||||
1. Sort all param keys alphabetically, exclude 'sign' and empty values.
|
|
||||||
2. Concatenate key+value pairs with no separators.
|
|
||||||
3. Wrap with appSecret on both sides.
|
|
||||||
4. MD5 -> UPPERCASE 32-char string.
|
|
||||||
|
|
||||||
All values must already be strings before calling this.
|
|
||||||
"""
|
|
||||||
sorted_keys = sorted(
|
|
||||||
k for k in params
|
|
||||||
if k != "sign" and params[k] is not None and str(params[k]).strip() != ""
|
|
||||||
)
|
|
||||||
param_string = "".join(f"{k}{params[k]}" for k in sorted_keys)
|
|
||||||
raw_string = f"{app_secret}{param_string}{app_secret}"
|
|
||||||
return hashlib.md5(raw_string.encode("utf-8")).hexdigest().upper()
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# TRACKSOLID CLIENT
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class TracksolidClient:
|
|
||||||
def __init__(self, cfg: dict):
|
|
||||||
self.cfg = cfg
|
|
||||||
self._token: str | None = None
|
|
||||||
self._token_expires_at: float = 0.0
|
|
||||||
self.session = requests.Session()
|
|
||||||
# No Content-Type set here — requests sets it automatically to
|
|
||||||
# application/x-www-form-urlencoded when data= is used (matching tspostman.py)
|
|
||||||
|
|
||||||
def _post(self, params: dict) -> dict:
|
|
||||||
"""
|
|
||||||
Sign and POST params using x-www-form-urlencoded encoding.
|
|
||||||
Confirmed working approach from tspostman.py:
|
|
||||||
- Cast all values to strings
|
|
||||||
- Use data= (form-encoded), NOT json=
|
|
||||||
"""
|
|
||||||
str_params = {
|
|
||||||
k: str(v)
|
|
||||||
for k, v in params.items()
|
|
||||||
if v is not None and str(v).strip() != ""
|
|
||||||
}
|
|
||||||
str_params["sign"] = build_sign(str_params, self.cfg["app_secret"])
|
|
||||||
|
|
||||||
log.debug("POST %s params=%s", self.cfg["api_url"], str_params)
|
|
||||||
resp = self.session.post(
|
|
||||||
self.cfg["api_url"],
|
|
||||||
data=str_params, # form-encoded, NOT json=
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = resp.json()
|
|
||||||
log.debug("Response: %s", json.dumps(data))
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _common_params(self, method: str) -> dict:
|
|
||||||
return {
|
|
||||||
"method": method,
|
|
||||||
"timestamp": utc_timestamp(),
|
|
||||||
"app_key": self.cfg["app_key"],
|
|
||||||
"sign_method": "md5",
|
|
||||||
"v": "1.0",
|
|
||||||
"format": "json",
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_token(self) -> str:
|
|
||||||
"""Return a valid access token, fetching a new one only when needed."""
|
|
||||||
if self._token and time.time() < self._token_expires_at - 60:
|
|
||||||
log.debug("Reusing cached token.")
|
|
||||||
return self._token
|
|
||||||
|
|
||||||
log.info("Obtaining new access token ...")
|
|
||||||
params = self._common_params("jimi.oauth.token.get")
|
|
||||||
params.update({
|
|
||||||
"user_id": self.cfg["user_id"],
|
|
||||||
"user_pwd_md5": self.cfg["user_pwd_md5"],
|
|
||||||
"expires_in": self.cfg["expires_in"], # already a string per tspostman.py
|
|
||||||
})
|
|
||||||
|
|
||||||
data = self._post(params)
|
|
||||||
if data.get("code") != 0:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Auth failed — code={data.get('code')} message={data.get('message')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._token = data["result"]["accessToken"]
|
|
||||||
self._token_expires_at = time.time() + int(data["result"]["expiresIn"])
|
|
||||||
log.info("Token acquired. Valid for %s seconds.", data["result"]["expiresIn"])
|
|
||||||
return self._token
|
|
||||||
|
|
||||||
def update_vehicle(self, imei: str, fields: dict, dry_run: bool = False) -> dict:
|
|
||||||
"""
|
|
||||||
Call jimi.open.device.update for one IMEI.
|
|
||||||
fields: dict using local key names (see row_to_fields below).
|
|
||||||
Returns the API response dict.
|
|
||||||
"""
|
|
||||||
if dry_run:
|
|
||||||
log.info("[DRY-RUN] IMEI %s -> %s", imei, fields)
|
|
||||||
return {"code": 0, "message": "dry-run"}
|
|
||||||
|
|
||||||
token = self.get_token()
|
|
||||||
params = self._common_params("jimi.open.device.update")
|
|
||||||
params["access_token"] = token
|
|
||||||
params["imei"] = str(imei)
|
|
||||||
|
|
||||||
# Map local field names -> Tracksolid API field names
|
|
||||||
api_field_map = {
|
|
||||||
"license_plate": "vehicle_number",
|
|
||||||
"vehicle_name": "vehicle_name",
|
|
||||||
"vehicle_icon": "vehicle_icon",
|
|
||||||
"driver_name": "driver_name",
|
|
||||||
"driver_phone": "driver_phone",
|
|
||||||
"vehicle_model": "vehicle_models",
|
|
||||||
"vin": "carFrame",
|
|
||||||
"engine_number": "engineNumber",
|
|
||||||
"device_name": "device_name",
|
|
||||||
"fuel_per_100": "oilWear",
|
|
||||||
"sim": "sim",
|
|
||||||
"remarks": "remarks",
|
|
||||||
}
|
|
||||||
for local_key, api_key in api_field_map.items():
|
|
||||||
val = fields.get(local_key)
|
|
||||||
if val:
|
|
||||||
params[api_key] = str(val)
|
|
||||||
|
|
||||||
return self._post(params)
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# CSV PARSING
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def clean(val) -> str | None:
|
|
||||||
"""Return None for NaN/empty values, else a stripped string."""
|
|
||||||
if pd.isna(val) or str(val).strip() in ("", "nan", "NaN"):
|
|
||||||
return None
|
|
||||||
s = str(val).strip()
|
|
||||||
if s.endswith(".0") and s[:-2].lstrip("+-").isdigit():
|
|
||||||
s = s[:-2]
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def row_to_fields(row: pd.Series) -> dict:
|
|
||||||
"""Map one CSV row to local field names used by update_vehicle()."""
|
|
||||||
return {
|
|
||||||
"device_name": clean(row.get("Device Name")),
|
|
||||||
"vehicle_name": clean(row.get("Vehicle Name")),
|
|
||||||
"vehicle_icon": clean(row.get("Vehicle Icon")),
|
|
||||||
"license_plate": clean(row.get("License Plate No.")),
|
|
||||||
"vehicle_model": clean(row.get("Vehicle Model")),
|
|
||||||
"driver_name": clean(row.get("Driver Name")),
|
|
||||||
"driver_phone": clean(row.get("Telephone")),
|
|
||||||
"sim": clean(row.get("SIM")),
|
|
||||||
"vin": clean(row.get("VIN")),
|
|
||||||
"engine_number": clean(row.get("Engine Number")),
|
|
||||||
"fuel_per_100": clean(row.get("Fuel/100km")),
|
|
||||||
"remarks": clean(row.get("Remarks")),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# MAIN
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Bulk-update Tracksolid vehicle info from CSV."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--dry-run", action="store_true",
|
|
||||||
help="Show what would be sent without making update API calls."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--csv", default=CSV_PATH,
|
|
||||||
help=f"Path to the logistics CSV (default: {CSV_PATH})"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--limit", type=int, default=None,
|
|
||||||
help="Only process the first N rows (useful for testing)."
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# ── Load CSV ───────────────────────────────────────────────────────────────
|
|
||||||
csv_file = Path(args.csv)
|
|
||||||
if not csv_file.exists():
|
|
||||||
log.error("CSV file not found: %s", csv_file)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
df = pd.read_csv(csv_file, dtype={"IMEI": str})
|
|
||||||
log.info("Loaded %d rows from %s", len(df), csv_file)
|
|
||||||
|
|
||||||
if args.limit:
|
|
||||||
df = df.head(args.limit)
|
|
||||||
log.info("Limiting to first %d rows.", args.limit)
|
|
||||||
|
|
||||||
df = df[df["IMEI"].notna() & (df["IMEI"].str.strip() != "")]
|
|
||||||
log.info("%d rows have a valid IMEI.", len(df))
|
|
||||||
|
|
||||||
if df.empty:
|
|
||||||
log.warning("No rows to process. Exiting.")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# ── Initialise client & verify auth ───────────────────────────────────────
|
|
||||||
client = TracksolidClient(CONFIG)
|
|
||||||
|
|
||||||
if not args.dry_run:
|
|
||||||
try:
|
|
||||||
client.get_token()
|
|
||||||
except Exception as exc:
|
|
||||||
log.error("Authentication failed: %s", exc)
|
|
||||||
log.error("Check TS_USER_ID, TS_USER_PWD_MD5, TS_APP_KEY, TS_APP_SECRET.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# ── Process rows ───────────────────────────────────────────────────────────
|
|
||||||
results = []
|
|
||||||
success = 0
|
|
||||||
failed = 0
|
|
||||||
skipped = 0
|
|
||||||
|
|
||||||
for idx, row in df.iterrows():
|
|
||||||
imei = str(row["IMEI"]).strip()
|
|
||||||
fields = row_to_fields(row)
|
|
||||||
|
|
||||||
# Skip rows where every updatable field is empty
|
|
||||||
if not any(fields.values()):
|
|
||||||
log.warning("Row %d (IMEI %s): no updatable fields, skipping.", idx + 1, imei)
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Row %d/%d — IMEI: %s | Plate: %s | Driver: %s",
|
|
||||||
idx + 1, len(df), imei,
|
|
||||||
fields.get("license_plate") or "—",
|
|
||||||
fields.get("driver_name") or "—",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp = client.update_vehicle(imei, fields, dry_run=args.dry_run)
|
|
||||||
if resp.get("code") == 0:
|
|
||||||
log.info(" OK")
|
|
||||||
success += 1
|
|
||||||
else:
|
|
||||||
log.warning(
|
|
||||||
" API error — code=%s message=%s",
|
|
||||||
resp.get("code"), resp.get("message")
|
|
||||||
)
|
|
||||||
failed += 1
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
"imei": imei,
|
|
||||||
**fields,
|
|
||||||
"api_code": resp.get("code"),
|
|
||||||
"api_message": resp.get("message"),
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as exc:
|
|
||||||
log.error(" Exception for IMEI %s: %s", imei, exc)
|
|
||||||
failed += 1
|
|
||||||
results.append({
|
|
||||||
"imei": imei,
|
|
||||||
**fields,
|
|
||||||
"api_code": "EXCEPTION",
|
|
||||||
"api_message": str(exc),
|
|
||||||
})
|
|
||||||
|
|
||||||
if not args.dry_run:
|
|
||||||
time.sleep(CONFIG["request_delay"])
|
|
||||||
|
|
||||||
# ── Summary & audit CSV ───────────────────────────────────────────────────
|
|
||||||
log.info("-" * 60)
|
|
||||||
log.info(
|
|
||||||
"Done. OK: %d Failed: %d Skipped: %d | Total: %d",
|
|
||||||
success, failed, skipped, success + failed + skipped
|
|
||||||
)
|
|
||||||
|
|
||||||
out_path = "tracksolid_update_results.csv"
|
|
||||||
pd.DataFrame(results).to_csv(out_path, index=False)
|
|
||||||
log.info("Audit results written to %s", out_path)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,348 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Tracksolid Pro - Bulk Vehicle Information Updater
|
|
||||||
Updates vehicle details via the jimi.open.device.update API endpoint
|
|
||||||
using data from the Fireside logistics CSV.
|
|
||||||
|
|
||||||
Signing approach taken directly from tspostman.py (confirmed working):
|
|
||||||
- POST as x-www-form-urlencoded (NOT JSON)
|
|
||||||
- All parameter values cast to strings before signing
|
|
||||||
- expires_in passed as string '7200', not integer
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python tracksolid_update.py [--dry-run] [--csv path/to/file.csv] [--limit N]
|
|
||||||
|
|
||||||
Environment variables (or edit CONFIG below):
|
|
||||||
TS_USER_ID - Your Tracksolid account username
|
|
||||||
TS_USER_PWD_MD5 - MD5 hash of your password (lowercase)
|
|
||||||
TS_APP_KEY - Your appKey from JIMI
|
|
||||||
TS_APP_SECRET - Your appSecret from JIMI
|
|
||||||
TS_API_URL - API base URL (defaults to EU node)
|
|
||||||
TS_CSV_PATH - Path to the logistics CSV
|
|
||||||
"""
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# CONFIGURATION — edit here or set environment variables
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
CONFIG = {
|
|
||||||
"user_id": os.getenv("TS_USER_ID", "Fireside Communications"),
|
|
||||||
"user_pwd_md5": os.getenv("TS_USER_PWD_MD5", "81a1b005efd3596073e38efd8a2fd3fd"),
|
|
||||||
"app_key": os.getenv("TS_APP_KEY", "8FB345B8693CCD00BB70D528C0D4019E"),
|
|
||||||
"app_secret": os.getenv("TS_APP_SECRET", "3177c89993b446c6aced0d7c56375d2c"),
|
|
||||||
# EU node confirmed for this account
|
|
||||||
"api_url": os.getenv("TS_API_URL", "https://eu-open.tracksolidpro.com/route/rest"),
|
|
||||||
"expires_in": "7200", # string, not int — matches tspostman.py
|
|
||||||
"request_delay": 0.5, # seconds between API calls
|
|
||||||
}
|
|
||||||
|
|
||||||
CSV_PATH = os.getenv("TS_CSV_PATH", "20260414_FS__Logistics_-_final_fixed.csv")
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# LOGGING
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format="%(asctime)s %(levelname)-8s %(message)s",
|
|
||||||
handlers=[
|
|
||||||
logging.StreamHandler(sys.stdout),
|
|
||||||
logging.FileHandler("tracksolid_update.log", encoding="utf-8"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# SIGNING UTILITIES (ported directly from tspostman.py)
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def utc_timestamp() -> str:
|
|
||||||
"""UTC time formatted as yyyy-MM-dd HH:mm:ss."""
|
|
||||||
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
|
|
||||||
def build_sign(params: dict, app_secret: str) -> str:
|
|
||||||
"""
|
|
||||||
Tracksolid signing algorithm (matches tspostman.py exactly):
|
|
||||||
1. Sort all param keys alphabetically, exclude 'sign' and empty values.
|
|
||||||
2. Concatenate key+value pairs with no separators.
|
|
||||||
3. Wrap with appSecret on both sides.
|
|
||||||
4. MD5 -> UPPERCASE 32-char string.
|
|
||||||
|
|
||||||
All values must already be strings before calling this.
|
|
||||||
"""
|
|
||||||
sorted_keys = sorted(
|
|
||||||
k for k in params
|
|
||||||
if k != "sign" and params[k] is not None and str(params[k]).strip() != ""
|
|
||||||
)
|
|
||||||
param_string = "".join(f"{k}{params[k]}" for k in sorted_keys)
|
|
||||||
raw_string = f"{app_secret}{param_string}{app_secret}"
|
|
||||||
return hashlib.md5(raw_string.encode("utf-8")).hexdigest().upper()
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# TRACKSOLID CLIENT
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
class TracksolidClient:
|
|
||||||
def __init__(self, cfg: dict):
|
|
||||||
self.cfg = cfg
|
|
||||||
self._token: str | None = None
|
|
||||||
self._token_expires_at: float = 0.0
|
|
||||||
self.session = requests.Session()
|
|
||||||
# No Content-Type set here — requests sets it automatically to
|
|
||||||
# application/x-www-form-urlencoded when data= is used (matching tspostman.py)
|
|
||||||
|
|
||||||
def _post(self, params: dict) -> dict:
|
|
||||||
"""
|
|
||||||
Sign and POST params using x-www-form-urlencoded encoding.
|
|
||||||
Confirmed working approach from tspostman.py:
|
|
||||||
- Cast all values to strings
|
|
||||||
- Use data= (form-encoded), NOT json=
|
|
||||||
"""
|
|
||||||
str_params = {
|
|
||||||
k: str(v)
|
|
||||||
for k, v in params.items()
|
|
||||||
if v is not None and str(v).strip() != ""
|
|
||||||
}
|
|
||||||
str_params["sign"] = build_sign(str_params, self.cfg["app_secret"])
|
|
||||||
|
|
||||||
log.debug("POST %s params=%s", self.cfg["api_url"], str_params)
|
|
||||||
resp = self.session.post(
|
|
||||||
self.cfg["api_url"],
|
|
||||||
data=str_params, # form-encoded, NOT json=
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = resp.json()
|
|
||||||
log.debug("Response: %s", json.dumps(data))
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _common_params(self, method: str) -> dict:
|
|
||||||
return {
|
|
||||||
"method": method,
|
|
||||||
"timestamp": utc_timestamp(),
|
|
||||||
"app_key": self.cfg["app_key"],
|
|
||||||
"sign_method": "md5",
|
|
||||||
"v": "1.0",
|
|
||||||
"format": "json",
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_token(self) -> str:
|
|
||||||
"""Return a valid access token, fetching a new one only when needed."""
|
|
||||||
if self._token and time.time() < self._token_expires_at - 60:
|
|
||||||
log.debug("Reusing cached token.")
|
|
||||||
return self._token
|
|
||||||
|
|
||||||
log.info("Obtaining new access token ...")
|
|
||||||
params = self._common_params("jimi.oauth.token.get")
|
|
||||||
params.update({
|
|
||||||
"user_id": self.cfg["user_id"],
|
|
||||||
"user_pwd_md5": self.cfg["user_pwd_md5"],
|
|
||||||
"expires_in": self.cfg["expires_in"], # already a string per tspostman.py
|
|
||||||
})
|
|
||||||
|
|
||||||
data = self._post(params)
|
|
||||||
if data.get("code") != 0:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Auth failed — code={data.get('code')} message={data.get('message')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._token = data["result"]["accessToken"]
|
|
||||||
self._token_expires_at = time.time() + int(data["result"]["expiresIn"])
|
|
||||||
log.info("Token acquired. Valid for %s seconds.", data["result"]["expiresIn"])
|
|
||||||
return self._token
|
|
||||||
|
|
||||||
def update_vehicle(self, imei: str, fields: dict, dry_run: bool = False) -> dict:
|
|
||||||
"""
|
|
||||||
Call jimi.open.device.update for one IMEI.
|
|
||||||
fields: dict using local key names (see row_to_fields below).
|
|
||||||
Returns the API response dict.
|
|
||||||
"""
|
|
||||||
if dry_run:
|
|
||||||
log.info("[DRY-RUN] IMEI %s -> %s", imei, fields)
|
|
||||||
return {"code": 0, "message": "dry-run"}
|
|
||||||
|
|
||||||
token = self.get_token()
|
|
||||||
params = self._common_params("jimi.open.device.update")
|
|
||||||
params["access_token"] = token
|
|
||||||
params["imei"] = str(imei)
|
|
||||||
|
|
||||||
# Map local field names -> Tracksolid API field names
|
|
||||||
api_field_map = {
|
|
||||||
"license_plate": "vehicleNumber",
|
|
||||||
"driver_name": "driverName",
|
|
||||||
"driver_phone": "driverPhone",
|
|
||||||
"vehicle_model": "vehicleModels",
|
|
||||||
"vin": "carFrame",
|
|
||||||
"engine_number": "engineNumber",
|
|
||||||
"device_name": "deviceName",
|
|
||||||
"fuel_per_100": "fuelPer100km",
|
|
||||||
}
|
|
||||||
for local_key, api_key in api_field_map.items():
|
|
||||||
val = fields.get(local_key)
|
|
||||||
if val:
|
|
||||||
params[api_key] = str(val)
|
|
||||||
|
|
||||||
return self._post(params)
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# CSV PARSING
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def clean(val) -> str | None:
|
|
||||||
"""Return None for NaN/empty values, else a stripped string."""
|
|
||||||
if pd.isna(val) or str(val).strip() in ("", "nan", "NaN"):
|
|
||||||
return None
|
|
||||||
return str(val).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def row_to_fields(row: pd.Series) -> dict:
|
|
||||||
"""Map one CSV row to local field names used by update_vehicle()."""
|
|
||||||
return {
|
|
||||||
"license_plate": clean(row.get("License Plate No.")),
|
|
||||||
"driver_name": clean(row.get("Driver Name")),
|
|
||||||
"driver_phone": clean(row.get("Telephone")),
|
|
||||||
"vehicle_model": clean(row.get("Vehicle Model")),
|
|
||||||
"vin": clean(row.get("VIN")),
|
|
||||||
"engine_number": clean(row.get("Engine Number")),
|
|
||||||
"device_name": clean(row.get("Device Name")),
|
|
||||||
"fuel_per_100": clean(row.get("Fuel/100km")),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# MAIN
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Bulk-update Tracksolid vehicle info from CSV."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--dry-run", action="store_true",
|
|
||||||
help="Show what would be sent without making update API calls."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--csv", default=CSV_PATH,
|
|
||||||
help=f"Path to the logistics CSV (default: {CSV_PATH})"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--limit", type=int, default=None,
|
|
||||||
help="Only process the first N rows (useful for testing)."
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# ── Load CSV ───────────────────────────────────────────────────────────────
|
|
||||||
csv_file = Path(args.csv)
|
|
||||||
if not csv_file.exists():
|
|
||||||
log.error("CSV file not found: %s", csv_file)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
df = pd.read_csv(csv_file, dtype={"IMEI": str})
|
|
||||||
log.info("Loaded %d rows from %s", len(df), csv_file)
|
|
||||||
|
|
||||||
if args.limit:
|
|
||||||
df = df.head(args.limit)
|
|
||||||
log.info("Limiting to first %d rows.", args.limit)
|
|
||||||
|
|
||||||
df = df[df["IMEI"].notna() & (df["IMEI"].str.strip() != "")]
|
|
||||||
log.info("%d rows have a valid IMEI.", len(df))
|
|
||||||
|
|
||||||
if df.empty:
|
|
||||||
log.warning("No rows to process. Exiting.")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# ── Initialise client & verify auth ───────────────────────────────────────
|
|
||||||
client = TracksolidClient(CONFIG)
|
|
||||||
|
|
||||||
if not args.dry_run:
|
|
||||||
try:
|
|
||||||
client.get_token()
|
|
||||||
except Exception as exc:
|
|
||||||
log.error("Authentication failed: %s", exc)
|
|
||||||
log.error("Check TS_USER_ID, TS_USER_PWD_MD5, TS_APP_KEY, TS_APP_SECRET.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# ── Process rows ───────────────────────────────────────────────────────────
|
|
||||||
results = []
|
|
||||||
success = 0
|
|
||||||
failed = 0
|
|
||||||
skipped = 0
|
|
||||||
|
|
||||||
for idx, row in df.iterrows():
|
|
||||||
imei = str(row["IMEI"]).strip()
|
|
||||||
fields = row_to_fields(row)
|
|
||||||
|
|
||||||
# Skip rows where every updatable field is empty
|
|
||||||
if not any(fields.values()):
|
|
||||||
log.warning("Row %d (IMEI %s): no updatable fields, skipping.", idx + 1, imei)
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Row %d/%d — IMEI: %s | Plate: %s | Driver: %s",
|
|
||||||
idx + 1, len(df), imei,
|
|
||||||
fields.get("license_plate") or "—",
|
|
||||||
fields.get("driver_name") or "—",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp = client.update_vehicle(imei, fields, dry_run=args.dry_run)
|
|
||||||
if resp.get("code") == 0:
|
|
||||||
log.info(" OK")
|
|
||||||
success += 1
|
|
||||||
else:
|
|
||||||
log.warning(
|
|
||||||
" API error — code=%s message=%s",
|
|
||||||
resp.get("code"), resp.get("message")
|
|
||||||
)
|
|
||||||
failed += 1
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
"imei": imei,
|
|
||||||
**fields,
|
|
||||||
"api_code": resp.get("code"),
|
|
||||||
"api_message": resp.get("message"),
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as exc:
|
|
||||||
log.error(" Exception for IMEI %s: %s", imei, exc)
|
|
||||||
failed += 1
|
|
||||||
results.append({
|
|
||||||
"imei": imei,
|
|
||||||
**fields,
|
|
||||||
"api_code": "EXCEPTION",
|
|
||||||
"api_message": str(exc),
|
|
||||||
})
|
|
||||||
|
|
||||||
if not args.dry_run:
|
|
||||||
time.sleep(CONFIG["request_delay"])
|
|
||||||
|
|
||||||
# ── Summary & audit CSV ───────────────────────────────────────────────────
|
|
||||||
log.info("-" * 60)
|
|
||||||
log.info(
|
|
||||||
"Done. OK: %d Failed: %d Skipped: %d | Total: %d",
|
|
||||||
success, failed, skipped, success + failed + skipped
|
|
||||||
)
|
|
||||||
|
|
||||||
out_path = "tracksolid_update_results.csv"
|
|
||||||
pd.DataFrame(results).to_csv(out_path, index=False)
|
|
||||||
log.info("Audit results written to %s", out_path)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Loading…
Reference in a new issue