Skip to content

feat(app): let the HUD follow the ticker's stand, act on the shown sender, and float it over other apps - #575

Open
khagele wants to merge 2 commits into
efiten:masterfrom
khagele:feat/555-hud-pip
Open

feat(app): let the HUD follow the ticker's stand, act on the shown sender, and float it over other apps#575
khagele wants to merge 2 commits into
efiten:masterfrom
khagele:feat/555-hud-pip

Conversation

@khagele

@khagele khagele commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Closes #555

The problem

The HUD was written from every capture, before the filter ran. Narrow the map to one target and the readout still jumped to every passer-by on the public channel. It was read-only: selecting or ignoring that sender meant opening the target sheet. And the moment another app came in front, there was nothing left to look at.

My changes in this PR

Two commits, one per half of the issue.

One filtered/all stand for the ticker and the HUD (state.rxMode). The ticker's header toggle reports through onModeChange and takes setMode back; the HUD's Filtered/All pill flips the same stand. With a filter set you look at the filtered set on both. In filtered mode the HUD keeps the last reception the filter let through, with that reception's own RSSI, SNR, sender and age. A closed eye on the pill says the filter kept something out since then; the count is in the aria-label and tooltip. Flipping to All puts the last capture on the HUD at once.

Quick actions on the shown sender: Target (only this one, the popup's Isolate), Add (to the selection) and Ignore, through the events the map popup and the target sheet already use. Target and Add follow the list's own rule, exported as isTargetKind. The pure rules are in app/src/hudmode.js.

The float readout (app/src/floatreadout.js). Float draws the HUD's reading onto a canvas, streams it into a <video> and takes that fullscreen. Chrome for Android moves a fullscreen video into its picture-in-picture window by itself when you press Home or switch apps; Chrome's Media Session auto-PiP is desktop-only, so fullscreen is the path. The window shows the tier colour as tint and bar, the RSSI, SNR and age, the sender, the stand with the eye, the BLE and MQTT dots, and Disconnected in amber. Android's previous/next buttons on the window (Media Session previoustrack / nexttrack) scrub the ticker's playhead, so the window steps through the same list the ticker shows. Where the browser lacks canvas capture or a way to take a video out of the page, the button is not shown.

FAB stack +34 px, the height of the new row, so the clearance above the HUD is unchanged. Decision log: docs/2026-09-04-hud-follows-the-filter.md. Changelog entries in both copies.

Verification

Suite Result
app vitest 926 passed, build and eslint clean
web vitest 509 passed
web e2e whatsnew.spec.js, --fail-on-flaky-tests 13 passed (the changelog copies)

Seven mutation checks on the HUD tests, each red on one test. Browser at 360x780 and 412x915, feeding a signed advert through the capture path from the console: two receptions kept out by a target filter show the eye; All shows the last capture; the ticker's toggle flips the HUD's pill; Target sets the chip and reads active. The row uses 302 of 328 px at 360 with the float button, 320 with the eye. FAB clearance above the HUD is 60 px on both widths, as before. The float canvas draws as designed, stepping back moves the playhead to the previous reception with its own age, stepping forward to the newest makes it follow again, and closing pauses the stream and resets the button.

Not verified here, and worth a field check: whether Android Chrome floats a muted canvas-stream video the way it floats a playing clip, and whether the page keeps its timers while it does. The browser pane cannot go fullscreen without a real tap. The capture path is unchanged either way.

Decisions worth a look

🤖 Generated with Claude Code

khagele and others added 2 commits September 4, 2026 15:00
…t on the shown sender

The HUD was written from every capture, before the filter ran: narrow
the map to one target and the readout still jumped to every passer-by
(efiten#555). The receptions ticker already had a filtered/all toggle, and
Kasper's steer was that with a filter set you look at the filtered set
on both, so this is one stand for both surfaces, flipped from either.

state.rxMode holds the stand; createReceptionLog reports its header
toggle through onModeChange and takes setMode back. In filtered mode a
reception the filter keeps off the map stays off the HUD, and the HUD
keeps the last one that passed with that reception's own RSSI, SNR,
sender and age. A closed eye on the pill says the filter kept something
out since then; the count is in the aria-label and tooltip. Flipping to
all shows the last capture at once.

Three quick actions on the shown sender, Target, + Target and Ignore,
dispatch the events the map popup and the target sheet already use.
Target and + Target follow the list's own isTargetKind rule; Ignore
needs only an id. The pure rules live in hudmode.js; app.js keeps the
glue. The FAB stack moves up 34 px, the height of the new row, so the
clearance above the HUD stays what efiten#264 measured (60 px, measured on
this branch at 360 and 412).

Left out on purpose: picture-in-picture, which gets its own design
round; efiten#453 (re-render once a name resolves) stays open.

Verified: app 912 unit tests, build and eslint clean; web 509 unit
tests and the whatsnew e2e (changelog copies). Seven mutation checks,
each red on one test. Browser at 360 and 412: two hidden receptions
show the eye, All shows the last capture, the ticker toggle flips the
HUD, Target and Ignore act and read active. Row 308 of 328 px at 360.

Part of efiten#555

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the window's buttons

Leave the app and the hunt had nothing to look at (efiten#555). Float draws
the HUD's reading onto a canvas, streams it into a <video> and takes
that fullscreen. Chrome for Android moves a fullscreen video into its
picture-in-picture window by itself when you press Home or switch
apps; Chrome's Media Session auto-PiP is desktop-only, so fullscreen
is the path. captureStream(0) with requestFrame per draw, so nothing
depends on requestAnimationFrame, which a hidden page never runs.

The window is the HUD: tier colour as tint and left bar, RSSI in
white, SNR and age, the sender through senderReadout, the stand with
the eye, the BLE and MQTT dots, and Disconnected in amber. Android's
previous/next buttons on the window scrub the ticker's playhead
(rxStepIndex, step, active, following on the ticker), so the window
steps through the list the ticker shows, each reception with its own
age; the newest row makes it follow again. Where the browser lacks
canvas capture or a way to take a video out of the page, the button
is not shown.

The row reorders per the design round: Target, Add, Ignore, Float,
then the stand pill at the right edge. Add and Ignore keep one word
in every state so the row holds five controls at 360 px (302 of
328 px, 320 with the eye).

Verified: app 926 unit tests, build and eslint clean; web 509 and the
whatsnew e2e. Browser at 360: the canvas draws as designed, stepping
back shows the previous reception with its own age, stepping forward
to the newest follows again, closing pauses the stream and resets the
button. Not verified here: Android floating a muted canvas video the
way it floats a clip, and the page keeping its timers while it does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

app: leaving the app throttles the hunt, and the HUD offers nothing to act on

1 participant