Skip to content

Make the board read the feed, so two pages stop mapping it twice - #84

Merged
aenawi merged 1 commit into
developfrom
hud/board-reads-the-feed
Sep 13, 2026
Merged

aenawi merged 1 commit into
developfrom
hud/board-reads-the-feed

Conversation

@aenawi

@aenawi aenawi commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Target

  • This is going into develop: the normal case for a fix, a chapter, a
    lab change, a typo
  • This is a release: develop → main, shipping to the published guide
  • This is a hotfix: straight into main, because published advice is
    currently locking readers out or leaving them exposed

What this changes

The mapping from a frame on the wire to a drawing on the board existed twice —
app.js made it from the replies to its own POSTs, replay-app.js made it
from the frames in a file — and two copies written against the same wire have
to stay identical or a recording stops looking like the session it recorded.
hud/driver.js is now the one copy, and both pages hand it { board, feed }
instead of driving the board by hand.

Closes #82

What moved

hud/driver.js subscribes to four types and nothing else:

state    ->  board.setState
verdict  ->  the set-piece for its id, or probe/report as a fallback
flow     ->  board.probe
packet   ->  whatever the shot asked to watch

feed is the widget contract's feed and nothing more — on, want, and the
release functions they hand back — so window.LabFeed satisfies it and so
does the player replay.js builds out of a file. That substitution is the
same one the three shipped widgets already rely on, and it is what lets one
set of listeners serve a live lab and a recording.

What each page kept

app.js keeps the POSTs, the busy guard, the text trace, the tabs and the
rails. It still reads the lab back after a change, because a switch has to
move the picture without waiting up to three seconds for the poll — but it
hands that snapshot to driver.state() rather than to the board, so the
held-frame rule below only exists in one place.

replay-app.js keeps the transport, the moments and the verdict line. The
sentence is deliberately still the page's: a Result carries text as well as a
shot, and a reader with no WebGL should still get a readable session out of a
recording.

Two rules, now owned once and pinned by tests

set and preset draw no shot. A switch flip moves the board by changing
the lab and letting the next state snapshot redraw it; the board has never
rendered one as an event. A driver that played a shot per verdict would draw
something no reader ever saw, and it would look like a feature.

A held frame outranks a snapshot. run() calls hold() the moment a shot
starts, and setState repaints the standing board underneath it — putting
back a machine expired-key moved, undoing the dim the shot drew over. So a
snapshot arriving under a held frame waits.

One behaviour changed here, and on purpose. The waiting snapshot is now kept
rather than dropped, and it goes on just before the next shot clears the
board — which is the order the live page used to get by fetching the state
before playing the shot. stat is the wire's heartbeat, sent every tick
whether or not anything moved, so it releases the snapshot when no shot
follows. Before this, flipping a switch under a held frame half-wiped the
frame on the live page (refresh() called setState with no guard) and was
dropped outright on the player (leaving a board drawn from before the flip).
Both now do the same thing, and it is the thing the guard was written for.

Driven in the lab

control rebuilt from this branch with docker compose up -d --build --no-deps control, against the running lab on 2026-09-13:

  • probe → drew off the flow frame. rung 5 of 5, chip direct · punched,
    reached 100.71.4.3:22
  • scan-public → set-piece drew off the verdict frame: camera on lab-vps,
    four ports reading no answer, HELD · ufw default incoming policy: deny,
    rung 4 on the ladder
  • scan-tailnet → INCONCLUSIVE · it never got onto the tailnet, so the policy was never the thing under test
  • flipped vps.allowPublic22 under scan-public's held frame → the frame was
    still intact, and the page verdict updated. Flipped back to false, which
    is how the lab was found
  • flat view → the shot draws nothing and the text trace still lands, with
    lab-tab moving to packets as before
  • recorded 39 frames of that session and opened them in replay.html → the
    sniff set-piece and the probe both drew off the same driver. Opened a
    second time to exercise the driver being rebuilt on a new player. Console
    empty on both pages throughout

Not in this change

Two things noticed while driving it, neither touched, neither introduced here:

  • the HOLDING chip stays up after a probe that follows a held set-piece,
    because nothing between them calls clearScratch(). Same before and after
  • GET /favicon.ico 404s on both pages

