GET /camera/{id}/snapshot read {id} off the path, then never used it again. It
unconditionally shelled out to rpicam-still and streamed back a brand-new live frame at a
hardcoded 1280x720 — for any id, camera or not.
This is the exact route ServerNodeManager.readCameraSnapshot() calls to fetch a peer-owned
camera’s frame for a cross-server vision LLM (krill#860/#861). So a capture on host A would
publish snapshot=A, wake an LLM on host B, and the LLM’s fetch of “the frame that woke it”
would return a freshly-captured frame B instead — silently, since both are valid JPEGs. Two
Camera nodes on the same host also returned identical bytes, because the id that distinguished
them was discarded before it could matter.
The route predates the feature that made it load-bearing. It shipped with the original Camera
node (krill@1c66f6f23, 2026-03-30) as a “capture me a frame right now” endpoint for the UI, well
before any node published meta.snapshot or before a peer route existed at all — at the time,
ignoring {id} in favor of “whatever camera is attached to this Pi” was harmless because there
was exactly one camera and no published state to prefer. krill#842 later taught the camera
processor to publish captured frames onto meta.snapshot.value, and krill#860/#861 wired an LLM
to fetch that exact path across hosts — but nobody revisited this route to make it serve what it
now had to serve.
ServerNodeManager.readNode(id) resolves {id} to a node; a new pure helper,
cameraSnapshotDecision(node, live) in server/.../io/CameraSnapshotDecision.kt, decides
what to do with it: 404 if it’s missing or not a KrillApp.Project.Camera, serve the bytes at
meta.snapshot.value if one has been published and ?live=true wasn’t passed, otherwise
capture live at the node’s own meta.resolution (not a hardcoded 1280x720).Routes.kt executes that decision: reads the published file, or falls
through to a captureLiveCameraSnapshot() helper that runs rpicam-still at the requested
resolution. A published path that’s gone missing from disk also falls through to a live
capture rather than 404ing outright.?live=true preserves the original “capture a fresh frame now” behavior for the UI’s own use.ServerCameraProcessor and
GET /camera/{id}/snapshot both produce “the current image for a camera”, but only one of
them ever looked at the other’s output. When a processor starts publishing state under a
node’s meta, grep for any route that already serves that node type and ask whether it
should be reading the same field instead of independently recomputing it.cameraSnapshotDecision(node, live) follows the peerProjection(nodes, selfId)
shape from krill#190 — take the already-resolved inputs, return a decision, let the route
handler do the I/O. The four-way branch (missing / wrong type / no frame yet / explicit live
request) is covered by CameraSnapshotDecisionTest with zero filesystem or subprocess
interaction.