A service for managing and dispatching missions, providing health checks, and logging mission executions.
bash
git clone https://source.modulotech.fr/modulosource/powerpanne/powerpanne/houston/archimede/dispatch.git
cd dispatch
Run the FastAPI server:
bash
docker compose up --build # on Mac os
docker-compose up --biuld # on Windows
GET /Returns the README.md rendered as HTML in the browser.
GET /livez, GET /ready — the Kubernetes probes/livez (liveness): {"status": "ok"} as soon as the event loop answers — nothing else is
checked, so a slow upstream or an engine still loading never gets a pod killed./ready (readiness): 200 once THIS worker holds the engine (warm-up done), 503
{"status": "loading"|"failed", "engine": {...}} before or if the load failed. A pod joins
the Service only when it can decide; a broken image never becomes ready.GET|POST /healthDiagnostic endpoint (which roster, fleet, PTAC table, HexaSpeed and PowerPanne state this pod holds). Not a probe.
Response
200 OK on seccessjson
{
"status": "ok"
}
GET /dispatch — the PowerPanne linkThe 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.
logging for debugging and monitoring.