--- url: /01-korido.md --- # Part 1 — Korido: the product and its world Before a single tracker signal can mean anything, you need the shape of the thing it feeds. This part is that orientation. It answers what Korido is and the problem it exists to solve, who works inside it, how a reading travels from a truck on the Douala–N'Djamena corridor to the map an owner reads and the alert on their phone, and the shared vocabulary every later chapter leans on. Read it once, top to bottom, and the rest of the book has somewhere to land. ```mermaid flowchart LR A["What is Korido
the product & its people"] -->|"then zoom in on the machine"| B["System at a glance
how a signal flows"] B -->|"then pin down the words"| C["Vocabulary
the shared terms"] class A,B,C surface classDef surface fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 ``` * **[What is Korido](./what-is-korido)** — the corridor and its two-thousand kilometres of blind spots, the four roles (**owner**, **driver**, **admin**, **viewer**), the surfaces each audience meets, and the one principle everything rests on: the hardware tracker on the truck is the single source of truth for where that truck is — never the driver's phone. * **[System at a glance](./system-at-a-glance)** — one signal followed end to end, from the tracker through Flespi, the API Worker, the queue, and the Engine Worker to the map and the alert; then the four **data planes** — reference, plan, observed, and intelligence — that organise everything Korido stores and computes. * **[Vocabulary](./vocabulary)** — the glossary the whole handbook shares: waypoint, corridor, mission, trip, stop, gap, fix, heartbeat, Route Guard, and the rest, each defined once in plain language. Everything in this book happens inside one **tenant** — a single trucking business, isolated from every other. That word, and every other one the later parts assume you already know, is defined here first. ## How it connects * Next: [Part 2 — Telemetry](../02-telemetry/) follows the life of a single signal from the tracker to the live vehicle state. * The surfaces introduced here get full chapters of their own, starting with the [fleet app](../07-surfaces/fleet-app); the tenancy and isolation model is in [Tenancy and security](../08-platform/tenancy-and-security). --- --- url: /01-korido/what-is-korido.md --- # What Is Korido ## What this chapter covers Korido is a multi-tenant fleet-tracking platform built for long-haul trucking on the Douala–N'Djamena corridor between Cameroon and Chad. This chapter explains the problem Korido exists to solve, who its users are, the surfaces they work in, and the one principle that anchors everything the platform believes about a truck's location. ## The picture ```mermaid flowchart TB subgraph FIELD[On the corridor] TRK["Hardware tracker
on the truck"] DRV["Driver + driver app"] end KOR(("KORIDO")) TRK -->|"GPS source of truth · via Flespi"| KOR DRV -.->|"display-only GPS,
mission + status updates"| KOR KOR --> OWN["Owner
fleet app · owner app"] KOR --> ADM["Admin
Korido platform staff"] KOR --> CUST["Customer
tracking portal"] KOR --> WA["WhatsApp
alerts + bot"] class TRK source class DRV warn class KOR engine class OWN,ADM,CUST,WA surface classDef source fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef engine fill:#fff7ed,stroke:#ea580c,color:#7c2d12 classDef surface fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 ``` ## The problem Korido solves The Douala–N'Djamena corridor is roughly two thousand kilometres of road that a truck crosses over several days. Along the way it climbs out of the port at Douala, threads through Cameroonian towns, waits at weighbridges and toll gates, queues at the border, clears customs, and finally reaches N'Djamena in Chad. A single mission can span two countries, multiple stops, and long stretches of thin cellular coverage. That length is exactly what makes the work hard to see. On a corridor this long, the things a fleet owner most needs to know are the things that happen far from the depot: * **Breakdowns.** A truck that stops moving in the middle of nowhere costs money every hour, and the owner often learns about it late, by phone, after the driver has already lost time. * **Border waits and formalities.** Time spent at the frontier, at customs, or at a weighbridge is real cost, but without measurement it blends into "the trip took a while." * **Fuel loss and tracker interference.** A sudden fuel drop, a tracker cut from power, or a truck parked somewhere it should not be is invisible unless something is watching continuously. * **Opaque trips.** When did the truck actually leave? Where did it stop, and for how long? Was it on the agreed route the whole way? Without a system, the honest answer is usually a guess reconstructed after the fact. Korido turns that opacity into a continuous, measurable record. A tracker on every truck streams position and telemetry; the platform reconstructs the journey into trips, stops, gaps, and border crossings; it watches the truck against its agreed route; and it raises alerts the moment something worth knowing happens — a fuel drop, a route departure, a truck gone dark. What was a phone call after the fact becomes a live picture and a searchable history. ## Tenants and fleets Korido is a SaaS platform, so it hosts many trucking businesses at once. Each business is a **tenant** — its own fleet, its own drivers, its own clients, its own missions. A tenant's data is theirs alone: every truck, trip, alert, and report belongs to exactly one tenant and is never visible to another. Isolation is enforced on every read, so a tenant only ever sees its own corridor. Within a tenant, the working unit is the **fleet**: the vehicles the business operates, the trailers they pull, the drivers who staff them, and the clients whose cargo they carry. The platform is designed to serve a real operator's fleet — on the order of fifty to a hundred trucks — not a demo. ## The four roles Korido recognises four roles. Two are the working roles inside a tenant, one belongs to the platform itself, and one is held in reserve. * **Owner** — the fleet operator. Owners run the business inside Korido: they see the live map and dashboard, create and assign missions, read the diary and reports, manage vehicles, trailers, drivers, and clients, and receive the alerts that matter. The owner is the primary paying user. * **Driver** — the person behind the wheel. Drivers use the driver app to receive mission assignments, confirm loading and progress, and stay in sync with the office, including where coverage is thin. * **Admin** — Korido platform staff. Admins operate across every tenant from the Admin Portal, handling tenant support, fleet health, the shared road network, and coverage. This role belongs to the Korido platform itself. * **Viewer** — a reserved, read-only role for future use, for people who should see a fleet without changing anything. The two roles that live inside a tenant are **owner** and **driver**. Admin is a platform-wide role, and viewer is held in reserve. ## The surfaces at a glance Each audience meets Korido through a surface built for how they work. | Surface | Who it serves | What it is for | | --- | --- | --- | | **Fleet app** | Owners and their office staff | The main workspace: live map, dashboard, missions, vehicle diary, fuel, clients, trailers, tracking links | | **Admin portal** | Korido platform staff | Cross-tenant operations: tenant support, fleet health, the shared road network and corridors, coverage | | **Driver app** | Drivers | A mobile app for mission assignments, loading and progress confirmation, and offline-tolerant sync | | **Owner app** | Owners on the move | A native mobile app centred on the live map and vehicle status | | **Customer tracking portal** | The fleet's own customers | A shareable link that lets a shipper follow their cargo without a login, gated by phone verification | | **WhatsApp** | Owners, drivers, and customers | Alerts, one-time codes, and a bot for quick questions, in the channel people already use | The fleet app and the owner app serve the same person in different postures — one at a desk, one in a pocket. The customer tracking portal is the only surface built for people outside the fleet, and it shows a deliberately narrow, privacy-safe view. ## The GPS source-of-truth principle Everything Korido claims about where a truck is rests on one rule: the truck's location comes from the hardware tracker fitted to the vehicle — a VL863-class device — and reaches Korido through Flespi, the telemetry gateway that speaks to the tracker fleet. The hardware tracker is the single source of truth for position and vehicle telemetry. The driver's phone is not. The driver app may show the driver their own location on a map for their convenience, but that phone GPS is **display-only**: it is never sent to the backend, never stored, and never used to decide where a truck is, whether it has deviated, or whether it has arrived. A driver could leave their phone at home and Korido would still track the truck perfectly, because the truck — not the phone — carries the tracker. This separation is deliberate and load-bearing. It means a truck's record cannot be spoofed or accidentally distorted by a phone in the wrong pocket, that tracking survives a dead or forgotten phone, and that every downstream judgement — trips, Route Guard, fuel, arrivals — is built on one trustworthy positional signal rather than two competing ones. ## How it connects * [System at a glance](./system-at-a-glance) walks the signal from tracker to app and introduces the four data planes the rest of the book is organised around. * [Vocabulary](./vocabulary) defines the terms used throughout — waypoint, corridor, Route Guard, mission, trip, gap, and the rest. * The platform's tenancy and isolation rules are detailed in [Tenancy and security](../08-platform/tenancy-and-security). * The surfaces get full chapters of their own, including the [fleet app](../07-surfaces/fleet-app), the [driver](../07-surfaces/driver-app) and [owner](../07-surfaces/owner-app) apps, the [customer tracking portal](../07-surfaces/tracking-portal), and [WhatsApp](../07-surfaces/whatsapp). --- --- url: /01-korido/system-at-a-glance.md --- # System At A Glance ## What this chapter covers This chapter follows one signal end to end — from the tracker on a truck to the map an owner reads and the alert that reaches their phone — and then names the four **data planes** that the rest of the handbook is organised around. The planes are the book's mental model: once you can place any feature in one of them, the whole system fits together. ## The picture ```mermaid flowchart LR TRK["Hardware trackers
on the trucks"] -->|"GPS + telemetry"| F["Flespi
telemetry gateway"] F -->|"webhook"| API["API Worker
authenticate · validate ·
normalize · batch"] API -->|"canonical batch"| Q[["positions queue"]] Q --> ENG["Engine Worker
fleet state machine ·
scheduled jobs"] ENG -->|"positions, trips, stops,
gaps, traversals, events"| DB[("PostgreSQL
+ PostGIS")] ENG -->|"live state, route, ETA,
place labels"| KV[("KV cache")] ENG -->|"alerts"| WA["WhatsApp"] ENG -->|"push"| MOB["Driver &
Owner apps"] DB --> APP["Fleet app ·
Admin portal"] DB --> PORTAL["Customer
tracking portal"] class TRK,F source class API ingress class Q queue class ENG engine class DB,KV store class WA,MOB,APP,PORTAL surface classDef source fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef ingress fill:#eff6ff,stroke:#2563eb,color:#1e3a8a classDef queue fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef engine fill:#fff7ed,stroke:#ea580c,color:#7c2d12 classDef store fill:#ecfdf5,stroke:#047857,color:#064e3b classDef surface fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 ``` ::: tip Read it this way Ingress accepts and normalizes, the queue absorbs delay, the engine interprets, and the surfaces read the result. Tenant ownership is resolved in the engine, never trusted from the queue payload. ::: ## Following the signal A truck's tracker emits a reading — a position, or a position-less status frame carrying voltage, ignition, or fuel — and hands it to **Flespi**, the gateway that speaks to the whole tracker fleet on Korido's behalf. Flespi delivers it to Korido as a webhook. The **API Worker** owns that front door. It authenticates the webhook, validates the payload's shape and bounds, and normalizes every tracker family's dialect into one canonical telemetry shape, so that battery, power, signal, movement, and fuel mean the same thing no matter which device sent them. It groups the normalized readings into a batch and places that batch on a queue. The API Worker's job ends there: it stays at the edge, close to ingress and auth, and leaves interpretation to the Engine Worker. The **Engine Worker** consumes the queue and does the thinking. It resolves each reading to a known device and vehicle, loads the vehicle's recent context, and runs it through the fleet state machine — the logic that decides whether the truck is moving, parked, or gone dark; that opens and closes trips; that detects stops and classifies them; that notices signal gaps; that watches the truck against its route; and that reads fuel and raises events. The Engine writes the results to the database and cache, publishes the events that deserve an alert, and runs the scheduled jobs that keep everything current between messages. Processing is serial per truck, so a vehicle's story is assembled in order, one reading at a time. From there the picture flows outward. The **database** holds the durable record that the fleet app, admin portal, and customer tracking portal read from; the **cache** holds the live, fast-changing state — current position, active route, ETA, human-readable place labels. **Alerts** leave through WhatsApp and push, so a fuel drop or a route departure reaches a person while it still matters. ## The four data planes Everything Korido stores and computes belongs to one of four planes. They stack: the reference plane describes the world, the plan plane says what should happen, the observed plane records what did happen, and the intelligence plane learns from the accumulation. ```mermaid flowchart TB subgraph REF["Reference plane · curated, shared across tenants"] W["waypoints"] RS["road_segments"] C["corridors"] GC["geocode_cache"] end subgraph PLAN["Plan plane · per tenant"] M["missions"] MT["mission_templates"] RU["rule_sets"] CV["convoys"] end subgraph OBS["Observed plane · immutable facts, per tenant"] P["positions"] TR["trips"] ST["stops"] G["data_gaps"] WV["waypoint_visits"] ST2["segment_traversals"] EV["vehicle_events / fuel_events"] DV["corridor_deviations"] end subgraph INT["Intelligence plane · learned across the fleet"] SZ["slowdown_zones"] H["route_hotspots"] AN["corridor_anomalies"] AZ["segment analytics"] end REF --> PLAN PLAN --> OBS OBS --> INT INT -.->|"expected drag feeds ETA & guard"| PLAN class W,RS,C,GC reference class M,MT,RU,CV plan class P,TR,ST,G,WV,ST2,EV,DV observed class SZ,H,AN,AZ intelligence classDef reference fill:#ecfdf5,stroke:#047857,color:#064e3b classDef plan fill:#eff6ff,stroke:#2563eb,color:#1e3a8a classDef observed fill:#fff7ed,stroke:#ea580c,color:#7c2d12 classDef intelligence fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 ``` ### The reference plane — the map The reference plane is Korido's shared, curated model of the road itself: the named places a truck can be (**waypoints**), the curated geometry of each stretch of road between two of them (**road\_segments**), the named paths that string those places into routes (**corridors**), and the reverse-geocoding label cache that turns a raw coordinate into a human place name (**geocode\_cache**). This plane is maintained by Korido platform staff and shared across every tenant — the corridor from Douala to N'Djamena is the same road for everyone who drives it. Configure it once, and every fleet uses it. ### The plan plane — what should happen The plan plane is where a tenant declares intent. A **mission** is one planned journey for one vehicle: pick a route, assign a driver and truck, and it carries the load from origin to destination through an ordered set of waypoints. **Mission templates** capture a route and its presets so a common run becomes a one-tap dispatch; **rule\_sets** hold the driving policy that applies to a mission — speed limits, night-driving windows, authorized fuel stations, prohibited stops; and **convoys** group missions that travel together. This plane is per tenant and editable: it is the office's stated plan for the work. ### The observed plane — what actually happened The observed plane is the immutable record of reality, written by the Engine as telemetry arrives. Raw readings land as **positions**; the state machine assembles them into **trips** (the driving windows), **stops** (parked intervals, each classified — customs, weighbridge, breakdown, fuel station, and more), **data\_gaps** (windows where the truck went dark, classified by likely cause), **waypoint\_visits** (arrivals at and departures from named places), and **segment\_traversals** (one truck's observed crossing of one road segment — the spine of all later analytics). Alongside these sit **vehicle\_events** and **fuel\_events** — the noteworthy moments, from a fuel drain to a route departure — and **corridor\_deviations**, the record of every excursion off the agreed route. These facts are never rewritten; they are the ground truth everything else is measured against. ### The intelligence plane — what we learn The intelligence plane sits above a single tenant and learns from the whole fleet's accumulated facts. Recurring slow patches cluster into **slowdown\_zones**; recurring stop-and-event clusters become **route\_hotspots**; disruptive incidents that block progress are captured as **corridor\_anomalies**; and the segment\_traversals accumulate into **analytics** that compare a truck with itself over time, a truck with its peers, and a driver with other drivers, surfacing correlations no single trip could reveal. This plane feeds back into the plan plane: what has been learned about a stretch of road sharpens the ETA and the Route Guard expectations for the next truck that drives it. ## How it connects * [What is Korido](./what-is-korido) sets up the problem, the roles, and the source-of-truth principle this pipeline depends on. * [Vocabulary](./vocabulary) defines every term named in the four planes. * The ingestion and normalization path is detailed in [The ingestion pipeline](../02-telemetry/ingestion-pipeline) and [Part 3 — The fleet engine](../03-fleet-engine/). * The plan plane's mission and ETA behavior is covered in [Progression and ETA](../05-missions/progression-and-eta); the reference plane's Route Guard behavior in [Route Guard](../04-road-network/route-guard). * The layered data model and the tenant-isolation rules behind it live in [Data architecture](../08-platform/data-architecture) and [Tenancy and security](../08-platform/tenancy-and-security). --- --- url: /01-korido/vocabulary.md --- # Vocabulary ## What this chapter covers This is the glossary the whole handbook leans on. Each term is defined once here, in a single paragraph, in plain language. Later chapters bold a term on first use and assume this page for the rest. The definitions are ordered alphabetically; the diagram below shows how the main terms relate as a signal becomes a journey. ## The picture ```mermaid flowchart TB subgraph MAP["The map · reference"] W["waypoint"] --> RSG["road segment"] --> C["corridor"] end subgraph PLANNED["The plan"] C --> M["mission"] MT["mission template"] --> M RU["rule set"] --> M CV["convoy"] --> M end subgraph SIGNAL["The signal"] HB["heartbeat"] --> FX["fix / status frame"] --> QF["quality flag"] end subgraph JOURNEY["The journey observed"] QF --> TR["trip"] TR --> S["stop"] TR --> G["gap"] TR --> WV["waypoint visit"] TR --> STV["segment traversal"] M --> DV["deviation · via Route Guard"] end subgraph LEARNED["What the fleet learns"] STV --> HS["hotspot"] STV --> SZ["slowdown zone"] DV --> AN["anomaly"] end ``` Everything below happens inside one **tenant** — one trucking business, isolated from every other. ## Glossary **Anomaly** — a disruptive incident on a corridor that blocks or badly slows forward progress: a road closure, a blockade, a major backup. Korido captures anomalies (`corridor_anomalies`) as cross-fleet intelligence, each with an expected time cost, and when an anomaly blocks progress it turns an affected mission's ETA confidence to blocked rather than pretending the truck will arrive on time. **Client** — the operator's customer: the shipper or consignee a mission is run *for* (`clients`). A client record is deliberately lean — a name and a contact — but it carries the operational weight of saved **client locations**, a default corridor and driving policy that quick-assign reads, and the tracking links shared with that customer. A client is soft-deleted, never erased, so past missions that named it stay readable. **Client location** — a saved association between a client and a map **waypoint** in a role — pickup, delivery, or both (`client_locations`) — optionally labelled. Client locations are what let picking a client in the mission wizard pre-fill the route's origin and destination, so a routine run starts with its endpoints already chosen. **Convoy** — a group of missions dispatched to travel together, several trucks running the same corridor at once for safety or coordination. One `convoys` row ties the missions together as a single dispatch and reporting unit; the quick-assign flow can create N missions and one convoy in a single pass. **Corridor** — a named, ordered path of waypoints that defines a reusable route, such as Douala to N'Djamena. A corridor is the sequence of places, and each leg resolves to a **road segment** when needed. Corridors are curated once and attached to missions, templates, and clients, so a route worth running is a route worth naming. **Deviation** — an excursion off the agreed route. When a truck leaves its mission's **Route Guard** region, Korido opens a deviation (`corridor_deviations`) and records when it left, when it returned, how far and how long it strayed, and why the excursion closed. Deviations are how off-route behaviour becomes a measurable, alertable fact. **Document** — a photo of paperwork a run generates — a customs form, a signed delivery note, a fuel receipt, a vehicle paper (`documents`) — captured by a driver on the phone, attached to the mission and truck it belongs to, and worked by an owner in a review center. A document moves through a small status set as it is reviewed — **Validé**, **À corriger**, or **Rejeté** — and a correction or rejection sends the driver a directed retake request. **Driver** — a person record that is both a login identity and a mission assignee: a tenant user with the driver role, keyed by a phone number that is their one-time-code login to the driver app. A driver record is the prerequisite for both dispatching — every mission names a driver — and driver-app access, and deactivating one preserves the missions it ran. **Fix** — a position reading that carries real coordinates: the truck's actual location at a moment in time. A reading with no coordinates is a no-fix reading, delivered as a **status frame**. All geometry — trips, distance, deviations, arrivals — is built only from fixes; no-fix readings still feed liveness, ignition, and fuel, but never place the truck on the map. **Gap** — a window in which Korido stops receiving usable positions and the truck goes dark. A gap opens once silence passes a threshold — 600 seconds while the truck was moving, 1800 seconds while it was stationary — and closes when signal returns. It is then classified by likely cause (`data_gaps`): `signal_loss`, `power_off`, `stationary_offline`, `tampering_suspect`, or `unknown`. **Heartbeat** — any contact from the tracker at all — a fix, a status frame, any telemetry — that proves the hardware is alive. The heartbeat clock answers "when did we last hear from this device?", which is distinct from the last position fix. When a device's heartbeat falls silent past the tenant's threshold (default 30 minutes), Korido raises the offline alert. **Hotspot** — a place where stops and events recur across the fleet: a customs post, a weighbridge, a chronic bottleneck. Korido clusters observed stops and events across tenants into hotspots (`route_hotspots`), each carrying its own statistics and an expected time cost, so a known slow place is anticipated on the next run rather than rediscovered every time. **Mission** — one planned journey for one vehicle: a truck and driver assigned to carry a load along a corridor from origin to destination. A mission moves through an eight-state lifecycle — created, assigned, en route to origin, active, paused, arrived, completed, cancelled — carries its ordered waypoints, its Route Guard region, its resolved rule set, and its live ETA, and only one mission is active per vehicle at a time. **Mission template** — a saved preset that turns a common run into a one-tap dispatch. A template carries the corridor and direction, the preferred vehicle, driver, and trailer, cargo defaults, and the rule set — everything a mission needs — and ranks itself by how often it is used so the right template surfaces first. **Quality flag** — the label the engine attaches to a fix that records how far to trust it: `accepted`, `spike`, `jitter`, or `late`. It lets Korido keep every reading for the record while deciding which ones may move the truck, alter the route, or fire an alert. No-fix readings carry no quality flag, because they are never classified as positions. **Road segment** — the curated geometry of one road between two waypoints: a centre-line and a buffered region an admin has drawn once (`road_segments`). Because one road (Douala to Yaoundé) is shared by many corridors, its geometry is stored once and reused; a reverse row exists only where a road genuinely diverges by direction of travel. Road segments are the region source for Route Guard and the geometric baseline for per-segment analytics. **Route Guard** — Korido's corridor-monitoring feature and the name it wears in every user-facing surface. Route Guard holds each active mission's route as a region and continuously checks the truck's trusted position against it, opening a **deviation** when the truck strays and clearing it when it returns. It is what turns "stay on the agreed road" into an enforced, alertable promise. **Rule set** — a named driving policy attachable to a mission (`rule_sets`): maximum speed and tolerance, night-driving window and mode, hours-of-service limits, authorized fuel stations, and prohibited stops. Rule sets resolve by inheritance — mission, then template, then client, then corridor, then tenant default — and apply live, so editing a policy takes effect on missions already under way. Their enforcement producers emit vehicle events for speeding, night-driving violations, unauthorized refuelling, and prohibited stops. **Segment traversal** — one truck's single observed crossing of one road segment: the atomic fact "this vehicle drove this stretch, this time" (`segment_traversals`). Each traversal records transit and driving time, distance, speed, stops, gaps, and idle, denormalized with the driver, vehicle, cargo, client, and calendar context of the run. Segment traversals are the analytics spine — the rows that, in aggregate, answer why leg four is always slow or which driver clears customs fastest. **Slowdown zone** — a stretch of corridor where trucks reliably lose speed: a rough patch, a long climb, a congested approach. Korido detects raw slow events and clusters them across the fleet into slowdown zones (`slowdown_zones`), each with an expected time cost, so recurring slowness is modelled rather than mistaken for a one-off. **Status frame** — a tracker message that carries telemetry but no coordinates: a voltage heartbeat, an ignition change, a fuel reading sent while the truck is parked. Status frames are kept as no-fix position rows; they keep a vehicle's live tiles fresh — ignition, battery, signal, voltage — advance liveness, and feed fuel and tamper detection, without ever being read as a location. This is why a parked truck streaming status frames still shows live status instead of freezing at its last fix. **Stop** — an interval during which a truck is parked, reconstructed by the engine and classified by why it stopped. The classification is one of seventeen exhaustive categories, including `authorized_waypoint`, `border_crossing`, `weighbridge`, `customs`, `toll_gate`, `fuel_station`, `mandatory_rest`, `breakdown`, `roadblock`, and `clandestine`. Classification is what turns "stationary for three hours" into "three hours at the border." **Tenant** — one trucking business on the platform: its own fleet, drivers, clients, missions, and history, isolated from every other business. Isolation is enforced on every read — each tenant-scoped record carries a tenant identifier and each query filters on it — so no read ever crosses from one business's data into another's. **Tracking link** — a scoped, expiring URL an owner shares so a customer can follow one delivery on the tracking portal (`tracking_links`). Mission, client, and convoy links are gated by the last four digits of the recipient's phone; ad-hoc links ask for the full phone because they have no mission context. A link covers one mission or several, carries a lifetime of 7, 14, or 20 days (capped at 20), and can be extended or revoked at any time — revoking rotates its secret so every gate pass already issued stops working. **Trip** — a continuous driving window: the truck moving from when it sets off until it next parks. Trips carry distance, speed, and idle summaries and the place labels of where they began and ended, and they are the backbone of the vehicle diary — the human-readable story of a vehicle's day, assembled from raw positions. **Waypoint** — a named, geofenced point on the map: a port, a depot, a customs post, a border, a fuel station. Waypoints can nest, such as a port inside a city, and their geofences are what let Korido detect that a truck has arrived somewhere meaningful. They are the nodes from which corridors, missions, and visits are built. **Waypoint visit** — the record of a truck entering and leaving a waypoint's geofence: arrival time, departure time, and dwell (`waypoint_visits`). Waypoint visits are how Korido measures time spent at named places — how long at customs, how long loading at the depot — and how it marks the boundaries between the segments of a journey. ## How it connects * [What is Korido](./what-is-korido) introduces the product, the roles, and the tenant model these terms live inside. * [System at a glance](./system-at-a-glance) places most of these terms in the four data planes and shows the signal that produces them. * Each cluster of terms gets a full chapter: trips, stops, and gaps in [Part 3 — The fleet engine](../03-fleet-engine/); corridors, road segments, deviations, and Route Guard in [Route Guard](../04-road-network/route-guard); missions, rule sets, and segment traversals in [Progression and ETA](../05-missions/progression-and-eta). --- --- url: /02-telemetry.md --- # Part 2 — Telemetry: the life of a signal Part 1 drew the map. This part follows a single tracker message across it: from the radio frame a device assembles in the cab, through the front door that authenticates and normalizes it, to the grade that decides how far to trust it, and finally to the live answer a dispatcher reads — *is this truck driving, parked, GPS-blind, or gone dark?* One idea threads the whole part, and every chapter earns it back: **a silent GPS does not mean a silent truck.** A parked truck stops sending positions on purpose while its tracker keeps proving it is alive, and telling that apart from real failure is the quiet work this part exists to do. ```mermaid flowchart LR T["Tracker frame"] -->|"delivered by Flespi"| I["Ingestion
authenticate · validate · batch"] I -->|"stored as a row"| Q["Position quality
grade the fix"] Q -->|"trusted fixes + heartbeats"| L["Liveness
the live vehicle state"] ``` * **[Trackers and what they report](./trackers-and-signals)** — the device families Korido normalizes into one shape, the two kinds of message (position **fixes** and telemetry-only **status frames**), the signals a frame carries, and the BLE probes that read a fuel tank's level. * **[The ingestion pipeline](./ingestion-pipeline)** — how a message authenticates, is validated field by field, is batched under a hard size budget, and becomes a durable `positions` row — with three archives catching everything that cannot follow the happy path, so no real telemetry is silently lost. * **[Position quality](./position-quality)** — the quality ladder that grades every fix from a trusted reading down to an audit-only reject, and the rule underneath it: only telemetry the engine actually trusts may move a truck's state. * **[Liveness](./liveness)** — the two-clock model (is the *tracker* alive? do we know *where* it is?) and the states it produces: driving, stopped, idle, **"Stationné — dernière position connue"**, **"Sans GPS"**, and offline. ## How it connects * Before: [Part 1 — Korido](../01-korido/) sets up the product, the roles, and the GPS source-of-truth principle this pipeline depends on. * Next: [Part 3 — The fleet engine](../03-fleet-engine/) turns the graded stream of positions into trips, stops, gaps, and fuel events. --- --- url: /02-telemetry/trackers-and-signals.md --- # Trackers And What They Report ## What this chapter covers Every fact Korido knows about a truck begins as a radio message from a hardware tracker bolted into the cab. This chapter introduces the tracker families the platform supports, the two kinds of message they send — position fixes and status frames — and the individual signals those messages carry, including the BLE fuel probes that read a tank's level. It sets up the single most important idea in this part of the book: **a silent GPS does not mean a silent truck.** ## The picture ```mermaid flowchart TD BLE["BLE fuel probe
in the tank"] -. broadcasts level .-> T T["Hardware tracker
wired into the truck"] -->|truck moving| FIX["GPS fix frame
position + telemetry"] T -->|truck parked / no motion| STAT["Status frame
telemetry, no position"] T -->|periodic proof of life| HB["Voltage / info heartbeat"] FIX --> F["Flespi
(device connectivity layer)"] STAT --> F HB --> F F -->|webhook| K["Korido"] ``` The tracker is the **source of truth** for where a truck is. Phone GPS from the driver app is shown to the driver but never sent back to the platform, so nothing downstream ever has to reconcile two competing ideas of a truck's position. ## The device families Korido normalizes several tracker families into one shared shape, so the rest of the platform never has to know which brand of hardware a given truck carries. Three families matter today. | Family | Protocol | Typical units | Parked reporting | Sends device events while parked | | --- | --- | --- | --- | --- | | **JimiIoT VL-series** | JimiIoT | VL863, VL802, VL103 | report-by-motion — periodic status frames at an interval set by device configuration | yes | | **Teltonika FMB** | Teltonika FMB | FMB920, FMB003 (CAN) | roughly **hourly** at rest | no | The JimiIoT VL-series shares one protocol and one configuration across every model, so Korido treats VL863, VL802, and VL103 as a single family. The families differ in the details — a Teltonika reports its battery as a voltage where a JimiIoT reports it as a percentage; a JimiIoT frame carries a high-precision capture timestamp that a Teltonika frame may omit — but Korido collapses those variants at the door. Downstream code reads one canonical set of signals and never branches on hardware brand. The two rhythms matter enormously. A Teltonika that legitimately sleeps for an hour and a JimiIoT whose parked reporting follows its own configured cadence cannot share one flat "you are offline now" rule. Set the threshold short and every sleeping Teltonika trips a false alarm; set it long and a genuinely dead JimiIoT stays invisible for that whole hour. ```mermaid flowchart TD FLAT["One flat
'offline after N minutes' rule"] FLAT -->|"N set short"| A["Sleeping Teltonika
(hourly) → false alarm every rest"] FLAT -->|"N set long"| B["Dead JimiIoT
real outage missed"] EXP["One silence expectation
per device model"] -->|"judged against its own rhythm"| OK["Every family
read correctly"] ``` Korido therefore carries one explicit silence expectation per device — never a learned guess — which [Liveness](./liveness) explains in full. ## Fixes versus status frames — report-by-motion A tracker sends two structurally different kinds of message. * A **GPS fix frame** carries coordinates: latitude, longitude, speed, heading, plus all the telemetry below. It is what draws the truck on the map. * A **status frame** carries the same telemetry but *no coordinates*. It is the tracker saying "here is my state" without a fresh satellite fix. These trackers are **report-by-motion**: while a truck drives, it streams fixes; while it sits parked, it stops computing new fixes and falls back to periodic status frames and voltage heartbeats. This is by design, not a fault. A parked truck that has sent no GPS fix for six hours but is still sending hourly status frames is a perfectly healthy, perfectly located truck — it simply has not moved. ```mermaid sequenceDiagram participant Truck participant Korido Truck->>Korido: GPS fix — driving, 62 km/h Truck->>Korido: GPS fix — arriving, 4 km/h Note over Truck: engine off, parked Truck->>Korido: Status frame — ignition off (no fix) Truck->>Korido: Status frame — ignition off (no fix) Note over Korido: last position is known and unchanged
the truck is confidently parked, not "lost" ``` Holding onto this distinction is what lets Korido tell a parked truck apart from a truck whose GPS has genuinely failed. Both stop producing fixes; only one keeps producing status frames while the engine is off. That difference drives the **"Stationné — dernière position connue"** versus **"Sans GPS"** decision in [Liveness](./liveness). ## The signals a message carries Whether it carries coordinates or not, a frame is dense with operational signal. Korido persists all of it, so a message shape never quietly drops a field. | Signal | What it tells the fleet | | --- | --- | | **Ignition** | engine on or off, from a wired connection on VL863 and Teltonika FMB models | | **Battery level** | the tracker's own backup battery — warns before the device dies | | **External voltage** | the truck's electrical supply feeding the tracker; a clean drop from present to cut while the device keeps transmitting is read as a main-power disconnection — an interference-relevant signal | | **GSM signal level** | cellular reception strength where the truck currently is | | **Alarm line** | the hardware SOS / panic input | | **Engine immobilizer** | the remote engine-block state — whether the truck's starter is cut, and every time it flips | | **Defense / vibration** | factory defense state and tamper/shock detection | | **CAN data** (where wired) | vehicle speed, engine RPM, coolant temperature, engine load, fuel counters, diagnostic trouble codes, and VIN read straight off the truck's own bus | | **BLE fuel** | tank level from a Bluetooth probe (below) | CAN signals appear only when the tracker is physically wired into the truck's data bus — an FMB003 on a CAN harness reports engine RPM and ECU fuel consumption; a tracker with no bus connection simply reports null for those fields, which means "not equipped," never "lost in transit." ## BLE fuel sensors Fuel is measured by a **BLE fuel probe** sitting inside the tank. The probe is a capacitive sensor: the height of liquid around it changes the capacitance it reads, so its raw output is a measure of **fuel height**, not volume. The probe broadcasts that reading over Bluetooth; the tracker overhears it and relays it to Korido inside its normal telemetry stream. Two probe families are in the corridor fleet, told apart by the name the probe broadcasts: * **JimiIoT KF201S** — the probe reports its own calibrated full-scale value on every message, and Korido divides the raw reading by that value to get the height fraction. * **Escort** (broadcast names beginning `TD_`) — the probe's full-scale field is a meaningless placeholder, so Korido substitutes the sensor's known fixed twelve-bit range (0 to 4095) instead. Without that substitution every Escort tank would read as permanently full. From there both families are handled identically. The height fraction is not the same as the fuel fraction, because a truck tank has a rounded-rectangular cross-section — the middle of the tank holds far more litres per centimetre than the top or bottom. Korido therefore passes the height fraction through a **height-to-volume calibration curve** to get a true **volume percentage**, which is the single number the platform reasons about and shows. Once a truck's tank capacity is recorded, that percentage becomes litres for display. The probe also reports its own temperature and battery, and an error-state code. Korido corrects the two values the sensor mislabels on the wire (temperature and battery are reported at the wrong scale and rescaled on the way in), and treats the error code — not the GPS fix — as the authority on whether a reading is trustworthy. ## Heartbeats as proof of life The humblest message a tracker sends is a bare voltage or info **heartbeat**: no position, sometimes almost no telemetry, just the device announcing that it is powered and connected. That is exactly its value. Every message — fix, status frame, or heartbeat — advances the moment Korido last heard from the physical device. That timestamp, the **message clock**, is the foundation of the whole liveness model: it answers "is the tracker alive?" independently of "do we have a fresh position?" [Liveness](./liveness) builds the two-clock model on top of this idea. ## Edge cases * **A parked truck goes quiet on GPS but keeps heartbeating.** This is the normal resting state of a report-by-motion tracker, not a malfunction. The last known position stays valid and the message clock stays fresh. * **The Escort probe reports a nonsense full-scale.** Its full-scale field is a fixed placeholder; Korido ignores it and uses the sensor's known twelve-bit range, otherwise every Escort-equipped truck would read as 100% fuel forever. * **A fuel reading arrives with no GPS fix.** BLE fuel frames — especially from Escort probes, and from any truck sitting in a satellite dead zone — routinely carry a real tank level and no coordinates. The reading is still valid; its validity is decided by the sensor's error code, never by whether a satellite fix happened to accompany it. * **A sensor error code is non-zero.** The tank-level number is suppressed (the gauges and the fuel logic skip it), but the raw reading, temperature, battery, and error code are all still stored as evidence. * **CAN fields are null.** The tracker is not wired to that truck's data bus; the fields mean "not equipped," never "dropped." ## Known limitations The signal set a tracker can offer is bounded by how it is wired and how it is built, and a few facts follow from that: * **Power-cut detection needs a wired voltage line.** The main-power disconnection signal reads the truck's external supply voltage falling from present to cut. A unit that does not report external voltage — because it is not wired to the truck's supply, or its model does not measure it — cannot produce that signal, so a power cut on such a unit is not detectable from voltage alone. Absence of the reading is treated as no information, never as a cut. * **CAN telemetry exists only on a bus-wired truck.** Engine RPM, ECU fuel counters, coolant, and VIN appear only where the tracker is physically joined to the vehicle data bus. On a truck with no bus connection these fields are null — "not equipped," not "lost." * **A parked report-by-motion truck shows its last known position, not a live one.** By design, these trackers stop computing fixes while parked, so no fresh position exists to display. Korido holds the last known fix and marks the truck confidently parked; it does not — and cannot — show live movement for a truck that is reporting none. This is the feature working as intended, and the liveness model is built precisely to tell it apart from real GPS failure. ## How it connects * Next: [The ingestion pipeline](./ingestion-pipeline) — how these messages authenticate, validate, and become durable rows. * Then: [Position quality](./position-quality) — how a fix earns trust. * Then: [Liveness](./liveness) — how fixes and heartbeats produce the driving / parked / Sans GPS / offline states. --- --- url: /02-telemetry/ingestion-pipeline.md --- # The Ingestion Pipeline ## What this chapter covers This chapter follows a tracker message from the moment Flespi delivers it to the moment it becomes a durable `positions` row. Along the way it authenticates, gets validated field by field, is batched into the queue, and is processed by the engine — with three separate archives catching the traffic that cannot follow the happy path. The design goal is that no real telemetry is ever silently lost: a bad message is rejected loudly or set aside for investigation, never dropped on the floor. ## The picture ```mermaid sequenceDiagram box rgb(236,254,255) External source participant Flespi end box rgb(239,246,255) Ingress and buffer participant API as API Worker (ingress) participant Queue as Positions queue end box rgb(255,247,237) Engine authority participant Engine as Engine Worker end box rgb(236,253,245) Durable store participant DB as Database (positions) end Flespi->>API: POST telemetry batch (webhook) API->>API: Authenticate (IP allowlist + shared token) API->>API: Validate each item Note over API: unparseable body → 400 (Flespi retries)
invalid item → invalid archive (batch continues) API->>API: Normalize + bound-guard numerics API->>Queue: Enqueue size-bounded groups Queue->>Engine: Deliver batch Engine->>Engine: Resolve device → vehicle → tenant Note over Engine: unknown device → unregistered archive
registered but no vehicle → heartbeat only Engine->>DB: Write positions (fixes and no-fix rows) Engine-->>Queue: ack / retry per message Note over Queue,Engine: message keeps failing → dead-letter archive ``` ::: info Authority boundary The queue carries normalized telemetry, not tenant authority. The engine resolves the registered device, vehicle, and tenant from current records before any tenant-scoped write. ::: The split is deliberate. The ingress side authenticates, validates, and normalizes transport data. It never decides which tenant owns a message. The engine side resolves the registered device, its vehicle, and its tenant from current database state before it writes anything, so a stale or forged queue payload can never attribute a truck's position to the wrong fleet. ## The behavior ### Authentication Flespi posts each telemetry batch to Korido's ingest endpoint. Two checks gate it: the caller's IP must be on the allowlist, and the request must carry the shared secret token. The client IP is read through the same cross-runtime helper the whole platform uses, so the check behaves identically in local development and in production. ### Validation, field by field Every item in the batch is validated on its own against the telemetry schema. Two failure modes are handled very differently: * **A malformed request body** — JSON that will not even parse — is rejected with a `400`. This tells Flespi the delivery failed, so Flespi retries it. Whole-body rejection is reserved for transport corruption only. * **A single invalid item** inside an otherwise-good batch is isolated: it is written to the **invalid archive** and the batch proceeds. A schema-poison item is a per-item concern, never a reason to reject the whole delivery. ### Out-of-range values are nulled, never dropped A handful of telemetry columns are narrower than any plausible wire value — GPS dilution figures carry sentinel values like 99.99 that mean "no usable reading," and a voltage glitch can briefly exceed a column's range. A value past a column's capacity would fail the database write and roll back the *entire* batch into a retry loop. Korido prevents that at normalization: an out-of-range field drops to null and the row — and every other row in the batch — survives. | Signal | Accepted range | | --- | --- | | External power voltage | 0 … 999.9 V | | GPS dilution (HDOP / PDOP) | 0 … 99.9 (sentinels mean "no reading" → null) | | Sensor battery current | −99.999 … 99.999 (signed: charge vs discharge) | | CAN fuel rate | 0 … 999.99 | | CAN fuel consumed | −99999.99 … 99999.99 | | Altitude, CAN speed / RPM / coolant, event codes | whole-number sensor range | Nulling one field is the correct outcome: it means "this one reading was unusable," and it costs the batch nothing. ### Batching into the queue Valid, normalized rows are grouped into queue messages under a strict byte budget, because the queue has a hard 128 KB per-message ceiling. The grouping serializes each row once and accounts for the JSON scaffolding, so every group provably fits. A single row that is *itself* larger than the budget — a bloated config dump, say — is flagged and sent alone in its own message, so it survives; the queue's own hard limit is the final gate. If a group fails to send after earlier groups already went out, the response reports the true per-group count that made it through. Flespi replays the whole batch, and the duplicate protection below absorbs the resend cleanly. ### Engine processing into `positions` The engine receives queued batches, resolves every unique device against the registered-device records, groups the rows by device, sorts each device's stream by capture time, and runs them through the fleet state machine. The end product is `positions` rows — the durable, append-only record of everything a truck's tracker reported. ### Fixes and status frames share one path A status frame shares the exact same table and pipeline as a fix: it rides the same route and lands as a `positions` row with an empty position. It carries its full telemetry — ignition, battery, GSM, voltage, alarm, fuel — and it advances the device's heartbeat, feeds fuel detection, and drives ignition memory. It simply skips the geometry-dependent steps, because it has no geometry. A no-fix row keeps every signal despite being positionless. ### SIM identity and configuration snapshots On rare configuration frames a tracker includes a full settings dump — its alarm slots, geofences, defense mode, and the SIM's identity numbers. Korido parses that dump once at the door into structured fields, safely bounded so a corrupt or hostile dump degrades to a clipped snapshot rather than blowing the queue budget. The engine then reconciles the SIM identity against the device's known SIM: the first identity observation is recorded, and a later mismatch is a SIM-swap warning — a tamper signal. The full settings snapshot is stored against the device, advancing only when a newer dump arrives. ### Observability The pipeline is instrumented so "are my heartbeats being processed?" is answerable from a dashboard rather than a guess. Counters track status-frame volume per device family, oversized single items, messages from a registered device that has no vehicle link, and SIM-swap mismatches. Batch traces link the queue consumer back to the ingress that produced the message. ## Edge cases * **Unparseable request body.** Returns `400`; Flespi retries the whole delivery. This is the only case where the entire body is rejected — it means the transport itself was corrupt. * **One poison item in a good batch.** Archived to the invalid archive; the rest of the batch is normalized and queued. Archive failure is logged and counted but never blocks valid telemetry. * **A numeric that would overflow its column.** The single field is nulled at normalization; the row and its batch survive. * **The same device reports the same capture instant twice.** The row is stored once. The capture timestamp is kept at microsecond precision, so two genuinely distinct readings inside the same millisecond both survive; a true duplicate is absorbed. On an exact-instant tie, a fix-bearing message wins over a coordinate-less one. * **A single frame larger than the queue budget.** It is sent alone in its own message; the 128 KB queue limit is the last line. * **Partial queue-send failure.** The true per-group queued count is reported, Flespi replays the batch, and dedup absorbs the rows that already landed. * **An unknown device (unregistered IMEI).** The message is written to the **unregistered archive** for investigation and acknowledged, which stops the retry loop. No position is written, because there is no vehicle or tenant to attach it to. * **A registered device with no vehicle link.** Its heartbeat still advances — the device is alive and the connectivity view sees it — and the traffic is counted, but no position is written until it is linked to a vehicle. * **A message that keeps failing engine processing.** After bounded retries it parks in the **dead-letter archive**, where it can be replayed once the underlying cause is fixed. Status frames replay through this path just like fixes. ## Known limitations The pipeline's guarantee is that no real telemetry is silently lost — not that every message arrives exactly once, and not that Korido sees traffic no device sent to Flespi. * **Delivery is at-least-once, so duplicates are structural.** Flespi retries a failed delivery and a partial queue-send is replayed whole, which means the same reading can reach Korido more than once. Duplicate suppression on the capture timestamp is therefore a permanent, load-bearing part of the design, not an occasional safety net. * **Flespi is the single connectivity layer.** Every tracker reaches Korido through Flespi's webhook. The platform reasons only about what Flespi delivers; a device that cannot reach Flespi is a silence Korido detects through liveness, not a message it can recover. * **Archived traffic waits for a human.** An unparseable body, a poison item, an unregistered device, and a message that exhausts its retries are each preserved in an archive — but clearing them back into the record is a deliberate, investigated replay, not an automatic recovery. ## How it connects * Before: [Trackers and what they report](./trackers-and-signals) — the messages entering the pipeline. * Next: [Position quality](./position-quality) — how the engine grades each fix it stores. * Then: [Liveness](./liveness) — how the stored rows become live vehicle state. --- --- url: /02-telemetry/position-quality.md --- # Position Quality ## What this chapter covers A tracker's raw stream arrives noisy: positions jump, clocks drift, parked trucks report phantom crawl speed, and messages stored during a dead zone arrive hours late in a burst. This chapter explains the **quality ladder** — how every stored row is graded, from a clean trusted fix down to an audit-only reject — and why that grade governs everything the engine does next. The rule underneath it all: only telemetry the engine actually trusts is allowed to move the truck's state. ## The picture ```mermaid flowchart TD R["Incoming row"] --> HF{"Has a GPS fix?"} HF -->|no| NF["NO-FIX row
status frame, no coordinates"] HF -->|yes| PF{"Passes pre-filter?
real coords · timestamp within tolerance ·
speed 0–300 km/h"} PF -->|no| FILT["FILTERED
audit only"] PF -->|yes| DUP{"Duplicate or
out-of-order?"} DUP -->|yes| LATE["LATE
audit only"] DUP -->|no| CL{"Classify"} CL -->|clean| ACC["Accepted — trusted"] CL -->|lower confidence| JIT["Jitter — degraded"] CL -->|impossible for state| SPK["Spike
audit only"] ACC --> ST["Advances vehicle state"] JIT --> ST NF --> LV["Advances liveness, ignition, fuel
(skips the map machines)"] ``` ## The behavior Every fix that survives the pipeline is assigned one grade. The grade is what the rest of the platform reads. * **Accepted (trusted).** A clean fix. It renders on the authoritative trail and it can advance every part of vehicle state. * **Jitter (degraded).** An accepted-but-lower-confidence fix. It is kept because stop and trip detection still benefit from it, but it is filtered out of the clean map trail. * **Spike.** A structurally valid message whose position is unusable — a cell-tower fallback location or a jump implying an impossible speed. It is stored for audit and replay but never allowed to change state. * **Filtered.** A fix hard-rejected before classification because it fails a basic physical sanity check. Stored for audit only. * **Late.** A fix that arrived after a newer row had already been processed — a dead-zone backfill or a post-reconnect flush. Kept distinctly so it stays observable and replayable, but excluded from the live trail. * **No-fix.** A status frame with no coordinates — never classified, because there is nothing to grade. Only **accepted** and **jitter** rows advance the vehicle's movement state. Spike, filtered, and late rows are inert: they never open a trip, close a stop, resolve a gap, advance a mission, or raise an alert. ### The pre-filter: basic physical sanity Before a fix is graded in detail, it must clear a coarse filter that rejects the physically impossible: * missing or impossible coordinates, including the null island (0, 0) * a timestamp more than **5 minutes** in the future * a timestamp more than **30 days** in the past * a speed above **300 km/h**, or a negative speed A row that fails becomes **filtered**. It can be stored for the audit trail, but it is barred from touching operational state. ### Classification: earning or losing trust Surviving fixes run a first-match ladder of checks, each of which can pull a fix down from trusted. The checks look for, among other things: * a duplicate or out-of-order capture timestamp * the device's own "this fix is invalid" signal * CAN bus evidence that the truck is stationary while GPS claims speed * movement reported while the ignition is off * **jitter**: a position wandering within a tight window (about 30 m of displacement over a 60-second window) that betrays a stationary truck's GPS drift rather than real travel * an implied speed between fixes above **200 km/h** — physically impossible on the corridor * reported speed that disagrees with the distance actually covered * suspicious null-speed and very-low-speed readings A fix that trips a serious check becomes a **spike**; a fix with milder imperfections becomes **jitter**; a clean fix stays **accepted**. ### Buffered delivery When a truck drives through a cellular dead zone, its tracker stores readings and flushes them in a burst once it reconnects. That burst is real history — but it is old history, and it must never be mistaken for a live signal. Korido detects **buffered delivery** three ways: * the device itself flags the record as buffered — taken as authoritative * the device's clock drifted far enough from the network's that its capture time cannot be trusted as live * the delivery reached Korido more than **5 minutes** after the network first received it, meaning the ingress path itself backlogged A buffered row still runs the full classification and state-machine logic, so the engine can faithfully reconstruct what happened during the dead zone. What it does **not** do is advance the live clocks. This is the crucial guarantee: a delayed flush can prove a truck's past, but it can never make the truck look currently connected. If buffered data advanced the live clocks, a truck that drove into a dead zone and stopped reporting would appear online every time a stale batch finally caught up. ### No-fix rows A status frame persists as a **no-fix row**: a real telemetry record with an empty position and no quality grade — a legitimate record that simply has no coordinates to grade. It advances the truck's heartbeat and ignition memory, feeds fuel detection, and contributes evidence when a signal gap is being resolved, while every map-and-geometry machine steps over it. ### How quality shapes everything downstream The quality grade is the gate to all higher meaning: * Only **trusted movement** opens a trip — and only once the truck has genuinely left a 200 m stationary-noise radius, so a burst of parked GPS jitter opens nothing. * Stops, gaps, distance, mission progress, and Route Guard all read accepted (and, where useful, jitter) rows and ignore the rest. * Spike, filtered, and late rows exist purely as an audit and replay record. * The clean map trail shows only accepted fixes; degraded jitter is kept for detection but held off the authoritative trail. The whole point of the ladder is that one bad GPS point can never rewrite the dashboard, close a stop, advance a mission, or trigger a reroute. ## Edge cases * **A parked truck emits Doppler speed-noise.** A stationary truck can report 5–7 km/h with its position jittering within ~25 m. The jitter check and the 200 m departure radius together reject it, so those seconds of noise open no phantom trip. * **Two readings inside the same millisecond.** The capture timestamp is preserved to microsecond precision, so two genuinely distinct same-millisecond readings both survive the duplicate check instead of one being silently absorbed. * **A dead-zone flush arrives hours late.** It is graded buffered: fully classified so the history is reconstructed, but barred from advancing the live clocks, so it cannot make a long-silent truck look live. * **A device clock jumps into the future.** The future-tolerance limit and a clamp on the live clock stop a single corrupt future timestamp from permanently suppressing that truck's offline detection. * **A cell-tower fallback location.** Reported when GPS is unavailable, it lands as a spike — stored for audit, never allowed to move the truck on the map. ## Known limitations The quality ladder is a filter. It decides how far to trust each fix; it cannot manufacture accuracy the satellite signal never had. * **Consumer GPS error is physical, and the ladder mitigates rather than eliminates it.** Jitter around a parked truck and the occasional wild spike are inherent to how a low-cost GPS receiver works. The checks catch the cases that break a physical rule — impossible speed, drift within a tight radius, a disagreement with CAN — but a wrong position that happens to violate no rule can still be graded accepted. The ladder makes a bad point rare and inert, not impossible. * **A cell-tower fallback is rejected, so a GPS-starved truck shows no fresh position.** When a device cannot get a satellite fix it may report a coarse tower-based location; the ladder grades that a spike and holds it off the map. The trade is deliberate — a truck in a long GPS-dead stretch is shown at its last trusted position rather than jumped to an unreliable one — but it means no live position is drawn for that window. ## How it connects * Before: [The ingestion pipeline](./ingestion-pipeline) — how rows reach the grader. * Next: [Liveness](./liveness) — how graded fixes and heartbeats become the live driving / parked / Sans GPS / offline states. * Trips, stops, and signal gaps — the structures built from accepted rows — are covered in [Part 3 — The fleet engine](../03-fleet-engine/). --- --- url: /02-telemetry/liveness.md --- # Liveness: Alive, Located, Parked, Or Dark ## What this chapter covers "Is this truck online?" is the wrong question, because it hides two different facts: whether the *tracker* is still talking, and whether we still have a *fresh position*. This chapter explains the **two-clock model** that keeps those facts apart, and the states it produces — driving, stopped, idle, **"Stationné — dernière position connue"**, **"Sans GPS"**, and offline. It ends on the single decision that trips people up most: a parked truck that has gone quiet on GPS is normal, and telling that apart from real GPS failure is the whole job of this layer. ## The picture — two clocks ```mermaid flowchart LR M["Any message
(fix · status frame · heartbeat)"] --> MC["Message clock
last heartbeat"] F["Accepted, live GPS fix"] --> LC["Location clock
last online fix"] MC --> Q1{"Is the tracker alive?"} LC --> Q2{"Do we know where it is?"} class M,F source class MC,LC store class Q1,Q2 warn classDef source fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef store fill:#ecfdf5,stroke:#047857,color:#064e3b classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 ``` * The **message clock** is the last time *any* message arrived from the tracker — a fix, a status frame, or a bare heartbeat. It answers **"is the device alive?"** It drives the device-offline alert. * The **location clock** is the last time an *accepted, live* GPS fix arrived. It answers **"do we know where the truck is?"** It drives signal-gap detection. A status frame advances the message clock but not the location clock. That single asymmetry is why a report-by-motion truck can be provably alive and provably un-moved at the same time — and it is what makes the distinction between parked and **"Sans GPS"** possible. A buffered dead-zone flush is the opposite case: it advances **neither** clock, so a stale batch catching up can neither fake live contact nor close a real gap. ## The behavior — the states Korido resolves one activity state per truck from its open structural records and its two clocks, in strict priority order. ```mermaid flowchart TD A{"Signal gap open?"} -->|no| B{"Open trip?"} A -->|yes| G{"GPS-denied-while-alive
AND confidently parked?"} G -->|no| OFF{"Ignition ON
and GPS-denied?"} OFF -->|yes| SANS["Sans GPS (amber)"] OFF -->|no| OFFLINE["Offline"] G -->|yes| GS{"Open stop?"} GS -->|yes| STOP["Stopped"] GS -->|no| PARKD["Stationné — dernière position connue
(amber: position may be stale)"] B -->|yes| DRIVE["Driving"] B -->|no| C{"Open stop?"} C -->|yes| STOP C -->|no| P{"Confidently parked?
heartbeat < 2 h · ignition off · known position"} P -->|yes| PARKC["Stationné — dernière position connue"] P -->|no| IDLE["Idle"] class DRIVE,STOP,PARKC safe class SANS,PARKD,IDLE warn class OFFLINE risk classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d ``` ::: tip Current behavior A parked report-by-motion truck is allowed to hold an old map position while its heartbeat and telemetry stay fresh. The state turns amber only when the location clock is stale in a way the engine cannot explain as ordinary parked sleep. ::: * **Driving.** An open trip. The truck is moving on an accepted-fix trail. * **Stopped.** An open stop — the truck has halted somewhere the engine is tracking. At an accountable place (a port, border, checkpoint, warehouse, or depot) this reads as "at checkpoint." * **"Stationné — dernière position connue".** The report-by-motion resting state. The heartbeat is fresh, the ignition is off, and the last known position is held. A report-by-motion tracker sends no fixes while parked, so a missing fix here is *normal* — the truck is simply parked where we last saw it. Rendered in the calm default tone: no badge, no alarm. * **"Sans GPS".** The heartbeat is fresh, the ignition is **on**, and there is still no fresh fix. A running truck that cannot get a GPS position means genuine GPS failure or jamming. Rendered amber — this one *is* a problem. * **Idle.** Heartbeat present but none of the confident-parked conditions met — a fallback resting state, marked inactive once silence passes the configured threshold. * **Offline.** A signal gap is open and the confident-parked test fails. Location observability is genuinely lost. ### The confident-parked test A truck is **confidently parked** only when all three hold: 1. the heartbeat is fresh — a message within the last **2 hours** 2. the ignition reads **off** (taken from the freshest telemetry, which status frames keep current even with no fix) 3. there is a **known last position** to show The 2-hour bound is twice the roughly-hourly cadence of a parked tracker: one missed heartbeat is expected, but after two the truck has been quiet too long for "confidently parked" and it falls back to plain idle or offline. ### Where "Sans GPS" versus parked is decided The two look identical at first glance — both are alive, both lack a fresh fix. The ignition is the tiebreaker. Ignition **off** plus fresh heartbeat is a parked truck resting: **"Stationné — dernière position connue"**. Ignition **on** plus fresh heartbeat plus no fresh fix is a working truck that has lost GPS: **"Sans GPS"**. Reserving the amber Sans GPS state for the ignition-on case is what stops every overnight-parked truck from lighting up as a fault. ### When GPS dies but the truck is parked A signal gap can open on a confidently parked truck — the engine notices the location clock has gone stale. If that gap is specifically a **GPS-denied-while-alive** gap and the truck is confidently parked, an open stop still wins as the more specific truth; otherwise the truck shows as **"Stationné — dernière position connue"** in the amber degraded tone, carrying a quiet "position may be stale" signal rather than a false alarm. This exception resolves entirely inside the gap branch and never falls through to the driving branch. That matters: a truck whose GPS dies mid-trip could otherwise keep an open trip and render a confident "driving" forever. Resolving the GPS-denied gap up front prevents that. ### The device-offline alert Separately from the display states, the message clock drives a hardware device-offline alert. Because a sleepy Teltonika and a chatty JimiIoT stay silent for wildly different windows, each device carries one explicit **silence expectation**, resolved from a single chain: 1. a per-device override set from the admin portal — a plain minutes value, between one minute and 24 hours — if present, otherwise 2. the device model's known sleep cadence (roughly hourly for Teltonika FMB; for the JimiIoT VL-series, the interval set by the device's own configuration), otherwise 3. the tenant's default The per-device override sits at the top of that chain: when Korido staff set an expected-silence value for a unit, it overrides the model cadence and the tenant default outright. It is the direct lever for a tracker whose real rhythm differs from its model's typical cadence. The truck counts as offline only once it has been silent past that expectation **plus a 10-minute grace beat** — one missed heartbeat is within cadence; the second is worth alerting on. The customer tracking portal and the owner alert read the same boundary, so a customer never sees a live dot on a truck the owner already knows is dark. ## Edge cases * **The central case — a parked report-by-motion truck.** Fresh heartbeat, ignition off, no new fix for hours. This is **"Stationné — dernière position connue"** in the calm tone, never **"Sans GPS."** The absence of fixes is the tracker working as designed. * **Genuine GPS failure or jamming.** Fresh heartbeat, ignition on, no fresh fix. This is **"Sans GPS"** in amber — a real fault worth surfacing. * **GPS dies mid-trip.** The GPS-denied gap resolves inside the gap branch and never falls through to "driving," so a GPS-dark truck cannot show a confident driving state indefinitely. * **Heartbeat older than 2 hours.** Confident-parked is lost; the truck falls back to plain idle or, if a gap has opened, offline. * **Sleepy device versus chatty device.** A flat offline threshold would spam false alerts on the hourly Teltonika and hide real outages on the minute-cadence JimiIoT. The per-device silence expectation resolves both correctly. * **A buffered flush after a dead zone.** It advances **neither** clock — its timestamps rebuild the truck's past without ever counting as live contact — so it cannot fake a live position or close a real signal gap. ## Known limitations The two-clock model draws sharp lines, and the sharpness comes from a couple of fixed choices worth naming. * **The calm parked window is a fixed two hours, separate from the offline alert.** A truck counts as confidently parked only while its last heartbeat is within two hours — a flat bound, not the per-device silence expectation that governs the offline alert. A tracker deliberately configured to sleep longer than two hours between parked heartbeats therefore slips out of the calm **"Stationné — dernière position connue"** state into plain idle before its offline expectation has elapsed. The two boundaries answer different questions and move independently. * **The parked-versus-"Sans GPS" split needs a trusted ignition-off reading.** The whole distinction rests on the ignition line: ignition off means resting, ignition on with no fix means a fault. A tracker that offers no ignition signal can produce neither verdict — it never enters the confident-parked state and never raises **"Sans GPS."** The clean split is available only on units wired to read ignition. ## How it connects * Before: [Position quality](./position-quality) — which fixes are trusted enough to advance the location clock. * Before: [Trackers and what they report](./trackers-and-signals) — why status frames keep the message clock fresh while a truck is parked. * Signal gaps, and how the engine opens and resolves them, are built in [Part 3 — The fleet engine](../03-fleet-engine/). The read models that serve these states to every screen are covered in [Part 8 — The platform](../08-platform/). --- --- url: /03-fleet-engine.md --- # Part 3 — The fleet engine: turning signals into meaning Part 2 delivered a clean, quality-graded stream of tracker positions into the database. This part is where those positions stop being dots on a map and become an operating diary: *this truck is driving; it stopped at the border for six hours; it lost signal in a dead zone north of Garoua and came back forty kilometres later; forty litres disappeared while it was parked overnight.* The fleet engine is a set of small, cooperating state machines. Each one owns a single question and remembers its answer between batches of telemetry: * **[Vehicle state](./vehicle-state)** — what is this truck doing right now, and what does the engine need to remember to keep answering that as new signals arrive. * **[Trips](./trips)** — the driving windows: what opens one, what keeps it open, and every way it closes. This is where the engine works hardest to tell a real departure from GPS noise. * **[Stops](./stops)** — when a truck goes still, how long it has to stay still before that counts, and how the engine labels *why* it stopped: border, customs, weighbridge, rest, breakdown. * **[Signal gaps](./gaps)** — the silence story. When a truck goes dark, what the engine gathers while it waits, and how it decides what happened once the truck comes back. * **[Fuel](./fuel)** — from a capacitive probe reading a liquid height, through a calibration curve, to a fuel-loss alert with a location and a confidence score. A single rule underlies all of them: **the engine processes one vehicle's signals strictly in order, one batch at a time.** Everything in this part depends on that guarantee. It is the first thing the next chapter explains. --- --- url: /03-fleet-engine/vehicle-state.md --- # Vehicle state: what the engine remembers ## What this chapter covers Every other chapter in this part builds on one idea: the engine keeps a small, durable **memory** for each vehicle and updates it one signal at a time. This chapter describes that memory — the anchors, clocks, and flags the engine carries from batch to batch — the activity states a truck moves through, and why the whole design depends on processing one vehicle's signals strictly in order. ## The picture The engine's core question is simple: *is this truck moving, standing still, or have we lost it?* Internally it holds three activity states — **moving**, **stationary**, and **unknown** (the state a freshly-seen truck starts in) — plus overlays for an open trip, an open stop, or an open signal gap. The product surfaces project those into the five states a person actually reads on the dashboard. ```mermaid stateDiagram-v2 [*] --> Idle: first signal, engine on [*] --> Parked: first signal, ignition off Driving --> Stopped: stationary confirmed (3 min still) Stopped --> Driving: departure confirmed (real movement) Idle --> Driving: departure confirmed Driving --> Idle: engine on, not yet moving Stopped --> Parked: ignition off, heartbeat still fresh Parked --> Driving: departure confirmed Parked --> Stopped: ignition on, still standing Driving --> Offline: signal lost while moving (10 min silent) Stopped --> Offline: signal lost (30 min silent) Idle --> Offline: signal lost Offline --> Driving: returns, was moving through the gap Offline --> Stopped: returns, was parked through the gap Offline --> Parked: returns to a known spot, ignition off note right of Parked Report-by-motion trucks send no fixes while parked. A missing fix here is NORMAL, not a fault. end note ``` Two of these five deserve their own chapters — **[Driving](./trips)** is a trip, and **[Offline](./gaps)** is a signal gap. **Stopped** and its labelling get **[their own chapter too](./stops)**. This chapter is about the memory that makes all the transitions possible. ## The behavior ### One vehicle, one order, one writer The engine's most important guarantee is that a single vehicle's signals are processed **serially, in device-timestamp order, by exactly one writer at a time.** When a batch of a truck's positions arrives, the engine locks that truck's state row, replays the batch against it in chronological order, writes the results, and only then releases the lock. No two batches for the same truck run at once. This is not a performance choice — it is a correctness requirement. The state machines in this part are written the way a person reads a trace: *given where the truck was and what it was doing, this next fix means X.* That reasoning only holds if fixes arrive in order and nothing else mutates the truck's state underneath. Two batches racing would let one open a trip while the other opens a stop, and the database guards that allow only one open trip, one open stop, and one open gap per vehicle would reject the second writer outright. Serial processing is what keeps the diary coherent. The consequence for the reader: everything below is a story about **one truck's state being advanced one fix at a time.** Where a decision needs the truck's history, that history is already in the state memory — the engine never has to go re-read the raw trail mid-batch. ### The memory the engine carries Between batches, each vehicle's state is stored as a compact record. The families that matter for this part: * **Activity** — `moving`, `stationary`, or `unknown`, plus the open trip, open stop, and active crawl window if any. * **Anchors** — the reference positions the engine measures against. The **last usable fix** anchors local motion reasoning; the **last trusted fix** is a stricter anchor used for structural distance and Route Guard; the **stationary reference** is the spot a standing truck is measured against to detect a real departure. A **jitter anchor** tracks whether the truck has actually moved across a sliding 60-second window. * **Liveness clocks** — several distinct timestamps, never collapsed into one. *Last telemetry* is the last time any message arrived; *last online fix* is the last usable GPS fix and is the clock the gap detector watches; the vehicle row also carries the timestamp of the position currently shown on the map. These differ on purpose: a truck can send a heartbeat (advancing last telemetry) without a GPS fix (leaving the online-fix clock frozen), which is exactly the signature of a truck alive but GPS-denied. * **Ignition memory** — the last known ignition state and the moment it last went off, held with a debounce (see below). * **Gap and deferral markers** — an open gap's details and, when the truck reappears somewhere improbable, a pending-relocation marker. * **Fuel detector state** — the reference level and open fuel episode carried forward so drain and fill detection spans batches. Two of these are *derived* rather than stored twice: a vehicle is **online** exactly when it has no open gap, and its **last known position** is the pending relocation candidate if one is being confirmed, otherwise the last usable fix. The stored state is the authority for the running state machines. Alongside it, the vehicle row keeps a set of denormalised columns — current position, ignition, battery, last-message time — so the live map and dashboards read a single fast row instead of scanning the position history. Those columns are rewritten at the end of every successful batch. ### Ignition memory without GPS A tracker reports far more than location. Even parked in a basement with no sky view, a wired-to-ignition tracker keeps sending **status frames**: ignition, battery, power, alarms. The engine updates its ignition memory from *every* row, fix-bearing or not, so it always knows whether the engine is running even when it cannot see where the truck is. Ignition memory is guarded two ways so a glitch cannot mislead it: * **Flicker guard.** An "ignition off" reading is discarded as a wiring or CAN glitch when the same frame shows real motion — CAN vehicle speed above 5 km/h, or GPS speed above 30 km/h. A truck at highway speed is still running. * **Debounce.** The engine records the moment ignition first went off and only *trusts* the off-state once it has held for at least 60 seconds. Several behaviours — treating a parked wired-ignition truck's silence as normal sleep rather than a fault, for instance — key on this debounced off-state, never on a single raw sample. Ignition memory being GPS-independent is what lets a parked truck be understood as parked. It is the difference between **"Stationné — dernière position connue"** (a calm, expected parked state) and **"Sans GPS"** (a real GPS failure), a distinction the [gaps chapter](./gaps) turns into what the operator sees. ### Wired versus unknown ignition Not every tracker is wired to the ignition line. The engine learns a device's capability from its model: the first time a wired-ignition model reports a real ignition reading, the engine promotes that vehicle's ignition mode to **wired** and unlocks the ignition-aware behaviours — treating ignition-off as authoritative over noisy GPS speed, corroborating movement with ignition, and suppressing false gaps on a truck sleeping overnight. Promotion is one-way: a vehicle never drops back to unknown, so a single model-less batch cannot erase what the engine learned. Trucks whose trackers offer no ignition signal simply never reach those branches and are reasoned about from GPS and CAN alone. ### How the five display states are derived The engine's three internal activity states, combined with the overlays and the liveness clocks, project to the five states a person reads: | Display state | When | |---|---| | **Driving** | An open trip; activity is moving. | | **Stopped** | An open, confirmed stop (still for at least 3 minutes). | | **Idle** | Engine running, not yet moving — the between-states position. | | **Parked** — *"Stationné — dernière position connue"* | Heartbeat fresh (within 2 hours), ignition off, and a known last position. A report-by-motion truck sends no fixes while parked, so a missing fix here is normal. | | **Offline** | An open signal gap that is not a confidently-parked truck. Shown as **"Sans GPS"** when the truck is alive and its engine is on but no fix arrives. | The parked distinction is what stops a correctly-parked overnight truck from lighting up as a fault. It requires a fresh heartbeat (within 2 hours — twice the once-an-hour parked reporting cadence), a trusted ignition-off reading, and a last known position to show. ## Edge cases * **A heartbeat with no fix.** A status frame (ignition, battery, no coordinates) advances the last-telemetry clock and the ignition memory but **not** the online-fix clock. The dashboard learns a message arrived — the truck is alive — without pretending the position is fresh. This is what keeps a GPS-denied window visible instead of being masked by an otherwise-healthy tracker. * **A future-dated timestamp.** A tracker with a skewed clock can stamp a frame in the future. The liveness clock is clamped to now-plus-tolerance when it advances, so one corrupt timestamp cannot push the clock hours ahead and permanently suppress the truck's offline alert. * **An ignition-off flicker at speed.** A lone "ignition off" sample sandwiched between running samples at highway speed is rejected by the flicker guard and never disturbs ignition memory, so it cannot trip a false parked or sleep state. * **A model-less batch.** If a batch arrives before the device model is known, the already-learned wired ignition mode is preserved — the engine never forgets a capability it has confirmed. * **A replayed old batch.** Buffered signals that arrive late reconstruct history (they can build a past trip or stop) but are held back from the two liveness clocks, so a dead-zone flush never makes a truck look live right now. The [trips](./trips) and [gaps](./gaps) chapters cover how that reconstruction works. ## Known limitations The memory model is what makes the engine's reasoning trustworthy, and two of its guarantees are also its boundaries. * **One vehicle is advanced by a single writer, strictly in order.** This is a correctness requirement, not a tuning knob: the state machines read like a person reading a trace, and that only holds if a truck's fixes arrive in sequence with nothing else mutating its state. The consequence is that a single vehicle's backlog is worked through sequentially — the engine parallelises *across* trucks, never *within* one. A truck that dumps a long buffered flush is reconstructed in order, one fix at a time. * **The ignition-aware behaviours depend on knowing the device model.** Treating ignition-off as authoritative, corroborating movement with ignition, and suppressing a sleeping truck's false gap all unlock only once the engine has learned a vehicle's tracker is wired for ignition. A truck whose model is never identified is reasoned about from GPS and CAN alone and never reaches those branches. Promotion is also one-way — a vehicle confirmed as wired never drops back — so a model correction that should *narrow* a capability is not something the running memory reverses on its own. ## How it connects * Position quality, the liveness clocks, and the parked / **"Sans GPS"** / offline semantics originate in [Part 2 — Telemetry](../02-telemetry/). * The **Driving** state is a trip: [Trips](./trips). * The **Stopped** state and its labelling: [Stops](./stops). * The **Offline** state is a signal gap: [Signal gaps](./gaps). * Fuel detector state, also carried in this memory across batches: [Fuel](./fuel). * Defined terms (anchor, liveness clock, report-by-motion, crawl) recur across the book: [vocabulary](../01-korido/vocabulary). --- --- url: /03-fleet-engine/trips.md --- # Trips: a driving window's life ## What this chapter covers A **trip** is a single driving window — the truck left, drove, and came to rest. It is the atom the vehicle diary is built from, and it is where the engine works hardest, because the hardest question in the whole system is deceptively simple: *did this truck actually start moving, or is a parked tracker just twitching?* This chapter walks a trip from the evidence that opens it, through what keeps it alive, to every way it closes — and the edge cases that make each of those hard. ## The picture A trip opens only on trusted motion evidence, extends while the truck keeps moving (including a slow **crawl** state for queues and checkpoints), and closes on one of several causes. ```mermaid stateDiagram-v2 Standing --> Standing: movement signal (not yet 2, or inside 200 m) Standing --> Driving: departure confirmed Driving --> Crawling: slow inching (≤ 5 km/h) Crawling --> Driving: strong movement resumes Driving --> Standing: stop confirmed / ignition off + still Crawling --> Standing: stop confirmed once the crawl lapses Driving --> Standing: gap resolved — truck was parked / relocated Driving --> Driving: gap resolved — truck drove through Driving --> Standing: staleness safety net (open > 24 h) note right of Driving started_at is backdated to the first movement signal — the recorded start is when motion truly began, not when the engine confirmed it. end note ``` ## The behavior ### The building block: a movement signal Before a trip can open, the engine needs evidence the truck is really moving, not just reporting noise. A parked truck's GPS can report a phantom 5–7 km/h from Doppler drift while its coordinates jitter within a few metres. Trusting the speedometer alone would open a trip every time that happens. So a **movement signal** requires *two independent things to agree*: 1. The tracker's reported speed is above 5 km/h, **and** 2. the *implied* speed — the actual ground distance between this fix and the last one, divided by the time between them — is above 2 km/h. Reported speed says the wheels are turning; implied speed says the truck is somewhere new. A jittering parked truck satisfies the first and fails the second: its speedometer reads 6 km/h but it hasn't gone anywhere, so no movement signal is produced. Only a fix the quality pipeline already rated **trusted** is eligible; a degraded fix never generates a movement signal. This is the multiple-signal rule, and it is the source of the engine's resistance to phantom trips. ### What opens a trip A standing truck (activity stationary or the fresh **unknown** state) is evaluated for departure on every fix. A trip opens by one of three paths: ```mermaid flowchart TD fix[New fix on a standing truck] --> ms{Movement signal?} ms -->|yes| count[Count it; remember the first as the
departure moment] ms -->|no, but ignition-corroborated| count ms -->|no, slow trusted progress| crawl[Count a crawl-progress fix] ms -->|no, ordinary trusted fix| reset[Reset the candidate] count --> gate{≥ 2 movement signals
AND moved past 200 m?} crawl --> cgate{≥ 2 crawl fixes
AND moved ≥ 300 m
within the crawl window?} gate -->|yes| open[Open trip] cgate -->|yes| open gate -->|no| wait[Keep waiting] cgate -->|no| wait ``` * **The ordinary departure.** Two movement signals accumulate *and* the truck has left a 200-metre **stationary-noise radius** around where it was standing. The radius is the guard that in-place jitter can never clear: a genuine departure crosses it within a fix or two at corridor reporting cadence, while Doppler noise stays inside it forever. A truck reporting motion but never leaving the radius opens no trip at all. * **Ignition corroboration.** On a wired-ignition truck, once a departure is already pending, an ignition-on fix moving faster than a low threshold counts toward the two signals even if that single fix is imperfect — the engine running plus some speed is corroborating evidence the truck is underway. * **The crawl departure.** Some journeys begin at walking pace — edging out of a packed yard or a border queue. Two trusted slow-progress fixes that together carry the truck at least 300 metres within the crawl window open a trip directly into a crawl state, without ever producing a fast movement signal. ### Backdating: the recorded start is when motion began There is a subtlety worth calling out because it shows up everywhere downstream. The engine cannot *confirm* a departure until the truck clears the 200-metre radius — which happens a fix or two after it actually pulled away. If the trip's start time were the confirmation fix, every trip would begin 200 metres and a minute late. Instead, the engine remembers the **first** movement signal as the departure moment and backdates the trip's start to it. Only *confirmation* waits for the radius; the recorded start reflects when the truck truly began moving. Distance, duration, and the diary all read the honest start. ### What extends a trip, and the crawl state While a trip is open, each trusted fix accumulates distance, moving time, and top speed. The interesting part is slow movement. A truck in a border queue or a congested urban approach moves a little, stops a little, moves again — for hours. Treating each pause as the end of a trip and each nudge as a new one would shred one crossing into dozens of fragments. The **crawl** state absorbs this. When a moving truck drops to inching speed (at or below 5 km/h, or reporting null speed), the engine opens a crawl window and tracks the **frontier** — the furthest it has progressed from the crawl's start. Each real step forward of at least 30 metres advances the frontier. A crawl becomes **proven** once the truck has covered at least 100 metres across at least two frontier advances, and while a proven crawl is recent (its last progress within the 10-minute pause grace), it **suppresses stop detection** — a truck genuinely creeping forward is not "stopped" just because it paused for a light. A strong movement signal closes the crawl and resumes ordinary driving. ### How a trip closes A trip has one open lifetime and exactly one close cause: * **Arrival at a stop.** The truck goes still and stays still long enough for a stop to confirm (3 minutes — see [Stops](./stops)). The stop opens and the trip closes, timed to the moment the truck came to rest. * **Ignition off while stationary.** On a wired-ignition truck, an ignition-off reading is treated as speed zero regardless of what GPS drift reports. That makes the truck a stationary candidate, and once it holds, the stop confirms and the trip closes. A CAN speed above 5 km/h vetoes this — an ignition-off reading contradicted by the vehicle bus is read as a flicker. * **Gap resolution.** If the truck goes dark mid-trip and the engine later decides it was **parked** or **relocated** during the silence, the trip closes at the moment the gap began. If instead it decides the truck **drove through** the gap, the trip continues unbroken. The [gaps chapter](./gaps) owns that decision. * **The staleness safety net.** A trip that somehow stays open longer than 24 hours — a truck that vanished without a clean resolution — is closed by a background sweep so the diary never carries an eternally-open driving window. ### Merged artifacts: one crossing, not eighteen fragments The 200-metre radius stops most border-queue noise from ever opening a trip. But occasionally a queue *does* creep across the radius before halting again, opening a real — but degenerate — micro-trip that dies at the next halt. Repeated for six hours, a single customs crossing could fragment into a run of tiny trips and stops. The engine catches this at close time. When a just-opened trip is degenerate — under 300 metres and under two minutes of driving — and it halts within 45 minutes and 200 metres of the stop it just left, the two fragments are recognised as **one continuing stop**. The stray trip row is closed but stamped a **merged artifact**: it stays in the record for audit, but the diary and the per-segment statistics exclude it, and no "trip completed" milestone fires. From the operator's side, the six-hour crossing reads as one long stop, which is what it was. Because the engine processes one truck's batch serially, this merge happens in the same transaction as the fragment it folds — there is no race. ## Edge cases * **Same-second fixes.** Two fixes can share a device timestamp to the microsecond — a fix-bearing frame and a status frame, or a duplicate upload. On an exact tie the fix-bearing frame is ordered first so it wins the database's once-only insert. Any later frame at or before the last processed timestamp is flagged **late** and skipped from structural state, so a duplicate can never open a second trip or re-close an open one. * **Buffered-data floods arriving late.** A truck leaving a dead zone can dump hours of stored fixes at once. Those buffered fixes still run the full departure and crawl logic, so the engine can *reconstruct* a trip that happened during the outage — but they are held back from the liveness clocks, so a late flush never makes the truck look like it is moving right now. History is rebuilt; the present is not faked. * **A gap opening mid-trip.** When silence opens a gap while a trip is running, the engine freezes the fact that the truck *was* driving, closes any active crawl, and waits. On return it uses that frozen activity plus the distance and timing across the gap to decide whether the trip continues or closes — a short reconnect with real displacement on a truck that was moving reads as drove-through and keeps the trip alive. * **Jitter near a geofence.** A truck parked just inside a waypoint's edge can jitter across the boundary. Because a trip needs real displacement past the 200-metre radius — not just a boundary crossing — the jitter never opens a phantom trip, and the low-speed-near-anchor fixes are rated degraded, keeping them out of the movement-signal count entirely. * **A departure that never completes.** A pending departure that collects a signal or two and then goes quiet — the truck rocked but never left — expires after 15 minutes with no crawl progress, and the candidate resets. A truck standing in one place for 30 minutes resets its stationary reference outright, so stale half-departures never accumulate into a false start. ## Known limitations Resistance to phantom trips is the engine's proudest property here, and it is bought with a deliberate trade. * **A real move shorter than the departure radius is not recorded as a trip.** The 200-metre stationary-noise radius is what makes in-place jitter incapable of opening a trip — and the same rule means a genuine short reposition that never clears the radius before halting again produces no trip. The engine chooses to under-count tiny moves rather than invent trips from noise; on a long-haul corridor that trade is nearly always right, but it is a trade. * **Reconstructing a trip taken in the dark depends on the tracker having buffered.** When a truck drives through a dead zone, the engine can rebuild the trip only from the fixes the device stored and replayed on reconnect. A tracker that buffered nothing leaves a silence the gap logic classifies by inference, not a trip rebuilt from real positions. * **Crawl and stop share a fuzzy border.** The crawl state absorbs stop-start creep through a queue so a crossing does not shred into fragments, and the merge rule folds the fragments that slip through back into one stop. Both are heuristics tuned to the corridor's queues; an unusual creep pattern can still sit right on the line between "still crawling" and "stopped." ## How it connects * The trusted/degraded quality flags and the movement thresholds a trip depends on come from [Part 2 — Telemetry](../02-telemetry/) and the [vehicle-state memory](./vehicle-state). * A confirmed stop is what most often closes a trip: [Stops](./stops). * A mid-trip silence and its resolution: [Signal gaps](./gaps). * Trips roll up into mission distance and become measurable driving segments in [Part 5 — Missions](../05-missions/); per-trip fuel accounting is in [Fuel](./fuel). --- --- url: /03-fleet-engine/stops.md --- # Stops: when standing still becomes meaningful ## What this chapter covers When a truck goes still, three questions follow: *is this a real stop or a momentary pause? where is it? and why did it stop?* This chapter covers how the engine turns a stationary truck into a confirmed **stop**, how it enriches that stop with idle time and a place, how it infers the reason — border, customs, weighbridge, rest, breakdown — and how it decides whether the stop is authorized, raising an alert when it is not. ## The picture ```mermaid flowchart TD fix[Fix on a moving/unknown truck] --> mv{Movement signal?} mv -->|yes| notstill[Not stationary — reset] mv -->|no| crawl{Proven, recent crawl?} crawl -->|yes| notstill crawl -->|no| cand{Stationary candidate?
speed ≤ 5 km/h, or ignition off,
or null speed timed out} cand -->|no| notstill cand -->|yes| dur{Still for ≥ 3 minutes?} dur -->|not yet| hold[Keep the stationary clock running] dur -->|yes| open[Open stop, backdated to when it went still
· close the trip if one was open] ``` ## The behavior ### From stationary to a confirmed stop Stop detection runs on every usable fix while the truck is moving or in the fresh unknown state. The engine asks, in order: * **Is there a movement signal?** If the truck is really moving (the [two-part rule](./trips#the-building-block-a-movement-signal)), it is not a stop candidate — the stationary clock resets. * **Is a crawl suppressing the stop?** A truck proven to be creeping forward (see [Trips](./trips#what-extends-a-trip-and-the-crawl-state)) is not stopped just because it paused; a recent proven crawl blocks the stop. * **Is it a stationary candidate?** The truck qualifies when its effective speed is at or below 5 km/h. A wired-ignition-off reading counts as speed zero outright (unless CAN speed contradicts it, marking a flicker). A fix with no speed reading and ignition off is stationary; a fix with no speed but ignition *on* starts a 5-minute timer, and only if the null-speed condition persists past it does the truck become stationary — a running engine gets the benefit of the doubt for a while. * **Has it lasted?** A candidate only becomes a **stop** after it has held continuously for **3 minutes**. Like a trip's start, the stop is backdated to the moment the truck first went still — ahead of the moment the engine confirmed it. When the stop confirms during an open trip, the trip closes at the same instant the stop opens. There is exactly one open stop per vehicle at a time. One geometry guard is worth noting: a trusted fix reporting no speed but sitting well outside the noise radius from the last trusted position is treated as *motion*, not a stop — the truck clearly moved even though the speedometer went quiet. ### Enrichment: idle, place, and aggregates A bare stop is a start time and a coordinate. At close, the engine enriches it in a single step: the **idle time** within the stop (engine-on but not moving, derived from engine RPM, voltage, and ignition), the **nearest waypoint** and whether the truck sat inside its footprint, the reverse-geocoded **place label**, and speed and idle aggregates. This enrichment is isolated so that if the place lookup or an aggregate fails, only the enrichment rolls back — the stop itself stays closed and correct. The place label arrives asynchronously. A background reconciliation pass fills in labels the geocoder could not resolve at close time, so a stop that closes in a poorly-mapped area still gains a human-readable place once the label resolves. ### Classifying the reason A closed stop is sorted into one canonical category. Classification is **deterministic** — the same stop always classifies the same way, so a backfill reproduces live results exactly — and it works from most specific evidence to least: ```mermaid flowchart TD inside{Inside an operator-curated
waypoint?} -->|yes| bytype[Use the waypoint type:
border → border_crossing
customs → customs
weighbridge → weighbridge
fuel_station → fuel_station …] inside -->|no| place{Geocoded place
metadata usable?} place -->|yes| byplace[Fuel pump, customs post,
restaurant, rest area …] place -->|no| fuel{Refuel event
during the stop?} fuel -->|yes, short| station[fuel_station] fuel -->|no| dur[Duration + time-of-day rules] dur --> rest[mandatory_rest · overnight_rest ·
meal_break · breakdown · traffic · unknown] ``` * **A confirmed waypoint wins.** If the truck sat inside an operator-curated waypoint, its type sets the class directly: a border waypoint gives **border-crossing**, a customs post gives **customs**, a weighbridge gives **weighbridge**, a fuel station gives **fuel-station**, and operational sites (warehouse, depot, port, checkpoint) give a generic authorized-waypoint. A toll gate counts only for a short pass-through (5 minutes or less); a long dwell there falls through to the duration rules. A fuel station is capped at 30 minutes — dwell longer and the truck stopped for more than fuel, so the engine looks at meal or rest instead. * **Then geocoded place metadata.** Outside any curated waypoint, the reverse geocoder's category can still pinpoint a fuel pump, a customs barrier, a police post, a restaurant, or a rest area — used with the same duration gates so an OSM "restaurant" only counts as a meal break in the right window. * **Then a coincident refuel.** A short stop overlapping a detected fill event is a fuel station even where no waypoint exists yet — the pump gives it away. * **Then duration and time of day**, evaluated in the corridor's local clock: * **Mandatory rest** — the driver is near the continuous-driving limit (within 85% of it) and the stop is at least as long as the required break. * **Overnight rest** — at least 6 hours overlapping the local night window (22:00–05:00). * **Meal break** — 30 to 90 minutes during the midday window (11:00–14:59 local). * **Breakdown** — at least 20 minutes with the engine off, away from any waypoint, with no refuel. Engine-off-in-the-open for that long reads as trouble, not a rest. * **Traffic** — a short unscheduled halt under 10 minutes away from any waypoint. * Anything the rules can't place stays **unknown** rather than being guessed. ### Authorized versus unauthorized, and the alert Independently of *why* the truck stopped, the engine asks *whether it was allowed to stop here*. A stop is **authorized** when its nearest waypoint is one of the types where a corridor truck is expected or required to halt — warehouses, depots, rest areas, fuel stations, ports, and the legally-mandatory control points (borders, checkpoints, weighbridges, customs posts, toll gates). A city waypoint marks a zone, so proximity to one does not authorize a stop. When a stop opens **without** an authorizing waypoint nearby, the engine raises an **unauthorized-stop** alert. The alert shares the stop's lifetime: it stays open while the truck is stopped and auto-resolves the moment the stop closes, so a long unauthorized halt is one alert, not a stream of them. A second, rule-based alert sits alongside it. A fleet can attach a rule set that lists specific waypoints as **prohibited** stopping places. A stop opening inside one of those fires a **prohibited-stop** alert — distinct from the unauthorized-stop alert, because it answers a different question (a named blacklist, not the absence of an authorizing type). It too resolves when the stop closes. Fleets with no rule set attached see only the type-based unauthorized-stop behaviour. ## Edge cases * **A short pause resets the stationary clock.** A truck idling at a traffic light for 90 seconds never reaches the 3-minute floor, so no stop opens and no trip closes. The stationary clock simply resets when it moves again. * **A creeping border queue.** A truck inching forward under a proven crawl is held back from opening a stop while the crawl is recent, so a slow crossing reads as continuous movement rather than a chain of micro-stops. Where a fragment does slip through, the [merged-artifact rule](./trips#merged-artifacts-one-crossing-not-eighteen-fragments) folds it back into one stop. * **Ignition off with drifting GPS.** A parked wired-ignition truck whose GPS drifts a few km/h is still recognised as stopped — the ignition-off reading overrides the noisy speed. A CAN speed above 5 km/h is the only thing that vetoes it, catching a flickering ignition line on a truck that is actually rolling. * **Null speed with the engine running.** A tracker that reports position but no speed while ignition is on waits out a 5-minute timer before it counts as stopped, so a brief speed dropout mid-drive doesn't manufacture a stop. * **A stop in an unmapped area.** The place label may be blank at close time; the reconciliation pass fills it in later. The classification still runs on duration, time of day, and any nearby geocoded category, so the stop is never left uncategorised just because its label lagged. ## Known limitations Detecting *that* a truck stopped is robust; explaining *why* is only as good as the map and the evidence around it. * **Reason classification is bounded by waypoint curation and geocoder coverage.** The most specific answers come from an operator-curated waypoint or usable geocoded place metadata. Outside a curated waypoint and in a poorly-mapped stretch, the engine has only duration, time-of-day, and a coincident refuel to work from — enough to name a rest, a meal, or a breakdown, but not enough to pinpoint every stop. * **"Unknown" is a deliberate outcome.** Classification is deterministic and never guesses: a stop the rules cannot place stays unknown rather than being assigned a plausible-but-unproven reason. That keeps the record honest at the cost of leaving some stops uncategorised. * **Authorization is a property of nearby waypoint *types*.** A stop is authorised when an appropriate waypoint type is close by; the engine cannot know a halt was sanctioned by the office if no waypoint expresses it. A fleet closes that gap by curating waypoints and attaching a rule set. ## How it connects * A confirmed stop is the most common way a trip closes: [Trips](./trips). * Waypoints, their types, and their footprints — the geometry classification leans on — are in [Part 4 — The road network](../04-road-network/). * A refuel overlapping a stop, and its effect on both classification and fill confidence, is in [Fuel](./fuel). * The unauthorized-stop and prohibited-stop alerts, and how they reach people, are part of [Part 6 — Fleet intelligence](../06-intelligence/). --- --- url: /03-fleet-engine/gaps.md --- # Signal gaps: the silence story ## What this chapter covers Trucks on the corridor go dark — GPS dead zones, cellular holes, overnight sleep, and, occasionally, a jammer or a cut power line. A **signal gap** is how the engine represents silence as a first-class fact instead of a frozen dot. This chapter covers when a gap opens (and the important case where it deliberately does *not*), what evidence the engine gathers while a truck is dark, how it decides what happened once the truck returns — including the careful handling of a truck that reappears far away — and what the operator sees at each stage. ## The picture ```mermaid stateDiagram-v2 [*] --> Online Online --> Online: parked wired-ignition truck,
silence within the 1 h sleep ceiling
(no gap opened) Online --> GapOpen: online-fix clock silent past threshold
(moving 10 min · standing 30 min) GapOpen --> GapOpen: gather boundary + in-gap evidence
(GNSS, power, defense, vibration, fuel) GapOpen --> Parked: returned near where it went dark GapOpen --> Driving: moved through the gap
(or buffered fixes prove it) GapOpen --> Deferred: returned far away — hold to confirm Deferred --> Relocated: next fix confirms the jump Deferred --> Parked: next fix near the original spot
(first reading was false) Deferred --> GapOpen: unconfirmed past 1 batch / 30 min
(candidate set aside, gap stays open, flagged) Parked --> Online Driving --> Online Relocated --> Online ``` ## The behavior ### When a gap opens The engine watches the **online-fix clock** — the last time a usable GPS fix arrived, which a heartbeat with no coordinates does not advance. When the silence on that clock passes a threshold, a gap opens. The threshold depends on what the truck was doing when it went quiet: * **Moving** — 10 minutes. A driving truck should report often; a short silence is already suspicious. * **Standing or unknown** — 30 minutes. A stopped truck reporting less is ordinary, so the engine waits longer before calling it a gap. The gap is timed from the *last online fix*, not the fix that revealed the silence — so its start reflects when the truck actually went dark. Whatever the truck was doing at that moment (moving, standing) is **frozen** onto the gap, to be used later when deciding what the silence meant. A gap can open two ways. The **reactive** path opens it the moment a fresh fix arrives and shows the clock has been silent too long. The **proactive** path — a background sweep every two minutes — opens it for a truck that has sent *nothing*, because no fix will ever arrive to trigger the reactive path on its own. ### The parked-truck exception: no gap for a sleeping truck A wired-ignition truck parked overnight cuts its ignition and heartbeats only once an hour to save power. Its online-fix clock goes silent for an hour at a time — and that is completely normal. Opening a gap for it every night would cry wolf. So the engine suppresses the gap when three things hold together: the tracker is a wired-ignition model, its ignition has been **debounced off** (held off past the flicker window, not a single stray sample), and the silence is still within the parked-sleep ceiling of one hour. A sleeping truck is understood as sleeping. The moment any of those fails — a non-wired chatty device, or an ignition-on truck, or silence past the ceiling — the suppression lifts and a real gap opens. Deep-sleep-capable models get their ceiling stretched further, to at least twice their configured sleep-reporting interval, so a device that heartbeats every 90 minutes is judged against its own rhythm. ### Two flavours of silence Every gap opens with a **reason** that captures *how* the truck went quiet, because it changes what the silence probably means: * **GPS-denied-while-alive** — the tracker is still transmitting (a heartbeat arrived within the last 10 minutes) but its location fixes have stopped. The device is alive; only its eyes are shut. This is the signature of a GPS dead zone or active jamming. * **Observability loss** — the tracker has stopped transmitting entirely — the device itself has gone silent, nothing left to observe. ### Evidence gathered while a truck is dark A gap actively gathers evidence while it is open. Every frame that arrives during the silence — status frames a GPS-denied truck keeps sending — is folded into the gap's evidence: whether GNSS reported itself unavailable, whether the factory defense **defense** flag dropped, whether a **vibration** alarm fired, whether the truck switched to internal battery. The engine also captures the two boundary frames that matter most: the **last frame before** the truck went dark (its state as it left — power present or cut, alarms quiet or latched) and the **first frame on return** (a tamper or jamming flag still set on recovery corroborates a hostile blackout). It notes the fuel level before and after, so a tank that lost fuel during the dark is visible. Every signal is kept as a strict tri-state — reported true, reported false, or never reported — so that **absence never raises an alarm**. A device that simply never sends a defense flag is treated as unknown — neither tamper-free nor tampered. ### How a gap resolves When a usable fix returns, the engine decides what the silence meant, in this order of trust: 1. **The plausibility cap comes first.** If the straight-line distance across the gap implies a speed no truck achieves — above 200 km/h — the return reads as a transport event (the truck was shipped on another truck), a device hot-swap, or a GPS spike, and the gap resolves as **relocated**, overriding everything else. 2. **Buffered evidence then wins.** If the return is physically plausible and the truck replayed stored fixes from *inside* the gap window, the trip and stop machines have already reconstructed what it was doing. The engine trusts that reconstruction over any guess from the return fix alone. 3. **Distance and frozen activity.** Otherwise the engine reasons from how far the truck moved and what it was doing when it went dark: a moving truck that reconnects nearby after a short silence **drove through**; a truck that barely moved, especially with GNSS reported denied or ignition off throughout, **stayed parked**; a large displacement points toward relocation. ```mermaid flowchart TD RET["A usable fix returns"] --> CAP{"Implied speed across
the gap over 200 km/h?"} CAP -->|yes| RELOC["Relocated · low confidence
transport, hot-swap, or spike"] CAP -->|no| BUF{"Buffered fixes from
inside the gap window?"} BUF -->|yes| RECON["Trust the reconstructed
trip / stop movement"] BUF -->|no| DIST{"How far did it move?"} DIST -->|"barely — GNSS denied
or ignition off throughout"| PARK["Stayed parked · high confidence"] DIST -->|"moved, reconnected nearby"| DROVE["Drove through · medium confidence"] DIST -->|"large jump"| DEFER["Defer — confirm before believing"] classDef observed fill:#dbeafe,stroke:#2563eb,color:#172554 classDef decision fill:#fef3c7,stroke:#d97706,color:#431407 classDef safe fill:#dcfce7,stroke:#16a34a,color:#14532d classDef warn fill:#ffedd5,stroke:#ea580c,color:#431407 classDef risk fill:#fee2e2,stroke:#dc2626,color:#450a0a classDef plan fill:#f3e8ff,stroke:#9333ea,color:#3b0764 class RET observed class CAP,BUF,DIST decision class RECON,PARK,DROVE safe class RELOC risk class DEFER plan ``` The three outcomes carry different confidence: parked-during-gap is high confidence (the truck simply sat there offline), drove-through is medium (offline but moving), and relocated is low (offline and moved an implausible distance). A large jump does not resolve immediately — it drops into the deferred path below, where the engine waits for a second fix before it will believe the truck teleported. ### Deferred relocation: confirm before you believe it A truck that reappears far from where it went dark is the trickiest case. The first return fix might be a genuine relocation — or it might be a single bad fix that will vanish on the next reading. Accepting it immediately would teleport the truck on the map and close its trip on the strength of one possibly-spurious point. So when a large jump appears, the engine **defers**. It marks the gap ambiguous and waits for the next usable fix to break the tie: * If that next fix is back **near the original spot**, the jump was a false reading — the truck was parked all along. * If it is **away** from the original spot, the relocation is confirmed. While the engine is deferring, it shows the map dot at the unconfirmed candidate so the operator sees the possible new location, but it keeps the *frozen* last known position as the anchor it will fall back to. If no confirming fix arrives — the truck stays silent — the engine waits at most one more batch or 30 minutes, then sets the unconfirmed candidate aside: the gap stays open, the display reverts to the frozen last-known spot, and the gap is flagged as an **unconfirmed relocation candidate**. It never silently commits to a jump it could not verify. ### Backfill recovery Sometimes the hole fills in later. A truck that was dark for two hours dumps its buffered fixes when it reconnects, and those fixes land *inside* the already-open gap window. If the return did not already trip the impossible-speed cap, the engine uses the reconstructed movement to resolve the gap as drove-through, and the diary gains the trip that happened during the outage. The silence is retroactively explained by the data that was merely delayed. ### Security signatures at close At resolution the engine checks the gathered evidence for a hostile signature, which overrides the ordinary classification: * **Tampering suspected** — a hardware defense or vibration alarm fired, *or* the jamming signature (GPS denied while alive *and* the truck physically moved during the blackout), *or* fuel dropped while a tamper flag was set (a fuel-loss-in-the-dark signal). A hardware alarm is high confidence; an inferred jammed-and-moved pattern alone is medium, since it could be a transport event. * **Power off** — the truck's external power was already cut when it went dark, with no tamper alarm to escalate it. The device stopped because it lost its supply, not because it slept. This requires a *positive* power-absent reading — a device that merely lost coverage or ran its backup battery flat while still plugged in never trips it. ### What the operator sees at each stage * **A confidently-parked truck going quiet** shows **"Stationné — dernière position connue"** — the calm parked state, no alarm — because a report-by-motion truck sending nothing while parked is expected. A GPS-denied gap on such a truck keeps it parked but tints the freshness amber. * **An alive truck with its engine on but no fix** shows **"Sans GPS"** in amber — a real GPS failure or jamming, not a normal park. * **A truck that stopped transmitting entirely** shows **offline**, and once its heartbeat clock passes the threshold the liveness sweep raises a device-offline alert. * **A jammed-but-alive truck** raises a distinct GPS-jamming-suspected alert — the device keeps heartbeating, so the device-offline alert stays silent, and without this the operator would get nothing for an actively-jammed vehicle. * **On return**, a gap that closed with a tamper signature raises a tracker-anomaly alert that auto-resolves as the gap ends — the tampering window is already over, so it delivers its warning and clears itself on the same close; a very long gap raises an extended-offline alert; and the device-offline alert auto-resolves the instant any message proves the tracker is back. ## Edge cases * **A future-skewed heartbeat during a gap.** The freshness test that separates GPS-denied from observability-loss is clamped against clock skew, so a tracker stamping a frame in the future cannot fake "still alive" and mask a real transmission loss. * **A historical gap processed long after the fact.** When old telemetry is reprocessed, the engine looks for the pre-gap boundary frame within three days of the gap's *own* start, not three days before now — so a gap from last month still resolves against the frame that actually preceded it. A gap whose last prior fix is older than that gets no boundary frame rather than an arbitrarily ancient one. * **A relocation that never confirms.** A truck that jumps and then goes silent has its candidate set aside after one extra batch or 30 minutes: the gap stays open, it is flagged as an unconfirmed relocation candidate, and it is shown at its frozen last-known spot — the engine records its uncertainty rather than hiding it. * **A gap that never closes cleanly.** A gap left open longer than a week is closed by a background sweep as an unknown, low-confidence outcome, so no truck carries an eternally-open silence. * **Innocent silence is never an alarm.** Parked sleep, a flat backup battery with main power still present, or a plain coverage hole satisfies none of the security triggers, so it falls through to the ordinary parked or signal-loss classification. Only a positively-reported anomaly escalates. ## Known limitations A gap is a story assembled from what a silent truck happened to reveal before and after the dark. That shapes what the engine can and cannot promise. * **A resolution is a confidence-ranked reading.** Parked, drove-through, and relocated each close with an explicit confidence — high for a truck that plainly sat still, medium for one that moved while offline, low for an implausible jump. The engine states how sure it is; a low-confidence outcome is a best inference, and it is labelled as one. * **Absence of a signal is never treated as an alarm.** Every security signal is kept as a strict tri-state — reported true, reported false, or never reported — and only a *positively-reported* anomaly escalates. The deliberate cost is that a tampering that produces no positive signal at all — no defense drop, no vibration, no fuel loss, no jammed-and-moved pattern — resolves as ordinary silence, not as tampering. Korido would rather stay quiet than cry wolf on a device that simply went dark. * **An unconfirmed relocation waits for a second fix before it is decided.** When a truck reappears far away and then goes silent before a second fix can confirm the jump, the engine holds the gap open, reverts the display to the frozen last-known spot, and flags the candidate as unconfirmed. It records the uncertainty rather than committing to a teleport it could not verify — which means such a gap can stay open until a later fix or the weekly safety-net sweep settles it. ## How it connects * The online-fix clock, the liveness semantics, and the parked / **"Sans GPS"** / offline display come from [Part 2 — Telemetry](../02-telemetry/) and the [vehicle-state memory](./vehicle-state). * A mid-trip gap that resolves as drove-through keeps a trip alive; parked or relocated closes it: [Trips](./trips). * Fuel lost during a dark window feeds the tamper signature and fuel-loss story: [Fuel](./fuel). * Device-offline, jamming, tracker-anomaly, and extended-offline alerts reach people through [Part 6 — Fleet intelligence](../06-intelligence/). --- --- url: /03-fleet-engine/fuel.md --- # Fuel: from a probe reading to a fuel-loss alert ## What this chapter covers A fuel sensor reports a number many times an hour. Turning that stream into trustworthy answers — *how full is the tank, did someone refuel, did someone lose fuel unexpectedly, and how sure are we* — takes a full pipeline: reading the right sensor, converting a liquid height into a volume, smoothing out noise, detecting fills and drains, scoring how much to believe each one, pinning a location on it, and rolling it up into per-trip cost. This chapter walks that pipeline end to end. ## The picture ```mermaid flowchart LR ble[BLE probe frame
height reading] --> fam{Sensor family} fam -->|KF201S| div1[Divide by the sensor's
own calibrated range] fam -->|Escort| div2[Divide by the fixed
12-bit scale 0–4095] div1 --> h[Height fraction 0–1] div2 --> h h --> curve[Height → volume
calibration curve] curve --> vol[Volume-% · liters] vol --> smooth[Median smoothing] smooth --> sm[Fill / drain
state machine] sm --> conf[Confidence scoring
+ location recovery] conf --> out[Gauge · events · alerts ·
per-trip cost] ``` ## The behavior ### Two sensor families, one basis The corridor fleet runs capacitive fuel probes over Bluetooth, wired into the tracker. Two families are recognised by the sensor's Bluetooth name prefix, and they differ only in how a raw reading becomes a fraction of a full tank: * **KF201S** carries its own calibrated full-scale value in each frame; the raw reading is divided by that. * **Escort** uses a fixed 12-bit scale — the raw reading runs 0 to 4095, and that 4095 is the divisor. (Escort frames also carry a bogus scale field the engine deliberately ignores; trusting it would peg every reading at 100%.) From that point on the two families are identical. The result is a **height fraction** between 0 and 1, and everything downstream — the calibration curve, the smoothing, the state machine, per-vehicle tank capacity — is shared. Each reading also stamps which sensor it came from and keeps the raw numbers, so a level can be recomputed later under a better per-sensor calibration. ### Height is not volume Here is the subtlety that a capacitive probe forces on the whole system: **the probe measures the height of the liquid, not its volume.** A truck tank has a rounded-rectangle cross-section — the corners curve away near the top and bottom. So a tank that is half-full *by height* is very close to half-full by volume, but a tank a tenth full by height holds noticeably *less* than a tenth of its volume, because the rounded bottom removes capacity down there. The engine corrects for this with a **height-to-volume calibration curve** — an eleven-point map from height fraction to volume fraction. A height of 0.1 maps to a volume of about 0.082; 0.9 maps to about 0.918; the straight middle of the tank stays close to linear. Passing every reading through this curve gives the single canonical basis the rest of the system uses: **volume-percent**. Liters follow directly once a per-vehicle tank capacity is set — volume-% times capacity — and stay hidden until it is, so the gauge always shows a percentage and only promises liters when it can be honest about them. ### Smoothing and error codes A single probe reading is noisy — fuel sloshes, the probe bounces. The engine never reacts to one reading. It keeps a rolling history and works from the **median** of a window whose size follows a per-vehicle filter level (the default smooths over the last 15 readings). Until it has a full window it makes no strong judgement — a deliberate cold-start silence that stops a freshly-installed sensor from inventing events. Each frame also carries a sensor **state code**. A non-zero code means the reading is invalid: the engine nulls the level so neither the gauge nor the state machine uses it, but it still stores the error code, the raw values, the sensor temperature, and the sensor battery as evidence. (Two of those arrive mislabelled by the upstream pipeline and are corrected on the way in — temperature scaled back up, battery voltage scaled back down.) A reading is judged valid by its error code, regardless of whether the frame had a GPS fix. Escort probes and any sensor in a dead zone routinely report fuel on frames with no location at all — those still carry a real tank level, and the gauge shows them. ### Detecting fills and drains The state machine has three states — **normal**, **draining**, **refilling** — and works by comparing the current smoothed median against a **reference level** set when it last settled into normal: * A median that drops more than the drain threshold below the reference opens a **drain**. The threshold is a volume (5 litres by default) converted to percentage points using the tank capacity. * A median that rises more than the fill threshold above the reference opens a **fill** (also 5 litres by default). * An open event **closes** once the level holds steady for the join window (5 minutes) — the tank has settled at its new level. Two refinements keep the detector honest: * **While driving, the reference level tracks fuel burned.** The reference follows the median *downward* as the truck moves, so ordinary consumption never accumulates into a false drain. A refill still rises above the reference and is caught; only a genuine loss *while parked* keeps the reference frozen and trips the drain. * **A sensor swap resets the baseline.** A jump of more than 30 percentage points between two readings is read as a sensor replacement or recalibration. The history and reference reset, and any open event closes immediately as a **sensor-reset** — bookkeeping that carries no volume and raises no alert. A short continuation of a just-ended event rejoins it, and the detector is rate-limited so a thrashing sensor cannot flood the alert stream. ### How much to believe an event: confidence Every closed event carries a confidence — **high**, **medium**, or **low** — that downstream readers weigh before acting. The scoring differs between drains and fills because the available corroboration differs. A **drain** can be cross-checked against the engine itself. The tracker reports the ECU's cumulative fuel-consumed counter, so the engine knows how many litres were *burned* over the drain window. If the tank-level drop exceeds what the engine burned by a clear margin (at least 5 litres of unexplained loss), the drain is corroborated and rated **high** — fuel left the tank that the engine never used. If the drop is fully explained by burn, the drain is vetoed as ordinary consumption. With no usable CAN counter, a drain is **medium**. There is no other route to a high-confidence drain. ```mermaid flowchart TD D["Drain event closes"] --> CAN{"CAN fuel-consumed
counter usable?"} CAN -->|no| MED["Medium confidence"] CAN -->|yes| CMP{"Tank drop vs
litres actually burned"} CMP -->|"drop exceeds burn by ≥ 5 L"| HIGH["High confidence
unexplained loss"] CMP -->|"fully explained by burn"| VETO["Vetoed — ordinary consumption"] ``` A **fill** has no engine cross-check, so it earns high confidence from context. A fill is **high** when any of these holds: * **A large, clean rise while stopped** — at least a 15-percentage-point gain that coincides with an open stop. A refuel that big physically requires being parked at a pump. * **At a known fuel station** — the nearest waypoint is a fuel station. * **Matched to a receipt** — a driver-submitted fuel receipt reconciled to the event (see below). Otherwise a fill is **medium**. A sensor-reset close is always **low** and never alerts. ```mermaid flowchart TD F["Fill event closes"] --> ANY{"Any high-confidence sign?"} ANY -->|"≥ 15 pp rise during an open stop"| HIGH["High confidence"] ANY -->|"nearest waypoint is a fuel station"| HIGH ANY -->|"reconciled to a driver receipt"| HIGH ANY -->|"none of these"| MED["Medium confidence"] SR["Sensor-reset close"] --> LOW["Low confidence · never alerts"] ``` ### Pinning a location on a fuel event A fuel event often opens on a frame with no GPS fix — exactly the dead-zone and Escort case above. A drain or fill with no coordinate would render as "position inconnue" with no place. So at close, the engine recovers a location the way a person reads a trace: it brackets the fuel-change time with the surrounding GPS fixes and takes the **nearest one within 30 minutes** (admitting only good-quality, non-buffered fixes, whose timestamps are reliable for the join). The recovered coordinate does three things: it becomes the event's position, it drives the nearest-waypoint lookup (so a fill can be recognised as at-station), and it lets the place label resolve to a real name. When no fix exists in the window, the event stays location-less rather than guessing. ### Receipts raise confidence Drivers can submit fuel receipts. When a receipt reconciles to a detected fill within the same 30-minute window, the fill is promoted from medium to **high** — a human-confirmed pump transaction is the strongest corroboration a sensor-detected fill can get. The promotion is idempotent and applies out of band, after the event has already closed. ### Fuel-loss and drain alerts A drain becomes a **fuel-loss anomaly** only when the level change looks unexplained rather than ordinary burn. The engine escalates to the critical channel for a drain that happened while the truck was **stationary**, or for one the engine cross-check corroborates as loss beyond burn. A moving truck's loss is otherwise consumption until proven otherwise, so a drain that sees motion during its window loses its stationary flag, and with it its claim on the critical channel unless the cross-check corroborates it. ::: warning Hard exclusions Known fuel-station drains are treated as fueling context and never become `fuel_drain_anomaly` alerts. A CAN explanation that accounts for the loss as burn is also a veto. CAN-corroborated unexplained loss bypasses the stationary gate, but it still has to clear the magnitude and rate floors. ::: Beyond that, an anomaly has to clear a magnitude floor and a rate floor: an actionable drain is fast (at least 8 percentage points per hour), while slow sensor drift or legitimate idle burn is not. When the tank capacity is unknown, a percentage-point floor stands in for the litres test so the anomaly never silently disables itself. ### Fuel becomes cost: per-trip and per-traversal accounting When a trip closes, the engine derives its fuel story from the readings inside the trip window: the start and end level, the litres consumed, and the efficiency in litres per 100 km. Consumption is the net level drop *plus* any mid-trip refill added back — a refuel mid-trip otherwise hides the burn behind it. Efficiency is only computed when the trip is long enough for the number to mean something. The same accounting runs per **segment traversal**, where it prefers the ECU cumulative-consumed delta (which is immune to mid-window refills) over the tank-level drop. These per-trip and per-segment figures are what feed fuel cost and efficiency reporting. ## Edge cases * **A dead-zone tank.** A probe reporting fuel on fix-less frames still drives the gauge and can still open events; the level's validity is decided by its error code, not by the missing GPS fix, and the location is recovered at close by the nearest-fix time-join. * **A parked overnight drain.** The per-vehicle detection window anchors to each truck's *own* newest reading rather than a wall-clock floor, so a truck parked and offline for days still accumulates a usable median from its own recent history — precisely the profile a parked fuel-loss anomaly needs to be caught in. * **A fill and a drain that are really a sensor swap.** A greater-than-30-point jump resets the baseline and closes any open event as a sensor-reset, so an installer swapping a probe never produces a phantom refuel or drain anomaly. * **A mid-trip refuel.** It shows up as a fill event *and* is added back into the trip's consumed litres, so the trip's efficiency reflects fuel burned, not the raw start-minus-end delta. * **A long-silent truck on the dashboard.** Fuel older than 7 days is hidden from the live dashboard rather than shown as a stale value. This display cutoff is independent of the engine's detection window, which keeps working from each truck's own history regardless of how long the truck has been quiet. * **A truck that reports more than one fuel probe.** Every reading is stored stamped with the identity of the sensor that produced it, its raw values, and the derived level, so two probes on the same truck are kept as distinct, self-identified series. ## Known limitations Fuel is the noisiest signal Korido reasons about, and the pipeline is honest about where its answers stop. * **The two probe families do not measure to the same fineness.** A KF201S carries its own calibrated full-scale value, so its fraction is anchored to a real calibration. An Escort has no usable scale on the wire, so Korido divides by the sensor's fixed twelve-bit range — a coarser, uncalibrated basis that reads a little rougher near the extremes. Both are trustworthy for detecting a fill or a drain; they are not identically precise gauges. * **Height-to-volume runs through one shared curve.** A single eleven-point curve models the rounded-rectangle cross-section every corridor tank is assumed to share. A tank whose true shape departs from that curve carries a small volume error, largest near a full or empty tank. * **High-confidence fuel-loss anomalies need a CAN fuel counter.** A drain can only be rated high confidence by cross-checking the tank drop against litres the engine actually burned. Without a usable ECU fuel-consumed counter, a drain is capped at medium — real, but uncorroborated. And until a per-vehicle tank capacity is recorded, litres stay hidden and percentage-point floors stand in for the litre-based tests. * **The live gauge stops claiming after 7 days.** Fuel older than 7 days is hidden from the dashboard rather than shown as a stale value, so a long-silent truck's tile makes no fuel claim at all. The detection window is independent and keeps working from each truck's own history — but the *displayed* gauge is deliberately bounded to recent readings. ## What's ahead * **Multi-tank trucks.** Today the pipeline reasons about one tank per vehicle. Because every reading already stamps which sensor produced it and keeps its raw values, Korido will be able to give a twin-tank truck a left and a right tank, each with its own capacity and calibration curve, without having lost any of the history captured under the single-tank model. * **Recalibration without losing the past.** Since the raw probe numbers are retained alongside every derived level, Korido will be able to recompute a truck's fuel history under an improved per-sensor calibration — sharpening old readings rather than discarding them when a better curve is known. ## How it connects * The fuel detector's state is part of the [vehicle-state memory](./vehicle-state) carried across batches; readings ride the same telemetry stream described in [Part 2 — Telemetry](../02-telemetry/). * A fill overlapping a stop both raises fill confidence and helps classify the stop as a fuel station: [Stops](./stops). * Fuel lost during a dark window contributes to a tamper signature: [Signal gaps](./gaps). * Refuel, off-station-refuel, and drain anomaly alerts reach people through [Part 6 — Fleet intelligence](../06-intelligence/); per-trip and per-segment fuel cost surfaces in [Part 5 — Missions](../05-missions/). --- --- url: /04-road-network.md --- # Part 4 — The road network: the map every mission runs on Part 3 turned one truck's raw signals into an operating diary — driving, stopped, dark, refuelled. That diary was about the truck moving *through* space. This part is about the space itself: the reusable map a mission is planned against and judged by. The map is deliberately shared. The same Douala port, the same Touboro border, the same stretch of asphalt between Yaoundé and Ngaoundéré serve every fleet that runs the corridor, so Korido curates each piece **once** and lets every mission reference it. The three chapters build that map from the ground up: ```mermaid flowchart LR w[Waypoints + road geometry
places and roads] --> c[Corridors
named ordered paths] --> r[Route Guard
a mission's frozen
acceptance region] ``` * **[Waypoints and road geometry](./waypoints-and-roads)** — the atoms of the map: named geofenced places (each with an operational and a tight core radius), the enter/exit/dwell **visits** they record, and the curated road segments between them. This is also where the **Tier-1 boundary** idea is introduced — the reason only some places can anchor a comparison. * **[Corridors](./corridors)** — a corridor is a named, ordered sequence of waypoints. This chapter shows how its waypoint pairs *resolve* to shared roads when a mission is built, how an operator pins a preferred road per leg, and how *inference* runs the other way — from a pickup and a delivery back to a matching corridor. * **[Route Guard](./route-guard)** — Korido's name for corridor monitoring. A mission's resolved roads and waypoint geofences are unioned into a single **acceptance region**, frozen at creation, and a conservative deviation lifecycle decides — through noise — when a truck has genuinely left its route. A single principle carries the part: **one geometry per road, curated once and shared everywhere.** Fix a road in one place and every corridor and every mission that runs it is corrected at once. With the map in hand, Part 5 puts trucks to work on it. --- --- url: /04-road-network/waypoints-and-roads.md --- # Waypoints and Road Geometry ## What this chapter covers The road network is the reusable map every mission is planned against: named places a truck can arrive at, and curated road geometry between them. This chapter describes **waypoints** (geofenced places), **waypoint visits** (the enter/exit/dwell record), and **road segments** (one curated geometry per road per route variant). Corridors assemble these into named paths in the [next chapter](./corridors); Route Guard turns a mission's roads into an acceptance region in [Route Guard](./route-guard). ## The picture ```mermaid erDiagram waypoints ||--o{ waypoints : "parent (port-in-city)" waypoints ||--o{ road_segments : "from / to" waypoints ||--o{ waypoint_visits : "enter / exit / dwell" road_segments { text name uuid from_waypoint_id uuid to_waypoint_id int width_m geometry center_line geometry polygon text ors_variant } waypoints { text name text waypoint_type geometry position int radius_m int core_radius_m uuid parent_waypoint_id } ``` A waypoint is a point plus two radii. A road segment is a curated line between two waypoints, thickened into a polygon. A visit is what actually happened when a truck was inside a waypoint's geofence. ```mermaid flowchart LR subgraph City geofence core((core radius
segment boundary)) op((operational radius
arrival / departure)) end core -.inside.-> op op -->|truck crosses| enter[enter → visit opens] enter --> dwell[dwell accumulates] dwell -->|truck crosses out| exit[exit → visit closes] ``` ## Waypoints — named geofenced places A **waypoint** is a named place with a geographic centre and a circular geofence. Waypoints are the vocabulary of a route: an operator plans a mission by naming the places it passes through. Every waypoint carries a **type** that says what kind of place it is: * `city`, `port`, `border`, `customs`, `weighbridge`, `toll_gate`, `fuel_station`, `warehouse`, `depot`, `checkpoint`, `rest_area`, and `custom`. Type carries operational weight, and the reason is worth setting up carefully, because it underpins everything Korido learns from a fleet. To compare how long two missions took on the same road, you need places both missions are *guaranteed* to pass through in the same way — otherwise you are comparing legs that started and ended at different points. Only some places give that guarantee. The places that do are **Tier-1 boundaries** — stable points at which one mission can be measured against another. Only cities are universally mandatory: every truck on the same physical road passes through the same cities regardless of cargo, direction, or exemptions. Borders, customs posts, weighbridges, and fuel stations are visited *conditionally* — one truck stops, the next is waved through — so they are recorded as facts and dwell context rather than as comparison boundaries. A depot or warehouse becomes a Tier-1 boundary only when a specific mission names it as its own origin or destination. ```mermaid flowchart TD place{What kind of place?} place -->|city| t1[Tier-1 boundary
every truck passes it the same way
→ a comparison anchor that cuts a leg] place -->|this mission's
origin or destination| t1 place -->|border · customs · weighbridge
fuel · rest · checkpoint| ctx[Context place
visited conditionally
→ recorded as a fact *inside* a leg] ``` This one distinction is what makes segment analytics comparable, and it carries all the way into [segments and traversals](../05-missions/segments-and-traversals). ### Two radii Each waypoint stores two radii, and they answer two different questions. * `radius_m` is the **operational radius** — the geofence a truck is considered "at" the place within. It is generous on purpose: a port sprawls, and a truck manoeuvring in the yard should still read as present. * `core_radius_m` is the **tight core radius** — a smaller ring, defaulting to a quarter of the operational radius, used to time the clean enter and exit moments that bound a segment. A wide operational radius would make transit timing noisy; the core radius gives a crisp boundary. Where the core radius is not set, the operational radius is used in its place. ### Parent nesting — a port inside a city Waypoints nest. A `port` sits inside the `city` that contains it through a **parent** link. Picture a fuel tanker loading at the Douala port, bound for N'Djamena — the truck this part will follow. The port geofence sits wholly inside the Douala city geofence: ```mermaid flowchart TD city["Douala — city geofence"] --> port["Douala port — nested waypoint
(parent: Douala)"] port --> note["Leaving the port is not leaving the city.
The parent link is how the engine knows a
departure is only *suspected* when the tanker
clears the port, and confirmed only when it
clears the outer city."] ``` This is what lets Korido handle a two-stage departure: the tanker leaving the port geofence has begun moving, but it has not yet left the city, so loading is only *suspected* until the outer city geofence is crossed. The nesting is what the mission engine reads to avoid declaring a departure the truck never made. That mechanism is described in [the mission lifecycle](../05-missions/mission-lifecycle). ### Global or tenant-private A waypoint can be **global** — shared by every fleet — or private to one tenant. The Douala port, the Touboro border, the Ngaoundéré weighbridge are the same physical places no matter which company's trucks pass through them, so they are curated once and shared. A tenant's own depot is private to that tenant. An operator can also drop an ad-hoc place on the map while building a mission (an "add a place" mode on the route map); that place is created as a tenant waypoint and spliced into the route in one step. ### Geometry versioning Whenever an operator edits a waypoint's position or radii, its **geometry version** is bumped. Historical records that referenced the old geometry keep their own frozen snapshot of the position and radius, so a place moved today does not silently rewrite what a traversal recorded last month. The version is the join key that lets analytics detect "this place's geometry changed since this row was written." ## Waypoint visits — enter, exit, dwell A **waypoint visit** is the record of one stay inside one waypoint's geofence. It opens the moment the truck crosses in and closes when it crosses out. Each visit records: * the `entered_at` and `exited_at` times and the resulting **dwell** duration; * the exact entry and exit positions (which can sit up to a full radius away from the centre); * how many stops happened inside the geofence and how far the truck moved within it; * the **approach direction** — the compass bearing and eight-way sector on entry and exit, and a **through-movement** flag that is true when the truck drove through the place rather than reversing back out. Northbound and southbound trucks on the corridor sit on different historical-traffic distributions, so this direction data feeds later analysis. Only one visit per truck-and-waypoint can be open at a time. When a truck crosses a waypoint boundary, one visit closes and, at the next place, another opens. Those closing and opening moments are the cuts that divide a mission into measurable **segment traversals** — the subject of Part 5's final chapter. ## Road segments — one geometry per road A **road segment** is the curated geometry of one road between two waypoints. It is the reference against which a truck's real path is judged: is it still on the road it was meant to take? Each road segment stores: * a `from` and a `to` waypoint; * a **centre line** — the raw routed path between them, kept so the polygon can be regenerated at a different width without re-routing; * a **polygon** — the centre line thickened by half the road's width, cached so the engine never has to buffer geometry on the hot path; * a **width** (between 50 m and 5 km) that sets how far off the centre line still counts as "on the road"; * an **ORS variant** label (`default`, `alt_1`, `alt_2`) so two genuinely different roads between the same pair of waypoints — say a mountain route and a valley route — can both exist without colliding. ### One direction, reused both ways The central design rule is: **one geometry per road, stored in one travel direction**. The road from Douala to Yaoundé is the same asphalt whether a truck is driving north or south, so it is curated once. A mission travelling the other way simply reuses that geometry — the acceptance polygon is undirected, and each traversal carries its own direction, so one stored geometry serves both directions. A **reverse override** exists only where the two directions genuinely differ — one-way streets, or split truck routes where loaded and empty trucks are sent along different roads. In the pilot corridor this is the exception: only a handful of the road pairs need a distinct reverse geometry; the rest are single geometries reused both ways. When a reverse override does exist, a reader serving the reverse direction prefers the explicit reverse row and otherwise falls back to the forward one. Because a byte-identical mirror and a genuine divergence look the same to the database, the "one geometry per road" rule is upheld in the admin tooling, not by a constraint. The corridor editor curates *roads*, and "add reverse override" is a deliberate action an operator takes only when a road really splits. ### Shared, not owned Road segments are **global**, shared across every corridor that uses them, because one road belongs to many. The Douala–Yaoundé road is part of the N'Djamena trunk and the Bangui branch at once. Curating it once and letting every corridor reference it is what keeps the map consistent: fix the geometry in one place and every mission on every corridor that uses that road benefits. ## Edge cases * **A truck idling in a yard past the end of the road.** On arrival, a truck can be inside a waypoint's operational geofence but a few hundred metres past where the buffered road ends. That short stretch of city driving is inside the waypoint's geofence, so it does not read as leaving the road. Route Guard unions the waypoint geofences into the acceptance region precisely to absorb this; see [Route Guard](./route-guard). * **Reverse travel with no override.** With no reverse row, a southbound truck is judged against the northbound geometry. Because containment is direction-free and the polygon is symmetric about the centre line, this is exact — the reverse row would only ever differ if the physical road did. * **A waypoint moved after the fact.** Editing a place's position or radius does not rewrite history: existing visits and traversals keep the snapshot of the geometry they were measured against, and the geometry version records that a change happened. * **Core radius unset.** Older waypoints created before the tight core radius may not have one; the operational radius stands in until a backfill sets it, so segment timing degrades gracefully rather than breaking. ## How it connects * [Corridors](./corridors) — how ordered waypoints resolve into a named path of roads. * [Route Guard](./route-guard) — how a mission's roads and waypoint geofences become a frozen acceptance region. * [Segments and traversals](../05-missions/segments-and-traversals) — how waypoint-visit boundaries cut a journey into measurable pieces. * Part 2 (telemetry) and Part 3 (the fleet engine) — where geofence entry and exit are actually detected from the position stream. --- --- url: /04-road-network/corridors.md --- # Corridors ## What this chapter covers A **corridor** is a named, ordered path of waypoints — configured once and used by every mission that runs it. This chapter explains how a corridor owns no geometry of its own, how its consecutive waypoint pairs resolve to [road segments](./waypoints-and-roads) when a mission is created, how per-leg variant pins let an operator lock a preferred road, and how corridors are curated in the admin portal. It builds directly on the waypoints and roads from the previous chapter and feeds the mission-creation flow in [Part 5](../05-missions/creating-missions). ## The picture ```mermaid flowchart TD C[corridor
named ordered path] --> CW1[waypoint 1
origin] C --> CW2[waypoint 2
interior] C --> CW3[waypoint 3
interior] C --> CW4[waypoint 4
destination] CW1 -. "pair (1,2)" .-> R1[road segment] CW2 -. "pair (2,3)" .-> R2[road segment] CW3 -. "pair (3,4)" .-> R3[road segment] R1 --- note[resolved at mission-creation time,
honoring per-leg pins] ``` The corridor is the list of places, in order. The roads between them are found when a mission is built. ## A corridor owns no geometry A corridor is a **named ordered sequence of waypoints** and nothing more. The roads are found by looking at each consecutive pair of waypoints in the sequence and resolving that pair to a curated road segment at the moment a mission is created. This is deliberate, and it is what makes shared trunks fall out for free. The Douala–Yaoundé road belongs to the N'Djamena corridor and the Bangui corridor at the same time. If a corridor owned its geometry, that road would have to be drawn and maintained twice, and the two copies would drift. Because the corridor only names waypoints and the road is resolved from the shared, globally-curated map, one road edit propagates to every corridor that runs it. The ordered sequence lives in the corridor's waypoints. Each entry has a **sequence order** and a **role** — `origin` for the first, `destination` for the last, `waypoint` for everything in between. Role is display metadata; corridor matching cares about membership and ordering, not role. ## Per-leg variant pins "Configure once" includes the choice of *which* road. Where a pair of waypoints has more than one curated road between them — a fast toll route and a free alternative, say — an operator can **pin** the preferred variant for that leg. The pin lives on the waypoint that starts the leg: it names the road segment to use when the corridor is resolved for a mission. Where no pin is set, resolution picks the canonical variant for the pair. Pinning means the operator's routing preference is captured in the corridor itself, so every mission built from that corridor inherits it without anyone re-choosing the road each time. ## Global and tenant-private corridors Corridors come in two scopes, mirroring the waypoints and roads they are built from. * A **global corridor** has no tenant and is shared by every fleet. The Douala–N'Djamena trunk is the same path regardless of who runs it, so it is curated once, centrally, as the source of truth. * A **tenant-private corridor** belongs to one fleet — a route only that company runs, or a variation it prefers. A corridor's name is unique within its scope: two tenant-private corridors in the same fleet cannot share a name, and two global corridors can never share a name across the whole platform — the shared trunk map holds exactly one corridor per name. A retired corridor frees its name for reuse. Deleting a corridor never orphans the missions built from it: a mission keeps its own resolved roads and frozen acceptance region, so a corridor is a *preset and a provenance record* — it hands the mission its roads and region once, then plays no further part. ## Resolution and inference Two operations turn a corridor into a mission route, and they run in **opposite directions**. Resolution starts from a named corridor and produces roads; inference starts from two places and produces a corridor. Keeping the two apart is the key to reading this chapter. ```mermaid flowchart LR subgraph R["Resolution — corridor is known"] direction TB rc[Named corridor] --> rr[Look up each
waypoint pair] --> rroads[Ordered road segments
= mission route] end subgraph I["Inference — corridor is unknown"] direction TB ip[Pickup + delivery] --> is[Search corridors
covering both places] --> id[Derive → trim
to the requested legs] end ``` **Resolution** runs when a mission is created from a corridor. Each consecutive waypoint pair is looked up in the road map, honoring any pins, and the ordered list of resulting road segments becomes the mission's route. A travel **direction** — forward along the corridor's sequence, or reverse — decides which way the sequence is read; reverse simply folds the order. **Inference** runs the other way, in quick-assign, when the operator has a pickup and a delivery but has not named a corridor. Korido searches for a corridor whose path *covers* both places — where both the pickup and the delivery appear in the sequence with compatible ordering. A match can be: * **forward or reversed** — the corridor is a path, and either travel direction is a candidate; * **endpoints or interior** — the pickup and delivery can be the corridor's ends, or an interior sub-path of a longer corridor. When they name an interior stretch, the full path is derived and then **trimmed** to the requested legs. That last case is worth seeing concretely. The Douala–N'Djamena corridor threads a chain of waypoints — Douala, Yaoundé, Bertoua, Garoua-Boulaï, Ngaoundéré, the Touboro border, then Moundou, Kélo and Bongor into N'Djamena. Suppose a dispatcher only needs the Yaoundé → Ngaoundéré stretch. Inference matches the long corridor, derives its full ordered path, and trims away the outer legs — the mission carries just that middle span, the cities between pickup and delivery still on it: ```mermaid flowchart LR D["Douala
(trimmed away)"] --- Y["Yaoundé
(pickup)"] --- Be["Bertoua"] --- GB["Garoua-Boulaï"] --- N["Ngaoundéré
(delivery)"] --- rest["Touboro · Moundou ·
Kélo · Bongor · N'Djamena
(trimmed away)"] classDef kept fill:#dff,stroke:#068 classDef trimmed fill:#eee,stroke:#999,color:#999 class Y,Be,GB,N kept class D,rest trimmed ``` At the pilot's scale this is a plain search over a few dozen corridors — no special index is needed. The mechanics of how inference plugs into quick-assign are in [creating missions](../05-missions/creating-missions). ## Curation in the admin portal Corridors and their roads are curated in the admin portal. The editor is organised **per road**: an operator works on the geometry of one road between two waypoints at a time, seeing its centre line and buffered polygon on the map. This per-road view is what keeps the "one geometry per road" rule visible and enforceable — an operator curates roads, and corridors reference them. Adding a reverse geometry is an **explicit action**. "Add reverse override" is a deliberate choice an operator makes only when a road genuinely splits by direction. The default is a single geometry reused both ways, so the tooling does not tempt anyone into curating mirror copies that would only drift apart. ## Edge cases * **A leg with no curated road.** If a corridor names a pair of waypoints that has no road segment between them yet, resolution cannot complete that leg. The mission can still be created, but with no full acceptance region — and Route Guard skips a mission that has no corridor polygon rather than raising false deviations. Curating the missing road repairs every future mission on that corridor. * **Interior sub-path.** When inference matches an interior stretch of a longer corridor, the derived route is trimmed to the requested pickup-to-delivery span — that trimmed span is all the mission carries. * **Both directions match.** A pickup and delivery can appear in a corridor such that both forward and reverse reading are valid candidates; the requested travel direction disambiguates. * **A road re-curated mid-flight.** Editing a road's geometry re-unions the acceptance region of the in-flight missions that use it, so an active mission benefits from a correction without being rebuilt. That propagation is described in [Route Guard](./route-guard). ## Known limitations * **Matching is membership and ordering, not proximity.** A corridor is a list of places; inference matches a pickup and delivery only when both already appear on some named corridor with compatible ordering. It has no geometric sense of "near" — a pair no corridor covers produces no match, and the operator assembles the route by hand or drops ad-hoc places onto it. * **A named path is only as good as its curated roads.** A corridor names places. A leg whose road is not yet curated resolves to an incomplete acceptance region, and Route Guard skips a mission with no corridor polygon — correct monitoring on that leg waits until the road is drawn. ## What's ahead * **Border crossings and breakdowns as first-class entities.** Today a border crossing and a breakdown are recorded as facts and measured durations *inside* a run — a border is a waypoint the truck dwells at, a breakdown is a classified stop. The next step is to promote each to an entity with its own lifecycle: border crossings gaining a compliance record of their own once cross-border reporting is on the table, and breakdowns becoming tracked incidents inside a maintenance workflow. * **Learned corridor proposals.** Corridors are curated by hand today, as the single source of truth. As real traffic accumulates, Korido will cluster the roads fleets actually drive and *propose* new corridors and road variants for an operator to confirm — curation stays human, but the map begins to suggest its own extensions. ## How it connects * [Waypoints and road geometry](./waypoints-and-roads) — the places and roads a corridor is assembled from. * [Route Guard](./route-guard) — how a resolved corridor becomes a frozen acceptance region for a mission. * [Creating missions](../05-missions/creating-missions) — quick-assign, corridor inference, and templates in practice. * [Driving rules](../05-missions/driving-rules) — a corridor can carry a default rule set that a mission inherits. --- --- url: /04-road-network/route-guard.md --- # Route Guard ## What this chapter covers **Route Guard** is Korido's name for corridor monitoring: watching whether a truck on a mission is still on the road it committed to, and raising a **deviation** when it is not. This chapter describes the mission's frozen **acceptance region**, the deviation lifecycle with its hysteresis and eight close reasons, and the notifications a deviation raises. It builds on the roads and corridors of the previous two chapters, and it is an engine-side chapter, so its edge cases are load-bearing. ## The picture ```mermaid stateDiagram-v2 [*] --> Inside Inside --> Watching: outside while moving Inside --> Inside: outside while stationary (no event) Watching --> Inside: back inside (reset) Watching --> Open: enough consecutive
outside trusted fixes Open --> Returned: inside while moving
(returned_to_corridor) Open --> Open: still outside /
inside while stationary Open --> Abandoned: mission ends,
corridor replaced,
reassigned, relocated,
stale safety net Returned --> [*] Abandoned --> [*] class Inside,Returned safe class Watching warn class Open,Abandoned risk classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d ``` The two intermediate phases — **Watching** before a deviation opens, and the requirement of *movement* to confirm a return — are what keep a single noisy GPS point from ever creating or clearing an incident. ## The acceptance region Every mission has an **acceptance region**: the area within which the truck is considered on-corridor. It is the union of two things: * the **buffered polygons of the mission's roads** — each road segment's centre line thickened by its width; and * the mission's **own waypoint geofences** — each waypoint buffered by its operational radius. ```mermaid flowchart LR roads[Buffered road polygons
centre lines × width] --> u(( ∪ )) geos[Mission's waypoint geofences
ports, cities, borders] --> u u --> region[Acceptance region
one multi-polygon,
frozen on the mission] class roads,geos plan class u warn class region safe classDef plan fill:#eff6ff,stroke:#2563eb,color:#1e3a8a classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b ``` ::: tip Current behavior The acceptance region is mission-specific and frozen. Later road curation helps future missions, while an active mission keeps the region its deviations were measured against unless the plan is deliberately reassembled. ::: Unioning the waypoint geofences into the region is what absorbs the ordinary city driving on arrival and departure. Our Douala tanker, inside the port geofence but a few hundred metres past where the buffered road ends, is still inside the region — so manoeuvring in a yard never reads as leaving the corridor. ### Frozen at creation The acceptance region is **frozen when the mission is created**. It is assembled once, from the roads the corridor resolved to plus the mission's waypoints, and stored as a single multi-polygon on the mission. From then on, an active mission's guard never shifts underneath it. This matters because an open deviation references the region it was measured against; if the region could change mid-flight, an in-progress incident would lose its meaning. The region is re-assembled in exactly two controlled cases: when the mission's corridor is edited, and when an admin edits a road's geometry and that change is propagated to the in-flight missions using it. Both are deliberate acts on the plan. A mission with no corridor has no acceptance region. Route Guard skips such a mission entirely rather than inventing a boundary — the fleet engine still tracks its stops and gaps, but no deviation is possible. ## The deviation lifecycle Route Guard is deliberately conservative. It moves through phases, and each phase exists to reject noise. 1. **Inside the region.** Nothing happens. 2. **Outside while stationary.** Nothing happens — a parked truck just off the line is not deviating anywhere. 3. **Outside while moving.** Route Guard starts **watching**. 4. **Watching, back inside.** The watch resets. A brief clip of a corner is not an incident. 5. **Watching, still outside.** Outside evidence accumulates fix by fix. 6. **Watching, enough consecutive outside trusted fixes.** A **deviation opens**. 7. **Open, still outside.** The deviation stays open. 8. **Open, inside while moving.** The truck has **returned** to the corridor. 9. **Open, inside while stationary.** The deviation stays open until movement confirms the return — a truck that drifts back into the region and parks has not demonstrably rejoined the route. Degraded or jitter-flagged positions never open deviation state; only trusted fixes count. The watching phase itself has a time-to-live, so a stale watch that never resolves does not linger. An open deviation records the excursion in full: where and how fast the truck left, how far it strayed at the furthest point, how long it lasted broken down into driving and stationary time, how many stops happened off-corridor, and which road segment it strayed from (resolved as the closest segment to the exit point). Every deviation is persisted — even the ones that never reach a person — because they are the raw material for corridor analytics. ## The eight close reasons A deviation closes as either **returned** or **abandoned**, and the close reason records *why* in human terms: * **Returned to corridor** — the truck drove back onto the route. The ordinary, healthy ending. * **Mission arrived** — the mission reached its destination while the deviation was still open. * **Mission terminated** — the mission was completed or cancelled out from under the open deviation. * **Corridor replaced** — the mission's corridor was changed, so the old excursion is measured against a region that no longer applies. * **Vehicle reassigned** — the truck was moved to a different mission. * **Relocated during signal loss** — the truck reappeared somewhere new after a gap in the signal, so route continuity can no longer be trusted and the old deviation cannot be honestly continued. * **False-start recovery** — the mission was flagged as a false start and rolled back, taking its open deviation with it. * **Stale safety net** — a background sweep closed a deviation that had been left open with no fresh evidence, so nothing lingers forever. Only one deviation per truck can be open at a time. The close reason is always recorded; a deviation is never silently dropped. ## Notifications Persisting a deviation and **notifying** a person are two separate decisions. Every deviation row is written for analytics; only the owner-or-dispatcher alert is gated, and it is gated to fight fatigue: ```mermaid flowchart TD dev[Deviation opens] --> persist[Write the row
always — even for replays] persist --> notify{Notify a person?} notify -->|replayed history| no[No page — it already happened] notify -->|returns within 60s| no notify -->|off-corridor stop,
per-stop alert off by default| no notify -->|otherwise| yes[Alert the owner / dispatcher] class dev,persist observed class notify warn class no safe class yes risk classDef observed fill:#fff7ed,stroke:#ea580c,color:#7c2d12 classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d ``` * **Historical replays never page.** A deviation reconstructed from replayed history persists its row but never notifies a person — it already happened. * **A brief deviation cancels its own alert.** A deviation that returns within 60 seconds suppresses its still-queued open-notification, so one noisy excursion never reaches the owner even though the row is kept. * **Per-stop off-corridor alerts are opt-in.** A stop that happens while a truck is off-corridor always writes its fact, but the per-stop notification is off by default. In practice nearly every off-corridor stop is unauthorised, so alerting on each one by default would be pure noise; a tenant that wants that visibility turns it on. ## Edge cases * **No corridor, no guard.** A mission with no acceptance region is skipped by deviation detection; the engine falls back to treating a long ignition-off stop as pause evidence rather than a route judgement. * **A pause freezes an open deviation.** When a mission pauses, an open deviation is preserved as-is rather than being closed as returned — a legitimately paused truck sitting off the line is not resolving the excursion, and forcing it closed would lose the story. * **A signal gap relocates the truck.** If a gap ends with the truck somewhere it could not have driven continuously, the open deviation's continuity is closed with the "relocated during signal loss" reason, because the path between exit and reappearance is unknown. * **The engine's memory disagrees with the database.** The persisted open deviation rows are authoritative. If the engine's in-memory view and the stored rows disagree, persistence self-heals toward what the database holds, so a restarted or re-ordered run never double-opens or loses an incident. * **A road edited mid-mission.** Editing an admin-curated road re-unions the acceptance region of the active missions that use it, so a corridor correction reaches trucks already on the road without rebuilding their missions. ## Known limitations * **Duration separates driving from stationary, not pause.** An open deviation's elapsed time is decomposed into the driving portion and the stationary portion, measured against the truck's trips and stops across the excursion window. A distinct *paused* component is not broken out today — the driving and stationary figures carry the story, and any slice that belongs to neither sits in the unaccounted remainder rather than its own number. * **The guard reacts only to trusted fixes.** Deviation state opens and clears on trusted positions alone; degraded and jitter-flagged fixes are ignored. During a stretch of heavily degraded signal the guard therefore holds its current verdict rather than reacting to noisy points — correct by design, but it means an excursion that begins and ends entirely within a degraded window can go unseen until clean fixes resume. ## How it connects * [Waypoints and road geometry](./waypoints-and-roads) — the polygons that make up the acceptance region. * [Corridors](./corridors) — the resolved roads a mission's region is assembled from. * [The mission lifecycle](../05-missions/mission-lifecycle) — pause, arrival, false-start, and reassignment, which drive several close reasons. * [Progression and ETA](../05-missions/progression-and-eta) — how a blocked road ahead, separate from a deviation, changes the arrival estimate. * Part 3 (the fleet engine) for signal gaps and the trusted-position filter, and Part 6 (fleet intelligence) for how deviation alerts reach people. --- --- url: /05-missions.md --- # Part 5 — Missions: the work trucks do on the road Part 4 handed us the map. This part is the job that runs on it. A **mission** is Korido's unit of operational accountability: one truck, one driver, one planned route, one set of pickup and delivery milestones, one audit trail — the thread that ties a fleet's assets, its plan, its live tracking, and its analytics together. To keep the abstractions concrete, the whole part follows one run: a **tanker loading fuel at the Douala port, bound for N'Djamena**, roughly 2,000 km across two countries. We meet it first as assets and the driver who runs them, learn who the haul is *for*, then plan its route, watch it depart and arrive, govern how it may drive, estimate when it lands, cut its journey into the measured legs that teach the next mission, and file the paperwork it produces along the way. ```mermaid flowchart LR a[Fleet assets] --> cl[Clients] --> l[Mission lifecycle] --> c[Creating missions] c --> d[Driving rules] --> e[Progression + ETA] --> s[Segments + traversals] --> doc[Documents] ``` * **[Fleet assets](./fleet-assets)** — the things a mission is carried by and the person who runs it: the vehicle that streams telemetry, the trailer that often holds the cargo, the tracker-and-SIM stack wired into the cab, and the driver assigned to the haul. How each is registered, linked, swapped, and marked available or off the road. * **[Clients](./clients)** — who the work is *for*: the customer record, its saved pickup and delivery locations, the default corridor and driving policy it lends to a mission, and the tracking links shared with it. * **[The mission lifecycle](./mission-lifecycle)** — the eight states a mission moves through, how it advances on its own as the truck crosses geofences, how it pauses and resumes, how a mistaken start is unwound, and the rule that a vehicle can be on only one live mission at a time. * **[Creating missions](./creating-missions)** — dispatching in a tap: quick-assign from a client's templates or from a pickup-and-delivery pair, convoy dispatch across several trucks, and what a mission write actually commits before external routing enriches it. * **[Driving rules](./driving-rules)** — the policy a mission runs under: speed, night driving, hours of service, and authorized-refuel and prohibited-stop places. Why this policy is applied *live* while the route geometry stays frozen. * **[Progression and ETA](./progression-and-eta)** — how Korido answers "when will it arrive?" without ever promising — a continuously re-estimated arrival built from per-segment history, known frictions, and rest modelling, each estimate carrying its own confidence. * **[Segments and traversals](./segments-and-traversals)** — where a journey becomes comparable: waypoint boundaries cut the run into measurable legs, each frozen with its facts, its context dimensions, and the prediction made beforehand — the raw material for Part 6. * **[Documents](./documents)** — the paperwork a run generates: a driver captures customs forms, delivery notes, and fuel receipts on the phone, they attach to the mission and truck, and an owner works them in a review center with a retake loop back to the driver. By the time our tanker reaches N'Djamena it has left behind a full, comparable record of the run — every leg timed, every stop and deviation attributed, every litre accounted for, every document captured and reviewed. That record is where Part 6 begins: the fleet intelligence that turns one mission's facts into what a whole fleet can learn. --- --- url: /05-missions/fleet-assets.md --- # Fleet Assets ## What this chapter covers A mission is carried by physical things a fleet owns and driven by the person at the wheel: the **vehicle** that provides power, telemetry, and a driver seat; the **trailer** that often carries the actual cargo; the **tracker and SIM** wired into the truck that make the whole platform possible; and the **driver** assigned to run it. This chapter describes those assets as an owner manages them — how a vehicle is registered, how a trailer is described and linked to a tractor, how the device layer (a tracker plus its SIM, installed in a vehicle, sometimes swapped) is understood from the owner's side, and how a driver record is created — and it is the register the mission plan draws its vehicle, trailer, tracker, and driver from. Through this part we follow one concrete job: a **tanker that loads fuel at the Douala port and runs the 2,000 km trunk to N'Djamena**. It starts here as three assets and the person who will run them — the truck, the tanker trailer hitched to it, the tracker wired into the cab, and the driver assigned to the haul — and the following chapters plan, drive, and measure the run they make. ## The picture Three asset kinds relate in a simple shape: a trailer is hitched to at most one vehicle, a tracker is installed in at most one vehicle, and a SIM lives inside at most one tracker. Missions record which trailer they used as durable history. ```mermaid erDiagram vehicles ||--o| trailers : "hitched to (one at a time)" vehicles ||--o| devices : "tracker installed in" devices ||--o| sim_cards : "SIM installed in" devices ||--o{ device_installations : "install / swap history" devices ||--o{ sim_installations : "SIM change history" missions ||--o| trailers : "used (kept as history)" vehicles { text registration_number text vehicle_type text make_model_year asset_status status } trailers { text registration_number trailer_type trailer_type int max_payload_kg date technical_inspection_expires_at asset_status status } devices { text imei text device_model device_status status } ``` The vehicle is the hub: the trailer hangs off it, the tracker sits inside it, and the tracker's SIM sits inside the tracker. Only the vehicle produces position. ## Vehicles — the powered asset A **vehicle** is a registered truck (or a `pickup` or a `van`). It carries the facts an owner needs to identify and manage it: a **registration number** unique within the fleet, a **type**, and **make, model, and year**. Each vehicle also carries an **asset status** and can be archived, so a sold or written-off truck leaves the working lists without erasing the history attached to it. The vehicle is the only asset that streams telemetry, and it does so through the tracker installed in it — never through a phone. Everything the platform knows about where a truck is, whether it is moving, and how much fuel is in its tank is anchored to the vehicle's own hardware, which is why the vehicle is the anchor every trip, stop, mission, and alert hangs from. Beyond the identity fields, the vehicle row is also where the engine keeps each truck's **live state** — its last known position, ignition, battery, signal, and fuel — refreshed at the end of every telemetry batch so the live map and fleet overview read one fast row. That live-state machinery is the subject of [Part 2 — Telemetry](../02-telemetry/) and [Part 3 — The fleet engine](../03-fleet-engine/); here the point is simply that identity and live state share the one vehicle record. ## Trailers — the passive asset A **trailer** is a first-class fleet asset in its own right, distinct from the vehicle it's hitched to. On the corridor a trailer is frequently the commercial unit — it holds the cargo — while the truck provides the power, the driver, and the tracker. **A trailer has no tracker and no position stream of its own**, so its whereabouts and availability are inferred from the truck it is hitched to and from the missions that used it. A trailer records the operational description an owner needs: * a **registration number**, unique within the fleet; * a **type** — one of `flatbed`, `container`, `tanker`, `tipper`, `reefer`, or `other`; * physical specs — **axle count**, **wheel count**, **length**, **maximum payload**, and **tare weight**; * **make, model, and year**; * a **technical inspection expiry** date; * an **asset status**. The specs are recorded for operations and future compatibility checks; today mission creation does not hard-enforce payload, axle, or inspection compatibility as a domain rule — the numbers inform the operator rather than block the dispatch. ### Linking a trailer to a tractor The current physical association between a trailer and a truck is a direct **link**. The rule is one-to-one on both sides: a trailer is hitched to **at most one** vehicle, and a vehicle pulls **at most one** trailer. Linking is an owner action — Korido does not try to detect a hitch automatically — and it can be updated whenever a trailer is physically moved in the yard. Because a trailer can only be on one truck and a truck can only pull one trailer, linking a trailer that is already committed elsewhere is a **swap**, handled in one clean step: ```mermaid flowchart TD start([Link trailer T to vehicle V]) --> chk{Both exist and
not archived?} chk -->|no| reject[Reject the link] chk -->|yes| free1[Unhitch any trailer
currently on V] free1 --> free2[Unhitch T from
its previous vehicle] free2 --> set[Hitch T to V] set --> done([One trailer on V,
T on one vehicle]) ``` The swap frees both sides first so the one-per-vehicle rule is never violated, even momentarily. Changing a trailer's status to **retired**, **breakdown**, or **maintenance** also unhitches it — an asset that is off the road should not appear hitched to a working truck. ### A trailer on a mission is history When a mission is created with a trailer, two things happen together: the trailer is recorded on the mission, and it is hitched to the mission's vehicle in the same step. That mission record is **durable history** — the mission keeps its trailer reference even after the trailer is later unhitched, retired, or archived. The present hitch answers "what is on this truck right now"; the mission rows answer "which trailer ran which job," and the two are kept separate on purpose so neither erases the other. A trailer's detail view leans on that history: it shows the missions the trailer served, with their count, distance, and tonnage, giving a passive asset a legible operating record built entirely from the missions it ran. ## The device layer, as owners see it Under every tracked truck sits a small stack of hardware: a **tracker** wired into the cab, a **SIM** inside the tracker that carries its data, and optional **components** such as a fuel probe or a CAN reader. Korido's platform team procures and owns this hardware; an owner meets it as *the thing installed in my truck that makes tracking work*. ```mermaid flowchart LR SIM["SIM card
(carries the data)"] -->|installed in| DEV["Tracker
(GPS source of truth)"] COMP["Components
fuel probe · CAN reader · antenna"] -.->|wired to| DEV DEV -->|installed in| VEH["Vehicle"] VEH -->|telemetry| KOR["Korido"] ``` From the owner's side, the device is identified by its hardware identity (its IMEI) and its **model**, and the connection is what matters day to day: an owner sees that a tracker is installed, how fresh its signal is, and whether it has gone quiet. Each device can also carry a per-device **silence expectation** — how long this particular unit may stay quiet before it counts as offline — which sits at the top of the offline-detection chain described in [Part 2 — Telemetry](../02-telemetry/). The deeper inventory — SIM cost, supplier, procurement batch — is platform bookkeeping, kept in the [admin portal](../07-surfaces/admin-portal) rather than the owner's view. ### A tracker's lifecycle, and swaps A tracker moves through a clear set of states — from **in stock**, to **assigned** to a fleet, to **installed** in a specific truck, and eventually to a terminal state when it is **faulty**, **returned**, **decommissioned**, or **lost**. An installed tracker is always tied to a vehicle; a tracker in stock belongs to no fleet and no truck. ```mermaid stateDiagram-v2 [*] --> in_stock in_stock --> assigned: assigned to a fleet assigned --> installed: fitted to a truck installed --> faulty: fails in service installed --> returned: pulled from service faulty --> decommissioned returned --> decommissioned installed --> lost decommissioned --> [*] lost --> [*] ``` Hardware moves. A tracker that fails is swapped for a replacement; a truck that changes tracker gets a new unit installed. Every one of these moves is written to a durable **install history** — who fitted or removed a unit, when, and why (an initial install, a faulty swap, a reinstall) — so the full story of which tracker was in which truck at which time is always recoverable, and a swap records the unit it replaced. The SIM inside a tracker is tracked the same way: swapping a SIM writes its own history row rather than overwriting the last one. Nothing about a device or SIM change is destructive — the current install is one row in a chain that keeps every prior fitting. ## Drivers — the people who run the assets The assets above are inert until a **driver** takes the wheel — the one part of the register that is a person. A driver record does double duty: it is the **login identity** that lets someone into the driver app, and it is the **assignee** a mission is dispatched to. Creating one is therefore the prerequisite for both: sending a truck on a mission requires a driver named on it, and reaching the driver app requires a driver record to sign in as. ```mermaid erDiagram driver ||--o| phone_identity : "phone → OTP login" driver ||--o{ missions : "assigned to (driver required)" driver ||--o| vehicles : "currently at the wheel of" driver { text name text email text phone locale language } ``` A driver record carries a **name**, an **email**, a **phone number**, and a **language** — French or Arabic — for the copy they receive. The phone is the key fact: it is the **login identity**, the number a driver enters to receive a one-time code and sign in, and for that reason it is set when the record is created and not edited afterward from this screen — *"le téléphone est l'identifiant de connexion."* The driver app never carries a password; the phone plus a one-time code is the whole of it, which is why a driver must exist in the register before they can log in at all. Once created, a driver is what a mission is assigned to and what a truck records as its current driver, and their detail view gathers the trucks they are assigned to and their recent missions. A driver holds **at most one active mission at a time** — dispatch turns away an attempt to give a driver a second mission while their first is still running, because one person cannot drive two trucks at once. Deactivating a driver — **"Désactiver"** — detaches them from their trucks and hides them from the assignment pickers, but preserves the mission history they built: *"l'historique des missions est conservé."* Like a retired vehicle or trailer, a driver leaves the working fleet without erasing the record of the work they did. ## Asset status and availability Vehicles and trailers share one **asset status** vocabulary, and it answers a single question: *is this asset available for work?* ```mermaid stateDiagram-v2 [*] --> active active --> in_mission: committed to a running mission in_mission --> active: mission ends active --> maintenance: taken off the road for service active --> breakdown: failed in the field maintenance --> active: back in service breakdown --> active: repaired active --> retired: sold / written off retired --> [*] ``` * **active** — available. * **in\_mission** — committed to a mission in progress. * **maintenance** — deliberately off the road for service. * **breakdown** — failed and unavailable. * **retired** — permanently out of the fleet. The status is an **availability marker**. `maintenance` and `breakdown` tell operators an asset is off the road and remove it from the pool of things that look ready to dispatch, capturing availability alone — what broke, what it cost, what parts were used, and how long the downtime ran live outside the status field. A trailer's **technical inspection expiry** is treated the same way — it is tracked and surfaced, and the fleet can count how many inspections have lapsed, so an owner can see compliance at a glance without the platform pretending to run the workshop. ## Edge cases * **A trailer with no truck.** A trailer can exist unhitched — sitting in the yard, waiting for a job. It stays a full asset with its own status and history; it simply has no current vehicle. * **A trailer that changes trucks.** Re-hitching is a swap: the previous truck is unhitched and any trailer already on the new truck is freed, so the one-per-vehicle rule holds without a manual unlink first. * **A retired trailer's past missions stay readable.** Archiving or retiring a trailer unhitches it from its truck but never rewrites the missions it ran — the mission history survives the asset leaving the working fleet. * **A tracker swapped between trucks.** The install history keeps both fittings, so a truck's telemetry record is never confused about which unit was reporting when, and the replaced unit is named on the new install. * **A device in stock.** A tracker not yet assigned belongs to no fleet and no truck; its heartbeat can still be seen at the platform level, but no fleet-scoped data is written for it until it is installed in a vehicle. * **Specs that inform but do not block.** A trailer's payload and axle figures guide an operator picking a trailer for a load, but they are not a hard dispatch guard today, so a mission is never silently blocked by a spec mismatch the operator did not see. * **A deactivated driver's missions stay readable.** Deactivating a driver detaches them from their trucks and removes them from the assignment pickers, but the missions they ran keep their driver reference — the operating record survives the person leaving the working fleet, exactly as a retired trailer's missions do. ## What's ahead * **A maintenance module.** Today an asset's status carries availability — `maintenance` and `breakdown` take a truck or trailer out of the dispatch pool, and a trailer's technical-inspection expiry is tracked and counted — but the platform stops short of running the workshop. The direction is a full maintenance module: maintenance events for vehicles and trailers alike, a preventive, corrective, and incident lifecycle, parts and line items, photo attachments, capture from the driver app where it helps, and inspection-expiry warnings wired into a workflow rather than surfaced as a bare count. The truck-versus-itself trends of Part 6 — efficiency or dwell quietly degrading over time — are the early-warning signals such a module is built to act on. ## How it connects * [Creating missions](./creating-missions) — how a vehicle, driver, and trailer are chosen for a mission, and how picking one can fill in the others. * [The mission lifecycle](./mission-lifecycle) — where the chosen vehicle and trailer do their work, and the one-active-mission-per-vehicle rule. * [Segments and traversals](./segments-and-traversals) — where the trailer and vehicle become context stamped onto every measured leg. * [Part 2 — Telemetry](../02-telemetry/) — how the installed tracker's signal becomes the vehicle's live position, and the silence expectation behind offline detection. * [The admin portal](../07-surfaces/admin-portal) — where the platform team procures, assigns, installs, and retires the hardware an owner sees fitted to a truck. * [The fleet app](../07-surfaces/fleet-app) — the owner surface where vehicles, trailers, and drivers are registered, linked, and managed. * [The driver app](../07-surfaces/driver-app) — the surface a driver record is the login identity for, entered by phone and a one-time code. * [Clients](./clients) — the register's counterpart: who the work is done *for*. --- --- url: /05-missions/clients.md --- # Clients ## What this chapter covers Every mission is run *for* someone — the shipper or consignee whose cargo moves down the corridor. Korido keeps a **client** record for each of them, and that record does real work: it remembers the places a client ships from and to so dispatch can pre-fill them, it carries the client's usual corridor and driving policy so a mission built for them starts already shaped, and it is where an owner manages the tracking links that let the client follow a delivery. This chapter describes the client record, the saved locations attached to it, how both feed mission creation, and how a client's tracking links are administered. ## The picture A client is a small hub. Locations tie it to the map; missions record which client they served; tracking links are grouped by the client they were shared with; and two defaults — a corridor and a driving policy — lean into the next mission built for that client. ```mermaid erDiagram clients ||--o{ client_locations : "saved pickup / delivery places" client_locations }o--|| waypoints : "is a waypoint in a role" clients ||--o{ tracking_links : "links shared with this client" clients ||--o{ missions : "work done for this client" clients { text name text contact_name text contact_phone text contact_email } client_locations { client_location_role role text label } ``` The client record is deliberately lean — a name and a way to reach someone — with the operational weight carried by the locations and defaults attached to it. ## The client record A **client** is one of the operator's customers: the party that owns the cargo. A tenant keeps as many as it needs. The record itself is minimal — a **name** (required and unique within the fleet), an optional **contact** (a person, a phone, an email), and free **notes**. That is all a mission form needs to surface a client, and it is enough to attach every downstream convenience to. A client's only path out of the active list is deactivation: **"Désactiver"** hides it from the new-mission pickers while leaving every past mission that named it fully readable: *"les missions passées restent accessibles."* A deactivated client can be reactivated at any time, and a name already in use is refused so two live clients can never collide. ## Saved locations A client ships from and to particular places, and typing those in for every mission would be wasted work. So a client carries **saved locations** — its **"Lieux associés"** — each one a named map **waypoint** tagged with the role it plays for this client: * **"Prise en charge"** — a pickup place, where this client's cargo is loaded; * **"Livraison"** — a delivery place, where it is dropped; * **"Les deux"** — a place that serves as both, typical of a port or depot that is an origin on exports and a destination on imports. Each association can carry a short **label** — *"Entrepôt 2"*, *"Port berth A"* — to tell two locations of the same role apart. Because a location points at a real waypoint on the shared map, it stays meaningful as that map evolves: a waypoint a client depends on cannot be quietly deleted out from under it — the association must be cleared first, and removing the association leaves the waypoint itself untouched. ## How a client shapes a mission The payoff for keeping clients is speed at dispatch. Picking a client in the mission wizard hydrates the plan from everything the record remembers. ```mermaid flowchart TD pick([Pick a client]) --> loc[Saved locations] pick --> corr[Default corridor] pick --> pol[Default driving policy] loc -->|"pickup / both"| origin[Pre-fill origin] loc -->|"delivery / both"| dest[Pre-fill destination] corr -->|"if it covers the origin + destination"| suggest[Suggested corridor] pol --> chain[Enters the rule-set chain
at the client level] origin --> plan[The mission plan] dest --> plan suggest --> plan chain --> plan ``` The client's saved locations pre-fill the route pickers by role — a **pickup** or **both** location proposes the origin, a **delivery** or **both** location proposes the destination — so a routine run starts with its endpoints already chosen. On top of that, a client can carry two **defaults**, set on the client's own form: a usual **corridor** ("Corridor par défaut"), which Korido suggests when it covers the picked origin and destination, and a usual **driving policy** ("Règles de conduite par défaut"), which enters the rule-set resolution chain at the client level. Each is optional and cleared as easily as it is set, and only a driving policy belonging to the fleet — or a corridor the fleet can see — is accepted. Both are described from the mission side in [creating missions](./creating-missions) and [driving rules](./driving-rules); here the point is that they belong to the client and travel into every mission built for it. ## Tracking links, per client The customer-facing side of a client is the **tracking link** — the scoped URL a customer opens to follow a delivery. An owner manages a client's links from the client's own record: a **"Générer un lien"** action pre-scoped to that client, and a list of the client's existing links showing each one's expiry and view count, with per-link actions to **extend** (**"Prolonger"**), **revoke** (**"Révoquer"**), **copy**, **open**, or **share over WhatsApp**. When a link is created its lifetime is chosen — **7, 14, or 20 days**, defaulting to 20 and capped at a hard maximum of 20 — and mission, client, and convoy links are gated by the last four digits of the recipient's phone. Revoking a link rotates its secret so every gate pass already issued stops working, closing the view cleanly. A link can cover a single mission or several, and one shared with a client for a convoy tracks the whole group. Alongside the per-client view, a fleet-wide tracking surface lists every link with its status, client, and convoy for an owner who wants the whole picture at once. What the customer then sees on the other side of the link is the subject of [the tracking portal](../07-surfaces/tracking-portal). ## Edge cases * **A place that is both pickup and delivery.** A port or depot that serves both directions is saved once, with the **"Les deux"** role, and it proposes itself as either endpoint in the wizard. * **A deactivated client with past missions.** Deactivating a client removes it from the new-mission pickers but preserves every mission that named it, so the operating record survives the client leaving the active book. * **A waypoint a client relies on.** A waypoint that is a client's saved location cannot be deleted while the association stands; the association is cleared first, which protects a client's remembered places from vanishing by surprise. * **A duplicate name or a duplicate location.** A second live client with the same name is refused, and adding a location a client already has under the same role is a no-op rather than an error. ## Known limitations * **The client record is a book, not a CRM.** It holds a name, a contact, notes, and its two mission defaults — enough to run dispatch and tracking — but not billing terms, addresses, credit, or a document file. Richer customer management is deliberately left out so the record stays fast to create and simple to reason about. ## What's ahead * **A fuller customer profile.** The lean record leaves room to grow toward a proper client profile — contacts and addresses, and a client's own document and billing context — without disturbing the mission and tracking flows that already lean on it. * **Proactive delivery updates.** Today a client pulls a live view from a link; the direction is to let an owner push a client the milestones that matter, so tracking becomes a conversation rather than a page the customer must refresh. ## How it connects * [Fleet assets](./fleet-assets) — the vehicles, trailers, trackers, and drivers a mission is carried by; clients are who that work is *for*. * [Creating missions](./creating-missions) — where a picked client's locations pre-fill the route and its defaults seed the plan. * [Driving rules](./driving-rules) — the rule-set chain a client's default driving policy enters. * [The tracking portal](../07-surfaces/tracking-portal) — what a customer sees through the links administered here. * [Waypoints and roads](../04-road-network/waypoints-and-roads) — the shared map points a client's saved locations are built from. --- --- url: /05-missions/mission-lifecycle.md --- # The Mission Lifecycle ## What this chapter covers A **mission** is the unit of operational accountability: one truck, one driver, one planned route, one set of pickup and delivery milestones, one audit trail. This chapter describes the eight states a mission moves through, how it advances automatically as the truck crosses geofences, how it pauses and resumes, how a mistaken start is unwound, and the rule that a vehicle can only be on one mission at a time. It is an engine-side chapter, so the edge cases are part of the contract. ## The picture ```mermaid stateDiagram-v2 [*] --> created created --> assigned: draft committed created --> cancelled assigned --> en_route_to_origin: departure detected assigned --> active: loading confirmed en_route_to_origin --> active: loading confirmed active --> paused: long stop, ignition off paused --> active: sustained movement / manual resume active --> arrived: reached destination waypoint active --> assigned: false-start recovery arrived --> completed: owner completes assigned --> cancelled en_route_to_origin --> cancelled active --> cancelled paused --> cancelled arrived --> cancelled completed --> [*] cancelled --> [*] class created warn class assigned,en_route_to_origin,active,paused,arrived safe class completed terminal class cancelled risk classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef terminal fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d ``` The two terminal states are **completed** and **cancelled**. Everything before them is a mission still in motion. ## The eight states * **created** — a draft placeholder reserved for future draft flows. Every operational mission today begins at `assigned`. * **assigned** — a truck and driver are committed to the mission, but the delivery leg has not necessarily begun. New missions start here. * **en\_route\_to\_origin** — the truck is driving to the pickup point, ahead of loading. This is the **pickup leg**. * **active** — loaded and underway on the main delivery leg. * **paused** — temporarily suspended; resume reactivates the same mission. * **arrived** — the truck has reached its destination; the mission stays open until it is completed or cancelled. * **completed** — terminal success. * **cancelled** — terminal cancellation. The valid transitions are fixed: | From | Can move to | Meaning | | --- | --- | --- | | `created` | `assigned`, `cancelled` | Future draft compatibility. | | `assigned` | `en_route_to_origin`, `active`, `cancelled` | Committed, main leg may not have started. | | `en_route_to_origin` | `active`, `cancelled` | Driving to pickup. | | `active` | `paused`, `arrived`, `assigned`, `cancelled` | Underway; `active → assigned` is false-start recovery. | | `paused` | `active`, `cancelled` | Suspended; resume reactivates. | | `arrived` | `completed`, `cancelled` | Reached destination; complete, or cancel if delivery is refused. | | `completed` | — | Terminal. | | `cancelled` | — | Terminal. | `arrived → active` is intentionally *not* valid. If a truck leaves the destination and comes back, the mission reached its destination once; the right response is to complete it, cancel it, or start a new mission — not to quietly reactivate a finished one. ## One active assignment per vehicle — and per driver A vehicle can be on only **one active assignment at a time**, and a driver can hold only **one active assignment at a time**. The guarded set for both rules is `assigned`, `en_route_to_origin`, `active`, and `paused`. Mission creation rejects any attempt to place a truck that already has an active assignment onto a second one, and equally rejects assigning a driver who already holds one — a driver cannot be at the wheel of two trucks at once. The rejection carries a clear message ("Ce chauffeur est déjà affecté à une mission") rather than a silent failure, and it fires the same way whether the clash is created at dispatch or introduced by editing a running mission's vehicle or driver. `arrived` sits slightly apart: it is a post-drive, not-yet-closed state. Treat an arrived mission as work that still needs a final decision, but the current one-active guards do **not** block reassigning that vehicle or driver once the mission has reached `arrived`. Closing it still matters for clean diaries, customer history, and reporting. ## Automatic advancement from geofences Two of the most important transitions happen on their own, driven by the truck crossing waypoint geofences rather than by anyone pressing a button. ### Departure — `assigned → en_route_to_origin` An assigned truck away from its origin is judged to have departed toward pickup when either signal fires: * **Travelled threshold** — the truck has covered more than **50 km** measured from the last real park onward. A separate accumulator is zeroed whenever a stop lasts longer than **30 minutes**, so a truck doing local errands over several days — drive across town, park overnight, drive again — cannot sum its way past 50 km and false-trigger a departure it never made. * **City exit** — the truck leaves a city geofence other than the one containing its origin. When the origin has no enclosing city, any city exit counts. ### Arrival — `active → arrived` The mission moves to arrived when the truck reaches the final destination waypoint. Waypoint evidence — the truck actually being inside the destination geofence — is what advances the mission, not a projected guess about where it should be. ### Confirming the load Between assignment and active is the moment the truck loads and departs the origin. This is where a start is easiest to get wrong: a truck shuffling around a sprawling port looks a lot like a truck leaving it. Korido confirms the load carefully so it never declares a start that did not happen: * A **single-stage origin** can activate when the truck exits the origin area with enough evidence. * A **two-stage origin** — a port or warehouse nested inside a larger city, like our tanker's Douala port — marks a *suspected* start when the inner geofence is exited and confirms only when the outer city is left. Re-entering the inner area clears the suspicion. * A driver can **confirm loading manually** when assigned to the vehicle and physically at the origin. The two-stage case is the [parent nesting](../04-road-network/waypoints-and-roads) from Part 4 doing its job. The port sits inside the city, so clearing the port still leaves the truck inside city limits: ```mermaid stateDiagram-v2 at_origin: Loaded, still in Douala port suspected: Suspected start (port cleared,
still inside the city) active: Active — main leg underway [*] --> at_origin at_origin --> suspected: exits inner port geofence suspected --> at_origin: re-enters the port (suspicion cleared) suspected --> active: exits the outer Douala city geofence ``` ## Pause and resume A pause changes how the ETA is read, how Route Guard behaves, and what a customer sees. A mission **auto-pauses** when it is active, an open stop has run past the lifecycle dwell threshold, the ignition is off, and the truck is either off-corridor or on a mission with no corridor. An operator can also pause manually with a reason: **system-detected**, **mechanical issue**, **driver rest**, **border delay**, **cargo issue**, or **administrative**. The crucial distinction is whether the pause has a **known duration**: * **Known-duration pause.** When the dispatcher records an estimated resume time — "the driver rests until 06:00 tomorrow" — the arrival estimator treats that time as the new departure anchor and keeps producing a real ETA from it. * **Indeterminate pause.** With no resume time, the arrival prediction is **frozen** and confidence drops to low. Korido would rather show an honest "we don't know when this resumes" than a precise-looking ETA it cannot stand behind. Resume clears the pause fields and returns the mission to active. A background prompt can nudge the owner or driver about a stopped active mission before manual intervention. ## False-start recovery Automatic loading inference can be wrong — a truck can shuffle around an origin in a way that looks like departure. **False-start recovery** unwinds it. When an owner flags a false start, the mission returns from `active` to `assigned`, the pause and prompt counters reset, the pickup-leg waypoints are rebuilt, trips and events created under the mistaken start are unlinked, any open deviation closes with a false-start reason, and the engine's mission sub-state is reset. The mission gets a clean second chance, free of the phantom departure. ## Edge cases * **A truck that goes dark for a long time.** A background safety net watches for a mission still non-terminal while the vehicle has been silent past a long threshold. When that happens it can cancel the mission, close open deviations, abandon unfinished prediction rows, clear the vehicle's active-mission marker, and write an audit entry explaining the automatic cancellation. This is deliberately exceptional — it represents a truck effectively gone, not an ordinary signal gap. * **Arrived is not gone.** Product logic must not treat an arrived mission as finished. The truck has reached the destination, but the mission still needs the owner to complete or cancel it. * **Departure accumulator reset.** The 30-minute reset that guards the travelled-threshold is shorter than the day-long pause that triggers a pickup-route rebuild, on purpose: departure detection cares about any genuine long park breaking the travel run, while a route rebuild only cares about a park long enough to imply a changed road. * **Lifecycle runs after waypoint detection.** Within a single telemetry batch, the mission state is decided *after* waypoint visits open and close, because those arrivals and exits are the evidence the lifecycle reads. Route Guard, ETA, and fuel logic then run against the settled mission state, never a half-updated one. ## How it connects * [Creating missions](./creating-missions) — how a mission enters `assigned` in the first place. * [Driving rules](./driving-rules) — the policy resolved onto a mission at creation and applied live. * [Progression and ETA](./progression-and-eta) — how pause, arrival, and blocking incidents shape the arrival estimate. * [Route Guard](../04-road-network/route-guard) — how lifecycle transitions drive several deviation close reasons. * Part 3 (the fleet engine) — the stops, gaps, and movement state machine that feed lifecycle evidence. --- --- url: /05-missions/creating-missions.md --- # Creating Missions ## What this chapter covers Dispatching a truck should take a tap. This chapter describes **quick-assign** — the flow that turns a client or a pickup-and-delivery pair into a fully-planned mission — the **templates** that make repeat runs one-tap, and **convoy dispatch** that sends several trucks down the same route as one group. It builds on [corridors](../04-road-network/corridors) and feeds the [driving rules](./driving-rules) a new mission inherits. ## The picture ```mermaid flowchart TD start([New mission]) --> q{Client known?} q -->|Yes| tmpl[Suggest client's templates
+ default corridor,
ranked by usage history] tmpl --> apply[Apply template:
corridor + direction + fleet
+ cargo] q -->|No| od[Pick pickup + delivery] od --> infer[Corridor inference:
find the named path covering both
either direction, endpoints or interior] infer --> derive[Derive waypoints → trim to sub-path] apply --> build[Build mission:
waypoints, road segments,
frozen acceptance region] derive --> build build --> convoy{Convoy?} convoy -->|Yes, × N trucks| grp[N missions + one convoy grouping] convoy -->|No| solo[One mission] ``` ## Quick-assign Quick-assign has two entry points, chosen by whether the operator knows the client. ### Client known — suggestions and one tap When the operator picks a **client** — a record described in full in [clients](./clients) — Korido offers what that client usually ships: * the client's **default corridor**, if one is set; and * the client's saved **pickup and delivery locations**, which pre-fill the route's origin and destination by role; and * the client's **templates**, ranked by usage — each template records how many times it has been used and when it was last used, so the routes this client actually runs float to the top. Choosing a template applies its preset — corridor, travel direction, preferred vehicle, driver, and trailer, and default cargo — in a single tap. The operator confirms, and the mission is built. ### No client — pickup, delivery, and inference When there is no client, the operator names a **pickup** and a **delivery** place. Korido then runs **corridor inference**: it searches for the named corridor whose path covers both places. As described in the corridors chapter, a match can run in either travel direction and can be the corridor's endpoints or an interior sub-path of a longer corridor. From the matched corridor, Korido **derives** the ordered waypoints between pickup and delivery, and **trims** them to exactly the requested span when the pair names an interior stretch. The derived waypoints become the mission's route; the corridor's roads resolve into the mission's ordered road segments; and their buffered polygons, unioned with the waypoint geofences, become the mission's frozen **acceptance region**. An operator can also drop an **ad-hoc place** on the route map — an "add a place" mode where a map click names a new waypoint, gives it a type and a position in the sequence, and splices it into the route immediately. ## Driving policy The mission's driving policy is chosen in the same flow. The creation wizard carries a **"Règles de conduite"** selector whose default option is *inherit* — and the inherit option names the policy the resolution chain would apply, showing the template's or the client's rule set by name when one is set, and the plain tenant-default wording when neither is. An operator who wants a specific policy picks a named **rule set** instead; that explicit choice is validated to the tenant before it is accepted. Whatever is chosen, the mission's rule set is **resolved at creation** through the precedence chain — explicit pick, then template, then client, then corridor, then the tenant default. A concrete `rule_set_id` is stored when the chain resolves to an explicit, template, client, or corridor policy, so a later change to *which* rule set that upstream object defaults to does not rewrite a mission already dispatched. When the chain falls all the way through to the tenant default, the mission stores `null`; runtime policy resolution then reads the tenant default that is current at the moment rules are applied. The full chain and how the resolved policy is applied live are the subject of [driving rules](./driving-rules). ## Templates A **template** is the full preset for a repeated run. It is what makes the client-known path a single tap. A template carries: * an optional **client scope** — global to the tenant, or narrowed to one client; * a **corridor** — required, because the corridor *is* the route preset. The origin, transit, and destination waypoints are derived from the corridor's ordered waypoints at apply time, honoring the template's travel direction (reverse folds the sequence). A route worth templating is worth naming; * a **preferred vehicle, driver, and trailer**; * **default cargo** — description, weight, and planned distance; * an optional **rule set** recorded on the template. Because the corridor is a real reference, the template's route carries full integrity: fix the corridor, and every template and mission built from it follows. ## Convoy dispatch Trucks on the Douala–N'Djamena corridor often travel together for security. A **convoy** is that grouping. Convoy dispatch is the same quick-assign flow run across N vehicles at once: it produces **N independent missions** — each with its own truck, driver, trailer, and engine state — plus **one convoy grouping** that ties them together for reporting and for the dispatcher's view. The convoy is a reporting and UX unit, not a shared state machine; each mission lives its own lifecycle. Convoy dispatch is **all-or-nothing**. Before it creates anything, it validates the whole group at once and rejects the submit if any truck already holds an active mission — naming *every* busy truck, not just the first, so the operator fixes the group in a single pass instead of clearing clashes one at a time. And if a create fails partway — a truck picked up a mission in the instant between the check and the write — the missions already created and the convoy grouping are rolled back automatically. A convoy dispatches whole or leaves nothing behind: the operator never lands a half-formed group with some trucks committed and others rejected. ## What creation writes Building a mission writes the durable plan first, then enriches it. The order matters: routing and ETA both call an external service, and no external call should ever be allowed to hold a database write open. So Korido splits the work at the transaction boundary. ```mermaid sequenceDiagram participant Op as Dispatcher participant DB as Mission write (one transaction) participant Ext as Routing + ETA (external) Op->>DB: Commit the durable plan Note over DB: contract · waypoints · road segments ·
frozen acceptance region · resolved rule set ·
stamped assigned · KRD-000001 DB-->>Op: Mission exists, ready to dispatch DB->>Ext: After commit — draw route geometry, seed first ETA Ext-->>DB: Enrich the committed mission ``` In one transaction Korido writes the mission contract — vehicle, driver, optional trailer, client, convoy, cargo, template, planned departure, origin, and destination — the ordered mission waypoints, the ordered mission road segments, the frozen acceptance region, and the resolved driving policy. The rule-set chain runs inside this same transaction, so a mission is never committed without its policy already decided. The mission is stamped as `assigned` and given a human reference number of the form `KRD-000001`. Route geometry and the first ETA estimate are computed *after* the transaction commits, so external routing work never holds the mission write open — and a routing hiccup can never lose a dispatch, only defer its first prediction. ## Edge cases * **Vehicle already busy.** Creation rejects any truck that already holds a non-terminal mission — the one-active-mission-per-vehicle rule is enforced at the moment of creation, not discovered later. * **Driver already busy.** Creation likewise rejects a driver who already holds a non-terminal mission on another truck — one driver, one active mission. In convoy dispatch the picker enforces this up front: once a driver is chosen for one truck in the group, they drop out of the choices for the others, so the same person can never be assigned to two trucks in a single submit. * **Interior sub-path.** When inference matches an interior stretch, the derived route is trimmed to exactly the pickup-to-delivery span, dropping the corridor's outer legs. * **Enrichment fails after commit.** If the post-commit route or ETA work fails, the mission still exists — it simply carries no prediction until a later refresh fills it in. A dispatch is never lost to a routing hiccup. * **A corridor leg with no road.** A mission can be built even when a leg has no curated road, but it will have an incomplete acceptance region, and Route Guard skips a mission with no corridor polygon. Curating the missing road repairs future missions. ## How it connects * [Clients](./clients) — the client record whose saved locations and defaults power the client-known path. * [Corridors](../04-road-network/corridors) — resolution and the inference that powers the no-client path. * [The mission lifecycle](./mission-lifecycle) — where a newly-created mission goes from `assigned`. * [Driving rules](./driving-rules) — the rule-set chain a mission's policy resolves through at creation, and the wizard's driving-rules selector. * [Progression and ETA](./progression-and-eta) — the arrival estimate seeded at creation and refreshed as the truck moves. --- --- url: /05-missions/driving-rules.md --- # Driving Rules ## What this chapter covers A **rule set** is the driving policy a mission runs under: how fast the truck may go, when it may drive at night, where it may refuel, where it may not stop, and how its hours of service are shaped. This chapter describes what a rule set holds, how a mission inherits one, why edits take effect immediately on active missions (unlike the frozen acceptance region), and what enforcement raises when a rule is broken. It connects the mission plan to the alerts of Part 6. ## The picture ```mermaid flowchart TD subgraph Rule-set precedence e[Explicit choice] --> t[Template] t --> c[Client default] c --> co[Corridor default] co --> td[Tenant default] end td --> rs[Effective rule set] rs --> live[Applied live to active missions] rs --> enf[Enforcement detectors] enf --> a1[speeding] enf --> a2[night-driving violation] enf --> a3[unauthorized refuel] enf --> a4[prohibited stop] ``` The chain runs most-specific to most-general: the first policy that resolves wins. ## What a rule set holds A rule set is a named driving policy with four groups of settings. **Speed.** A **maximum speed** and a **tolerance** above it. The tolerance is the grace band before an over-speed becomes an alert. When no maximum is set, speed is not enforced. **Night driving.** A **night window** defined by a start and end hour — defaulting to **22:00–05:00** in the tenant's timezone — and a **mode**: `forbid` (no night driving), `allow` (night driving is fine), or `reduced_speed` (a recognised night setting). The defaults target the corridor's realities. **Hours of service.** The inputs that shape how long a driver may work: * maximum **continuous driving** before a break — default **4.5 hours**; * required **break** length — default **45 minutes**; * maximum **daily driving** — default **9 hours**; * required **daily rest** — default **11 hours**. These feed the rest-time modelling in the arrival estimate, described in [progression and ETA](./progression-and-eta). **Places.** Two lists of waypoints attached to the rule set: * **authorized refueling stations** — a whitelist; a fill anywhere else is a violation; and * **prohibited stop locations** — a blacklist; a stop that begins inside one of these geofences is a violation. Both are real references to waypoints, so they stay meaningful as the map evolves. ## Inheritance A mission's policy is resolved **once, at creation**, walking the chain most-specific to most-general and stopping at the first level that names a rule set: 1. an **explicit choice** the operator made for this mission — validated to the tenant before it is accepted; 2. otherwise the **template** the mission was built from; 3. otherwise the **client's** default; 4. otherwise the **corridor's** default — honored only when that rule set belongs to the mission's own fleet, since a corridor can be shared across fleets and its default may point at another tenant's policy; 5. otherwise the **tenant default** — the single fall-back policy every tenant has. The resolved rule set is stored on the mission. When one of the first four levels names a policy, that exact rule set is pinned to the mission, so re-pointing a client's or corridor's default later does not rewrite a mission already dispatched. When none of them does, the mission records *no* explicit rule set and the engine reads the tenant default at run time — so an edit to the tenant default's own contents reaches those missions live, without anything being re-resolved. The creation wizard exposes this as the **"Règles de conduite"** selector. Its default *inherit* option names the policy the chain would apply — showing the template's or the client's rule set by name when one is set — and an operator can override it with any named rule set for this mission alone. ## Live, not frozen Here is the deliberate contrast with the acceptance region. A mission carries two kinds of plan, and they are treated as opposites on purpose: ```mermaid flowchart LR subgraph geo["Geometry — the acceptance region"] g1[Frozen at creation] --> g2[An open deviation is measured
against it; it must not move] end subgraph pol["Policy — the rule set"] p1[Live, no snapshot] --> p2[Edits bite immediately on
every active mission] end ``` A mission's geometry is **frozen** at creation because an open deviation references it. A mission's **policy is live**: rule-set edits take effect immediately on active missions, with no snapshot. If a dispatcher bans a fuel station today, it is banned for every truck already on the road today. Policy is meant to bite the moment it changes; geometry must stay stable so incidents keep their meaning. The two are different because they serve different masters — one is a live instruction, the other is a measurement baseline. ## What enforcement raises Enforcement reads the applicable rule set and its waypoint lists live, and raises alerts through the same pipeline that carries every other vehicle event: * **Speeding** — the truck exceeds the maximum plus tolerance. * **Night-driving violation** — the truck is moving during the night window under a policy that forbids it. * **Unauthorized refuel** — a fuel fill happens at a station outside the rule set's authorized whitelist. Korido already recovers where a fill happened, so an off-whitelist fill is caught — even at a genuine fuel station that simply sits off this mission's list. * **Prohibited stop** — a stop begins inside a prohibited-stop waypoint's geofence. Each of these is a detector emitting an event; how those events reach a person is the subject of Part 6. ## Edge cases * **A fuel station banned mid-mission.** Because policy is live, a station removed from the whitelist while a truck is en route is enforced from that moment; a fill there afterwards is an off-whitelist violation even though it was permitted at dispatch. * **No maximum speed set.** A rule set without a maximum speed simply does not enforce speeding — the field is optional, and its absence means "no limit configured," not "zero." * **Reduced-speed night mode.** At the enforcement boundary a night policy resolves to either `forbid` or `allow`, so a `reduced_speed` setting enforces like `allow` — it records the intent without capping the limit. * **No rule set resolved.** If every step of the chain comes up empty, the mission falls back to the tenant default, which always exists — a mission is never left with no policy at all. ## Known limitations * **Hours of service shape the estimate, not an alarm.** The four enforcement detectors raise speeding, night-driving, unauthorized-refuel, and prohibited-stop alerts. The hours-of-service inputs feed the arrival estimate's rest modelling, but a driver exceeding continuous or daily driving is not itself raised as a violation — hours of service is a planning input today, not an enforced alarm. * **Unauthorized-refuel enforcement is opt-in through the whitelist.** The refuel check flags a fill only against a rule set that actually lists authorized stations. A rule set with an empty whitelist does not treat every fill as unauthorized — it simply raises nothing, because "no list configured" reads as "not enforced," not "deny all." ## What's ahead * **A surface to author rule sets.** Policies are resolved through the chain, selected on the wizard, and applied live — but the rule sets themselves, their speed limits, night window, hours-of-service inputs, and place lists, are provisioned beneath the product rather than edited in it. The next step is an operator-facing surface to create and edit named rule sets and attach them as client, corridor, or template defaults. * **A modelled reduced-speed night mode.** The `reduced_speed` night mode is recognised as a setting; giving it a real lower-limit model — so night driving is *capped* rather than simply allowed or forbidden — is a planned refinement. ## How it connects * [Creating missions](./creating-missions) — where a mission's driving policy is set from the template, client, or corridor it is built from. * [Progression and ETA](./progression-and-eta) — how the hours-of-service inputs shape the rest-time modelling in the arrival estimate. * [Route Guard](../04-road-network/route-guard) — the deviation model that runs alongside stop and refuel enforcement. * Part 6 (fleet intelligence) — how speeding, night-driving, unauthorized-refuel, and prohibited-stop alerts reach owners and dispatchers. --- --- url: /05-missions/progression-and-eta.md --- # Progression and ETA ## What this chapter covers This chapter is about how Korido answers "when will it arrive?" — and, just as importantly, how it refuses to *promise* an answer. It describes the philosophy that **an ETA is an indicator, not a promise**, how live progression is tracked along the route, how a continuously re-estimated arrival is built from per-segment history, known frictions, and rest modelling, what the confidence levels mean (including **blocked**), and why alerts are progress-based rather than target-time breaches. ## The picture ```mermaid flowchart LR subgraph Inputs b[Per-segment baselines
history by hour-of-day,
seasonal adjustment over route] f[Known frictions
hotspot dwell, slowdown zones,
active incidents] r[Rest modelling
breaks, daily rest, night stops] end b --> est[Arrival estimator
every 10 minutes] f --> est r --> est proj[Live progression
position projected on route] --> est est --> pa[predicted_arrival_at
+ confidence] est --> al[Progress-based alerts:
route blocked, stalled] ``` Note what is *not* an input: a promised delivery time. Korido never measures the prediction against a target. ## ETA is an indicator, not a promise Nobody can promise arrival across 2,000 km of real corridor. The road from Douala to N'Djamena is a chain of borders, weighbridges, security stops, and the ordinary friction of a long haul through two countries. Any system that stamps a mission with a contractual arrival time and then measures reality against it is selling a fiction. So Korido does not. It keeps two clearly separated timestamps, and the separation is the whole design — one is inert, the other is alive: ```mermaid flowchart TD subgraph target["target_arrival_at"] t1[Display-only informational target] --> t2[The engine never reads it,
never compares, never alerts on it] end subgraph pred["predicted_arrival_at"] p1[Live best guess] --> p2[Re-estimated every ~10 min
while moving; carries its own confidence] end ``` * **`target_arrival_at`** is a display-only informational target arrival. The engine never reads it, never compares against it, and never raises an alert because it was missed. * **`predicted_arrival_at`** is the live, re-estimated best guess. It is refreshed roughly every **10 minutes** while the mission is moving, and it is honest about its own uncertainty. The product promise is a good *indicator*, continuously updated, that a dispatcher can act on — not a countdown to a deadline. ## Live progression Progression is where the truck is along its route right now. The truck's latest accepted position is projected onto the mission's stored route, and that projection yields the current segment, how far into it the truck is, how far it has come along the whole route, and how far remains. This is a geometry calculation over the committed route, cheap enough to do often, and it is what the map and the progress bar are drawn from. Progression is an *input* to the estimate, not the authority on which segment is done. Durable segment completion is decided by waypoint evidence — the truck actually crossing a geofence — not by projection, which keeps the record honest near boundaries and after reroutes. ## The re-estimated arrival The predicted arrival is built, every tick, by layering three kinds of knowledge over the remaining route. **Per-segment baselines.** For each remaining segment, Korido looks at the history of similar traversals of the same road, bucketed by **time of day**, and takes a typical driving time. A **seasonal adjustment** — the four-bucket Sahel model — is layered over the route as a whole. Where a segment has enough observed history, that empirical baseline is used; where it does not, a routed estimate bootstraps it until observations accumulate. This is why the estimate improves as a fleet runs a corridor: the road teaches the model how long it really takes. **Known frictions.** On top of the baselines, Korido adds the delays it already knows are on the road ahead: * **hotspot dwell** — the typical wait at recurring stop clusters (borders, weighbridges); * **slowdown zones** — stretches where trucks routinely lose speed; * **active incidents** — a live anomaly on the remaining route contributes its expected drag. **Rest modelling.** Finally, the estimate accounts for the human being driving. The hours-of-service inputs from the mission's [rule set](./driving-rules) insert mandatory breaks, daily rest, and night stops into a long leg — a truck that must rest 11 hours tonight has that rest folded directly into its arrival estimate. ## Confidence Every predicted arrival carries a **confidence** level, because a number without a sense of how much to trust it is worse than useless. * **High / medium / low** — reflect how much of the route rests on solid history versus bootstrap fallbacks, and how wide the spread of likely outcomes is. Low confidence also covers a mission paused with no known resume time, where the arrival is frozen rather than guessed. * **Blocked** — a level of its own, reserved for when the road ahead is genuinely closed. When an active incident blocks the road ahead, the estimate is frozen and marked blocked: the honest answer is "the road is closed," not a fabricated later time. When a recently-cleared incident intersects the mission's route, the mission surfaces a "route recovering" signal, so an operator sees the road opening back up. ## Alerts are progress-based Because there is no promise, there is nothing to breach. Delay alerts are built on **progress**, not on a target time: * **Route blocked** — an incident ahead blocks forward progress. * **Stalled** — the mission is paused with no known resume time, so its progress is indeterminate. There is no "arriving late versus the deadline" alert, because Korido makes no deadline to be late against. An operator is told the road is blocked or the truck has stopped moving — actionable facts — rather than that an invented promise has slipped. ## Edge cases * **A blocked road ahead.** When an incident blocks forward progress, the arrival prediction is frozen, confidence becomes blocked, prediction rows hold their last value, and ordinary delay alerts are suppressed for that tick — the mission is genuinely blocked. * **A pause with no known resume.** The arrival is frozen and confidence drops to low. Korido will not show a precise ETA for a mission that is explicitly suspended with no end in sight. * **A brand-new corridor.** With no observed history, every segment leans on the routed bootstrap, confidence is lower, and unknown dwell contributes zero rather than an invented number. The estimate sharpens as the fleet runs the route. * **The pickup leg.** Before loading, the estimate is the arrival at *origin*, and it is refined on every batch as the truck approaches — so a truck running ahead of schedule gives the dispatcher an early signal instead of a stale departure-time number. * **A rerouted segment.** When the truck's road changes mid-mission, only the affected segment's geometry is replaced; the rest of the route and its history are preserved, and stale prediction rows are superseded rather than deleted. ## Known limitations * **The estimate is only as sure as the road's history.** Confidence reflects how much of the remaining route rests on observed traversals versus routed bootstraps, and how wide the spread of likely outcomes is. A brand-new corridor leans entirely on the routed bootstrap and reports lower confidence until the fleet has run it enough times for the baselines to settle. * **When it cannot know, it freezes rather than guesses.** Two conditions take the estimate out of ordinary re-estimation: an active incident blocking the road ahead marks the prediction **blocked** and freezes it, and an indeterminate pause with no resume time freezes it at **low** confidence. In both cases Korido holds the last honest number instead of extrapolating one it cannot stand behind, and unknown dwell contributes zero rather than an invented delay. ## What's ahead * **Self-calibrating friction.** The frictions modelled today are hotspot dwell, slowdown zones, and active incidents, layered over per-segment baselines and a coarse four-bucket seasonal model. As traversals accumulate, Korido will attribute delay more finely — splitting a leg's drag across its causes and sharpening the seasonal and weather signal — so the estimate increasingly explains itself alongside predicting a time. ## How it connects * [Segments and traversals](./segments-and-traversals) — the observed history that per-segment baselines are learned from. * [Driving rules](./driving-rules) — the hours-of-service inputs behind rest modelling. * [The mission lifecycle](./mission-lifecycle) — pause with and without a known resume, and arrival. * [Route Guard](../04-road-network/route-guard) — deviation, distinct from a blocked road ahead. * Part 6 (fleet intelligence) — hotspots, slowdown zones, and incidents as cross-fleet learned frictions, and how progress alerts reach people. --- --- url: /05-missions/segments-and-traversals.md --- # Segments and Traversals ## What this chapter covers A journey only becomes something you can compare, price, and learn from once it is cut into measurable pieces. This chapter describes how **waypoint-visit boundaries** divide a mission into **segment traversals**, what each traversal records, how the prediction made beforehand is kept beside the actual outcome, and why these records are the raw material for the analytics of Part 6. It is the point where the road network of Part 4 and the mission of Part 5 turn into durable, comparable facts. ## The picture ```mermaid flowchart LR vA["visit: city A
(exit)"] -->|driving between| vB["visit: city B
(enter)"] vB --> t[["one segment traversal
A → B"]] t --> facts[transit + driving time · distance · speeds
stops · gaps · idle · deviations
fuel + cost · road variant taken] t --> dims[context: driver · client · cargo · trailer
convoy · vehicle · weather · season · calendar] t --> pva[predicted vs actual] ``` The exit of one Tier-1 waypoint and the entry of the next bound exactly one traversal. ## Boundaries cut the journey The cuts are the **waypoint visits** from [Part 4](../04-road-network/waypoints-and-roads). When a truck exits one **Tier-1 boundary** and enters the next, that span is one **segment traversal** — one observed crossing of one road by one truck. Only specific places are boundaries. Tier-1 boundaries are the cities every truck on the road passes through, plus the specific origin and destination of the mission at hand. Service stops, fuel stations, rest areas, borders, and ad-hoc pins are valuable context, but they are *not* automatically boundaries. This is the "compare like with like" rule: a Douala-to-Ngaoundéré segment can be compared across hundreds of missions, but a one-off rest stop cannot become its own statistical segment without making everything incomparable. Optional stops are recorded as facts *inside* a traversal. Take our tanker's first leg. Douala and Ngaoundéré are cities, so they cut; the weighbridge and the fuel stop between them are logged as context inside the Douala-Ngaoundéré traversal: ```mermaid flowchart LR A["Douala
(city — CUT)"] -->|driving| w["weighbridge
· fact inside ·"] -->|driving| f["fuel stop
· fact inside ·"] -->|driving| B["Ngaoundéré
(city — CUT)"] A -.->|one segment traversal: Douala → Ngaoundéré| B classDef cut fill:#dff,stroke:#068 classDef inside fill:#f6f6f6,stroke:#999 class A,B cut class w,f inside ``` ## What a traversal records A segment traversal is a rich, immutable fact. It records, for that one crossing: * **Time** — the **transit time** (wall clock from exit to entry) and the **driving time** within it. The gap between them is where the delay lives. * **Distance and speed** — how far the truck drove, and its average and maximum speed. * **What interrupted it** — the count and total duration of **stops**, of signal **gaps**, and of **idle** time. * **Deviations** — the total off-corridor distance and time during the span, and how many times the truck strayed. * **Fuel and cost** — the fuel consumed and the efficiency over the leg, the unit fuel price, and the total cost of the leg (fuel, driver, trailer, tolls, and overhead) where the inputs are available. * **The road actually taken** — a link to the curated road variant the truck drove, resolved by testing the truck's path against the road polygons. Where the path cannot be resolved to a known road, analytics fall back to a route grouping key, so the traversal is still comparable by its endpoint pair. And it records **context** — the dimensions that let one traversal be sliced against another without ever re-joining the mission: * the **driver**, the **client**, the **cargo** (loaded or empty, and its weight), the **trailer**, and the **convoy**; * the **vehicle's** make, model, and year; * the **weather** class, the **season** (a four-bucket Sahel model — dry-cool, dry-hot, wet-early, wet-peak), and the **calendar** class of the day; * the driver's cumulative driving since 04:00 local that day and over the trailing 7 days, for fatigue analysis; * the local **hour of day** and **day of week** the crossing fell on. These dimensions are captured *at the moment the segment closes*, frozen onto the row, so a report grouping thousands of traversals never has to reconstruct what was true at the time. ## Predicted versus actual, side by side Each traversal also preserves what the estimator **predicted** for it at the time the segment started — the predicted duration and the confidence level behind it — right next to what actually happened. Keeping the two together on the same row is what lets analysis ask "where did the model expect this leg to take, and how did reality differ?" without rebuilding the estimator's past state. This is not an accuracy scorecard to grade against a promise; it is a correlation input — one more dimension to combine with the others. ## Why this exists These records are the raw material for everything in Part 6. Once traversals accumulate, they can be combined to see what no single mission shows: a truck compared with itself over time (an early maintenance signal when efficiency or dwell quietly degrades), a truck compared with another, a driver with another, and correlations across season, weather, cargo weight, vehicle age, and time of day that are invisible until the dimensions are combined. Per-client profitability falls out of the client dimension; corridor benchmarks fall out across tenants. The whole apparatus of dimensions on a traversal exists precisely so those questions become plain queries. ## Edge cases * **GPS jitter at a boundary.** A noisy signal can produce enter-exit-enter within a single batch. The record is keyed to the closing visit, so a jittered boundary yields one traversal, not a burst of phantom crossings. * **Places passed but not entered.** Planned waypoints the truck drove close to but did not enter are captured as a soft list on the traversal, so analysis can see what was bypassed — while tolerating a rare later deletion of one of those waypoints. * **An unresolved road.** When the truck's path cannot be matched to a curated road variant, the road link is left empty and analytics group by the endpoint pair instead; the traversal survives, attributed more coarsely. * **Repeated visits to the same city.** A route that touches the same city twice is handled with occurrence-safe boundaries, so the right crossing closes the right segment. * **The final leg.** The destination segment can close after the mission reaches arrived — including when an owner completes the mission while the truck is already inside the destination geofence — so the last useful actual is captured rather than skipped. * **Geometry edited later.** Each traversal freezes a snapshot of the boundary waypoints' position and radius, so editing a place afterwards never rewrites what a historical row measured. ## How it connects * [Waypoints and road geometry](../04-road-network/waypoints-and-roads) — the visits and Tier-1 boundaries that cut the journey. * [Progression and ETA](./progression-and-eta) — the per-segment baselines these traversals feed back into, and the predictions preserved beside the actuals. * [The mission lifecycle](./mission-lifecycle) — arrival and completion, which close the final traversal. * Part 6 (fleet intelligence) — the analytics, scorecards, trends, and correlations built on these records. --- --- url: /05-missions/documents.md --- # Documents ## What this chapter covers A corridor run generates paper: customs forms stamped at the border, a signed delivery note at the depot, a fuel receipt at the station, a photo of a broken part on the shoulder. This chapter describes the full life of that paper as Korido handles it — a driver **captures** a document on the phone, it uploads when the network allows and **attaches** to the mission and truck it belongs to, an owner **reviews** it in a document review center, and a decision to send it back reaches the driver as a directed **retake** request. It is the evidentiary half of a mission: where segments and traversals are the measured record of the run, documents are its human-readable proof. ## The picture Capture and review are two people working the same document from opposite ends — the driver on the road, the owner at the desk — joined by an upload that survives poor coverage and a directed request that closes the loop when paper needs redoing. ```mermaid sequenceDiagram participant D as Driver (phone) participant Q as Local queue participant K as Korido participant O as Owner (review center) D->>Q: Capture photo · pick category Note over Q: Copied into app storage,
visible in history at once Q->>K: Upload when network allows K->>K: Attach to mission · vehicle · driver K->>O: Appears in the review center O->>K: Decide: Valider / À corriger / Rejeter K-->>D: "À corriger" or "Rejeter" →
directed retake request D->>Q: Recapture → new document ``` The two ends never block each other. A driver captures whether or not there is signal; an owner reviews whenever they get to it; the retake request waits in the driver's notification center until the driver acts. ## Capturing on the road Capture is built for a tired driver on a bad connection, so it is deliberately short: four large tiles, a camera in one tap, and an optional location tag. The four **capture presets** cover what a corridor run actually produces: * **"Carburant"** — a fuel receipt, with an optional prompt for the price and litres shown on the slip. * **"Dépenses"** — any other paid expense: a toll, a weighbridge fee, a repair bill. * **"Douane / livraison"** — the mission paperwork: a customs document, a cargo manifest, a road-control paper, or a signed delivery note. * **"Camion / permis"** — the vehicle and driver papers: registration, insurance, a driving licence, or a photo of a broken or replacement part. The two document-rich presets open a short type picker so a capture is filed precisely; the two expense presets can carry an amount, a vendor, and — for fuel — a litre count. Under the tiles, every capture is sorted into one of seven **categories** (mission, delivery, expense, vehicle, driver, maintenance, other) and a specific document type, so the owner's filters later have something exact to bite on. Each captured photo carries an optional **location tag** — where the phone was when the picture was taken — purely as context. It is never a fleet position: truck position comes only from the hardware tracker, and a document's location is a convenience stamp on an image, nothing more. ### Surviving the corridor The corridor has long stretches with no usable data, so capture works fully offline. Every photo is copied into the app's own storage the moment it is taken and entered into a local upload queue. ```mermaid stateDiagram-v2 [*] --> pending: captured, copied locally pending --> uploading: network available uploading --> uploaded: accepted by Korido uploading --> failed: upload error failed --> pending: retry (backoff) or manual "Réessayer" uploaded --> [*] ``` A queued document is visible in the driver's own history immediately, with a working preview, before it has reached the backend at all. Uploads retry on their own when connectivity returns, backing off between attempts and surviving an app restart; a genuinely stuck one can be re-sent by hand. Because each upload carries a stable identity, a retry that arrives twice is recognised as the same upload, so one capture always becomes exactly one document. A single photo is capped at 5 MB. ## The review center On the fleet side, documents land in a review center — a filterable list beside a preview-and-decision panel. The ordinary loop is driver capture and owner review, but platform admins can also list, preview, upload, and review tenant documents through the admin portal for support and operations work. The list is built to be sorted through fast. An owner filters by **category** and by **status**, narrows to a specific **mission, vehicle, trailer, or driver**, or searches across titles, plates, mission references, and driver names. Selecting a row opens the photo with its metadata — the capture context, any expense figures, and the automatic quality read — and, beneath it, the decision controls under the heading **"Décision propriétaire"**. A document moves through a small, honest set of states, and the owner's three decisions are what move it: ```mermaid stateDiagram-v2 [*] --> Recu: uploaded Recu --> Valide: Valider Recu --> ACorriger: À corriger (note required) Recu --> Rejete: Rejeter (confirm) ACorriger --> Valide: fixed on re-review Rejete --> Valide: fixed on re-review Valide --> [*] ``` * **"Valider"** accepts the document. It is marked **"Validé"** and its quality reads as acceptable; the driver sees the validation in their app. * **"À corriger"** asks for a fix and **requires a note** explaining what is wrong — a correction request with no reason would be no help to the driver. * **"Rejeter"** refuses the document outright, behind a confirmation step, with an optional note for the reason. Every decision is recorded twice over: the outcome, the note, the reviewer, and the moment are stamped onto the document itself, and a separate append-only **audit trail** captures the review as its own event. Nothing about a review is silent — the review center always answers "who decided what, and when." ## The retake loop A **"À corriger"** or **"Rejeter"** decision is a request back to the person who took the photo. When either lands on a document that has a related driver, Korido raises a directed **retake request** — a **"Documents à reprendre"** notification pushed to that driver — carrying enough context (the document, its type, the mission and truck) to deep-link straight to a recapture. On the driver's phone the request appears as **"Retour du bureau"**: the office's note, the outcome (**"Validé"**, **"À reprendre"**, or **"Refusé"**), the mission and truck it concerns, and a **"Reprendre le document"** action that opens the camera or gallery to send a fresh one. A single document can be reviewed more than once — rejected, recaptured, rejected again — and each rejection is a distinct request the driver must see: every deliberate correction or rejection raises its own fresh notification. The request is produced after the review is safely recorded and is isolated from it, so a delivery hiccup on the notification never undoes or delays the owner's decision. A **"Valider"** decision ends the review immediately, and a document with no related driver simply has no one to notify. ## Edge cases * **Captured with no signal.** A photo taken offline is stored, queued, and shown in history with a preview at once; it uploads when coverage returns. Closing or restarting the app does not lose it. * **The same upload arrives twice.** A retry that reaches the backend after the first attempt already succeeded is recognised by the capture's stable identity and ignored — one capture becomes exactly one document. * **Rejected again and again.** Each rejection or correction request is its own notification, so a driver working through several bad captures sees each request distinctly rather than one stale badge. * **A rejected document with no driver.** A document that carries no related driver can still be reviewed and filed; there is simply no retake request to send, because there is no one to send it to. * **The photo file is missing from storage.** The review center still shows the document's metadata and says plainly that the image itself is not present, rather than failing the whole row. ## Known limitations * **Quality checking is a light first pass, not extraction.** Each capture gets a simple quality read — flags like blurry, too dark, or cropped — but Korido does not yet read the document: it does not extract the amount from a fuel slip, the reference on a customs form, or the signature on a delivery note. The figures a driver types into the expense prompt are the figures Korido has; the image is proof for a human, not yet data for a machine. * **Driver captures are photos.** A driver capture is an image up to 5 MB. Multi-page PDFs and scanned files are outside the phone capture flow today, though the document store itself can hold private document objects such as admin-uploaded PDFs. ## What's ahead * **Documents that read themselves.** The natural next step is turning the photo into structured facts: reading the litres and amount off a fuel receipt to reconcile against the tank sensor, matching a customs reference to the border waypoint it was stamped at, confirming a delivery note against the mission it closes. The quality flags captured today are the groundwork for that intelligence. * **A wider owner capture net.** The same review center already supports platform-admin document work. The next owner-facing step is letting a fleet add documents from beyond the driver's phone — attaching vehicle papers, importing from another channel — so paperwork gathers in one reviewed place rather than several. ## How it connects * [The driver app](../07-surfaces/driver-app) — the capture surface, its offline queue, and the notification center where a retake request arrives. * [The fleet app](../07-surfaces/fleet-app) — the owner surface the review center lives in, beside the live map, diary, and alerts. * [The mission lifecycle](./mission-lifecycle) — the run a document is captured against and attached to. * [Fleet assets](./fleet-assets) — the vehicle, trailer, and driver a document files itself under. * [Segments and traversals](./segments-and-traversals) — the measured record of a run, of which documents are the evidentiary counterpart. --- --- url: /06-intelligence.md --- # Part 6 — Fleet intelligence: from signals to alerts and insight Parts 2 through 5 turned a stream of tracker frames into a structured record: where each truck is, what it is doing, which mission it is running, and how far along it has come. That record is faithful, but it is also silent — it will hold a fuel drain, a border closure, and a truck that is quietly getting slower on the same climb every week, and never tell a soul. This part is where the record starts to *speak*. Fleet intelligence reads the accumulated record and produces the two things a person actually acts on. One is an **alert**: this specific thing, on this truck, right now, matters — and here is who should know. The other is **insight**: this is what the road is really like, and this is how your trucks and drivers measure up on it. One is about attention in the moment; the other is about learning over time. Three chapters follow that arc, from the sharp end to the long view: * **[Alerts](./alerts)** — how a raw observation becomes a decision: is this worth a person's attention, whose, and through which channel. The event catalog that declares every alert's behavior once, the deduplication that keeps a persisting condition from becoming a storm, and the pipeline that carries an alert to a dashboard, a phone, and WhatsApp. * **[Spatial intelligence](./spatial-intelligence)** — what the *whole fleet* learns about the road. Slowdown zones where every truck crawls, hotspots where they all stop, and anomalies disrupting the corridor today — cross-tenant knowledge that sharpens every ETA and ages out when conditions clear. * **[Segment analytics](./segment-analytics)** — what a fleet learns from its own crossing records: truck against truck, driver against driver, and — most valuable of all — a truck against its own past, the earliest warning that something is wearing out. A single idea threads the three together: **intelligence is distilled from the same immutable record, never bolted onto a separate one.** An alert reads the events the engine already wrote; a benchmark reads the traversals it already froze; road knowledge is clustered from traces it already kept. This part only reads what the earlier parts established and turns it into something worth a person's time. --- --- url: /06-intelligence/alerts.md --- # Alerts: how Korido decides something matters and tells someone ## What this chapter covers Korido watches every truck continuously, but attention is scarce. This chapter is about the machinery that turns a raw observation — a speed reading, an immobilizer flip, a fuel-level drop — into a decision: *is this worth a person's attention, and if so, whose, and through which channel?* It describes the **event catalog** that declares those decisions once and for all, the **deduplication** that keeps a persisting condition from becoming a storm, the **delivery pipeline** that carries an alert to the dashboard and WhatsApp, stores phone push eligibility, and the **security detectors** that watch for fuel loss, tracker interference, and tamper signals. It closes with how an operator acknowledges and resolves what they have been told. ## The picture Every alert Korido can raise is one row in a single registry. That row says everything about how the alert behaves — how loud it is, who cares, and how it travels: ```mermaid flowchart LR subgraph Registry["The event catalog — one row per alert type"] direction TB R["severity info · warning · critical
category mission · route_guard · fuel · system · documents
mode immediate · digest · none · suppressed
channels WhatsApp · dashboard · portal · driver push eligibility
dedup key how repeats collapse"] end Signal["A signal is observed
(speed, fuel, alarm line, gap…)"] --> Detector["A detector fires"] Detector --> Lookup["Read this type's registry row"] Registry -.-> Lookup Lookup --> Row["An event row is written
to vehicle_events"] Row --> Deliver["Delivery is decided once, at insert"] ``` The registry is the spine of the whole system. Adding a new kind of alert is a single registry row plus one detector rule — never a scattered edit across the dashboard, the push-eligibility code, and the WhatsApp renderer, because all three read their behavior from this one place. ## The event catalog Korido recognises a fixed set of **event types** — refuelling, drain suspicion, low battery, excessive idle, towing, a Route Guard deviation, an unauthorised stop, a mission milestone, a tracker anomaly, a vehicle going offline, and several dozen more. Each one has exactly one entry in the catalog, and that entry declares six facts: * **Severity** — one of `info`, `warning`, or `critical`. Severity is the human weight of the event: `info` is a fact worth recording, `warning` asks someone to look, and `critical` demands action now. * **Category** — one of `mission`, `route_guard`, `fuel`, `documents`, or `system`. The category groups alerts for the person receiving them and, on a phone, decides the notification channel and its loudness. * **Delivery mode** — one of `immediate`, `digest`, `none`, or `suppressed`. This is the default urgency of delivery: send it now, roll it into an hourly summary, record it silently, or hold it back entirely. * **Channels** — four independent switches: WhatsApp, the in-app dashboard, the customer tracking portal, and the driver's push eligibility. An alert can light up any combination. * **Dedup scheme** — which rule collapses repeats of this type into a single alert (the next section). * **Copy key** — which French message template renders it for WhatsApp. Because every type must declare a complete row, there are no half-defined alerts: a new event type cannot ship until its severity, delivery, and routing are all decided. A few representative rows show how the catalog encodes intent: | Event type | Severity | Mode | Channels | |---|---|---|---| | `towing_detected` | critical | immediate | WhatsApp · dashboard · driver push eligibility | | `fuel_drain_anomaly` | critical | immediate | WhatsApp · dashboard | | `off_station_refueling` | warning | immediate | WhatsApp · dashboard | | `speeding` | warning | digest | WhatsApp · dashboard | | `low_battery` | info | suppressed | dashboard | | `waypoint_arrival` | info | none | dashboard · portal | ::: info Current rollout Notification rows, channel decisions, and push due-times are current behavior. Outbound Expo push is currently gated off by worker configuration, so mobile apps surface notifications from their in-app centers until the push rollout is enabled. ::: A tenant can override the delivery mode and channels for any type — a fleet that does not want speeding in its WhatsApp digest turns that channel off — but the catalog is the default every tenant starts from. ## Deduplication: one alert for one condition A truck sitting in a customs queue with a low tank does not need a fresh "low fuel" alert every time its tracker reports. A persisting condition must raise **one** alert, not a storm. Korido enforces this with an **active-alert model**: while an alert is open, a repeat of the same condition finds the existing row and updates it. Two dedup mechanisms cover the two shapes an alert can take: * **Per-vehicle active slot.** For condition-style alerts (towing, an SOS line, a standalone tamper) there is one open slot per `(tenant, vehicle, event type)`. The first firing opens it; every repeat while it is open is absorbed. When the condition clears and the alert is resolved, the slot frees for the next occurrence. * **Idempotency key.** For event-style alerts the row carries a natural key so retries and re-scans converge on the same row. The key is shaped per type: **once per vehicle**, **once per trip / stop / gap / visit**, **once per mission**, **once per deviation**, or **once per vehicle per local calendar day**. Parked `low_fuel` and `low_battery` use the daily bucket — a truck parked in a dead zone reporting a low tank every cycle still produces exactly one alert per day. The active slot is deliberately per vehicle, not per mission, so two back-to-back missions share it. When a mission completes or cancels, any of its still-open, mission-scoped alerts are auto-resolved in the same step that closes the mission, freeing the slot for the next mission's alerts. ## The delivery pipeline When an event row is written, its delivery is resolved **once, at that moment** — the effective mode, the channel routing, and the moment it becomes eligible for driver push are all computed and stored on the row. Downstream schedules then filter by those stored fields in the database. This is what keeps the pipeline cheap and predictable: a digest-mode alert is structurally invisible to the immediate-dispatch schedule, and a suppressed alert is invisible to every channel. ```mermaid sequenceDiagram participant Eng as Engine (detection) participant Row as vehicle_events row participant Dash as In-app dashboard participant Push as Push schedule (gated) participant WA as WhatsApp dispatch participant Dig as WhatsApp digest (hourly) Eng->>Row: write event, resolve delivery once Note over Row: mode · channels · push-due time stored on the row Row-->>Dash: dashboard channel — visible immediately alt mode = immediate Row->>WA: scanned ~every 2 min WA->>WA: 5-min anti-flap confirm (warning)
critical sends at once WA-->>WA: deliver one French message per owner else mode = digest Row->>Dig: batched hourly Dig-->>Dig: one summary per tenant, up to 10 lines,
then "… et N autres alertes" else mode = none / suppressed Note over Row: recorded only — no send end Row->>Push: when push-due time arrives Push->>Push: quiet-hours gate (below) alt push delivery enabled Push-->>Push: deliver to the driver's phone else current rollout Note over Push: no Expo push leaves the worker
in-app center still shows the row end ``` Three behaviours in that pipeline are worth stating as rules: * **The anti-flap window.** A `warning`-severity alert that opens is held for **5 minutes** before it is sent. If the condition clears inside that window, the alert is quietly suppressed and never reaches a person — this is what stops a flickering signal from paging an operator. `critical` alerts skip the window and send at once, and `info` facts that are closed the instant they are recorded either deliver immediately or not at all. * **Quiet hours.** Driver push eligibility respects a per-user quiet-hours window, off by default and typically set to **22:00–06:00**. A push in the `route_guard` category is treated as time-sensitive: it is exempt from quiet hours and, once outbound push is enabled, can surface through the phone's focus modes. Every other category is a normal notification that stays silent during quiet hours. * **Digests.** The hourly digest gathers every digest-mode alert for a tenant into one French summary per verified owner. It shows up to **10 lines** and collapses the rest into a "… et N autres alertes" tail, so a busy hour is one readable message, not fifty. ## The security detectors, in product terms A cluster of detectors exists specifically to catch high-risk interference on the corridor. They read signals the tracker reports whether or not it currently has a GPS fix — a truck can be parked in a dead zone and still have its alarm line, immobilizer, and defense state read every cycle. * **SOS — the driver's hardware alarm line.** The tracker carries a wired alarm line (the panic button and other hardware alarms). Any non-zero alarm code raises `driver_alarm_violation` — **critical**, immediate, to WhatsApp, the dashboard, and the driver's phone. Because no authoritative table maps every code to a meaning, any non-zero code fires: a mislabelled alarm is better than a silent panic button. The raw code travels with the alert for triage. * **Immobilizer state change.** When the engine immobilizer flips — remotely blocked or unblocked — Korido compares it against the last positively known state and raises `engine_block_changed`. A vehicle's very first reading establishes the baseline state, so it never fires spuriously. Each real flip is its own record, so a block and an unblock minutes apart are two distinct events. * **Main-power disconnection.** The tracker reports the voltage of the vehicle power source it is wired to. When that voltage drops from clearly present (**6 V or more**) to cut (**1 V or less**) while the tracker keeps transmitting on its own internal battery, the vehicle harness has likely been cut or disconnected — a high-risk tracker-interference pattern — and Korido raises `power_disconnection`: **critical**, immediate, to WhatsApp and the dashboard. The **1–6 V dead-band** between "present" and "cut" means a noisy mid-range reading can never manufacture a disconnection. A tracker that never reports an external-voltage line simply never fires this detector, and the alert auto-resolves the moment external power comes back. * **Tamper while parked.** Two signals mean someone is physically at a stationary truck: the **factory defense state dropping** (disarmed without the engine starting) and a **vibration alarm** while parked with no trip open. Either raises `tamper_detected` — **critical**, immediate — with a reason of `defense_deactivated` or `vibration_while_parked`. These fire only *outside* a signal gap: the same signals seen *inside* a gap are evidence the gap-classification path already uses to decide the gap was a tampering event, and surface there as a tracker anomaly instead, so the physical signal is never counted twice. * **Towing.** A truck moving faster than the towing threshold with its **ignition off** is being towed. This raises `towing_detected` — **critical**, immediate, to WhatsApp, the dashboard, and the driver's phone. * **GPS-jamming suspicion.** A signature interference pattern is to jam GNSS while the tracker keeps transmitting: the heartbeat stays fresh, but no new position arrives. Korido reads this as a gap of the "GPS denied while alive" kind and raises `gps_jamming_suspected` — a **warning**, immediate — keyed to the gap so one jamming window yields one alert. It auto-resolves the moment real fixes resume. ## Acknowledgment and resolution An alert has a lifecycle, and which part of it a person touches depends on how the event was born. A `warning` or `critical` opens for someone to work: it moves `open` → `investigating` → `action_taken` → `resolved`, and acknowledging it stamps who did so and when. An `info` fact is different — it exists purely as a timeline record, so it is born `closed`, already in its terminal state the instant it is written: a waypoint arrival or a completed refuel documents itself, so it enters the log finished. And many events resolve on their own — reality contradicts them and they close themselves as `auto_resolved`, or a safety net retires a forgotten one as `expired`. ```mermaid stateDiagram-v2 direction LR [*] --> open: warning / critical raised [*] --> closed: info fact recorded open --> investigating: operator acknowledges investigating --> action_taken: operator acts action_taken --> resolved: operator closes open --> auto_resolved: reality contradicts it open --> expired: safety net retires it resolved --> [*] closed --> [*] auto_resolved --> [*] expired --> [*] ``` Many alerts resolve themselves the moment reality contradicts them: * `device_offline` auto-resolves on the first telemetry row of any kind — a status-only frame is proof the tracker is back, matching the same clock the alert was opened from. * `low_fuel_driving` auto-resolves when the tank rises back above the threshold. * `gps_jamming_suspected` auto-resolves when fixes return. * `power_disconnection` auto-resolves when external power returns — a positively-known present reading is required, so a tracker that simply stops reporting voltage never clears a real disconnect. * `tracker_anomaly` — the tampering signature Korido reads when a signal gap closes on a hostile blackout — auto-resolves as that gap ends. The tampering window is already over by the time the alert is raised, so the alert delivers its critical warning and clears itself on the same beat rather than sitting open, which keeps the vehicle's active slot free for the next gap. * Mission-scoped alerts auto-resolve when their mission reaches a terminal state. The engine can emit an open alert and an auto-resolve for a different condition in the same telemetry batch, so a truck's story stays current without an operator having to close stale rows by hand. What remains for the operator is the judgment work: looking at an open `warning` or `critical`, marking it under investigation, and resolving it once handled. ## Edge cases * **A flickering condition.** A signal that toggles faster than the 5-minute anti-flap window never pages anyone: the alert opens, the condition clears, and it is suppressed before dispatch. Only conditions that persist past the window reach a person. * **A parked truck in a dead zone.** Low fuel or low battery on a truck that reports only by status frame still fires — the fuel and battery detectors read the full telemetry feed, not just GPS fixes — but the daily-bucket key collapses every cycle into exactly one alert per day. * **Back-to-back missions.** Because the active dedup slot is per vehicle, a mission-scoped alert left open at mission end would block the next mission's alert. The terminal mission transition auto-resolves it, freeing the slot. * **The WhatsApp channel is off.** Turning off WhatsApp delivery never starves the driver's in-app notification or push eligibility: the phone path keys on its own stored due-time, entirely decoupled from the WhatsApp dispatch lifecycle. * **An unknown event type at the boundary.** A type that does not match the catalog resolves to fully suppressed with no channels — an unrecognised alert can never leak to a person. * **A block and an unblock in one window.** Immobilizer transitions are recorded as distinct, closed facts rather than a single open condition, so a block and its unblock minutes apart both survive instead of contending for one slot. ## Known limitations * **A detector can only fire on a signal the device actually sends.** The security detectors read fields the tracker reports — an alarm line, an immobilizer state, an external-voltage reading, a vibration flag. A device family that does not wire one of these cannot raise the alert that depends on it: a tracker with no external-voltage line never raises a power-disconnection, and one with no vibration sensor never raises a vibration tamper. Absence of a signal is treated as *unknown*, never as a negative reading, so a missing sensor stays silent rather than falsely reassuring. * **Hardware alarm codes carry no authoritative meaning.** Because no per-family table maps every alarm code to a specific cause, any non-zero code raises the same `driver_alarm_violation` and the raw code rides along for a person to interpret. This is deliberate — a mislabelled alarm is better than a silent panic button — but it does mean the alert names *that* an alarm fired, not precisely *which* one. ## What's ahead The event model already reserves a **documents** category — a driver can be prompted to re-submit a document today — but no fleet alert type is wired to it yet. The next step is document-lifecycle alerting: a truck about to run on an expired insurance paper, a permit lapsing before a border crossing, raised on exactly the same catalog machinery every other alert already rides, so a new documents alert is one registry row plus one detector rule rather than a new subsystem. ## How it connects * The detectors that raise fuel and security alerts live in [Part 3 — The fleet engine](../03-fleet-engine/) — see [Fuel](../03-fleet-engine/fuel) for how a probe reading becomes a fuel-loss anomaly and [Signal gaps](../03-fleet-engine/gaps) for gap classification, and [Route Guard](../04-road-network/route-guard) for deviation. * The mission milestones and delay alerts in the catalog come from [Progression and ETA](../05-missions/progression-and-eta). * WhatsApp rendering and the owner conversation are covered in [WhatsApp](../07-surfaces/whatsapp); the driver notification and push-eligibility channel in [The driver app](../07-surfaces/driver-app). * The active incidents that raise ETA-blocking alerts are the subject of [Spatial intelligence](./spatial-intelligence). --- --- url: /06-intelligence/spatial-intelligence.md --- # Spatial intelligence: what the whole fleet learns about the road ## What this chapter covers A single truck's crossing tells you about that truck. Thousands of crossings, pooled, tell you about the **road** — where every truck slows down, where they all stop, and where an active incident is blocking the way today. This chapter is about the three kinds of knowledge Korido builds from the whole fleet's traces: **slowdown zones** (the road is chronically slow here, and by roughly this much), **hotspots** (trucks repeatedly stop or struggle here), and **anomalies** (an incident is disrupting the corridor right now). It explains how each one feeds the ETA indicator, paints the live map, and — crucially — how each one ages out when conditions clear. Because a steep climb or a border queue is a property of the road and not of any one fleet, these three entities are **cross-tenant**: two operators watching the same physical place see the same knowledge. ## The picture All three are built the same way — many small observations from many trucks, clustered into one named thing that carries an expected time cost: ```mermaid flowchart TB Traces["Every truck's traces
(stops, slow stretches, transit times)"] --> Cluster["Cluster by place, across all tenants"] Cluster --> Zones["Slowdown zones
chronically slow sections"] Cluster --> Hotspots["Hotspots
recurring stop / struggle points"] Cluster --> Anoms["Anomalies
active incidents"] Zones --> ETA["ETA indicator
(additive drag terms)"] Hotspots --> ETA Anoms --> ETA Zones --> Map["The live map"] Hotspots --> Map Anoms --> Map Anoms -->|"blocks the road"| Blocked["ETA frozen · confidence 'blocked'"] ``` ## Slowdown zones: the road is slow here Whenever a truck moves consistently below the corridor's expected speed, that stretch is recorded as a slowdown observation — a single midpoint on the road, kept as a point for cheapness rather than a full traced path. On a schedule, Korido clusters those midpoints into **slowdown zones**: typed, recurring slow sections of road. * **What a zone is.** A cluster of slow observations turned into one shape — a circular buffer around the cluster's centre of mass — with a **kind**: `mountain_climb`, `mountain_descent`, `urban_congestion`, `rough_road`, `border_queue`, `roadwork_recurring`, or `sharp_curve`. * **Direction matters.** The same physical road can be slow uphill and fast downhill, so a zone carries a **bearing class** and the detector emits separate zones per direction when the speed distributions diverge. Mount Cameroon's climb is a zone; its descent is a different one. * **The expected time cost.** Each zone carries an **expected drag** in seconds — how much longer a truck takes because of the zone — computed from the gap between the zone's observed median speed and the corridor baseline, scaled by how much of the route overlaps the zone. A calibration schedule keeps this number current from fresh observations. It is capped at a sane ceiling so a detector glitch can never add days to an estimate. * **Confidence.** A zone carries a confidence between 0 and 1 that reflects how much evidence stands behind it, and per-hour, per-day, per-season, per-weather breakdowns so the drag can be read in context. ## Hotspots: trucks stop or struggle here Where slowdown zones are about *slow motion*, **hotspots** are about *stopping and struggling*. Korido clusters the fleet's stop records by place and event class into named hotspots, each with an **expected dwell time** — how long trucks typically wait there. * **What a hotspot is.** A recurring cluster of one event class: `weighbridge`, `border_post`, `customs`, `toll_gate`, `fuel_station`, `rest_area`, `meal_stop`, `commerce`, `checkpoint`, `roadblock_recurring`, `breakdown_zone`, `speeding_zone`, `harsh_braking_zone`, or `signal_loss`. A hotspot is a **place** — a weighbridge is the same weighbridge for every fleet, which is exactly why hotspots are cross-tenant. * **Expected dwell.** A hotspot holds median (p50) and near-worst (p90) dwell times, bucketed by hour of day, day of week, season, and calendar, with an overall baseline as the fallback when a bucket is thin. The route estimator reads the right bucket for the time a truck will actually arrive. * **Curation.** The detector finds candidate hotspots and leaves them unnamed; an operator reviews and gives a hotspot a display name. A well-observed tenant hotspot can be promoted to a verified global one that every fleet benefits from. Sub-places nest — a fuel station inside a city groups under the city. ## Anomalies: something is wrong on the corridor now Zones and hotspots are about the road's *permanent* character. **Anomalies** are about *today*: a strike, an accident, a bridge or road closure, a flood, civil unrest, a new checkpoint, a fuel shortage. They also cover tenant-private patterns — an unauthorised-stop pattern, a fuel-loss pattern, a driver-behaviour pattern — which stay private to the fleet that owns them. * **Mixed visibility.** An anomaly is either **cross-tenant** — a border closure is visible to every operator — or **tenant-private**. A single table holds both, and a strict rule keeps them straight: cross-tenant anomalies have no owner, private ones always do. * **Shape.** An anomaly's geometry is whatever fits the incident — a point for an accident, a polygon for a flood area, a line for a road closure. * **Expected delay.** Like zones, an active anomaly carries an **expected drag** — the extra seconds a route crossing it should expect. When the detector cannot model the impact, the estimator falls back to a conservative default rather than assuming zero. * **The "blocks the road" flag.** An operator can mark an anomaly as fully blocking the corridor — a closed border, an impassable road, a customs strike. This flag is decisive: for any mission whose remaining route crosses that geometry, the ETA schedule stops producing a number and instead reports a **blocked** confidence with a frozen arrival — because there is no honest estimate for a truck that physically cannot proceed. * **Severity.** Anomalies carry `info` / `warning` / `critical` severity, which routes them into the alert system described in [Alerts](./alerts). ## Feeding the ETA indicator Korido's ETA is **layered**: on top of a driving baseline, each active piece of spatial intelligence whose geometry the remaining route crosses contributes its own additive term: ```mermaid flowchart LR Base["Driving baseline
(segment history)"] --> Sum(("Σ")) Hot["Hotspot dwell
(expected wait)"] --> Sum Zone["Slowdown drag
(expected slow)"] --> Sum Anom["Anomaly drag
(expected delay)"] --> Sum Sum --> ETA["Predicted arrival"] Anom -.->|"blocks the road"| Freeze["Confidence 'blocked'
ETA frozen"] ``` The estimator allocates each drag term to the segment it overlaps, so a route's total predicted delay is the honest sum of the specific slow sections, waits, and incidents it will actually meet. A blocking anomaly overrides the sum entirely: no drag arithmetic can turn "the road is closed" into a plausible arrival time. ## Painting the map The same three entities render on the live map so a dispatcher sees the road the way the estimator does: slowdown zones as slow stretches, hotspots as the places trucks habitually wait, and anomalies as active incidents in their real shape — a point, a flood polygon, or a closure line — coloured by severity. Where an anomaly intersects a known hotspot (an incident at a border that already has a customs hotspot), the two are linked so a reader sees one correlated disruption rather than two stacked ones. ## Ageing out when conditions clear Spatial intelligence must forget as well as learn, or the map fills with ghosts — a strike that ended, a rest stop that closed. What makes this genuinely hard is that the three entities are three different *kinds* of truth, so no single expiry rule fits them all: an anomaly is an **incident** that ends, a hotspot is a **habit** that fades, and a slowdown zone is a **slow stretch** that can flatten out. Each therefore leaves the picture in its own way, and a daily maintenance pass applies the last two. **An anomaly closes the moment its incident clears.** An operator stamps the end time, or a detection pass closes a cross-tenant one on its own — a flood subsides, a border reopens. The status becomes `resolved` (or `false_positive` if it was never real, or `merged` if it was folded into another). Once inactive, an anomaly stops contributing drag and drops off both the ETA and the map, and any blocking freeze it held lifts on the same beat. ```mermaid stateDiagram-v2 direction LR [*] --> active: incident detected active --> resolved: incident clears active --> false_positive: dismissed — never real active --> merged: folded into another resolved --> [*] ``` **A hotspot goes dormant when the trucks stop coming.** When a place records no fresh stop for **90 days**, the daily pass marks it `dormant`: it stops shaping ETAs and fades from the working map, so a rest area that shut down does not haunt the corridor forever. A hotspot an operator has curated — named and confirmed as a real place — is exempt, because a human has already vouched that it belongs there. ```mermaid stateDiagram-v2 direction LR [*] --> active: cluster of stops forms active --> dormant: 90 days with no fresh stop (curated exempt) ``` **A slowdown zone resolves when the road is no longer slow.** Two forces retire a zone. The same daily pass marks a zone `resolved` after **60 days** with no fresh slow observation; and because zones are rebuilt from a rolling window of recent traces, a stretch that has sped up simply stops being re-clustered on the next rebuild. That rebuild works in bounded chunks — a fixed number of map bins per tick — so it stays cheap even as the corridor's history grows. ```mermaid stateDiagram-v2 direction LR [*] --> active: slow stretch clusters active --> resolved: 60 days with no fresh slow observation ``` The result is a picture of the road that reflects the last couple of months of reality — sharp where conditions persist, and quiet where they have moved on. ## Edge cases * **A zone that reverses with direction.** Climb-and-descent asymmetry is preserved by the bearing class; a single road can hold two zones with very different drags. * **A detector that cannot price an incident.** When an anomaly's impact can't be modelled, the estimator uses a conservative default rather than zero, so an unmodelled incident never silently disappears from the ETA. * **An incident on top of a known wait.** An anomaly intersecting a hotspot is linked to it so the correlated drag is read once, not double-counted at a border. * **A private pattern.** A fuel-loss or driver-behaviour anomaly stays scoped to the fleet that owns it and never appears on another tenant's map or ETA. * **A runaway drag value.** Both zones and anomalies cap their expected drag at a hard ceiling, so a bug in detection can never inflate an estimate without bound. * **A blocked road that reopens.** Clearing a blocking anomaly restores normal layered ETA on the next refresh — the freeze is a live consequence of the flag, not a permanent stamp on the mission. ## Known limitations * **Road knowledge needs traffic before it exists.** Slowdown zones, hotspots, and their expected costs are all clustered from accumulated crossings. A brand-new place, a rarely-driven branch, or a corridor in its first weeks carries thin evidence and low confidence, so ETAs there lean on the driving baseline and conservative defaults until enough trucks have passed to sharpen the numbers. * **The picture is deliberately recent.** Because zones and hotspots are rebuilt from a rolling window and age out after weeks of silence, a pattern that recurs only seasonally can fall out of the working picture between seasons and be re-learned when it returns. This keeps the map sharp on current reality at the cost of long memory. ## What's ahead The hotspot model already carries more of a lifecycle than the daily maintenance pass writes today. A quiet place is only ever marked dormant; the next steps are to **revive a dormant hotspot back to active** when trucks start stopping there again, and to **archive** one that is gone for good rather than leaving it dormant indefinitely — states the model already reserves. In the same direction, Korido will promote more well-observed tenant hotspots into the verified global set every fleet shares, so the corridor's collective memory keeps compounding. ## How it connects * The ETA layers these terms feed are assembled in [Progression and ETA](../05-missions/progression-and-eta). * The per-crossing analytics that net out this expected drag from real lateness are in [Segment analytics](./segment-analytics). * Anomaly severities flow into the alert system in [Alerts](./alerts). * Corridor geometry and how a route is matched against it are covered in [Route Guard](../04-road-network/route-guard). --- --- url: /06-intelligence/segment-analytics.md --- # Segment analytics: what fleets learn from traversal records ## What this chapter covers Every time a truck crosses the road between two Tier-1 measurement boundaries, Korido records the crossing as a single, immutable fact. Those boundaries are city waypoints and mission origin/destination waypoints, not every curated place on the road. This chapter is about what a fleet learns once those facts accumulate: how one truck compares to another on the same road, how one driver compares to another, and — the most valuable comparison of all — how a truck compares to its own past, which is the earliest warning that something is wearing out. It explains the **context dimensions** that make a comparison fair, the **cross-fleet corridor benchmarks** the platform can see across every tenant, and the **drill-down** from a period down to a single journey. ## The picture The unit of measurement is the **segment traversal**: one row per completed road crossing by one truck, from one Tier-1 boundary to the next. It carries not just how long the crossing took, but everything needed to explain *why* it took that long and to compare it fairly against another crossing. ```mermaid flowchart TB subgraph One["One traversal row — an immutable observed fact"] direction TB M["Measures
transit & driving seconds · distance · avg/max speed
stops · gaps · idle · deviations
fuel used, efficiency, cost, tolls
border · breakdown · slow-driving time"] D["Fairness dimensions
season · weather · calendar
cargo load · driver fatigue
vehicle make/model/year · time of day"] end Traversal["Truck crosses Tier-1 boundary A → Tier-1 boundary B"] --> One One --> Compare subgraph Compare["What the fleet learns"] direction LR TvT["truck vs truck
same road"] DvD["driver vs driver
same road"] TvS["truck vs its own history
degradation early-warning"] XF["cross-fleet
corridor benchmark"] end classDef observed fill:#dbeafe,stroke:#2563eb,color:#172554 classDef record fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef insight fill:#dcfce7,stroke:#16a34a,color:#14532d classDef benchmark fill:#f3e8ff,stroke:#9333ea,color:#3b0764 class Traversal observed class One,M,D record class Compare,TvT,DvD,TvS insight class XF benchmark ``` Analytics ship **query-first**: they are grouped queries over the traversal records and the indexes already built for them, not a separate warehouse. Materialised rollups and scorecards are an optimisation added only when a dashboard measurably needs them, never a precondition for asking a question. ## What a traversal records A traversal is closed the moment the truck enters the next waypoint, and at that instant Korido freezes a rich set of facts onto the row: * **Time.** Total `transit_seconds` (wall clock from departure to arrival) and `driving_seconds` (engine actually moving), plus the stop, gap, and idle time that make up the difference. * **Motion.** Distance covered, average and maximum speed. * **Attributable time.** Three slices that explain lateness — `border_crossing_seconds`, `breakdown_seconds`, and `slow_driving_seconds`. The slow-driving slice counts only the time beyond what a slowdown zone already expected, so a chronically slow stretch is not charged twice — once as the zone's own drag and again as fresh slow-driving time. * **Fuel and money.** Start and end tank percentage, litres consumed, efficiency in L/100km, the per-litre price at the time, tolls, and a total cost that folds fuel, driver wage, trailer rent, tolls, and overhead into one figure. * **Incidents.** Counts of stops, gaps, idle spells, speeding events, and deviations during the crossing. Because the row is immutable and self-contained, a comparison reads frozen facts exactly as they were true at the time. ## The comparisons Four comparisons fall out of the same records: * **Truck versus truck, same road.** Pool every tenant's crossings of the same waypoint pair and rank the trucks. Two trucks on the identical stretch, one consistently slower or thirstier, is a signal worth acting on. * **Driver versus driver, same road.** The same pooling, grouped by driver. Because the driver is stamped on every traversal, "show this driver's crossings over time" and "compare two drivers on the same segment" are direct groupings, read straight off the traversal row. * **A truck against its own history.** The most operationally valuable line. A truck that is steadily getting slower or less fuel-efficient on a road it has driven many times is showing early wear. These self-degradation trends are exactly the early-warning signal that feeds fleet maintenance planning. * **Cross-fleet corridor benchmark.** For platform administrators, the same records pooled across every tenant give a corridor-wide baseline: what a segment *normally* costs in time, regardless of who is driving it. ## The fairness dimensions A raw "truck A is slower than truck B" comparison is meaningless if A crossed in the wet season with a full trailer at night and B crossed dry, empty, and at noon. Korido stamps each traversal with the context that makes the comparison fair, so a query can hold those factors constant: * **Season** — a four-bucket Sahel calendar: `dry_cool`, `dry_hot`, `wet_early`, `wet_peak`. The road is a different road in the wet peak. * **Weather** — the nearest observed weather class at departure. * **Calendar** — a class such as `normal` or a holiday, from the tenant calendar. A crossing on a public holiday behaves differently at borders and checkpoints. * **Cargo load** — whether the truck was loaded and its cargo weight. A laden truck climbs slower and burns more. * **Driver fatigue** — accumulated driving time: the driver's cumulative driving since 04:00 local that day, and over the trailing 7 days. Fatigue is a measurable input to how a segment was driven. * **Vehicle make, model, and year** — stamped on the row so a comparison can hold the machine constant, or explicitly compare an old truck against a new one. * **Time of day** — the local hour and day of the week, derived automatically from the departure time, so day-versus-night and weekday-versus-weekend distributions are first-class groupings. * **Segment experience** — how many times this driver has crossed this exact pair before, so a first crossing is not judged against a veteran's. Holding these constant is what turns a number into an insight: *on the same road, same season, same load, at the same time of day, this truck is 20% slower than the fleet* is a claim a dispatcher can act on. ## The drill-down The analytics surface reads top-down, from the widest view to a single journey: ```mermaid flowchart LR Period["Period
7 / 30 / 90 days
fleet-wide KPIs, trends"] --> Mission["Mission
one shipment's crossings"] Mission --> Segment["Segment
one waypoint pair
p50 / p90, day vs night"] Segment --> Journey["Journey detail
one traversal
every frozen fact"] ``` A user starts at a **period** — 7, 30, or 90 days — sees the fleet-wide scorecards and trend lines, and narrows to a **mission**, then to a single **segment** (one waypoint pair), and finally to the **journey detail** of one traversal with all of its frozen facts. Each level is a grouping of the same records at a different granularity. At the segment level, the numbers are **percentiles, split by time of day**: the median (p50) and near-worst (p90) of both driving time and wall-clock time, bucketed into day, night, and overall. Percentiles rather than averages, because a single customs day should not drag the typical crossing. ## Cross-fleet corridor benchmarks Platform administrators see a **Segment Analytics** surface that reads across every tenant at once — the only place in Korido where tenant boundaries are deliberately crossed, because a road segment belongs to the corridor, not to a fleet. It answers two questions: * **The corridor overview** — one row per curated road segment, pooling every tenant's crossings of that segment's waypoint pair. Segments with no samples yet still appear, so the corridor map is complete rather than only showing roads that happen to have traffic. * **The segment detail** — for one segment, the day / night / overall p50 and p90 of driving and wall-clock time, the same percentile shape a tenant sees for its own trucks, but computed over the whole platform. This cross-tenant view is reachable only by an authenticated administrator; a tenant never sees another tenant's rows. ## Predicted versus actual Every traversal also freezes the estimate that was standing when the crossing began: the predicted duration at that moment, and how confident that prediction was. Because the prediction travels on the row, a dashboard can compute predicted-versus-actual gaps across many crossings without re-joining the prediction tables — the raw material for asking whether Korido's own ETAs are calibrated. ## Edge cases * **A segment with no samples.** The corridor overview keeps a curated segment visible even before any truck has crossed it, so a fresh corridor already shows its full map of segments. * **A crossing with no planned segment.** When a Tier-1 boundary pair is not part of a planned route, it can still record against the pair itself, so ad-hoc city-to-city or origin-to-destination crossings are measured rather than dropped. Non-Tier-1 places such as customs yards, fuel stations, and weighbridges remain stop and hotspot context; they do not create segment traversal rows by themselves. * **Double-counted lateness.** The slow-driving slice subtracts what a slowdown zone already expected, so a chronically slow stretch is never charged twice — once as the zone's drag and again as slow-driving time — and a crossing with no zone coverage counts its full elapsed time honestly. * **GPS jitter at a waypoint.** Rapid enter–exit–enter flapping at a boundary cannot produce duplicate traversal rows — a closed crossing is unique per arrival. * **Geometry edited after the fact.** A traversal freezes the waypoint positions and radii it was measured against, so an operator moving or resizing a waypoint later never silently rewrites historical crossings — a dashboard can even detect that the geometry changed since a row was written. ## Known limitations * **A fair comparison needs enough matched crossings.** Percentiles, day-versus-night splits, and context-held comparisons are only meaningful once a bucket holds enough samples. A thin bucket — a rarely-driven segment, an unusual season-and-load combination — falls back to a coarser overall baseline rather than reporting a confident figure from one or two crossings. * **Analytics read the live records directly.** The surface runs as grouped queries over the traversal rows and their indexes rather than a separate warehouse, which keeps every figure current and avoids a staleness gap — at the cost of heavier queries on the widest multi-month windows. ## How it connects * Traversals are produced as missions progress — see [Segments and traversals](../05-missions/segments-and-traversals) for how a journey becomes measurable segments and [Progression and ETA](../05-missions/progression-and-eta) for how the ETA layers are built. * The slowdown zones, hotspots, and anomalies whose expected drag the attributable-time slices net out are described in [Spatial intelligence](./spatial-intelligence). * Self-degradation trends feed maintenance planning — see [Fleet assets](../05-missions/fleet-assets) for the asset side. * The fuel figures on each traversal originate in the engine's fuel path — see [Fuel](../03-fleet-engine/fuel). --- --- url: /07-surfaces.md --- # Part 7 — The surfaces: what each audience sees Everything so far has been about what Korido *knows* — positions, trips, missions, alerts, road intelligence. This part is about who gets to *see* it, and in what shape. The same underlying truth is presented six different ways, because a dispatcher planning a convoy, a driver on the Chad border, an owner checking in from across town, a customer waiting on a delivery, and the Korido platform team all need very different windows onto it. The through-line of this part is that **each surface is defined as much by what it withholds as by what it shows.** A driver never sees the vehicle diary; a customer never sees a Route Guard breach; an owner's phone never shows raw sensor internals; a tenant never sees another tenant's trucks. Those boundaries are the design of each surface, built in from the start. Six chapters, roughly from the fleet outward to the platform: * **[The fleet app](./fleet-app)** — the web command center at app.korido.net, where an owner or dispatcher plans missions, watches the live map, works the alert queue, and reconstructs a truck's day in the vehicle diary. * **[The driver app](./driver-app)** — Korido on the driver's phone: mission awareness, paperwork capture, and notifications, built offline-first for long stretches of weak coverage — and never a source of position truth. * **[The owner app](./owner-app)** — the owner's glanceable native companion, answering "how is my fleet right now?" with functional facts only, never engineer-grade telemetry. * **[The tracking portal](./tracking-portal)** — Korido's public face at track.korido.net, where a customer follows one delivery through a gated, expiring link that shows only customer-safe facts. * **[WhatsApp](./whatsapp)** — a channel carrying alerts, a question-answering bot, and driver prompts to people who never open an app all day. * **[The admin portal](./admin-portal)** — the Korido platform team's console at admin.korido.net: tenant and hardware administration, curation of the shared road network, and the cross-fleet view no single tenant can have. Read together, they show one fleet refracted through its audiences and one channel — each seeing exactly what it should, and nothing it shouldn't. --- --- url: /07-surfaces/fleet-app.md --- # The Fleet App ## What this chapter covers The fleet app at **app.korido.net** is the web command center for a trucking company on the Douala-N'Djamena corridor. This chapter describes who uses it, the surfaces it gives a dispatcher or owner — live map, vehicle diary, mission creation and tracking, the fuel dashboard, the alerts center, and the fleet overview — and the deliberate lines the app does not cross. ## Who it is for The fleet app is the daily operating tool for the people who run a fleet: the **owner** and the dispatchers working on the owner's behalf. Only the `owner` role enters the app shell; a Korido support operator may enter through a short-lived impersonation handoff, but they see the tenant's own data through the tenant's session, never a cross-tenant view. Drivers do not use the web app — their surface is the [driver app](./driver-app). Customers never see it at all; they get a scoped [tracking link](./tracking-portal). The app is built around an operating rhythm: every screen exists to help answer the questions asked while trucks are moving. ```mermaid flowchart LR Plan["Plan
create a mission,
assign vehicle + driver"] --> Monitor["Monitor
live map,
freshness, alerts"] Monitor --> Respond["Respond
triage an alert,
call the driver"] Respond --> Reconstruct["Reconstruct
vehicle diary:
what actually happened"] Reconstruct --> Share["Share
tracking link
to the customer"] Share --> Plan class Plan,Share plan class Monitor surface class Respond warn class Reconstruct observed classDef plan fill:#eff6ff,stroke:#2563eb,color:#1e3a8a classDef surface fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef observed fill:#fff7ed,stroke:#ea580c,color:#7c2d12 ``` ## The fleet overview The landing surface is a **command center**: it leads with fleet attention, then geography, then the work queue: 1. A metric strip that makes fleet health scannable at a glance — active missions, open alerts, and the moving / stopped / offline split. 2. A live fleet map as the main visual surface. 3. Side panels (on desktop) or panel sheets (on mobile) for missions, alerts, and recent activity — the queue of things that need acknowledgement, investigation, or a phone call. Live dashboards refresh on a modest 30-second cadence. That is a deliberate compromise: recent enough for dispatch decisions, but not a real-time animation system that would burn battery and bandwidth on metered corridor connections. ## The live map The **live map** is a first-class workspace: it owns vehicle selection (backed by the URL so a view can be shared or reloaded), the fleet list, and a selected-vehicle detail panel with tabs for the current trip, vehicle identity, diagnostic telemetry, and alerts. Where the driver's phone number is known, the panel offers a one-tap call. The map's core job is to show each vehicle in a state a dispatcher can trust. Each vehicle carries a display state derived from its latest signal: ```mermaid flowchart TD Row["Vehicle's latest state"] --> Gap{"Open signal gap?"} Gap -->|"no heartbeat"| Offline["Hors service (offline)"] Gap -->|"heartbeat alive,
no GPS fix,
not parked"| SansGps["Sans GPS"] Gap -->|"no gap"| Trip{"On a trip?"} Trip -->|"moving"| Driving["En route (driving)"] Trip -->|"stopped on trip"| Stopped["Arrêté / Au point d'étape"] Trip -->|"ignition off,
heartbeat fresh,
known position"| Parked["Stationné —
dernière position connue"] class Driving,Stopped,Parked safe class SansGps warn class Offline risk classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d ``` ::: info Display contract The web app, owner app, and tracking portal all read from the same liveness contract. Differences between surfaces are privacy and wording choices, not separate state machines. ::: Three of these states are the ones that make corridor tracking honest, and the app names them plainly: * **"Stationné — dernière position connue"** — the truck is parked. Corridor trackers stop sending GPS fixes while a truck sleeps, so a fresh heartbeat with the ignition off and a known position means the truck is simply parked where it was last seen. While the last heartbeat stays fresh — within roughly 2 hours — this reads as confidently parked; if the tracker is alive but cannot reconfirm its fix, the map shades the same parked state amber to flag that the position may be stale. Once the heartbeat itself goes quiet, the truck reads as offline instead. * **"Sans GPS"** — the tracker is alive and talking, but it cannot get a satellite fix. This is a real, distinct condition, separate from a parked truck and separate from a truck that has gone silent. * **"Hors service"** — the tracker has stopped reporting entirely; there is an open signal gap. A vehicle that has never produced a position still appears in lists and summaries; it simply cannot be placed on the map. Missing position, stale signal, and offline are treated as meaningful states, never as rendering errors. The liveness rules behind these labels are the subject of [Part 2 — Telemetry](../02-telemetry/); the gaps that drive "Hors service" and "Sans GPS" come from [Part 3 — The fleet engine](../03-fleet-engine/). ## The vehicle diary The **vehicle diary** is the evidence layer — where an owner reconstructs what a truck actually did on a given day, and the answer to any dispute about delay, fuel, route, or driver activity. Its power is the pairing of a map trail with a time-ordered story: trips, stops, signal gaps, fuel events, waypoint visits, and Route Guard deviations only tell a coherent story when time and geography are visible together. The diary offers day, week, and calendar views. Today's view refreshes more often than historical review, which polls position history about once a minute. The workspace carries a KPI strip for the chosen period, a split map-and-timeline layout on desktop (a toggle on mobile), a replay panel that scrubs the day's telemetry — speed, fuel, engine, coolant, altitude, GSM — and story filters. The diary is allowed to interpret, but only from telemetry and explicit rules. A signal gap that overlaps an authorized stop reads differently from an unexplained offline stretch, and when a gap has been classified, the reason is discoverable in the story. The diary never invents a timeline row the engine did not produce. ## Missions: quick-assign and tracking Creating a mission is meant to be fast, because corridor work is repetitive. The app uses a guided wizard whose central move is **quick-assign**: the operator supplies the few facts only a human knows, and the wizard fills in everything it can infer from them. ```mermaid flowchart LR OD["Pick origin
+ destination"] -->|"infer the named corridor"| Cover["Route Guard coverage
attaches — no hand-drawn geometry"] Client["Known client"] -.->|"reusable pickup + delivery"| Plan["The mission plan"] Hist["Past trips"] -.->|"scored driver, vehicle, trailer"| Plan Tmpl["Template or clone"] -.->|"whole plan hydrated"| Plan Cover --> Plan class OD,Client,Hist,Tmpl source class Cover safe class Plan plan classDef source fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef plan fill:#eff6ff,stroke:#2563eb,color:#1e3a8a ``` The pivotal inference is the corridor: once the operator picks an origin and destination, Korido recognises the named corridor that connects them and suggests it, so Route Guard coverage attaches without anyone drawing geometry. Around that, a known client fills in reusable pickup and delivery locations, scored recommendations propose a driver, vehicle, and trailer from past trips, and a template or a cloned mission hydrates the whole plan at once. For a one-off haul, the operator drops ad-hoc pins on the map instead. The wizard also offers a **Règles de conduite** (driving policy) selector, so an operator can attach an explicit set of driving rules — speed, night driving, authorized stops — to the mission, or let it inherit the tenant's default. The owner UI collects intent, but the server owns mission authority. It rejects a mission for a vehicle that already has an active one — regardless of which driver is assigned — with a friendly validation message rather than a raw error, computes the planned distance from the waypoints itself (operators never type a distance), unions the selected corridor segments into a Route Guard polygon, and enriches ETA and predictions after the mission is safely created. The mission creation flow is covered in depth in [Part 5 — Missions](../05-missions/). Mission detail is the bridge between the plan and the evidence: it shows the planned route and cargo alongside the predicted ETA, the waypoint sequence with per-leg predictions, observed visits, and the measured segments. Lifecycle actions — force-start, pause, resume, complete, cancel — are offered strictly by the mission's current status, so an operator cannot skip an evidence-producing state just because a button exists. **Customer tracking** is managed from here too. An owner generates a per-mission link, sets its expiry, copies or shares it over WhatsApp, watches its access count, and revokes it — all without granting the customer any view of the dashboard. What the customer then sees is the [tracking portal](./tracking-portal). ## The fuel dashboard Fuel is operational evidence. The fuel surfaces present a tank **gauge** for current level, a stream of fuel **events** — refuels and drops detected by the sensor, joined with driver-submitted receipts where the mobile app captured them — and **rankings** that surface the vehicles or drivers worth a closer look over a rolling window. A vehicle-specific view drills into one truck's history. The framing is deliberate: a good fuel workflow separates sensor confidence, receipt timing, and expected consumption before anyone calls a discrepancy fraud. The detection rules that produce these events live in [Part 3 — The fleet engine](../03-fleet-engine/). ## The alerts center The **alerts center** is a triage queue over vehicle events. Each row carries a severity indicator, the event type, the vehicle reference, and a relative time, and moves through open, investigating, and action-taken states as an operator acknowledges and resolves it. The open-alert count feeds the shell and the fleet overview's attention model. The alerts center is a triage surface. It shows which events fired and lets an operator work them; it does not claim to monitor WhatsApp delivery, group recurrences, or manage escalation policy. How alerts are raised and delivered is the subject of [Part 6 — Fleet intelligence](../06-intelligence/) and the [WhatsApp channel](./whatsapp). ## The document review center The paperwork drivers capture on the road lands in a **document review center** — a filterable list of every uploaded document beside a preview-and-decision panel. An owner filters by category, status, or the mission, vehicle, or driver a document belongs to, opens the photo with its metadata, and records one of three decisions: **"Valider"**, **"À corriger"**, or **"Rejeter"**. A correction or rejection sends the driver a directed retake request; a validation simply confirms the document. The full capture-to-review lifecycle, its statuses, and the retake loop are the subject of [Part 5 — Documents](../05-missions/documents). ## The boundaries of this surface The fleet app is powerful because it is narrow. Its deliberate limits are part of the design: * **Corridors are curated in the admin portal.** An owner selects Route Guard coverage for a mission, but the shared waypoints, road segments, and named corridors are curated in the [admin portal](./admin-portal). This keeps route intelligence consistent across every tenant. * **The phone is never fleet truth.** Backend position comes only from the hardware tracker through the telemetry pipeline. A driver's phone GPS is display-only on their own app and is never synced into the fleet the owner sees. * **Planned distance and corridor geometry are server-authoritative.** The UI previews and guides; it never becomes the source of truth for distance, lifecycle state, or coverage. * **Partial modules stay honest.** Directions like maintenance and richer reporting appear only where real data flows exist. A disabled nav entry is preferred over a placeholder page that implies a guarantee the system does not yet make. ## How it connects * [Part 2 — Telemetry](../02-telemetry/) — the liveness rules behind "Sans GPS", "Stationné — dernière position connue", and "Hors service". * [Part 3 — The fleet engine](../03-fleet-engine/) — the trips, stops, gaps, and fuel events the diary and dashboards read. * [Part 4 — The road network](../04-road-network/) — waypoints, corridors, and Route Guard, curated in the admin portal. * [Part 5 — Missions](../05-missions/) — mission creation, lifecycle, and ETA in full. * [Part 5 — Documents](../05-missions/documents) — the capture-to-review lifecycle behind the document review center. * [The admin portal](./admin-portal) — where corridor coverage the app consumes is defined. * [The tracking portal](./tracking-portal) — what a shared link shows the customer. --- --- url: /07-surfaces/driver-app.md --- # The Driver App ## What this chapter covers The driver app is Korido on the driver's phone — a standalone native app built for the person behind the wheel on a multi-day Douala-N'Djamena haul. This chapter describes who it serves, what it helps a driver do (know the mission, capture paperwork, stay notified), the offline-first behavior that keeps it useful across long stretches of weak coverage, and the single rule that defines it: the phone is never the fleet's source of position truth. ## Who it is for The driver app is for the **driver**. It assumes a working phone and intermittent data, and it is designed to be usable without deep literacy or app fluency: guided choices, large touch targets, and French-first copy. It is a companion to the work, not a dispatch console — planning, corridors, and the vehicle diary all live on the [fleet app](./fleet-app), which the driver never opens. The single most important thing to understand about this surface: the truck's hardware tracker is the sole source of position truth, and the phone's GPS is for on-screen display only. ```mermaid flowchart LR Tracker["Hardware tracker
on the truck"] -->|"authoritative position"| Backend["Korido backend
(fleet truth)"] Backend --> FleetApp["Fleet app · owner app ·
tracking portal"] Phone["Driver's phone GPS"] -.->|"display only —
never synced"| DriverApp["Driver app screens"] Phone -. "context tag on
a document photo" .-> DriverApp class Tracker source class Backend store class FleetApp surface class Phone warn class DriverApp surface classDef source fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef store fill:#ecfdf5,stroke:#047857,color:#064e3b classDef surface fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 ``` ::: warning Boundary Driver phone GPS may help the driver orient or tag a document photo. It is never synced as fleet telemetry and never becomes the position an owner or customer sees. ::: ## What the driver sees and does The app is organized around a small set of tabs, each mapping to one thing a driver needs on the road. **Mission awareness.** The missions tab shows the driver's assigned work, and a mission detail screen carries the specifics of the current haul — origin, destination, waypoints, cargo, and schedule. When an owner assigns a mission, a notification row is created for the driver and appears in the in-app center; Expo push delivery is wired through the platform pipeline but currently gated off by worker configuration until rollout. Navigation aids help the driver orient toward the next waypoint; turn-by-turn routing remains the phone's own map, since Korido's job is to say *where to go*, not to redraw the road. **Capturing paperwork.** A corridor run generates documents — customs papers, a signed delivery note, fuel receipts, vehicle and driver papers, a photo of a repair. The documents tab is deliberately simple: four guided capture choices, camera in one tap, and an optional location tag attached to the photo purely as context. Each capture is queued locally and uploaded when the network allows, and a local history lets the driver review what they have sent, with previews that work even offline. When an owner sends a document back for a fix, the request returns here as a **"Documents à reprendre"** prompt that deep-links to a recapture. The whole capture-to-review lifecycle is the subject of [Part 5 — Documents](../05-missions/documents). **Staying notified.** The app carries an in-app notification center, with system push treated as a rollout-controlled delivery channel. Notifications are grouped into categories — mission, Route Guard, fuel, documents, and system — so a driver can tell an urgent route-guard alert from a routine acknowledgement. The driver also receives directed requests — such as a system prompt to confirm a pause when a mission looks stopped — which arrive as actionable notifications that deep-link straight to the right screen. The notification mechanics, categories, and preferences are shared with the [owner app](./owner-app) and detailed in [Part 6 — Fleet intelligence](../06-intelligence/). ## Offline-first behavior The corridor has long stretches where a phone has no usable data, so the app is built to keep working when it is offline and to reconcile when it reconnects. Documents are the clearest example. ```mermaid sequenceDiagram box rgb(236,254,255) Road user participant D as Driver end box rgb(254,249,195) Offline-first local state participant App as Driver app (local store) end box rgb(239,246,255) Connectivity participant Net as Network end box rgb(236,253,245) Backend participant API as Korido backend end D->>App: Capture document photo App->>App: Copy file into app storage, queue locally Note over App: Visible in history immediately (offline preview) App->>Net: Attempt upload Net-->>App: No connectivity Note over App: Stays queued — survives app restart Net-->>App: Connectivity returns App->>API: Upload queued documents API-->>App: Accepted ``` Every captured photo is copied into the app's own storage before any upload, so a retry survives closing or restarting the app and the driver's history shows a preview even before the file reaches the backend. Uploads retry automatically when connectivity returns. Session credentials are held in secure device storage, and small preferences and flags are kept locally, so the app opens and shows the driver's data without waiting on the network. ## The boundaries of this surface * **The phone is display-only for position.** The truck's hardware tracker is the sole source of fleet position truth. The driver's phone GPS is used only for on-screen orientation and as an optional context tag on a document photo — it is never synced to the backend and never becomes what an owner or customer sees. This is not a limitation to work around; it is the design. * **Owner-only planning.** The driver app does not create or edit missions and has no vehicle diary. Those are owner concerns. * **Document capture is a first pass.** Photos are validated lightly for basic quality; deeper checks like blur, glare, or automatic data extraction are future intelligence, not current behavior. * **A standalone app.** The driver app runs on its own native release cadence and does not share the web design system — its screens are built for the phone, not ported from the browser. ## How it connects * [Part 6 — Fleet intelligence](../06-intelligence/) — how notification records, push eligibility, and directed requests are produced and delivered. * [The owner app](./owner-app) — the sibling native app that shares the notification stream and its preferences. * [The fleet app](./fleet-app) — where the missions a driver sees are created and where their documents are reviewed. * [Part 5 — Documents](../05-missions/documents) — the full document lifecycle from capture here to owner review and the retake request back. * [Part 2 — Telemetry](../02-telemetry/) — why the hardware tracker, not the phone, is the source of position truth. --- --- url: /07-surfaces/owner-app.md --- # The Owner App ## What this chapter covers The owner app is Korido on the fleet owner's phone — a native companion that answers "how is my fleet right now?" from a pocket. This chapter describes who it serves, the fleet-at-a-glance experience it offers, the deliberate rule that it shows functional information only and never raw device telemetry, and how its notification preferences and push eligibility respect a person's sleep. ## Who it is for The owner app is for **owners and managers** who need to check on the fleet away from a desk. It is a glanceable, read-first surface, not a full operating console: the deep planning, mission creation, and evidence work stay on the [fleet app](./fleet-app) in the browser. Sign-in is by WhatsApp OTP, matching how owners already receive alerts. The guiding principle is that an owner should see everything that helps them run the business and nothing that only an engineer would read. ```mermaid flowchart LR subgraph Shown["Functional information — shown"] Status["Status
(moving, parked,
Sans GPS, offline)"] Conn["Connectivity
& signal freshness"] Pos["Position on the map"] Mission["Active mission
& driver contact"] Fuel["Fuel level & events"] end subgraph Hidden["Raw device telemetry — hidden"] Diag["Sensor diagnostics,
engine/CAN internals,
raw counters"] end Owner["Fleet owner"] --> Shown Owner -.->|"not this surface"| Hidden class Owner source class Status,Conn,Pos,Mission,Fuel safe class Diag risk classDef source fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d ``` ## Fleet at a glance The app opens onto the fleet the way an owner thinks about it: a live map and a searchable list. A **live-map** tab places the trucks on the corridor — moving, stopped, waiting, or offline — reading the same activity and position-freshness wire contract as the web app. A compact preview card can summarize a selected truck with primary actions, but the full picture always opens as a proper detail page. A **fleet** tab lists every vehicle with search and filtering over a fast, virtualized list, so an owner with a large fleet can find one truck instantly. Selecting a vehicle opens its detail page — kept inside the tab navigation so the bottom menu and native back gesture stay available — organized into functional panels: the active mission and its route, the vehicle itself, connectivity, and its alerts. A vehicle wears an honest activity label straight from the fleet's own wire contract: **"Stationné — dernière position connue"** for a parked truck, an amber-shaded variant when its last position may be stale, and **"Sans GPS"** for a tracker that is talking but cannot get a fix. The vehicle panel gathers identity, the linked trailer, the effective driver's contact, speed, total distance, alert count, and a fuel reading shown as a "150 L · 62 %" figure — a plain "-" when the truck carries no fuel sender. A separate **Connectivité** panel answers "is the tracker still reaching us?" with connection state, time since last contact, and signal quality, standing in for the raw engine and sensor readings an owner has no reason to parse. A profile tab rounds out the app with the owner's own identity and settings. ## Notifications, preferences, and quiet hours The owner app shares Korido's notification stream with the driver app. Notifications arrive in the same five categories — mission, Route Guard, fuel, documents, and system — and tapping one deep-links straight to the relevant vehicle. Expo push dispatch is wired into the platform pipeline but currently gated off by worker configuration, so the in-app center is the live delivery surface until rollout enables outbound push. What matters at the owner surface is control over the flow. A preferences screen lets an owner switch each category on or off and set a **quiet-hours** window so routine notifications defer to the end of the window instead of buzzing overnight. The one exception is deliberate: Route Guard alerts are treated as time-sensitive and are exempt from quiet hours, because a corridor deviation is the kind of thing an owner wants to know about the moment it happens. Quiet hours are evaluated in the owner's own local time, and the app auto-detects the timezone rather than asking anyone to pick one. On phones from vendors that aggressively kill background apps, a one-time onboarding nudge guides the owner to grant the battery permission that will let high-priority alerts through once outbound push is enabled. ```mermaid flowchart TD Event["Notification to send"] --> Cat{"Route Guard
alert?"} Cat -->|"yes"| Send["Eligible for immediate push
(quiet-hours exempt)"] Cat -->|"no"| Pref{"Category enabled
for this owner?"} Pref -->|"no"| Drop["Kept in the in-app
center, not pushed"] Pref -->|"yes"| Quiet{"Inside
quiet hours?"} Quiet -->|"yes"| Defer["Defer to end
of quiet window"] Quiet -->|"no"| Send class Event source class Cat,Pref,Quiet warn class Send safe class Drop,Defer surface classDef source fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef surface fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 ``` An in-app notification center keeps the history regardless of what was pushed, so a notification an owner chose not to receive as a buzz is still there to read later. With push dispatch disabled, eligibility and deferral decisions are still part of the record, but no Expo push leaves the worker. The full notification pipeline — how records are created, made push-eligible, dispatched when enabled, and reconciled — is covered in [Part 6 — Fleet intelligence](../06-intelligence/). ## The boundaries of this surface * **Functional information only.** The owner app deliberately shows what an owner needs to run the fleet — status, connectivity, position, mission, and fuel — and withholds raw device telemetry. Deep sensor diagnostics belong to operations tooling, not to the owner's glanceable view. * **Read-first.** The owner app does not create or edit missions and has no vehicle diary. Those workflows stay on the web [fleet app](./fleet-app). * **Documents are metadata-only.** An owner can see that a vehicle's documents exist; reviewing previews, diary history, and historical GPS tracks are not part of the owner mobile surface today. * **A standalone native app.** Like the driver app, it runs its own native release cadence and builds its screens for the phone rather than porting the web design system. It reaches the backend through the mobile API, which accepts only owner and viewer roles for these fleet reads. ## How it connects * [The fleet app](./fleet-app) — the full web operating surface the owner app glances at from a phone. * [The driver app](./driver-app) — the sibling native app sharing the notification stream and category model. * [Part 6 — Fleet intelligence](../06-intelligence/) — how alerts, in-app notifications, and push eligibility are produced and delivered. * [Part 2 — Telemetry](../02-telemetry/) — the liveness states the fleet list and map display. --- --- url: /07-surfaces/tracking-portal.md --- # The Tracking Portal ## What this chapter covers The tracking portal at **track.korido.net** is Korido's public face — the view a shipper or consignee opens from a link to follow one delivery. This chapter describes who it serves, what a customer sees (live progress, an arrival estimate, a journey timeline), the strict privacy filtering that shows only that one mission and only customer-safe facts, and how a link is gated, scoped, and eventually expires. ## Who it is for The portal is for **customers** — the shipper, broker, or recipient of a haul — who have no Korido account and never needed one. They arrive from a link an owner shared, typically over WhatsApp. The owner or dispatcher creates, shares, and revokes those links from the [fleet app](./fleet-app); the customer only views and, when finished, disconnects. The portal sits at a high-trust boundary. A dispatcher's console is dense with internal mission state, driver identity, device health, Route Guard internals, and tenant records. A customer should see none of that — only the delivery facts that help them orient on the Douala-N'Djamena corridor. Getting that boundary right is the portal's defining job. ```mermaid flowchart LR Owner["Owner (fleet app)"] -->|"generates link
for one mission"| Link["track.korido.net/m/{token}"] Link -->|"shared over WhatsApp"| Customer["Customer
(no account)"] Customer --> Gate["Phone gate:
last 4 digits
or full phone for ad hoc"] Gate -->|"verified"| View["Live delivery view
(customer-safe only)"] class Owner surface class Link warn class Customer source class Gate ingress class View safe classDef source fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef ingress fill:#eff6ff,stroke:#2563eb,color:#1e3a8a classDef surface fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b ``` ## Getting in: the phone gate A tracking link is a per-mission URL carrying an opaque, unguessable token. When the owner creates it, they enter the recipient's phone number; Korido stores only the gate proof it needs, never the phone number itself. Mission, client, and convoy-context links hash the **last four digits** because the customer already knows which run they received. Ad-hoc links hash the normalized **full phone number**, because there is no mission context on the link to help the customer recognize it. To open the view, the customer confirms the form that link expects. That gate is not a password — its job is to reduce accidental forwarding and confirm the viewer probably holds the phone the link was meant for. The real controls sit behind it: the token is unguessable, attempts are rate-limited, and once the gate passes, a signed cookie tied to that specific link keeps the customer in without re-entering the digits. A missing, revoked, or expired link is indistinguishable from a wrong-number attempt before the gate passes, so the portal never leaks whether a link exists. ## What the customer sees Past the gate, the portal opens as a full shipment view — map-first, with just enough context to recognize the load: the vehicle plate or reference, the cargo when available, origin and destination, the truck's current place, and how fresh that position is. **Live progress.** The map shows the truck's current public position and its recent trail, projected along the route. It refreshes on a roughly 30-second poll. Position freshness is told honestly, never faked: a fresh fix shows a small heartbeat with how long ago it was seen; a truck with no fix yet shows "awaiting first signal"; and a stale position — one past its device-specific staleness window, up to about 70 minutes — is shown with weak-coverage language that reassures the customer that corridor silence is normal while stating plainly when the truck was last seen. **The arrival estimate.** The portal shows the best available arrival estimate: the live predicted ETA when Korido has one, falling back to the owner's promised date, and finally to just the destination direction when neither exists. Low or uncertain confidence is shown as such rather than dressed up as a precise time. No negative incident signal ever reaches the customer — no breach states, no anomaly counts — so corridor trouble stays on the operator's side of the boundary. The one anomaly-derived signal a customer ever sees is a positive one: when a corridor anomaly that was disrupting the route has recently cleared, the peek card carries a quiet **"Itinéraire en rétablissement"** note — a reassurance that the road is recovering, shown only while arrival still matters and dropped once the delivery is done. The estimator behind these numbers lives in [Part 5 — Missions](../05-missions/). **The timeline.** A waypoint-based journey timeline shows the stops that matter — titles, arrival and departure times, dwell where known, and compact per-leg distance and duration — so the customer can read the trip as a story. The delivery status a customer sees is a curated **public phase**, not Korido's internal mission state: ```mermaid stateDiagram-v2 state "Programmé" as Scheduled state "En transit" as Transit state "En pause" as Paused state "Arrivé" as Arrived state "Livré" as Delivered state "Annulé" as Cancelled [*] --> Scheduled Scheduled --> Transit: truck departs Transit --> Paused: operator confirms a customer-safe pause Paused --> Transit: resumes Transit --> Arrived: reaches destination Arrived --> Delivered: delivery closed Transit --> Cancelled: cancelled Delivered --> [*] Cancelled --> [*] class Scheduled,Transit safe class Paused warn class Arrived,Delivered safe class Cancelled risk classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d ``` The six public phases — **Programmé**, **En transit**, **Arrivé**, **En pause**, **Livré**, **Annulé** — map many internal states down to language a customer understands. A pause is only ever shown when an operator has confirmed a customer-safe reason (mechanical, border, administrative, driver rest, cargo check); a pause the engine merely suspects is simply shown as "En transit". The customer never sees raw mission status. ## The privacy filter The portal's central rule is simple: raw internal data does not cross the public boundary. The customer receives an explicitly allowlisted set of facts, and a new data field is private by default until it is deliberately added to that public contract, translated, and privacy-reviewed. ```mermaid flowchart TD Internal["Full mission & telemetry state"] --> Filter{"On the public
allowlist?"} Filter -->|"yes"| Public["Plate/reference · public phase ·
origin/destination · ETA · current place ·
trail · waypoint timeline"] Filter -->|"no"| Blocked["Tenant IDs · driver & device IDs ·
raw mission status · battery & device health ·
corridor internals · Route Guard breaches ·
anomaly counts"] class Internal observed class Filter ingress class Public safe class Blocked risk classDef observed fill:#fff7ed,stroke:#ea580c,color:#7c2d12 classDef ingress fill:#eff6ff,stroke:#2563eb,color:#1e3a8a classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d ``` ::: warning Boundary The portal exposes a public DTO, not the fleet app's mission object. New fields stay private until they are deliberately added to the allowlist, translated, and reviewed against the customer boundary. ::: The portal deliberately hides operational detail. Route Guard remains the product name for corridor monitoring inside Korido, but the public view exposes none of its internals — no breach states, no active anomaly counts, no raw alert objects. Border and checkpoint waypoints appear as ordinary timeline stops. Every live payload is keyed by mission, because a customer tracks a shipment — and because one truck can appear in more than one linked shipment. The privacy contract itself is grounded in [Part 8 — The platform](../08-platform/). ## Link gating, scope, and expiry A link's whole lifecycle belongs to the owner, giving them full control over distribution and revocation: * **Scope.** A link can cover one mission or several, and one customer can receive a single link for a convoy of trucks — but a customer only ever sees the missions on their own link, never the wider fleet. * **Expiry.** When creating a link the owner picks its lifetime — 7, 14, or 20 days, defaulting to 20 — and the portal caps any link at a hard maximum of 20 days. Once expired, the view closes. * **Revocation.** An owner can revoke a link at any time; the next attempt to load it fails cleanly. The owner also sees each link's access count. Sharing is entirely an owner action. There is no customer-side "share" or "contact the carrier" control — distribution and revocation stay with the operator who owns the shipment. ## The boundaries of this surface * **One mission, customer-safe only.** The portal shows only the linked mission(s) and only the allowlisted, customer-facing facts. Internal identifiers, raw status, device health, and Route Guard internals never appear. * **Pull-only, today.** The customer refreshes a live view; the portal does not push milestone messages. Proactive delivery updates are a future direction. * **No accounts, no portfolio.** Access is per-link and anonymous behind the phone gate. There is no customer login or multi-shipment portfolio. * **Korido-branded.** The public view carries Korido's own "Livraison" chrome; a linked client name may appear as context, but the portal is not a white-label carrier site and offers no tap-to-call. ## Known limitations * **Staleness is judged per device, not on one universal clock.** The threshold past which the portal stops calling a position "live" is the same window the owner's offline alert uses: the device's own expected silence — a per-device override, else its model's reporting cadence, else the tenant default — plus a grace beat. In practice that lands roughly between **40 and 70 minutes**, depending on how talkative the tracker is: a chatty unit is called stale sooner than a sleepy one that only heartbeats hourly. This is deliberate — it keeps the customer's "live" indicator honest against the exact boundary the operator is alerted on — but it does mean two trucks can cross into "stale" at different ages. ## How it connects * [The fleet app](./fleet-app) — where an owner creates, shares, and revokes the links this portal serves. * [Clients](../05-missions/clients) — where a client's tracking links are administered, grouped by the customer they were shared with. * [Part 5 — Missions](../05-missions/) — the mission lifecycle and ETA the public phases and estimate are derived from. * [The WhatsApp channel](./whatsapp) — how tracking links reach customers. * [Part 8 — The platform](../08-platform/) — the tenancy and security model behind the public boundary. --- --- url: /07-surfaces/whatsapp.md --- # WhatsApp as a Channel ## What this chapter covers WhatsApp is a channel that reaches people where they already are. This chapter describes the three ways Korido uses it: delivering vehicle alerts to owners through approved French templates, answering owners' questions through a conversational bot, and prompting drivers. It covers the delivery windows and retry behavior that make alerts trustworthy, and the per-tenant and platform switches that decide whether anything is sent at all. ## Who it is for WhatsApp serves the people who do not sit in front of the fleet app all day. An **owner** receives corridor alerts and can ask the bot a quick question from the same thread that carries their sign-in codes. A **driver** receives directed prompts, such as a request to confirm a pause. Customers meet WhatsApp only as the carrier of a [tracking link](./tracking-portal); proactive milestone messages to customers are a future direction. The channel is French-first throughout — templates, bot replies, and prompts all speak the corridor's operating language. ```mermaid flowchart LR Engine["Fleet engine
vehicle events"] -->|"alerts"| Owner["Owner
(WhatsApp)"] Owner -->|"localiser · statut · mission"| Bot["Korido bot"] Bot -->|"reply"| Owner EnginePrompt["Engine
pause prompt"] -->|"directed"| Driver["Driver
(WhatsApp)"] ``` ## Alert delivery When the engine raises a vehicle event worth telling an owner about, the delivery decision is made once, at the moment the event is recorded, and then carried out by scheduled senders. Two things happen up front: the event waits out a short **anti-flap** delay of 5 minutes so a condition that instantly clears never becomes a message, and its delivery mode — immediate or digest — is resolved from the tenant's configuration and frozen onto the event. ```mermaid sequenceDiagram participant Engine as Fleet engine participant Event as Vehicle event participant Cron as Delivery sender participant Meta as WhatsApp Cloud API participant Owner as Owner Engine->>Event: Record event, resolve delivery mode Note over Event: 5-min anti-flap delay
auto-resolved events are suppressed Cron->>Event: Every 2 min — claim due "immediate" events Cron->>Meta: Send French alert template to each verified owner Meta-->>Owner: Alert delivered Cron->>Event: Record outcome (sent / retry / failed) Note over Cron: Hourly — group "digest" events
into one capped summary per tenant ``` Two delivery windows carry the traffic: * **Immediate alerts** are drained every 2 minutes. Each due event renders its own French copy and sends the vehicle-alert template to every verified owner of that tenant. Every send is recorded. * **Digest alerts** are gathered hourly and grouped into a single capped summary per tenant, sent through the digest template — so lower-urgency events arrive as one readable roundup. A separate path prompts a mission's **owner and driver** to confirm a pause through its own French template, sent directly by the engine when a mission looks paused. **Retry behavior** keeps delivery honest without spamming. A send that fails transiently is retried; after the third failed attempt the event is marked failed rather than retried forever. Status transitions only ever move forward, so an event cannot be delivered twice by a re-run. What actually went out is observable, down to the latency from the truck's timestamp to the send. ## The bot The same WhatsApp thread that delivers alerts also answers questions. An owner sends a short command and the bot replies from live fleet state. The current vocabulary is small and purposeful: * **localiser** — where is a vehicle now. * **statut** — a vehicle's current status. * **mission** — the active mission for a vehicle. * **aide** — help. * **menu** — the list of what the bot understands. Every inbound message is handled defensively. WhatsApp delivers webhooks at least once and re-delivers on any hiccup, so Korido dedupes each message on its unique Meta message id before acting — a redelivered message never runs twice or replies twice. Inbound numbers are rate-limited, and the "you are rate limited" notice itself is sent at most once per window so it cannot become the very flood it announces. A command is parsed into an intent, answered within the sender's own tenant scope, and the reply is recorded. ## Windows and switches Whether anything is sent at all is governed by two independent gates. ```mermaid flowchart TD Send["About to send an alert"] --> Global{"Platform delivery
enabled?"} Global -->|"off (pilot default)"| Hold["Held — event stays due,
not burned to failed"] Global -->|"on"| Tenant{"Tenant WhatsApp
enabled?"} Tenant -->|"off"| Suppress["Channel suppressed
for this tenant"] Tenant -->|"on"| Deliver["Deliver the template"] ``` * **Per-tenant enablement.** A tenant can switch its WhatsApp channel off. Both delivery paths honor that toggle on the same layer: an event whose type has WhatsApp disabled for its tenant is suppressed as channel-disabled — whether it would have gone out immediately or rolled into the hourly digest — while everything else for that tenant stays intact. * **The platform kill-switch.** Outbound WhatsApp is gated globally and is fail-safe off — during the pilot it defaults to off. While paused, due alerts stay pending rather than being attempted and burned to failed, so nothing is lost; they simply wait. Sign-in codes and bot replies are unaffected by the proactive-alert pause. The gate distinguishes a deliberate pause from a misconfiguration, and a paused deploy can never quietly send a broken message. Turning proactive delivery on is a deliberate operation, not a flag flip: the French templates must be approved on the WhatsApp side, the accumulated backlog of waiting alerts must be reconciled so the first live tick does not flood owners with stale news, and only then is the switch enabled. As a matter of channel hygiene, templates are French-first with explicit opt-in, and outbound volume is warmed up gradually — narrow high-value alerts before broad digests. ## The boundaries of this surface * **A messaging channel.** WhatsApp carries alerts, replies, and prompts. Alert triage and mission management happen in the [fleet app](./fleet-app). * **Owner and driver, today.** Alerts go to verified owners and directed prompts to drivers. Proactive customer milestone messages are future work. * **Alerts are auto-generated.** Outbound alerts are approved templates rendered from engine events, each one tied to the event that triggered it. * **Fail-safe by default.** When in doubt, the channel sends nothing and holds the work rather than risking a wrong or stale message. ## How it connects * [Part 6 — Fleet intelligence](../06-intelligence/) — the vehicle events that become alerts, and how they are prioritized. * [The fleet app](./fleet-app) — where owners triage the same alerts they receive on WhatsApp. * [The tracking portal](./tracking-portal) — the customer link WhatsApp delivers today. * [Part 8 — The platform](../08-platform/) — the scheduled senders, idempotency, and reliability behind delivery. --- --- url: /07-surfaces/admin-portal.md --- # The Admin Portal ## What this chapter covers The admin portal at **admin.korido.net** is the operating console for the Korido platform team. This chapter describes who works in it, the platform tasks it owns — tenant and hardware administration, curation of the shared road network, cross-fleet analytics, and operational tooling — and why its cross-tenant reach is deliberate. ## Who it is for The admin portal is for Korido staff holding the platform-only `admin` role. A tenant owner never becomes an admin, and an admin never becomes a tenant owner except through an explicit, audited impersonation handoff. That separation is the whole point: the portal answers platform questions a single company's dashboard cannot. * Which tenants exist, and are they configured correctly? * Which tenant has a telemetry, WhatsApp, fuel, queue, or freshness problem? * Are the shared corridors and waypoints healthy? * Where along the corridor is GSM coverage failing, across every fleet? * Which fleets are producing alert volume that needs a support call? The portal is a separate deployment from the fleet app with its own session and cookies. It is a daytime back-office tool: it ships a single light theme by design, with no dark mode. ```mermaid flowchart TD subgraph Platform["Platform administration"] Tenants["Tenants
list · onboarding · detail"] Inventory["Inventory
devices · SIMs · components"] Payments["Payments ledger"] end subgraph Network["Shared road network"] Waypoints["Waypoints"] Segments["Road segments"] Corridors["Named corridors"] end subgraph Ops["Operations & intelligence"] Analytics["Segment analytics"] GSM["GSM coverage"] Alerts["Alert volume"] Health["Platform health"] end Network -->|"consumed by"| FleetApp["Fleet app
mission planning"] class Tenants,Inventory,Payments risk class Waypoints,Segments,Corridors reference class Analytics,GSM,Alerts,Health intelligence class FleetApp surface classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d classDef reference fill:#ecfdf5,stroke:#047857,color:#064e3b classDef intelligence fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 classDef surface fill:#eff6ff,stroke:#2563eb,color:#1e3a8a ``` ## Tenants, hardware, and money The **tenants** list is the primary support index — tenant identity plus operational hints (vehicle and trailer counts, onboarding completeness, recent activity), built for a quick support scan. A tenant's detail page gathers its users, vehicles, trailers, documents, WhatsApp stats, and fuel configuration, and it is the launch point for owner impersonation. An onboarding wizard walks a new company from owner and truck through to an optional trailer and tracker in a few guided steps. Beyond the tenant walls, two platform-wide surfaces manage the physical fleet. An **inventory** surface tracks every device, SIM, component, supplier, and batch across all tenants plus Korido's unassigned stock, with the full provisioning lifecycle — create, assign, install, swap, uninstall, decommission. Editing a device also carries a per-device **expected-silence override** — the longest a specific tracker may stay quiet before it counts as offline, entered in minutes (from **1 minute to 24 hours**); left blank, the device falls back to its model's reporting cadence. It sits at the top of the silence chain the offline alert and the tracking portal both read, so a sleepy unit is told apart from a genuine outage without touching tenant-wide settings. A **payments** ledger records the platform revenue side with record, void, and CSV export. Impersonation is the support bridge into a tenant's own experience. An admin picks an eligible owner, the platform mints a short-lived handoff token, and the fleet app redeems it into a clearly-marked, reversible impersonated session. The portal does not mirror every owner feature just to support one — it hands the operator the real owner surface instead. ## Curating the road network The shared road network is platform intelligence: owners *select* corridor coverage for their missions, but only admins *define* it. This keeps Route Guard behavior consistent across every fleet and prevents one tenant from changing route logic that others depend on. The **corridor workbench** is a map-first editor organized into three layers that build on one another. ```mermaid flowchart LR W["Waypoints
ports, depots,
borders, checkpoints"] --> S["Road segments
centerline + width
→ buffered polygon"] S --> C["Named corridors
ordered waypoint route,
e.g. Douala → N'Djamena"] C -->|"per-leg curation"| Pin["Pin a variant
(default: automatic canonical)"] C -->|"per-leg curation"| Rev["Add reverse override
when only a forward
variant exists"] class W,S,C reference class Pin,Rev warn classDef reference fill:#ecfdf5,stroke:#047857,color:#064e3b classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 ``` * **Waypoints** are the named anchors along the corridor — ports, depots, border crossings, checkpoints, and known places. Editing a waypoint's geometry bumps a geometry version and is preflighted for downstream impact, because other structures reference it. * **Road segments** are the global travel corridors between a pair of waypoints. An admin previews candidate driving routes, then saves a segment as a centerline plus a width, which Korido buffers into the polygon Route Guard watches. Editing a segment's centerline or width recomputes that polygon — and recomputes the Route Guard polygon of any in-flight mission that references it, closing the affected active deviations with a replacement reason. A pure name or active-state change does not trigger that propagation. * **Named corridors** are ordered waypoint routes — Douala → N'Djamena and its siblings — assembled origin → interim → destination. This is the per-road curation view. For every consecutive leg, the admin can **pin a variant**: choose exactly which road segment carries that leg, with "automatic (canonical)" as the default. And where a leg has only a forward segment, an explicit **reverse override** action creates its opposite-direction variant, so a corridor driven the other way is guarded just as tightly. Naming a corridor once is what lets the fleet app's quick-assign infer coverage from an origin and destination. The model is deliberately additive. Segment deletion is a reversible archive — the segment is marked deleted and can be restored — and deliberately does not propagate: a geometry *edit* re-unions live missions and closes affected deviations, while a deletion simply retires the row. The full corridor and Route Guard model is the subject of [Part 4 — The road network](../04-road-network/). ## Cross-fleet analytics and benchmarks Because the admin portal sees every tenant, it is the natural home for benchmarks no single fleet could compute. **Segment analytics** pools every tenant's traversals of a curated segment into one performance picture: median and 90th- percentile transit times, day-versus-night breakdowns, and the slowest segments across a 7-, 30-, or 90-day window. Every curated segment appears even when it has no samples yet. These are deliberate platform-wide comparisons with no single tenant's identity attached — a shared map of how the corridor really performs. The intelligence behind them lives in [Part 6 — Fleet intelligence](../06-intelligence/). ## Operational tooling Three surfaces keep the platform observable and the corridor legible: * **GSM coverage** maps connectivity along the corridor across every vehicle and provider, revealing repeated dead zones, provider-specific black spots, and vehicle-specific antenna trouble. It is diagnostic context, not an automatic verdict — a coverage gap explains a silent stretch; it does not excuse every offline event. * **Alert volume** gives cross-tenant operational context: which fleets are producing unusual event volume and which event types dominate, so support knows where to reach out. This is distinct from the fleet app's per-tenant alert triage. * **Health** reads the backend's deep-health check and maps it into clear operational states for the database, telemetry freshness, storage, dead-letter depth, and queue backlog — the first stop for "is Korido healthy right now?" ## The boundaries of this surface * **Cross-tenant, on purpose.** The portal deliberately reads across tenant walls where a platform task requires it. That reach is confined to the admin role and the platform surfaces; it never leaks into the tenant-scoped fleet app. * **Owner access happens through impersonation.** Platform staff who need the owner experience impersonate an owner rather than rebuilding fleet workflows here. * **Global edits announce their blast radius.** Corridor and waypoint changes can affect active missions, so the workbench distinguishes name-only edits from geometry edits that propagate. * **Support context only.** WhatsApp stats and alert volume are triage context; they are not a messaging console with template editing, resend, or subscription management. ## How it connects * [Part 4 — The road network](../04-road-network/) — the corridor, waypoint, and Route Guard model the workbench curates. * [Part 6 — Fleet intelligence](../06-intelligence/) — the segment analytics and spatial intelligence surfaced here. * [Part 8 — The platform](../08-platform/) — tenancy, security, and the reliability signals the health page reads. * [The fleet app](./fleet-app) — the tenant surface that consumes the road network defined here. * [The WhatsApp channel](./whatsapp) — the delivery path the WhatsApp stats summarize. --- --- url: /08-platform.md --- # Part 8 — The platform: the guarantees underneath The earlier parts describe what Korido does. This one describes what it *promises* while doing it — the guarantees every feature quietly leans on, invisible on every screen. A dispatcher trusts that the fuel gauge belongs to this truck alone; that a position buffered through a dead zone still lands; that yesterday's timeline reads exactly as the truck reported it. These are properties of the platform, and this part is where they are made explicit. Three chapters cover the three promises in turn: how the data is shaped, who is allowed to see it, and how it survives failure. * **[Data architecture](./data-architecture)** — how Korido's data is organized into four planes (the road, the plan, what happened, what was learned), why the observed record is written once and never edited, and how a live screen is composed from durable facts at the moment it is asked for. * **[Tenancy and security](./tenancy-and-security)** — how one system carries many fleets with no fleet ever seeing another's trucks: a tenant filter enforced in the query layer and proven by a suite that tries to cross it, plus how people sign in, how support safely stands in an owner's shoes, and how a customer gets a private window without an account. * **[Reliability](./reliability)** — how Korido stays correct when trucks drive through dead zones and databases stall: at-least-once delivery with patient retries, a dead-letter archive that recovers a long outage, strictly serial per-truck processing, and the scheduled jobs that close whatever real life left hanging open. The unifying idea is that **these guarantees are structural.** Tenant scope is a type that makes the tenant explicit before a scoped query can run; isolation is completed by the predicate each query writes and by tests that try to cross it. Immutability is an append-only key; correctness under retry is idempotency built into every write path. The platform keeps its promises because breaking them was made hard to miss — which is exactly why the surfaces above can stay simple. --- --- url: /08-platform/data-architecture.md --- # Data Architecture ## What this chapter covers Korido's data is organized into four planes: what the road *is*, what work is *planned*, what actually *happened*, and what the fleet has *learned*. This chapter maps those planes and their major tables, then explains how a live dashboard is composed from them — including the one denormalized row that answers "where is this truck and what is it doing right now" — and why the raw record of what happened is written once and never edited. ## The picture The four planes flow in one direction. Reference geometry is configured once and underpins everything. Plans are drawn on top of that geometry. Observations stream in from the trucks and are matched against the plans. And intelligence is distilled from the accumulated observations back onto the geometry, where it sharpens the next plan. ```mermaid flowchart LR subgraph REF["Reference · configure once"] W[waypoints] RS[road_segments] CO[corridors] GC[geocode_cache] end subgraph PLAN["Plan · the intended work"] M[missions] MS[mission_segments] T[mission_templates] R[rule_sets] CL[clients] CV[convoys] end subgraph OBS["Observed · what happened"] P[positions] TR[trips] ST[stops] DG[data_gaps] WV[waypoint_visits] SG[segment_traversals] VE[vehicle_events] FE[fuel_events] end subgraph INT["Intelligence · learned across the fleet"] SZ[slowdown_zones] RH[route_hotspots] CA[corridor_anomalies] end REF --> PLAN PLAN --> OBS OBS --> INT INT -.sharpens.-> PLAN class W,RS,CO,GC reference class M,MS,T,R,CL,CV plan class P,TR,ST,DG,WV,SG,VE,FE observed class SZ,RH,CA intelligence classDef reference fill:#ecfdf5,stroke:#047857,color:#064e3b classDef plan fill:#eff6ff,stroke:#2563eb,color:#1e3a8a classDef observed fill:#fff7ed,stroke:#ea580c,color:#7c2d12 classDef intelligence fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 ``` The major entities and how they relate: ```mermaid erDiagram corridors ||--o{ road_segments : "paved with" corridors ||--o{ waypoints : "sequences" geocode_cache { point place label cached_name } clients ||--o{ missions : "ships with" convoys ||--o{ missions : "groups" mission_templates ||--o{ missions : "instantiates" rule_sets ||--o{ missions : "governs" corridors ||--o{ missions : "routes over" missions ||--o{ mission_segments : "planned as" devices ||--o{ vehicles : "installed on" vehicles ||--o{ positions : "emits" vehicles ||--o{ trips : "drives" vehicles ||--o{ stops : "rests in" vehicles ||--o{ data_gaps : "goes dark in" vehicles ||--o{ fuel_events : "fills or drains" vehicles ||--o{ vehicle_events : "raises" vehicles ||--o{ waypoint_visits : "arrives at" waypoints ||--o{ waypoint_visits : "visited by" missions ||--o{ segment_traversals : "measured as" road_segments ||--o{ slowdown_zones : "hosts" corridors ||--o{ route_hotspots : "reveals" corridors ||--o{ corridor_anomalies : "disrupted by" ``` ## The four planes **Reference — the road, configured once.** `corridors` are the named routes the fleet runs; each is sequenced from `waypoints` (ports, borders, checkpoints, depots, fuel stations, cities) and paved with `road_segments` (the geometry between consecutive waypoints). `geocode_cache` holds reverse-geocoded place names, so a coordinate becomes a human label without asking a mapping provider every time. This plane is shared operational reference: platform admins curate the corridor geometry, and owners select those corridors when they create missions rather than editing the road network themselves. **Plan — the work to be done.** A `mission` is one truck's assignment along a corridor, shipped for a `client`, optionally moving as part of a `convoy`. It can be spun up from a `mission_template` (a reusable shape for a recurring journey) and governed by a `rule_set` (the driving rules and checkpoints it must honor). A mission is decomposed into ordered `mission_segments` — the waypoint-to-waypoint legs that make the journey measurable. **Observed — what actually happened.** This is the plane the trucks write. Raw `positions` stream in from the tracker; from them the engine derives the structural record of activity: `trips` (movement), `stops` (rest), `data_gaps` (stretches where the signal went dark), `waypoint_visits` (arrivals at known places), `segment_traversals` (a leg completed, with how long it actually took), `fuel_events` (fills and drains from the tank sensor), and `vehicle_events` (the alert and judgment layer — a drain flagged as suspicious, a device gone offline). Everything in this plane hangs off a `vehicle`, which in turn is the truck a `device` is installed on. **Intelligence — learned across the fleet.** Distilled from many observations: `slowdown_zones` (stretches of road that are reliably slow), `route_hotspots` (places trucks recurringly dwell), and `corridor_anomalies` (active disruptions on a corridor). This plane feeds better arrival predictions back into the plan, often pooling evidence across fleets — which is why it carries its own privacy boundary: shared rows are aggregate geography, never another fleet's raw movements. ## Read models: composing the answer at query time The planes hold durable facts. The surfaces people actually look at — the fleet overview, the vehicle diary, the live map, a mission's detail, the fuel dashboard, the customer tracking view — are **read models** composed from those facts at the moment they are asked for. Each read model is a query that joins the open structural rows and current clocks into the shape a screen needs. Keeping durable facts normalized and composing views on demand keeps the truth in exactly one place, current by construction. The engine's structural facts are the source; the interface never re-derives them. A screen decides whether a truck is driving by reading whether a trip is open, and judges whether the signal is stale by reading whether a data gap is open. This keeps every surface telling the same story, because they all read the same facts. ## The vehicle's last-state row One question is asked constantly and must be cheap: *where is this truck and what is it doing, right now?* Answering it by scanning a truck's position history on every dashboard load would be wasteful, so the current state is **denormalized** onto the vehicle's own row and refreshed once at the end of each batch the engine successfully processes. That single row is the spine of the live map and the fleet overview. Its most subtle property is that it is filled from **two different clocks**, on purpose: ```mermaid flowchart TD A[Incoming batch of frames] --> B{Frame has a trusted GPS fix?} B -->|yes| C["Position fields
lat, lon, speed, heading
← last trusted fix"] B -->|"no fix (status/heartbeat frame)"| D["Telemetry tiles
ignition, battery, signal, voltage
← freshest frame, fix or not"] C --> E[(vehicle last-state row)] D --> E class A source class B warn class C safe class D warn class E store classDef source fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef store fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 ``` * **Position** — latitude, longitude, speed, heading — is anchored to the last *trusted* fix. A degraded or rejected trailing frame must never make a stale location look fresh, so the coordinate on the row is always one the engine accepted. * **Telemetry** — ignition, battery, signal strength, external voltage — is taken from the *freshest* frame, whether or not that frame carried a location. Trackers that report by motion send coordinate-less status frames while parked; those frames still prove the truck's ignition is off and its battery is healthy. So the visible ignition/battery/signal/voltage tiles stay live on a parked truck even as its map position deliberately holds still. This two-clock split is why a parked truck can honestly show a fresh battery reading and a location from an hour ago at the same time, and why the system never has to choose one misleading "last seen" timestamp. ## Where the different shapes of data live Korido spreads its data across three stores, each used for what it is best at. ```mermaid flowchart LR DB[(PostgreSQL
durable facts + read models)] KV[[KV
live-route + coordination]] R2[(R2
archives + dead-letter
documents + photos)] DB <--> KV DB --> R2 class DB store class KV queue class R2 archive classDef store fill:#ecfdf5,stroke:#047857,color:#064e3b classDef queue fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef archive fill:#fef2f2,stroke:#dc2626,color:#7f1d1d ``` ::: warning Boundary KV and R2 are not alternate sources of business truth. KV accelerates and coordinates; R2 preserves evidence, replay material, and private document objects. PostgreSQL remains the queryable record for tenant facts and read models. ::: ::: details Implementation note The four planes are a reading model, not four separate databases. The current implementation keeps the durable relational record in PostgreSQL, while KV and R2 hold the hot coordination state and bulk objects that support that record. ::: * **PostgreSQL** holds the four planes and every read model — the durable, queryable truth. * **KV** is a fast key-value store for hot, ephemeral state: the live-route store behind a customer's tracking view, one-time login codes, refresh-token hashes, rate-limit counters, and the soft locks that keep scheduled jobs from stepping on each other. It is fast and widely replicated, so it is used for coordination and caching, never as the sole home of irreversible business truth. * **R2** is object storage for cold and bulk data: archived telemetry batches, dead-lettered messages set aside for replay or offline inspection, and private document objects such as driver photos and admin-uploaded PDFs. For telemetry archives and DLQs, R2 is the last-resort evidence store — a successful archive there is proof a message was kept, not proof the database state derived from it is correct. ## Why observed facts are immutable and deduplicated The observed plane is **append-only**. A raw position is written once and never edited; the structural records derived from it are opened and closed but not rewritten after the fact. Immutability is what makes the record auditable: the history a report or an investigation reads is exactly what the truck reported at the time. That guarantee only holds if the same observation cannot land twice — and it can arrive twice, because a truck buffers positions through a dead zone and flushes them later, and because a message that fails processing is retried. So every raw observation is keyed by **one row per device per capture instant**: the device it came from plus the exact timestamp it was captured. A second copy of the same instant is silently dropped on insert. This one rule is what lets the whole pipeline be safely "at-least-once" — a batch can be delivered, retried, or replayed any number of times and the record stays clean, because a duplicated instant collides with the row already there and is discarded. The dedup key is the capture instant, not the mission, the trip, or the provider's own sequence number, so the guarantee survives a device being reassigned or its data being backfilled. Frames that carry no location — status and voltage heartbeats — are stored the same way, in the same table, on the same one-per-instant key, with an empty position. They stay off the map while still advancing the liveness clocks and feeding the fuel and gap logic, which is how the system can tell "the truck is quietly parked" apart from "the truck has gone dark." ## Edge cases * **A buffered flush after a dead zone.** Dozens of positions arrive at once, hours after they were captured. Each keeps its original capture instant, so they slot into history in the right order; any that overlap positions already stored collide on the key and are dropped. * **A replayed batch.** Re-sending a batch that was partly stored before it failed re-inserts only the missing instants and discards the rest — replay is a no-op for anything already recorded. * **A parked truck streaming coordinate-less frames.** Its map position holds at the last trusted fix while its telemetry tiles keep refreshing from the newest status frame, so the dashboard shows a fresh battery and an intentionally still location together. * **A failed batch.** When processing a batch fails, the engine-derived records roll back and the batch is retried, but liveness still advances outside the rolled-back work so a wedged-but-still-transmitting truck still looks alive. A batch with no fix advances only the message clock; a batch that carries a fix also advances the last position from its newest raw fix. The trips, stops, and gaps wait for a clean reprocess. * **A long-silent truck's fuel gauge.** The fleet overview stops showing a fuel reading once a truck has been silent for more than seven days, so the gauge cannot present a week-old value as current — even though the engine still runs fuel detection against that truck's own newest reading. * **A shared intelligence row.** Slowdown zones, hotspots, and corridor anomalies can pool evidence across fleets, but they are stored as aggregate geography. A tenant-facing query never surfaces another fleet's raw movements through them. ## How it connects * [Tenancy and Security](./tenancy-and-security) — the tenant filter that guards every table in these planes. * [Reliability](./reliability) — how at-least-once delivery, dedup, and serial-per-vehicle processing keep the observed plane correct under retries. * **Part 2 — Telemetry** and **Part 3 — The fleet engine** — how raw positions become the trips, stops, gaps, and fuel events of the observed plane. --- --- url: /08-platform/tenancy-and-security.md --- # Tenancy and Security ## What this chapter covers Korido carries many fleets on one system, and no fleet may ever see another's trucks. This chapter explains how that wall is built — an isolation model that lives in the query layer, not the database — plus who is allowed to reach what, how people prove who they are, how a support engineer safely stands in an owner's shoes, and how a customer gets a private window onto one shipment without an account. ## The picture Every request that touches fleet data passes through the same gate. A person proves who they are, that proof yields a verified **tenant** — the fleet they belong to — and from that point on every question the system asks the database carries that tenant's identity as a filter. ```mermaid flowchart TD A[Person or device] -->|phone + one-time code| B[Verified session] B -->|carries tenant + role| C{Which kind of work?} C -->|fleet owner or driver| D[Tenant-scoped read/write] C -->|KORIDO support| E[Cross-tenant service work] D -->|"every query filtered by tenant_id = my tenant"| F[(Database)] E -->|"explicit, reviewed, audited"| F G[Customer with a tracking link] -->|phone gate| H[One shipment, allowlisted view] H -->|scoped to the link's tenant| F class A,G source class B,H ingress class C warn class D safe class E risk class F store classDef source fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef ingress fill:#eff6ff,stroke:#2563eb,color:#1e3a8a classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d classDef store fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 ``` The single most important idea in this chapter: **the tenant filter is the wall.** The database applies no isolation of its own — the filter written into the query is the only thing standing between one tenant's rows and everyone else's. If a tenant-scoped query did not name its tenant, it would read everyone's rows. So Korido makes the tenant explicit in scoped query APIs, then proves the wall holds with a test suite that tries to cross it. ::: warning Boundary There is no hidden database isolation layer rescuing a missing predicate. A tenant-scoped read is correct only when it carries the verified tenant filter or joins through a tenant-filtered parent. ::: ## How isolation actually works Every tenant-scoped table carries a `tenant_id` column. Isolation is the discipline that **every** query against those tables includes the predicate `tenant_id = `, and that the tenant on the right-hand side comes from the verified login token, never from anything the caller can choose. Three things, working together, make this reliable rather than aspirational: 1. **A verified tenant, carried in a typed scope.** When a session is verified, the tenant id it proves is bundled with the database handle into a single scope value. Tenant query functions accept only that scope — never a bare database connection. The type system will not let you call those scoped query helpers without a scope, and the only way to build a scope is to supply a tenant id. "Forgot which tenant" therefore fails before the query helper can even be called. 2. **The predicate is written into every query.** The scope guarantees the tenant id is *available*; the author is responsible for putting it into the `WHERE`. For an ordinary tenant table the predicate is a direct match on `tenant_id`. Tables that legitimately hold shared reference rows alongside tenant rows — waypoints, corridor anomalies, WhatsApp message logs — filter with "the row is shared (`tenant_id IS NULL`) **or** it is mine," so shared geography stays visible while private rows stay walled. Junction tables scope through their tenant-owned parent. 3. **A regression suite proves no read crosses the line.** Because the wall is a convention that authors must uphold, it is verified empirically. A two-tenant isolation suite seeds two fleets into every table and asserts that a scope for fleet A never returns a single row belonging to fleet B. Crucially, these tests run as a database superuser — the most permissive possible condition — so a query that drops its predicate is caught by the suite failing, not by luck. A tenant query with no two-tenant test guarding it is considered unverified. A small set of paths are **cross-tenant by design** and use a plain database handle with no tenant bound: telemetry ingestion (which must look a device up before it knows whose truck it is), the scheduled background jobs, the KORIDO admin portal, the first moment of a customer tracking lookup (before the link's owner is known), and the shared-intelligence jobs that learn corridor patterns across the whole fleet. These are explicit exceptions, reviewed as such. When one of them already knows the tenant, it still writes the filter — a service handle is permission to cross tenants deliberately, not license to skip the predicate. ## The four roles and what each reaches Korido has four roles. Two belong to a fleet; two belong to the platform. | Role | Belongs to | Reaches | | --- | --- | --- | | **owner** | a tenant | The full fleet workspace for their tenant — vehicles, drivers, trailers, missions, alerts, Route Guard, fuel, reports. Everything is filtered to their tenant. | | **driver** | a tenant | Their own mission actions and receipt submissions through the mobile API, scoped to their tenant. Never another tenant's data. | | **admin** | KORIDO | The admin portal: cross-tenant platform operations and support. `admin` is a platform-only role — never a way to be "an owner of every fleet." | | **viewer** | a tenant | A read-only tenant role, recognized by the system for a future read-only access tier. | The rule behind the table: a fleet role can only ever act inside its own tenant, and a platform role is never treated as a tenant member. A support engineer who needs to see a specific fleet's workspace gets there through impersonation, described below, which is audited. ## Logging in: a phone and a one-time code A person signs in with only a phone number and a six-digit code. A worker-configuration switch governs how the code travels during staged rollout: with delivery off, sign-in uses a controlled bypass path and no outbound message is sent; with delivery on, the same flow sends the six-digit code over WhatsApp. ```mermaid sequenceDiagram box rgb(236,254,255) User participant U as Person end box rgb(239,246,255) Auth boundary participant K as Korido end box rgb(245,243,255) Delivery channel participant W as WhatsApp end U->>K: Enter phone number K->>K: Rate-limit this phone (max 5 requests/hour) alt Delivery switch off (staged rollout) K-->>U: Sign-in completes through the controlled bypass path else Delivery switch on K->>W: Send 6-digit code W-->>U: Code arrives end U->>K: Enter code K->>K: Rate-limit verify (max 20 attempts/hour), check code K-->>U: Session issued (cookies for web, token for mobile) ``` The code lives for ten minutes and tolerates three wrong guesses before it is thrown away. Two per-phone limits bound abuse independently of any single code: at most five code requests an hour, and at most twenty verification attempts an hour — the verification cap is what actually makes brute-forcing a six-digit code hopeless. Phone numbers are masked wherever they would otherwise appear in logs. Because the WhatsApp channel is brought live in stages, OTP delivery carries a worker-configuration switch for staged rollout. While the channel is still being provisioned, the switch lets a sign-in complete without dispatching an outbound message, so real authentication can be exercised end-to-end before proactive WhatsApp is enabled; flipping a single worker-config value then routes every code through live delivery once the channel is ready. Like every other credential, the switch lives in worker configuration rather than the repository, and it holds the pre-launch posture until delivery is deliberately turned on. ## Sessions A verified login yields a signed session token whose claims are read directly off the top of the token: who you are, your tenant, your role, and when the token was issued and expires. Nothing is nested or borrowed from an external identity provider — Korido issues and verifies its own tokens. * **The web apps** (fleet app, admin portal) keep the session in `HttpOnly` cookies scoped to the exact site that issued them. Server code verifies the cookie locally on each request; there is no round-trip to a central auth service. * **The mobile and machine APIs** carry the token as a bearer credential on each request. Access tokens are deliberately short-lived — eight hours for a fleet user, four hours for a platform admin. A longer-lived **refresh** credential (a high-entropy random value, of which only a hash is ever stored, kept for thirty days) lets a session renew itself quietly in the background without asking the person to log in again. When a session expires while someone is mid-task, the app follows the expiry to the login screen rather than stranding them on a dead page. ## Impersonation: support that stands in, on the record When a KORIDO support engineer needs to see exactly what a fleet owner sees, they impersonate that owner. This is a real, verified session for that owner's tenant — not a special "god view" — so every isolation rule above applies unchanged, and support sees precisely the owner's data and nothing wider. ```mermaid sequenceDiagram box rgb(254,242,242) Platform admin participant A as Admin portal end box rgb(239,246,255) Handoff boundary participant K as Korido end box rgb(236,253,245) Tenant workspace participant F as Fleet app end A->>K: Impersonate owner X K->>K: Confirm target is an owner, write audit row (start) K-->>A: Short-lived handoff token (valid ~2 minutes) A->>F: Redirect with handoff token F->>K: Redeem token K-->>F: Impersonation session (24h, no refresh) Note over F: Session records that the admin is acting as the owner F->>K: Exit impersonation K->>K: Write audit row (end) ``` The guardrails are deliberate. Only **owners** can be impersonated — never drivers, never other admins. The handoff from admin portal to fleet app rides a single-use token that lives about two minutes. The resulting session is capped at twenty-four hours and cannot silently refresh, so support access always ends. And both the start and the explicit exit are written to the audit log, so "who looked at this fleet, and when" is always answerable. The session itself records that an admin is *acting as* the owner, keeping the real actor visible behind the stand-in. ## Customer tracking links: signed, gated, expiring grants A fleet owner can hand a customer a link to watch one shipment — no account, no login. That link is a narrow, revocable grant: a deliberate opening in the wall. ```mermaid flowchart TD A[Customer opens tracking link] --> B{Link valid?} B -->|revoked or expired| Z["Uniform 'phone doesn't match' — reveals nothing"] B -->|valid| C[Ask for phone number] C --> D{Phone matches the shipment's contact?} D -->|no| Z D -->|yes| E[Issue signed gate cookie] E --> F[Allowlisted shipment view: position, ETA, progress] class A source class B,D ingress class C,E warn class F safe class Z risk classDef source fill:#ecfeff,stroke:#0891b2,color:#164e63 classDef ingress fill:#eff6ff,stroke:#2563eb,color:#1e3a8a classDef warn fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d ``` Four properties make the grant safe: * **Signed.** Passing the phone gate issues a cookie stamped with a signature the server can verify. The signature folds in the link's identity, the caller's phone, a per-link secret nonce, and the link's expiry moment. A cookie cannot be forged, and it cannot be lifted from one link onto another. * **Gated.** The customer must also enter a phone number that matches the shipment's contact. An anonymous caller who guesses or scrapes a link learns nothing: an invalid, revoked, or expired link returns the exact same "phone doesn't match" answer as a wrong phone, so the portal never confirms a link even exists. * **Expiring and revocable.** Every link carries an expiry, and an owner can revoke one at any time. Because both the expiry and the nonce are baked into the cookie's signature, expiring a link — or rotating its nonce to revoke it — instantly invalidates every gate cookie already handed out, not just future visits. * **Narrow.** What the customer sees is a purpose-built, allowlisted view — where the truck is, the estimated arrival, the delivery progress. It is assembled to exclude tenant identifiers, device identifiers, raw telemetry, driver phone numbers, and internal operational status. The gate and route lookups are also rate-limited per caller so the public surface cannot be used to probe the system or run its route engine for free. ## Secrets and webhook authentication Every signing key and provider credential — the session-token secret, the tracking-link signing secret, the telemetry-webhook secret, and the WhatsApp tokens — lives in Cloudflare's secret storage and is never committed to the repository; each Worker verifies against its own copy. Inbound webhooks are authenticated before they are trusted: the telemetry feed from the tracker provider must present its shared secret and originate from an allowlisted address range, and WhatsApp callbacks are checked against a verify token and a per-request signature. Rotating any of these has a real product effect — rotating the session secret ends active sessions on the Worker that verifies it, rotating the WhatsApp credentials can pause code delivery and notifications until the new value is live, and rotating the telemetry secret must be coordinated with the provider so live positions are never dropped — so rotation is treated as a deliberate, deploy-time operation. ## Edge cases * **A query that forgets its tenant.** It does not compile: tenant query functions cannot be called without a tenant-bearing scope. If one were somehow written by reaching for a raw handle, the two-tenant isolation suite — running as a superuser, the most permissive condition — fails when it returns a foreign row. * **A device with no fleet yet.** Telemetry ingestion runs cross-tenant precisely because it must resolve a device to its owner before it knows the tenant. A registered device not yet linked to a vehicle still has its heartbeat recorded, but no tenant-scoped rows are written for it. * **Shared geography vs. private rows on the same table.** Some tables hold both fleet-wide reference rows and per-tenant rows. Their queries admit shared rows (`tenant_id IS NULL`) *and* the caller's rows, never another tenant's — so a shared corridor stays visible to all while private anomalies stay walled. * **A stale support session.** An impersonation session cannot refresh and dies within twenty-four hours, so support access is never open-ended. Every start and exit is on the audit record. * **A leaked or scraped tracking link.** Without the matching phone it yields nothing, and it says nothing about whether the link is real, expired, or revoked. Revoking it — or letting it expire — voids the cookies already issued, because the signature is bound to the nonce and the expiry. * **A concurrent code-guessing burst.** The per-code three-attempt counter can be raced by simultaneous guesses, so the real guarantee is the per-phone verification cap (twenty per hour), which holds regardless of concurrency. ## What's ahead Korido reserves a fourth role, **viewer**, for a read-only tenant tier: a member who can see the fleet workspace — the map, missions, alerts, reports — without the authority to change anything. The role is already recognized across the system, so turning the tier on becomes a matter of granting it rather than reworking the permission model. The next step is the read-only surfaces that pair with it, so an accountant, an insurer, or a client-side observer can be handed a genuine window into a fleet without a seat at its controls — every isolation rule in this chapter applying to them unchanged. ## How it connects * [Data Architecture](./data-architecture) — the tables the tenant filter guards, and the read models each surface composes. * [Reliability](./reliability) — how the cross-tenant ingestion and background jobs stay correct under retries and failure. * **Part 7 — The surfaces** — what each role actually sees in the fleet app, admin portal, driver app, owner app, and the customer tracking portal. --- --- url: /08-platform/reliability.md --- # Reliability ## What this chapter covers Trucks drive through dead zones, networks drop, and a database can stall for a few minutes — none of which is allowed to lose a position or corrupt a truck's state. This chapter explains, in product terms, how Korido stays correct under those conditions: messages that are delivered at least once and retried until they land, a dead-letter archive that recovers what a long outage set aside, processing that is strictly ordered per truck, and a family of scheduled jobs that close whatever real life left hanging open. ## The picture Every position rides a queue between the moment it is received and the moment it is processed. That queue promises *at-least-once* delivery: it will keep handing a message back until the work succeeds. A transient failure — a database blip — is simply retried, with the gap between attempts growing each time, so a routine outage rides out inside the queue and never loses data. ```mermaid sequenceDiagram box rgb(236,254,255) External source participant T as Tracker feed end box rgb(254,249,195) Retry buffer participant Q as Positions queue end box rgb(255,247,237) Processing participant E as Engine end box rgb(236,253,245) Durable store participant DB as Database end T->>Q: Batch of positions Q->>E: Deliver E->>DB: Process + store alt Database healthy DB-->>E: Committed E-->>Q: Ack (done) else Database stalled E-->>Q: Retry (back off, try again later) Note over Q: Up to 14 retries after the first
delivery (15 in all), ~38 min of grace Q->>E: Deliver again end ``` The retries are patient by design: fourteen retries after the first delivery — fifteen delivery attempts in all — each backing off further than the last, from two seconds up to a five-minute ceiling. Those retry delays sum to roughly thirty-eight minutes of grace before a batch is considered stuck. A database or connection hiccup shorter than that window is absorbed entirely — the trucks keep reporting, the queue keeps the work, and once the database recovers everything drains through with nothing lost. ## When patience runs out: the dead-letter archive If a batch exhausts its retries — practically, only during an outage longer than the retry window — it moves down a chain of dead-letter queues and is finally archived to durable object storage, where it waits safely for as long as it takes. A scheduled replay job then closes the loop automatically. ```mermaid flowchart LR A[Positions queue] -->|retries exhausted| B[Dead-letter queue] B -->|still failing| C[Terminal dead-letter] C -->|archived| D[(Object storage)] D -->|"replay job, every 5 min"| A D -.->|"can't be parsed"| E[Poison — set aside, counted] class A,B queue class C,E risk class D store classDef queue fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef store fill:#ecfdf5,stroke:#047857,color:#064e3b classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d ``` ::: tip Current behavior Replay is deliberately idempotent: the archive re-enqueues before delete, and position deduplication makes a repeated replay harmless. ::: The replay job lists the archive a bounded page at a time, re-validates each batch, and puts it back at the head of the live pipeline — where it is processed exactly as a fresh arrival. It enqueues a batch *before* deleting it from the archive, so a crash mid-replay safely re-lists and re-sends it next time. Anything that cannot be parsed as a valid batch is parked as "poison" in a separate place and counted, never retried in an infinite loop. Because the archive does not age out while the queues themselves keep messages only a few days, the archive is what turns a survivable-for-days problem into a survivable-indefinitely one. This whole recovery loop is only safe because replay can be repeated harmlessly — which is the next idea. ## Idempotency everywhere: a retry never double-counts At-least-once delivery means the same message *will* sometimes be processed twice. The system is built so that reprocessing changes nothing the first pass already did. Every write path has a way to recognize "I have seen this before": * **Raw positions** are keyed to one row per truck per capture instant, so a replayed or buffered-then-flushed batch inserts only the instants missing and silently discards the duplicates. * **Alerts and events** are deduplicated: only one open event of a given kind can exist for a truck at a time, and one-off notifications carry a key that makes a repeat a no-op. * **The bulk sync path** from the mobile apps carries a per-batch key so a retried upload applies exactly once. * **Scheduled jobs** take a short-lived lock before running, so two ticks cannot do the same sweep at once. The discipline extends to jobs that *emit* something and then mark it done. Such a job always sends first and marks done only on a confirmed send — never the reverse. Marking done first would strand the record if the send then dropped, and because the consumer on the other end is itself idempotent, a resend is always safe. The rule throughout: never add a retry to a path until you have confirmed the path can be safely repeated. ## Strictly serial, per truck Turning a stream of positions into trips, stops, and gaps is the work of a state machine — and a state machine only makes sense if it reads events in order. Frames must be processed oldest-to-newest for one truck, because "the truck started moving" only means something relative to "the truck was stopped" a moment earlier. Feed the same truck's frames out of order, or two at once, and the machine reasons about a past that has already been overwritten. ```mermaid flowchart LR subgraph V1["Truck A — processed strictly in order"] A1[08:00] --> A2[08:02] --> A3[08:05] end subgraph V2["Truck B — its own independent order"] B1[08:01] --> B2[08:03] end class A1,A2,A3 safe class B1,B2 safe classDef safe fill:#ecfdf5,stroke:#047857,color:#064e3b ``` ::: warning Boundary Queue concurrency is part of the correctness model, not just performance tuning. Changing it requires a per-truck locking design that preserves oldest-to-newest state-machine input. ::: So the positions pipeline processes **one batch at a time**, never overlapping work for the same truck. Two guards enforce this. The queue that carries positions runs without concurrency, so the engine sees deliveries in sequence. And the database itself will only permit *one* open trip, one open stop, and one open gap per truck — a structural rule that would reject a duplicate the moment overlapping processing tried to create one. This is why raising queue concurrency is not a tuning knob: the engine's ordering assumptions and those one-open-record-per-truck rules both depend on serial processing, and changing it would require a per-truck locking design first. ## The scheduled jobs, in families Ingestion handles what the trucks actively report. A second engine — a set of scheduled jobs running on fixed cadences from every couple of minutes to weekly — handles what *should* have happened but didn't, and the slow work the fast path deliberately defers. The jobs fall into families by what they protect. ```mermaid flowchart TD subgraph DET["Detection — notice what didn't happen"] D1[liveness: open gaps + device offline] D2[corridor anomalies] D3[slowdown extraction] D4[predicted mission delay] D5[mission delayed-start] end subgraph SAFE["Safety nets — close what got stuck"] S1[stale trips / stops / gaps / visits] S2[stale missions] S3[stale deviations / stale fuel events] S4[cron watchdog] end subgraph REC["Reconciliation — finish deferred work"] R1[resolve place labels] R2[pattern stats + hotspot / slowdown rebuild] R3[decay old evidence] R4[hotspot promotion to global] end subgraph DEL["Delivery — get results out, recover the lost"] L1[notification + push eligibility] L2[hourly notification digest] L3[push receipt reconciliation when enabled] L4[event outbox sweep] L5[mission pause-prompt] L6[dead-letter replay] end class D1,D2,D3,D4,D5 observed class S1,S2,S3,S4 risk class R1,R2,R3,R4 intelligence class L1,L2,L3,L4,L5,L6 surface classDef observed fill:#fff7ed,stroke:#ea580c,color:#7c2d12 classDef risk fill:#fef2f2,stroke:#dc2626,color:#7f1d1d classDef intelligence fill:#f5f3ff,stroke:#7c3aed,color:#3b0764 classDef surface fill:#eff6ff,stroke:#2563eb,color:#1e3a8a ``` * **Detection** turns silence and geometry into events, because no dispatcher watches every truck continuously. A single liveness pass, every couple of minutes, notices both a truck whose location has gone stale (opening a data gap) and a tracker whose heartbeat has stopped (a device-offline alert) — two different failures it is careful to tell apart. Other detectors surface corridor disruptions, recurring slowdowns, missions predicted to run late, and missions that were scheduled to depart but have not yet started. * **Safety nets** bound how long anything can stay open. If a truck vanishes mid-trip, the state machine has an open trip with no natural close in sight; a safety net closes trips, stops, gaps, and visits that have stayed open past their reasonable life, cancels missions after a long tracker silence, and clears stale deviations and fuel events. A watchdog job even checks that the other jobs are still running. These are backstops that step in only once something has already gone wrong. * **Reconciliation** completes work the fast path defers so ingestion is never blocked on something slow. Turning a coordinate into a place name asks a mapping provider, so ingestion stores the coordinate now and a job attaches the label moments later. Learning fleet-wide patterns and hotspots likewise runs on its own cadence, and a decay job lets old evidence fade so intelligence stays current. A promotion pass lifts well-observed tenant hotspots into the shared global set every fleet benefits from. * **Delivery** carries results outward — dispatching immediate notifications, storing push eligibility for the rollout-controlled mobile channel, rolling lower-urgency alerts into an hourly digest, reconciling push receipts when outbound delivery is enabled, prompting a mission's owner and driver to confirm a suspected pause, and sweeping the event outbox — and includes the dead-letter replay loop that recovers parked telemetry. Two operating rules keep this second engine safe. Every job takes a soft lock so overlapping ticks do not collide, and any job that scans a wide window or does heavy geographic work must bound its work per tick — paging through its backlog rather than attempting everything at once — so a single job can never saturate the database. ## Environments and deployment Korido runs entirely on Cloudflare Workers — managed, serverless compute with no machines to keep alive. The backend is a two-Worker pipeline: an ingress Worker at the edge that authenticates callers, takes in telemetry and webhooks, and forwards work; and an engine Worker placed near the database in Europe that does the data-heavy processing, runs the scheduled jobs, and owns the queues. The fleet app, admin portal, and customer tracking portal each run as their own Worker, and the mobile apps ship through their own app-store release channel. Every Worker invocation is short-lived and stateless, with tight memory and code-size budgets, which is why durable state lives in the database and coordination lives in the fast key-value store rather than in any running process. Deploys are staged and independent, in an order that keeps a running system consistent while it changes underneath. Schema changes go first, and a breaking one is split into phases — add the new shape in a backward-compatible way, ship code that reads both shapes, backfill, then tighten — so every version of the code stays able to read whatever database state it meets. The engine deploys ahead of the ingress Worker whenever the ingress Worker is about to forward something new, then the web surfaces that depend on the new behavior follow, and mobile ships on its own cadence. Each surface is verified after it lands. This staging is what lets Korido evolve continuously without a maintenance window. ## Edge cases * **A database outage under forty minutes.** Fully absorbed by queue retries. No batch reaches the dead-letter chain; everything drains through when the database recovers. * **An outage longer than the retry window.** Batches divert to the dead-letter chain and are archived to durable storage, then automatically replayed once the root cause is fixed. Replay is a no-op for positions already stored, so it is safe to let it run. * **A malformed archived message.** It is parked as poison in a separate location and counted, never retried forever, so one bad message cannot stall the recovery of good ones. * **A replay that overlaps a partial success.** A batch that dead-lettered after some of its rows were stored re-inserts only the missing rows on replay; the already-stored instants collide on their key and are discarded. * **A truck that disappears mid-trip.** Ingestion leaves an open trip with no close; a safety-net job closes it once it has stayed open too long, and a long enough silence cancels the mission within a bounded window. * **A job that emits then marks done, interrupted between the two.** Because the emit is confirmed before the record is marked, an interruption leaves the record unclaimed and the next tick re-emits; the idempotent consumer makes the resend harmless. * **A heavy scheduled job on a large window.** It pages its work per tick, so it advances a bounded slice each run instead of holding the database under a long, saturating scan. ## How it connects * [Data Architecture](./data-architecture) — the append-only, deduplicated observed plane that makes at-least-once delivery and replay safe. * [Tenancy and Security](./tenancy-and-security) — the cross-tenant service paths the ingestion pipeline and scheduled jobs run on. * **Part 3 — The fleet engine** — the per-truck state machine whose ordering assumptions require strictly serial processing.