Repository navigation
Write the wire to a file, and play it back with no lab - #80
Merged
Merged
Conversation
Branch 4 of the HUD sequence, and the one the other three were for. A recording is a `.jsonl` of /api/stream/hud — one JSON object per line — and `ui/replay.html` reads one and draws it on the same board, with the same set-pieces and the same cards a theme docked, asking no server anything. Two ways to make one. `make record` curls the wire and strips the SSE framing; the **record** button in the live page's bar writes down the frames already arriving, so a recording is exactly what that page saw — including the capture somebody had switched on. It opens no connection and starts no process in a container, which is why it sits above the busy guard: the minute worth recording is the minute every other button is disabled. The opening `hello` grew a `page`: the /api/meta payload, the theme store and the widget registry. That is what makes a file complete — the player has no server to ask, so a recording carrying only events could not say which machines to cut doorways for or where the cards go. `metaPayload()` is now one function rather than two literals, because the copy that drifts is the one in the recording and nothing would say so. The player is a viewer and deliberately not a control surface: no switches, no attacks, because a recording has nothing to press them against. What is there is a transport, and a list of the moments — every Result the lab returned — to jump to. Three things that are wrong invisibly rather than loudly, so they are tested: - A scrub is not a fast replay. The snapshots are sent only when they changed, so seeking re-applies the ones that were standing and skips the packets and verdicts between — replaying at 60× would fire every set-piece it passed. - The hello is a header, not a moment. On a recording of a session already in progress it is older than everything else in the file. - The snapshots a recorder seeds from can be ten minutes old. They are stamped when they were written, with the measurement's own clock kept as `wasAt` — driven against the live lab, a seven-second recording claimed forty-two before this. `hud/scene.js` now owns `refs()` and `HUD_IDS`, because two pages create boards and eighteen querySelector calls copied into the second is how one quietly loses a readout. A check asserts every id is in both pages. `wire.js` holds the two lists both feed.js and replay.js read — what the wire carries, and which of those are snapshots rather than moments. A stale second copy of the latter is a player drawing yesterday's board after every scrub. The player needs an HTTP origin and not a lab: `python3 -m http.server` over `lab/control/ui` serves it with Docker not installed. It cannot be a double-click, because `<script type="module">` is blocked on `file:` and the board is a module.
This was referenced Sep 13, 2026
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.
Target
develop: the normal case for a fix, a chapter, alab change, a typo
develop→main, shipping to the published guidemain, because published advice iscurrently locking readers out or leaving them exposed
What this changes
The lab's wire can now be written to a file and watched back. A recording is a
.jsonlof/api/stream/hud— one JSON object per line — and a new page,control/ui/replay.html, reads one and draws it on the same board, with thesame set-pieces and the same cards a theme docked, asking no server anything at
all. This is branch 4 of the HUD sequence and the one the other three were for:
a captured attack is a file somebody can post, and anybody can watch it with no
containers running.
Two ways to make one.
make recordcurls the wire and strips the SSE framing.The record button in the live page's bar writes down the frames that are
already arriving, so a recording is exactly what that page saw — including the
capture somebody had switched on, and not the one they had not. It opens no
connection and starts no process in a container, which is why it sits above the
busy guard: the minute worth recording is the minute every other button is
disabled.
The opening
hellogrew apage— the/api/metapayload, the theme storeand the widget registry. That is what makes a file complete on its own: the
player has no server to ask, so a recording carrying only events could not say
which machines to cut doorways for, which actions have a set-piece, or where
the cards go.
The player is a viewer and deliberately not a control surface. No switches, no
presets, no attack buttons, because a recording has nothing to press them
against. What is there instead is a transport, and a list of the moments —
every
Resultthe lab returned — to jump to.Closes #
Kind
If this changes advice or a command
make recordis new andreplay.htmlis new, so both are claims aboutcommands. Here is what they were measured against.
A forty-five second recording, with a probe and one attack driven through it:
The first line carries what the player needs, with nothing else to ask:
Played back with no Docker in the path — a plain static server over
lab/control/ui, the lab untouched:Jumping to the probe in that file replays the measurement, not a summary of it:
And to the attack, which held:
The three docked widgets run against the player with nothing in them edited
except the one line below —
wirereadsstatus().replayso it stops claiminga connection it does not have:
The record button, driven on the live page:
And the round trip through the recorder, which is what caught the one real bug
below:
Checks
make -C lab checkpassesgofmtis clean (make checkfails on any output at all)SPDX-License-IdentifierheaderIf this touches the guide's pages
It does not. Nothing under
chapters/orassets/is touched; the guide stillopens from
file://because none of this is in it.If this touches the lab or sandbox engine
dangerstill outranksoklab/README.mdmoved with itThe engine is untouched: the player draws from the same
Resultthe APIreturned, carried on the feed since #78, and decides nothing. Time is not
faked is the box worth a sentence, because this branch is about time. The
player's clock is the frames' own
atvalues and the only arithmetic on themis a monotonic clamp — a file whose timestamps go backwards plays at the same
instant rather than hanging or replaying. The one place a timestamp is written
rather than read is the recorder's opening snapshots, and it is written because
the frame is being issued then; the measurement's own clock travels beside it
as
wasAtand nothing is lost. There is a test for both.Licence
CC BY-SA 4.0 (guide) or GPL-3.0-or-later (code), per the file I touched
Three bugs found by driving it, all fixed here
Each of these looked fine in the tests and was wrong in the browser, which is
the reason for driving it.
A seven-second recording claimed forty-two.
state,statusandnetcheckare sent only when they changed, so the snapshots a mid-sessionrecorder seeds from can be minutes old. Written with their own clock they
put the whole recording behind a gap of dead air. They are now stamped when
they were written, with
wasAtcarrying when they were measured.One rail in a
space-betweendeck lands on the left..deckhas alwayshad two rails, so nothing noticed. The player has one, and every inset was
then measured from the wrong side: the cards, the transport and the verdict
all squeezed into a column an inch wide over a board with the whole window
free beside it. Fixed with
.deck-right.The docks were initialised with a feed that did not exist yet. On the
live page the feed is there before the page is; here the player is built
when a file is opened. A one-shot
slots.inithanded every card a null feedand left all three saying "no feed on this page" for the rest of the
session — beside a board playing perfectly.
Not in this change
Found while driving this, left alone per one idea per branch:
.hud-overlayhas a fixed 12px bottom padding, so the board's ownverdict panel sits under a card docked in the
bottomslot. It reads--pad-rand--pad-lbut not--pad-b. This is on the live page too —select Warroom, run an attack, and the verdict is behind the tally card — so
it arrived with Dock cards around the board, so a theme can arrange the instrument #79 rather than with this.
The board is still not a widget. Making it one means it reading the feed
instead of being driven by
app.js, and this page drives it the same wayapp.jsdoes rather than changing that. Worth its own branch.Correction, 2026-09-13
The Environment block above was wrong when this was merged. Four of its six
lines were written from memory rather than read off this machine — macOS 15 for
26.6.2, Docker 28.5.1 for 29.7.2, Go 1.25 for 1.26, Headscale 0.26.1 for
0.29.3 — which is exactly the thing
AGENTS.md is there to
stop, and the form is where it is meant to be caught.
They have been replaced with the measured values. Nothing else in this body
changes: every number in the transcripts above was read off the run it claims
to be from, and the behaviour those versions produced is what is described.