DWH pipeline (new):
- dwh/261001_dwh_control.sql — watermarks + per-run audit log schema
- dwh/261002_bronze_constraints_audit.sql — ON CONFLICT key assertion
- dwh/261003_dwh_roles.sql — dwh_owner / grafana_ro contract assertion
- dwh/261004_dwh_observability_views.sql — v_table_freshness,
v_recent_failures, v_watermark_lag (readable by grafana_ro)
- docs/DWH_PIPELINE.md — operations runbook (setup, troubleshooting,
manual re-run, back-fill, rotation)
- DWH_Execution_Manual.md — reusable playbook for future data
projects (extract → blob → load pattern, 7 design principles,
snapshot-vs-incremental matrix, verification gates)
- docs/superpowers/{specs,plans}/2026-04-24-n8n-dwh-bronze-pipeline-*
— design spec + 27-task implementation plan
Security:
- dwh/260423_dwh_ddl_v1.sql — redacted plaintext role passwords to
'CHANGE_ME_BEFORE_APPLY' placeholders; added SECURITY header
documenting generation + rotation flow
Docs:
- CLAUDE.md — §3 adds tracksolid_dwh@31.97.44.246:5888 target,
§4 adds dwh/ + docs/DWH_PIPELINE.md to codebase map, §5 adds
bronze + dwh_control schema roll-up, §10 adds deploy task +
password rotation follow-up
Also includes miscellaneous in-progress files accumulated on this
branch (workspace, analytics notes, vehicle CSVs, extract helpers,
renamed markdown archives).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
16 KiB
CLAUDE.md — Fireside Communications · Tracksolid Fleet Intelligence
0. Commands
Package manager: uv (not pip/poetry)
uv sync # Install / sync dependencies
pytest tests/ # Run test suite
ruff check . # Lint
mypy . # Type check
Run a query via Docker (DATABASE_URL is internal-only):
DB=$(docker ps --filter name=timescale_db --format "{{.Names}}" | head -1)
docker exec $DB psql -U postgres -d tracksolid_db -c "SELECT COUNT(*) FROM tracksolid.devices;"
Run a migration file:
docker exec -i $DB psql -U postgres -d tracksolid_db < 07_your_migration.sql
1. What This Project Is
Fleet telematics ingestion and analytics stack for a telco first-line support client operating in Nairobi, Mombasa, and Kampala. The client dispatches field technicians to install, repair, and maintain home and business broadband, handle LOS signal faults, service migrations, and maintain outside plant infrastructure. The fleet is ~80 vehicles across three cities, all tracked via Tracksolid Pro (Jimi IoT API).
This repository ingests the Tracksolid Pro API into a TimescaleDB/PostGIS database and visualises fleet and operational KPIs in Grafana. The pipeline is deployed on Coolify at stage.rahamafresh.com.
Repository: https://repo.rahamafresh.com/kianiadee/tracksolid_timescale_grafana_prod.git
2. Tech Stack
| Layer | Technology |
|---|---|
| Ingestion | Python 3.12 — ingest_movement_rev.py, ingest_events_rev.py, webhook_receiver_rev.py |
| Shared utils | ts_shared_rev.py — token cache, DB pool, API signing, clean helpers |
| Database | PostgreSQL 16 + TimescaleDB 2.15 + PostGIS 3 (tracksolid_db) |
| Orchestration | Docker Compose on Coolify |
| Visualisation | Grafana (provisioned via custom image) |
| Workflow automation | n8n |
| API source | Tracksolid Pro / Jimi IoT Open Platform (eu-open.tracksolidpro.com/route/rest) |
| Backup | pg_dump sidecar → rustfs S3 (fleet-db bucket), nightly |
| Version control | Forgejo at repo.rahamafresh.com |
3. Instance & Connection Parameters
See docs/CONNECTIONS.md for the full shape. Summary:
- SSH:
ssh -i ~/.ssh/id_ed25519 kianiadee@stage.rahamafresh.com - 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 legacytracksolid_2schema no longer exists — migrations 02–06 applied 2026-04-18. - DB access:
DATABASE_URLpoints totimescale_db:5432(internal Docker network — not reachable locally). Usedocker execpattern above. Seedocs/CONNECTIONS.mdfor full reference. - DWH target DB:
tracksolid_dwhat31.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 withsslmode=require. Fed by the n8ndwh_extract+dwh_load_bronzeworkflows — seedocs/DWH_PIPELINE.md. - Container naming: Coolify appends a random suffix. Always resolve with:
e.g.docker ps --filter name=<service_name> --format "{{.Names}}" | head -1docker ps --filter name=timescale_db --format "{{.Names}}" | head -1 - Env vars: loaded from
.envviaenv_fileindocker-compose.yaml. Seedocs/CONNECTIONS.mdfor variable names. Never hardcode secrets.
4. Codebase Map
ts_shared_rev.py # Shared: config, signing, DB pool, token cache, clean helpers
ingest_movement_rev.py # GPS positions, trips, parking, track-list (high-res trail), device sync
ingest_events_rev.py # Alarm events polling (fallback for webhook push)
webhook_receiver_rev.py # FastAPI push receiver: /pushobd /pushevent /pushtripreport etc.
sync_driver_audit.py # One-shot: API↔DB driver/IMEI gap report + full upsert
import_drivers_csv.py # One-shot: populate 144 X3/JC400P devices from CSV (--apply to commit)
run_migrations.py # Applies SQL migrations in order at container startup
docker-compose.yaml # Services: timescale_db, ingest_movement, ingest_events,
# webhook_receiver, grafana
grafana/ # Grafana provisioning (baked into image)
n8n-workflows/ # n8n workflow exports (incl. dwh_extract, dwh_load_bronze)
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)
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
03..06_*.sql # Incremental migrations (06 adds assigned_city, dispatch_log, ops.*)
07_analytics_views.sql # Analytics views migration (applied 2026-04-21)
Dockerfile # Custom image for ingest/webhook containers
pyproject.toml # Python project + uv dependency spec
OPERATIONS_MANUAL.md # Day-to-day ops runbook
backup/ # pg_dump sidecar scripts and config
01_BusinessAnalytics.md # SQL analytics library — read before writing queries
20260414_FS__Logistics - final_fixed.csv # 144-device driver/vehicle source data
tracksolidApiDocumentation.md # API endpoint reference
260412_baseline_report.md # Fleet state snapshot (Apr 2026)
5. Database Schema — Key Tables
tracksolid.devices -- Device / driver / vehicle registry (63 rows; 0 driver_name populated)
-- IMEI mix: 353549* AT4 (23), 862798* X3/JC400P (23), 865135* X3/JC400P (10), 359857* (7)
-- Full CSV (144 devices) not yet imported — run import_drivers_csv.py --apply
tracksolid.live_positions -- Current fix per IMEI (19 rows; refreshed every 60s by ingest_movement)
tracksolid.position_history -- All GPS fixes (hypertable, partitioned by gps_time). ~519 rows (308 track_list + 211 poll).
-- pg_stat_user_tables shows 0 for hypertables — always COUNT(*) directly.
-- source: 'poll' (60s sweep) | 'track_list' (30m high-res)
tracksolid.trips -- Trip summaries: distance_km, driving_time_s, avg/max speed
tracksolid.parking_events -- Stop events with duration and address (0 rows — endpoint returning empty)
tracksolid.alarms -- Alarm events (alarm_type, alarm_name, alarm_time) — 10 rows, polling healthy
tracksolid.obd_readings -- OBD diagnostics (push only, awaiting webhook registration)
tracksolid.device_events -- Power on/off tamper events (push only)
tracksolid.ingestion_log -- API call audit trail — 875 runs / 24h, 0 failures at last check (2026-04-19)
tracksolid.dispatch_log -- Dispatch decisions for SLA tracking (migration 06; empty until ops integration)
tracksolid.schema_migrations -- Applied files: 02,03,04,05,06 (last 06 on 2026-04-18)
dwh_gold.fact_daily_fleet_metrics -- Nightly ETL aggregates per vehicle per day (run refresh_daily_metrics)
ops.service_log -- Workshop service history (migration 06)
ops.odometer_readings -- Physical odometer captures (migration 06)
ops.tickets -- Ticket skeleton for ops integration (migration 06; empty)
Full DDL: 02_tracksolid_full_schema_rev.sql + migrations 03–06.
Analytics views (migration 07_analytics_views.sql) — one view per BA-file query block, readable by grafana_ro:
tracksolid.v_fleet_today -- §9 per-vehicle today roll-up
tracksolid.v_vehicles_not_moved_today -- §2.3 alert source
tracksolid.v_active_dispatch_map -- §4.3 geomap source
tracksolid.v_currently_idle -- §2.2 idle lens
tracksolid.v_driver_aggregates_daily -- §3.1 + §3.2 aggression index source
tracksolid.v_fleet_km_daily -- §7 Panel 5 distance trend
tracksolid.v_alarms_daily -- §7 Panel 7 alarm frequency
tracksolid.v_utilisation_daily -- §7 Panel 8 utilisation heatmap (gated on dwh_gold ETL)
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.
DWH bronze layer (separate DB tracksolid_dwh) — populated by the n8n dwh_extract + dwh_load_bronze workflows. Operational details in docs/DWH_PIPELINE.md.
-- 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
Always read tracksolidApiDocumentation.md before adding a new endpoint call.
| Fact | Detail |
|---|---|
| Auth | OAuth2 — token cached in tracksolid.api_token_cache, refreshed via jimi.oauth.token.refresh |
| Signing | MD5: secret + sorted(k+v pairs) + secret — see build_sign() in ts_shared_rev.py |
| Batch limit | Max 50 IMEIs per call for most endpoints |
distance field |
Returns METRES, not km despite docs. Always divide by 1000. (FIX-M16) |
driverName/driverPhone |
From jimi.user.device.list — will be NULL if not set in Tracksolid Pro UI |
alarm_type field |
API polling returns alertTypeId/alarmTypeName — NOT alarmType/alarmName (FIX-E06) |
durSecond |
Parking endpoint returns durSecond, not seconds (FIX-M13) |
jimi.device.track.mileage |
startMileage/endMileage are cumulative odometer in metres |
| Rate limit | Code 1006 — back off and retry with re-sign (handled in api_post()) |
| OBD data | Push only via /pushobd webhook — no polling endpoint exists |
7. Fix History (do not regress)
| Fix ID | File | What it fixed |
|---|---|---|
| FIX-M11 | ingest_movement_rev.py |
Removed erroneous ×1000 on distance (was storing km as mm) |
| FIX-M13 | ingest_movement_rev.py |
Parking: added acc_type=0, account; mapped durSecond |
| FIX-M14 | ingest_movement_rev.py |
poll_track_list() — high-res GPS trail every 30m |
| FIX-M15 | ingest_movement_rev.py |
get_device_locations() — on-demand precision refresh |
| FIX-M16 | ingest_movement_rev.py |
distance from API is metres → divide by 1000 before storing |
| FIX-M17 | ingest_movement_rev.py |
sync_devices() ON CONFLICT now updates all 26 fields (was 5) |
| FIX-M18 | ingest_movement_rev.py |
sync_devices() pulls vehicleName/vehicleNumber/driverName/driverPhone/sim from jimi.track.device.detail — list endpoint returns null for these even when set |
| FIX-M19 | ts_shared_rev.py, ingest_movement_rev.py |
Multi-account support: fleet spans fireside, Fireside@HQ, Fireside_MSA (156 devices total). sync_devices, poll_live_positions, poll_parking iterate TRACKSOLID_TARGETS (comma-separated env var). New helper get_active_imeis_by_target() scopes parking calls to the right account |
| FIX-E06 | ingest_events_rev.py |
Alarm field mapping: alertTypeId/alarmTypeName/alertTime |
| BUG-02 | Migration 04 | Historical distance_m rows ÷1,000,000 → renamed to distance_km |
8. Working Rules
- No prod push without explicit user confirmation. Always state what you are about to push and wait.
- Never rewrite a migration that is already applied. Check
tracksolid.schema_migrationsfirst. Add a new numbered migration file for any schema change. - Read before writing. Before suggesting any code change, read the relevant source file. Before writing a query, check
01_BusinessAnalytics.mdfor an existing pattern. - Reuse shared utilities. All DB access via
get_conn(), all API calls viaapi_post(), all cleaning viaclean()/clean_num()/clean_int()/clean_ts()ints_shared_rev.py. Do not reinvent these. - Resolve container names dynamically. Never hardcode the Coolify suffix. Use
docker ps --filter name=<service>. - SSH only when asked. Default workflow is local code → commit → push. SSH into the instance only when explicitly asked to test or run something live.
- Secrets from env only. Connection strings, API keys, and passwords live in
.env. Reference variable names fromdocs/CONNECTIONS.md, never values. - Two developers, one incoming. Write code and docs that a second developer (mixed technical/operations background) can follow without prior context.
- Forgejo API auth: credentials stored in macOS keychain. Retrieve with
git credential fill(host=repo.rahamafresh.com). Use basic auth againsthttps://repo.rahamafresh.com/api/v1directly — noteaorghneeded. - Single live schema. All live data lives in
tracksolid. Aggregates live indwh_gold; workshop/ticket integrations live inops. Do not reintroduce references to the retiredtracksolid_2schema.
9. Fleet State (as of 2026-04-19)
| Metric | Value |
|---|---|
Registered devices (tracksolid.devices) |
63 total — 23 × 353549* (AT4), 23 × 862798* + 10 × 865135* (X3/JC400P), 7 × 359857* |
| Devices in CSV not yet imported | 144 (X3/JC400P); import_drivers_csv.py --apply will upsert names + plates |
| Driver names populated | 0 / 63 — pending CSV import |
| Live positions | 19 (latest fix 2026-04-19 10:25 UTC) |
| Trips recorded | 8 (latest 2026-04-19 08:34 UTC) |
| Alarms recorded | 10 (latest 2026-04-19 09:15 UTC) |
position_history rows |
519 (308 track_list + 211 poll); hypertable stats don't update in pg_stat_user_tables — query directly |
| Pipeline status | Running healthy: 875 runs / 24h, 0 failures across all six endpoints |
| Cities active | Nairobi (primary), Mombasa (deploying), Kampala (4 devices in CSV) |
| Service flags | KDK 829A GP (239,264 km), Belta KCU-647D (235,000 km) |
Latest full snapshot: 260412_baseline_report.md
10. Open Items (update as resolved)
| Priority | Item |
|---|---|
| HIGH | Run import_drivers_csv.py --apply — 144 X3/JC400P devices with names + plates waiting |
| HIGH | Register webhooks: /pushoil /pushtem /pushlbs (auto-register on push now done — commit 257643c) |
| HIGH | Investigate X3-63282 in Kampala — legitimate or unauthorised? |
| MEDIUM | Set fuel_100km per vehicle type to activate fuel cost calculations |
| MEDIUM | Investigate 44 silent devices (only 19 of 63 reporting) — SIM installed? Activated? |
| MEDIUM | Co-develop client KPI framework (see docs/KPI_FRAMEWORK.md) |
| LOW | Populate geofences — depot boundaries, city zones |
| 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 |