From 3fff74d938cdb4ebc6740bf5c434e05dd098fb86 Mon Sep 17 00:00:00 2001
From: Hashem Aldhaheri <158606+aenawi@users.noreply.github.com>
Date: Sun, 13 Sep 2026 05:55:39 +0400
Subject: [PATCH] Write the wire to a file, and play it back with no lab
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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 `
+
diff --git a/lab/control/ui/replay-app.js b/lab/control/ui/replay-app.js
new file mode 100644
index 0000000..d9715da
--- /dev/null
+++ b/lab/control/ui/replay-app.js
@@ -0,0 +1,599 @@
+/* SPDX-License-Identifier: GPL-3.0-or-later
+ Copyright (C) 2026 Hashem Aldhaheri */
+
+/* ============================================================
+ The player's page.
+
+ app.js is the control surface: it posts to the API, reads the lab back, and
+ plays the shot the reply describes. This is the same drawing with all of
+ that taken out. Nothing here posts anything, because there is nothing to
+ post to — the recording already happened, and this file's whole job is to
+ turn a file of frames into the board somebody would have been looking at.
+
+ Which means the mapping from a frame to a drawing is the interesting part,
+ and it is deliberately the same mapping app.js makes:
+
+ state the configuration, drawn continuously -> board.setState
+ verdict a Result from /api/action -> the set-piece for its id
+ a Result from /api/set or /api/preset -> the verdict line only
+ flow a Result from /api/probe -> board.probe
+ packet one line of tcpdump -> whatever asked to watch
+
+ `set` and `preset` draw no shot on purpose, and that is not a simplification.
+ On the live page a switch flip moves the board by changing the lab and
+ letting the next state snapshot redraw it — the board never renders a switch
+ as an event. A player that played a shot for every flip would be a player
+ showing something the reader never saw.
+
+ Everything here survives a page with no WebGL, a recording with no page
+ payload in it, and a file that is half a recording. What it will not survive
+ is being opened from `file://`, and nothing can: `
+
+
+
+