Skip to content

20260910 - The Tracker page, drawn from the tracker - #86

Merged
Purple10101 merged 5 commits into
mainfrom
20260910-stop-forwarding-frames
Sep 10, 2026
Merged

Purple10101 merged 5 commits into
mainfrom
20260910-stop-forwarding-frames

Conversation

@Purple10101

Copy link
Copy Markdown
Collaborator

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.

  • Renamed Tracker Preview to Tracker across seven surfaces, with a permanent 308 redirect (not 301: /clear is a POST).
  • Six of the tracker's ten event fields are kept instead of being swallowed in **kwargs. Three are diagnostics and appear only in the expanded row, so shadow_fraction is there when you are testing and invisible to a customer.
  • Flagged tracks get a dot, not a badge. The anomaly detector is not trusted yet and a badge reads as a verdict.
  • Colour is demoted to a pointing device. A twelve-hue wheel indexed by a hash of the track id, deliberately allowed to collide, because the row names the track.

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_capture continuously, 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_associated at 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 /reset and 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/events carries 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

  • 872 tests pass, ruff clean. test_tracker_routes.py is new; test_tracker_capture.py is deleted along with the module.
  • Browser pipeline exercised in node against the real wire format, including a case asserting the tracker's verdict wins over any local guess.
  • Running live on jonathan-node-1 since 10:16 UTC, fed by real radar, with a browser open on it through the Cloudflare tunnel across several restarts.

Resources measured on that node, before and after, with a viewer connected throughout:

before after
retina-gui RSS 52.8 MB 48.4 MB
retina-gui CPU 0.11% of a core
blah2 poll 5/s, 1.85 ms a call gone

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_frame is deleted here in the same release that turns on tracker_forward in retina-node. Exactly one process may feed that socket, and until this merges, retina-gui is the one holding it.

🤖 Generated with Claude Code

Purple10101 and others added 5 commits September 9, 2026 17:45
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>
@Purple10101
Purple10101 merged commit 5ebc218 into main Sep 10, 2026
2 of 3 checks passed
@Purple10101
Purple10101 deleted the 20260910-stop-forwarding-frames branch September 10, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant