Skip to main content

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:

EventPayloadWhen
snapshot{ tracks, generation }, up to 5000 tracks folded to the latest per idonce, 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:

EventCarries
track_updatea full fused track: id, classification, kinematics, uncertainty, custody, threat score, identification
track_removedthe id of a track that left the picture
obsone raw observation. High rate, excluded by default; opt in with ?kinds=obs
alerta reflexive alert with its lifecycle state
threata threat assessment
plana plan (carries its own generation)
posture, influencedecision-state changes
coa, engagement, bda, swarm_command, tasking, tasking_statusthe 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

  1. GET /api/picture once, remember generation.
  2. Open /api/stream, apply events as they arrive.
  3. On resync, go back to 1. Nothing else requires a second fetch.