Kind

  • Prose: clarity, typo, dead link
  • The advice was wrong, unsafe, or out of date
  • Lab or sandbox: code, checks, a new scenario
  • A divergence between the lab and the sandbox, closed or newly documented
  • Tests
  • Something else

If this changes advice or a command

Does not apply. No chapter, command or claim about the world moved — this is
the lab's own UI. What was measured is under Driven in the lab above.

Checks

  • make -C lab check passes — 59 tests, 18 files parse, everything held
  • gofmt is clean (make check fails on any output at all)
  • New source files carry an SPDX-License-Identifier header —
    hud/driver.js and checks/driver.test.mjs

If this touches the guide's pages

Does not apply. Nothing under chapters/ or assets/ changed, and no
guide page loads any file in this diff.

If this touches the lab or sandbox engine

  • Nothing is drawn that was not measured — the driver only ever passes a
    Result or a State snapshot through; it invents no fields and reads none
  • A failed attack does not render as a green tick — unchanged, the tone
    is still reading.js' judgement inside the set-piece
  • danger still outranks ok — unchanged, same reason
  • Time is not faked — the driver schedules nothing. A shot's choreography
    is still the set-piece's fixed length
  • The engine still returns the rung that decided the outcome, not a bare
    pass/fail — the Result is handed on whole
  • If a score moved, the comparison table in lab/README.md moved with it
    — no score moved; no check, attack or probe changed

Licence

  • I have the right to submit this, and I am fine with it going out under
    CC BY-SA 4.0 (guide) or GPL-3.0-or-later (code), per the file I touched

The mapping from a frame on the wire to a drawing on the board existed in
two places. app.js made it from the replies to its own POSTs; replay-app.js
made it from the frames in a file. Same wire, same four types, two copies —
and a copy that drifts is a recording that stops looking like the session it
recorded.

hud/driver.js is now the one copy. It takes { board, feed } and subscribes:
state to setState, a verdict that named an action to its set-piece, flow to
probe, and packet to whatever the shot asked to watch. `feed` is the widget
contract's feed and nothing more, so window.LabFeed satisfies it and so does
the player replay.js builds out of a file — which is what lets the same
three listeners serve a lab and a recording.

What the pages keep is what is genuinely theirs. app.js keeps the POSTs, the
busy guard, the text trace, the tabs and the rails; it reads the lab back
after a change and hands the snapshot to driver.state() rather than to the
board, because a switch has to move the picture without waiting for the poll.
replay-app.js keeps the transport, the moments and the verdict line — the
sentence lands even with no WebGL to draw the shot on.

Two rules the copies agreed on, now owned in one place and pinned by tests:

  `set` and `preset` draw no shot. A switch moves the board by changing the
  lab and letting the next snapshot redraw it; the board has never rendered
  one as an event, and a driver that played a shot per verdict would draw
  something no reader ever saw.

  A held frame outranks a snapshot. run() calls hold() the moment a shot
  starts and setState repaints the standing board underneath it, so a
  snapshot arriving under a held frame waits — and it is kept rather than
  dropped, so it goes on before the next shot rather than leaving the board
  standing on the configuration from before the last one. `stat` is the
  heartbeat that releases it when nothing else follows, which is the case
  where a switch flipped under a held frame used to half-wipe the frame on
  the live page and be dropped outright on the player.

Driven against the real lab on 2026-09-13, control rebuilt from this branch:
a probe drew from the `flow` frame (rung 5, "direct · punched"), scan-public
and scan-tailnet drew their set-pieces from `verdict` frames, a switch
flipped under scan-public's held frame left the frame intact, and the flat
view still draws nothing while the text trace lands. A 39-frame recording of
that session, opened in replay.html, drew the same sniff and probe off the
same driver, twice, with an empty console.

Closes #82
@aenawi
aenawi merged commit 01a2f9b into develop Sep 13, 2026
1 check passed
@aenawi
aenawi deleted the hud/board-reads-the-feed branch September 13, 2026 04:08
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.

The board is still not a widget: make it read the feed instead of being driven

1 participant