---
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.