Picture & stream
One GET returns everything current. One held connection then delivers every change. A client built this way is never stale and never polls.
Hydrate once, GET /api/picture
The picture is the whole current state in one response: tracks, threats, the active plan and its candidates, sensors, the fleet, protected assets, the region, the autonomy mode, fusion health, and what every integration is doing.
curl https://<your-backend-host>/api/picture
{
"ok": true,
"data": {
"generation": 4182,
"t": 341.5,
"tracks": [ { "track_id": "T-00492", "classification": { "…": "…" }, "…": "…" } ],
"threats": [],
"plan": null,
"candidate_plans": [],
"sensors": [ "…" ],
"assets": [ "…" ],
"engagement_mode": "manual",
"fusion_health": { "…": "…" }
}
}
It is the heaviest response the server builds, and it exists for one moment: the
moment a client connects. Fetch it once, open the stream, and never fetch it again
unless the stream tells you to (see resync below). Reading one field of the picture
by re-fetching all of it is the pattern the stream exists to replace.
generation is a counter that increments whenever the picture changes. Compare it
against the generation in stream events to know your copy is current.
Then hold the stream, GET /api/stream
curl -N https://<your-backend-host>/api/stream
const stream = new EventSource("https://<your-backend-host>/api/stream");
stream.addEventListener("track_update", (e) => {
const track = JSON.parse(e.data);
map.upsert(track.track_id, track);
});
stream.addEventListener("resync", async () => {
// The server dropped events this client never saw. Refetch the picture.
const { data } = await (await fetch("https://<your-backend-host>/api/picture")).json();
map.replaceAll(data.tracks);
});
The stream is standard text/event-stream with a keep-alive comment every 15
seconds. Events arrive named, so an EventSource listener per event type is the
whole client.
Events every client receives
These carry the connection's own lifecycle and the console's live slices. They are always sent, whatever filter you set:
| Event | Payload | When |
|---|---|---|
snapshot | { tracks, generation }, up to 5000 tracks folded to the latest per id | once, on connect |
resync | { generation, dropped } | the server dropped events for this client; re-fetch the picture |
live_state | { engagement_mode, stream_gen } or { fusion_health, stream_gen } | either changes |
engagement_slice | { threats, plan, candidate_plans, sim, stream_gen } | the engagement state changes |
fleet_slice | { assets, stream_gen } | any asset's live state changes |
sensor_slice | { sensors, stream_gen } | any sensor's state or pointing changes |
integration_slice | { integrations, candidates, stream_gen } | a protocol's state or the discovery list changes |
Domain events
One event per pipeline artefact, named by its kind:
| Event | Carries |
|---|---|
track_update | a full fused track: id, classification, kinematics, uncertainty, custody, threat score, identification |
track_removed | the id of a track that left the picture |
obs | one raw observation. High rate, excluded by default; opt in with ?kinds=obs |
alert | a reflexive alert with its lifecycle state |
threat | a threat assessment |
plan | a plan (carries its own generation) |
posture, influence | decision-state changes |
coa, engagement, bda, swarm_command, tasking, tasking_status | the decision-chain artefacts, as their frozen wire shapes |
Filtering
?kinds= takes a comma-separated list of domain event names and limits the stream to
them (the lifecycle and slice events above are always included):
curl -N "https://<your-backend-host>/api/stream?kinds=track_update,alert"
Omitting kinds sends every domain event except obs, which is per-detection and
high-rate. Ask for it only when you are building something that consumes raw
detections.
The contract, in short
GET /api/pictureonce, remembergeneration.- Open
/api/stream, apply events as they arrive. - On
resync, go back to 1. Nothing else requires a second fetch.