Skip to content

feat(app): make the HUD the ticker's playhead, so a name that resolves later reaches it - #583

Open
khagele wants to merge 3 commits into
efiten:masterfrom
khagele:feat/453-hud-name
Open

feat(app): make the HUD the ticker's playhead, so a name that resolves later reaches it#583
khagele wants to merge 3 commits into
efiten:masterfrom
khagele:feat/453-hud-name

Conversation

@khagele

@khagele khagele commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #453

Stacked on #575 (feat/555-hud-pip): that PR owns the HUD's showOnHud path and the float readout this change hangs off. Merge #575 first; this one then rebases to its own diff.

The problem

The HUD's sender line is written once, at capture, before the resolver has answered. The ticker rebuilds from enriched rows every tick, so the same packet read repeater_3_ there and via 2beb on the HUD for as long as it stayed on screen (field screenshot in the issue). AGENTS.md §5.4 rule 3: rendering from data that arrives later needs a re-render when it does.

The issue asked to keep the HUD on the last reception, separate from the ticker's playhead (#309). That model had grown a third readout since: the float window (#555) follows the playhead when scrubbed, while the HUD under the same thumb does not. Kasper's call on refinement: one readout.

My changes in this PR

  • The HUD is the ticker's playhead. onActiveChange puts the playhead row on the HUD, so a tap, a step from the float window and a marker tap move it (app/src/app.js). A reception that passes the filter always reaches the HUD: it puts the ticker back on the newest row if a scrub had moved it (follow() on the ticker) and goes on the readout at capture, before the tick. Flipping the stand rebuilds the ticker and follows again, so All still shows the last capture at once.
  • The tick redraws it from the enriched row. syncHudToPlayhead compares the playhead row with what is shown through sameReadout (app/src/hudmode.js: same reception, same sender line) and redraws on a difference. That is how the name lands: through senderReadout, so the # on a direct_hash and a path_hash stays.
  • The float readout draws the HUD's reception, which is now the playhead; its hidden count shows only while following, since a scrubbed playhead is a choice rather than something the filter kept off.
  • HUD tooltips name what they show; decision log docs/2026-09-05-hud-is-the-playhead.md; the app: leaving the app throttles the hunt, and the HUD offers nothing to act on #555 log points at it. Changelog entry in both copies.

Verification

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

sameReadout has three tests, mutation-checked: the same row again is the same readout, a landed name is not (red with the label comparison removed), another reception is not.

Browser, resolver stubbed, two receptions in the store: following shows the newest (alpha, 30s); one step back shows the older one (via abcd12, 1m 0s); two ticks later, still scrubbed, it reads via repeater_3_ with the same reception and age. Then, scrubbed again, a frame through processFrame (undecodable, -77 dBm, inside the filter): the HUD shows it at once, the ticker follows again, and the next tick keeps it there.

Not in this PR

🤖 Generated with Claude Code

khagele and others added 3 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>
…s later reaches it

The HUD's sender line was written once, at capture, before the resolver
had answered; the ticker rebuilds from enriched rows every tick, so one
packet read "repeater_3_" there and "via 2beb" on the HUD. Two readouts
that agree while following and drift as soon as you scrub: since efiten#555 the
float window moved with the playhead and the HUD under it did not.

One readout now (Kasper, 2026-09-05). The HUD shows the reception on the
ticker's playhead: a tap or a step moves it, a reception that passes the
filter puts the ticker back on the newest row and lands on the HUD at
capture, and every tick the enriched playhead row redraws it when it reads
differently, which is how the name lands, through senderReadout, with the
# on a hash id kept.

Closes efiten#453

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@efiten

efiten commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Master moved: #569, #574, #580, #581 and #589 are merged, master is at f14d205. The conflict is the changelog arrays: both sides prepend to app/changelog.json and web/changelog.json. A rebase is all this needs.

Notes below are from reading this PR's diff against master before those merges. Not a full audit, and the green checks are not part of it.

Nothing found to change in the diff itself.

Two ordering notes. #575's two commits are in this branch, so #575 is superseded by it rather than a separate merge. And this touches the same FAB stack offsets in app/src/styles/app.css that #579 and #586 do, so those three will conflict with each other in whatever order they go.

CI can fail this on soundengine.test.js for reasons unrelated to the change: #606.

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: the HUD sender freezes at capture time, so a name that resolves later never reaches it

2 participants