20260910 - The Tracker page, drawn from the tracker - #86
Merged
Merged
Conversation
Tracker Preview becomes the Tracker, on every surface, with a permanent redirect from the old URL since nodes have been in the field long enough for it to be bookmarked. 308 rather than 301 so /clear stays a POST. The Plotly legend is gone. It carried identity in a palette that repeated every eight tracks, grew without limit for four hours, and could not be operated. In its place a scrolling rail holds every track, sorted and filterable, with an eye to hide one and a bin to remove it. Removal is a browser-side action with a restore line in the footer: nothing ever leaves the buffer, so a removed track returns on reload by design. Colour is now a twelve-hue wheel indexed by a hash of the track id, and is deliberately allowed to collide: the row names the track, so colour points rather than identifies. Six of retina-tracker's ten event fields are now kept rather than swallowed in **kwargs, and write_event names them so the code says which one we drop on purpose. Three of the six are diagnostics, so the resting row answers "should I look at this" and the expanded row answers "what does the tracker think". Anomaly reasons stay out of the resting row entirely and a flagged track gets a dot rather than a badge, because the detector is not trusted yet and a badge would be read as a verdict. The four-hour buffer is unchanged: the node collects and retains all of it regardless. The new view window is applied when a snapshot is served, so narrowing to 15m deletes nothing and still ships a sixteenth of the bytes. Underneath, the refresh stops re-sending the whole buffer. The capture thread used to rebuild a full snapshot every three seconds and every viewer re-fetched all of it, measured at 878 ms and 44 MB per tick on a busy node. Now it appends and broadcasts a tick, and each SSE connection builds its own delta from the per-viewer queue attach() already handed out, so there is no cursor to negotiate. Three seconds of a busy node is 90 points, 2.9 KB and 0.05 ms. Points are columnar and rounded on the way in, which took a full snapshot from 44.0 MB to 14.1 MB on its own. That rounding also earns its keep twice: raw and track points now compare exactly, which is what lets the browser tell an associated detection from an unassociated one. That classification is done in the page rather than the node because association happens after a detection has been sent, so classifying at send time would need a reclassification protocol. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
retina-gui has been the transport between blah2 and the retina-tracker sidecar: polling blah2's API and pushing every frame down the sidecar's ingest socket. That was never a web interface's job, and it only existed because blah2_api's own forwarding was written, switched off, and would not have worked. It works now, so this stops. Detections travel blah2 -> blah2_api -> retina-tracker directly, and blah2_api forwards the frame it has already enriched with ADS-B truth, which is a better frame than the one this process was sending. Nothing here opens that socket any more. Nothing here could: it accepts one connection at a time and blah2_api owns it. Two callers were feeding it, not one. tracker_capture pushed every frame continuously, and Auto-Calibrate pushed them again during a dwell from its own poll of the same endpoint. Both deduplicate locally, neither knew about the other, and the tracker does not deduplicate at all, so every frame during a dwell was processed twice: two Kalman updates from one measurement, n_associated advancing at double rate, and a track reaching M-of-N on half the real evidence. That was happening precisely when confirmation was the decision being made. RESET moves off the detection socket to POST /reset on the sidecar's control surface, which is where a control message belongs and is now the only place it can go. It returns whether the sidecar confirmed, so a failed reset is visible where it used to be silent; a dwell that cannot clear the tracker still runs, but says so in its status rather than quietly reporting a confirmation it cannot vouch for. The test fakes are wired the way the node is: FakeBlah2Client forwards each frame it publishes to the tracker fake, standing in for blah2_api, and nothing connects them through the calibrator. moving_track_detections now floors its timestamps at the client clock, because the tracker sees every published frame now rather than only those the calibrator chose to forward, and a confirmation scripted off a stale one is rejected by the dwell's freshness guard and never offered again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
retina-gui kept its own four-hour record of detections and tracks, built by polling blah2 and tailing the sidecar's output. It does not need one: retina-tracker owns that record now and serves it, so this deletes 1,246 lines and replaces them with a proxy. /tracker/events carries the sidecar's stream byte for byte. Nothing is parsed on the way past: the sidecar frames the messages, owns the cursor for each connection and decides what a snapshot contains, so there is no second copy of the record here and no format knowledge to drift out of step. Proxying rather than linking at the sidecar's own port is what keeps the page behind the same session and Access checks as everything else, and what makes it work over the support tunnel, which routes paths on this hostname and not arbitrary ports. The page stops guessing. It used to infer which detections a track had claimed by comparing timestamps and delays, because association happens after a detection has already been sent and there was no way to be told. The tracker states it now, in three classes, so the layer control gains Unassoc and Below SNR as separate views: the tracker's miss list, and what the SNR gate threw away before it looked. Neither has ever been visible anywhere. "Clear buffer" keeps its meaning and changes address. It went to a local buffer; it goes to the sidecar's POST /history/clear, which wipes the record without touching the tracker, so an aircraft still overhead reappears on its own. The stream is consumed by named events rather than onmessage, because a snapshot means replace what you hold and a delta means append, and a snapshot can arrive mid-stream when the record is cleared underneath a viewer. Verified against a real sidecar rather than only in tests: 40 frames in, three classes and two tracks out through the proxy, and a 15-minute window correctly excluding points older than it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A plot with a list beside it wants every pixel, and this one had a 1500px cap and a paragraph of explanation above it. Nothing in base.html constrains the content block, so the cap was only ever self-imposed. Height follows the viewport too. A fixed 640px was the right shape inside a narrow column and leaves most of a large screen empty once the width is released; clamped so it stays usable on a laptop without becoming a letterbox on a monitor. The rail gains 24px, which is what its metadata line was truncating for. The intro paragraph goes. It described the three detection classes, which is worth knowing once and not on every visit, and the layer control's own tooltips already say it at the point of use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dead-code gate is right about this one. health() was written alongside reset() because the sidecar serves both, but nothing in retina-gui asks it: the Tracker page learns the sidecar is unreachable from the stream itself, which is the only place the answer was ever going to be wanted. The endpoint stays on the tracker, where the container's own probe uses it. Nothing here is dynamically referenced, so this is a deletion rather than a whitelist entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four commits. retina-gui stops being the transport between blah2 and retina-tracker, and the Tracker page is rebuilt on what the tracker can now tell it.
Depends on retina-tracker (control surface, classified detections,
/events) and retina-node (config + compose). All three ship together; see "Ordering" below.The rail (
2f10082)The Plotly legend carried identity in a palette that repeated every eight tracks, grew without limit for four hours, and could not be operated. It is replaced by a scrolling rail holding every track, with sort, filters, an eye to hide and a bin to remove.
/clearis a POST).**kwargs. Three are diagnostics and appear only in the expanded row, soshadow_fractionis there when you are testing and invisible to a customer.Off the ingest path (
227239e)Detections now travel blah2 → blah2_api → retina-tracker directly. Nothing here opens that socket; nothing here could, since it accepts one connection at a time and blah2_api owns it.
This fixes a live bug. Two callers were feeding the tracker:
tracker_capturecontinuously, and Auto-Calibrate again during a dwell from its own poll of the same endpoint. Both deduplicate locally, neither knew about the other, and the tracker does not deduplicate at all. So every frame during a dwell was processed twice — two Kalman updates from one measurement,n_associatedat double rate, and tracks reaching M-of-N on half the real evidence, precisely when confirmation was the decision being made.RESET moves to
POST /resetand now returns whether the sidecar confirmed it, so a failed reset is visible where it used to be silent.Drawn from the tracker (
e5df18d)1,246 lines deleted, replaced by a proxy.
/tracker/eventscarries the sidecar's stream byte for byte: no second copy of the record here, and no format knowledge to drift out of step. Proxying rather than linking at the sidecar's port keeps the page behind the same session and Access checks, and makes it work over the support tunnel, which routes paths and not arbitrary ports.The page stops guessing. It used to infer which detections a track had claimed by comparing timestamps and delays. The tracker states it now, so the layer control gains Unassoc and Below SNR as separate views: the miss list, and what the SNR gate threw away. Neither has ever been visible anywhere.
Layout (
66a7a71)Full width, viewport-relative height, intro paragraph removed.
Verification
test_tracker_routes.pyis new;test_tracker_capture.pyis deleted along with the module.Resources measured on that node, before and after, with a viewer connected throughout:
The 4.4 MB is almost exactly the four-hour buffer it no longer holds. The poll it stopped doing was 0.93% of a core, continuously. Each additional viewer costs one thread and ~0.02% of a core, and threads return to baseline on disconnect.
Ordering
send_frameis deleted here in the same release that turns ontracker_forwardin retina-node. Exactly one process may feed that socket, and until this merges, retina-gui is the one holding it.🤖 Generated with Claude Code