Dispatch API

A service for managing and dispatching missions, providing health checks, and logging mission executions.


Features


Installation

  1. Clone the repository:

bash git clone https://source.modulotech.fr/modulosource/powerpanne/powerpanne/houston/archimede/dispatch.git cd dispatch

Usage

Run the FastAPI server:

bash docker compose up --build # on Mac os docker-compose up --biuld # on Windows

Endpoints

GET /

Returns the README.md rendered as HTML in the browser.

GET /livez, GET /ready — the Kubernetes probes

GET|POST /health

Diagnostic endpoint (which roster, fleet, PTAC table, HexaSpeed and PowerPanne state this pod holds). Not a probe.

Response

json { "status": "ok" }

GET /dispatch — the PowerPanne link

The call PowerPanne makes (job Archimede::AiDispatcherJob, at mission creation, production only) to show a driver suggestion on the mission. Contract (test app/test_api_dispatch.py): request GET /dispatch?mission_id=<number> with header X-Token: <DISPATCH_API_KEY>; PowerPanne reads only recommended_driver_id (HTTParty does not raise on an error status), so every body carries it. Since marcellus#61 an answer without a driver says WHY, with a matching HTTP code — it used to be HTTP 200 for everything, errors included:

| HTTP | body | meaning | |---|---|---| | 200 | {"status_code": 200, "recommended_driver_id": 123} | a suggestion | | 200 | … null, "outcome": "aucun_candidat", "reason": … | the engine ran on the full snapshot: nobody can go | | 200 | {… 123, "degraded": ["powerpanne_records", …]} | a suggestion made without some input — e.g. the day's records unreadable: every driver looks idle | | 503 | … null, "outcome": "abstention", "reason": "powerpanne_indisponible", "detail": … | a PowerPanne read failed (refusal, timeout, 5xx) and nothing else described the mission — or the day's records are unreadable with ABSTAIN_WITHOUT_DAY_RECORDS=1 — nothing was decided | | 503 | … "reason": "flotte_absente" | PowerPanne described the mission without any driver | | 404 | … "reason": "mission_inconnue" | no PowerPanne instance knows the mission | | 422 | … "outcome": "requete_invalide" | mission_id is not a number | | 500 | … "outcome": "echec", "reason": "<exception>" | our failure (engine, database) |

Behind the route, the same process as the project_echo replay (docs/DISPATCH_AI.md § 12): the persisted answer for this mission if any (write-once, same mission → same answer: one atomic INSERT … ON CONFLICT that never replaces a stored driver, so concurrent calls all answer the first pick) → the mission's snapshot, first source that has it with its drivers: drivers_for_mission (X-Token, LEGACY_THIN_FALLBACK=1 by default — the only one that answers at creation time), then the PowerPanne V2 API (POWERPANNE_CLIENT_ID/SECRET: the live mission through POWERPANNE_AI_DATA_PATH, an already-assigned one through the day's ai_mission_data records, the V2 mission plus the day's fleet) → each driver's completed_missions of the day attached from the V2 records → the deterministic engine (no LLM agent). Two PowerPanne instances are served (dispatch_module/config.py INSTANCES): app.powerpanne.com (GPR, Jean Dépannage, ACL) and app.pn-powerpanne.fr (Périphérique Nord — companies 20/19/30, same OAuth client). A bare mission id is looked up on each instance in that order, and the enrichment reads the records of the instance that knew it (driver ids are reused across hosts). One log line per call: dispatch.legacy mission=… source=cache|thin|v2|v2_record|v2_mission|none instance=powerpanne|pn outcome=suggestion|aucun_candidat|abstention|echec enriched=<n> dur_ms=… pick=… auto=… vehicle=none|snapshot|light|full|unknown degraded=… reason=…. The towed vehicle is read in both PowerPanne shapes, the full one and the light one of its 22/10 refonte (app/engine/vehicles/powerpanne_shape.py, marcellus#60). GET /dispatch/{mission_id} (X-Token) returns the persisted decision.

Smoke after a deploy: DISPATCH_API_KEY=… python scripts/smoke_powerpanne_link.py https://dispatch.archimede.modulotech.fr <mission_id>.

POST /dispatch_mission — the engine for project_echo (Replay, Live)

Body: the snapshot {mission, drivers, event_at}. 200 = the rich recommendation; when an input failed while deciding (a featurizer that raised, HexaSpeed or the project_echo roster not answering, a skipped tie-break step…) the decision stays fail-soft and the body says so in degraded: [{"source", "detail"}]. Without a driver, detail keeps its historical string and outcome + reason tell the cases apart: 422 aucun_candidat (nobody can go), 422 abstention/flotte_absente (a snapshot without drivers — an upstream read failed), 422 requete_invalide, 500 echec. project_echo tombstones 4xx and retries 5xx.


Database Models


Development