Skip to content

Dock cards around the board, so a theme can arrange the instrument - #79

Merged
aenawi merged 1 commit into
developfrom
feat/hud-widgets-and-slots
Sep 12, 2026
Merged

aenawi merged 1 commit into
developfrom
feat/hud-widgets-and-slots

Conversation

@aenawi

@aenawi aenawi commented Sep 12, 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 page's furniture was in index.html, so adding a readout meant editing the
markup, the stylesheet and app.js — three files, and a fork for anyone who
wants a different instrument on the same lab. Now there are four docks around
the board (top, left, right, bottom), a card in one of them is a widget
folder under ui/hud/widgets/, and which card goes where is declared in a
theme's theme.json. The feed made the lab's data shareable; this makes the
layout data too.

Third of the four HUD branches. #77 gave the board a palette, #78 put the lab on
one wire, and this docks cards that read that wire. Record-and-replay is next and
is not in here.

Closes #

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

Checks

  • make -C lab check passes
  • gofmt is clean (make check fails on any output at all)
  • New source files carry an SPDX-License-Identifier header

If this touches the lab or sandbox engine

  • Nothing is drawn that was not measured
  • A failed attack does not render as a green tick
  • danger still outranks ok
  • Time is not faked
  • The engine still returns the rung that decided the outcome, not a bare pass/fail
  • If a score moved, the comparison table in lab/README.md moved with it (a
    test asserts these against a fixture, so a mismatch will fail loudly)

The engine is untouched — no scores moved. The boxes above are about the new
tally widget, which is the one thing here that draws a judgement. It counts
allowed and denied apart rather than merging them, because "rung 3 allowed eleven
and denied none" is a finding and "rung 3 answered eleven times" is not. A rung
nothing reached shows a zero and no bar, not an empty bar that reads as a
measurement. Driven against the lab: the attacker to lab-vps:22 was denied at
rung 4 and the card read 0 / 1.

What a widget is handed

export function create({ el, feed, options }) {
  const off = feed.on("verdict", (data) => { /* draw into el */ });
  return { destroy: off };
}

A div, the feed, and the options its placement carried. No board object, no page
elements, no controller. hud/scene.js is twenty-five coupled methods over 1300
lines of three.js and publishing it as a plugin API would freeze it the day
somebody wrote against it.

The board is therefore not a widget in this change, and that is deliberate
rather than unfinished: app.js still creates and drives it, and the docks' part
is reporting what they cover so the camera frames the diagram in the gap the
cards leave. Making it a real widget under the contract above means the board
reading the feed instead of being called — next branch's work. A privileged
contract handing one widget the board object would be the plugin API this change
exists to avoid.

The trap this hit

--pad-* is measured against the canvas and consumed against the window, and
those differ by the height of the rung ladder, which sits below the viewport
rather than over it. A card measured one way and positioned the other is short by
exactly one ladder, which put the verdict 40px inside the bottom card. Fixed by
measuring the docks against both rectangles, and by splitting --rail-* out of
--pad-*: the docks are positioned from the furniture alone, because a dock
positioned from a number that includes its own size is a layout that never
settles.

Measured, not expected

Against the running lab on 2026-09-12, Chromium at 1600x1000, control rebuilt
with docker compose up -d --build --no-deps control:

chapter   no .slots element in the DOM, no cards; --rail-* and --pad-* equal
warroom   wire@top tape@right tally@bottom, three widget.css loaded
          --rail-r=346px  --pad-r=616px  --rail-b=50px  --pad-b=163px
probe     lab-ubuntu -> lab-vps:22  reached at rung 5   tally 1 / 0
probe     evil-box   -> lab-vps:22  denied  at rung 4   tally 0 / 1
fold ]    tape card 984..1244 -> 1276..1536, board re-framed
820px     cards stack at the foot of the page, no horizontal scroll
overlap   verdict / view controls / board gauges vs every card: none
console   one error, the favicon 404 the page already had

Two things changed because of what driving it showed, not because of what the
type list said. The tape hid only stat at first; with the three-second polls
in, one probe put nineteen lines on the tape and two of them were the probe, so
status and netcheck are hidden by default too and hide: [] puts them back.
And an empty tape card read as a broken one, so it says what it is waiting for
until there is a frame to replace the line with.

evil-box was already running when I started and I did not stop or start
anything. Only the control container was rebuilt; the lab's machines were not
touched.

Tests

Ten new Go tests. The two that matter:

  • every placement in every embedded theme.json resolves against the registry —
    the failure it catches is silent in a browser, because a theme naming a widget
    that is not there gets a layout with that card quietly missing
  • every selector in every widget.css is anchored to .card[data-widget="<id>"]
    — the same test, and the same reason, as the one holding a theme to the nine
    --board-* properties

The JavaScript parse check now finds the widget folders rather than carrying a
list of them. A list there would have been the third place to edit when adding a
widget, and the one nobody remembers — and an unparsed widget is a card that says
"did not load" with the reason only in the console.

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 page's furniture was in index.html. Adding a readout meant editing the
markup, the stylesheet and app.js, which is three files and a fork for anybody
who wants a different instrument on the same lab. The feed made the data
shareable last week; this makes the layout data too.

There are four docks now — top, left, right, bottom — and a card in one of them
is a widget: a folder under ui/hud/widgets/ with a widget.json saying what it
is, a widget.js, and a widget.css scoped to its own card. widgets.go walks the
embedded UI at startup and serves the registry at /api/widgets, so adding a
widget is a folder and a rebuild. The same shape as the theme store, and
panels.go before it.

What a widget is handed is the whole seam: a div, the feed, and the options its
placement carried. No board object, no page elements, no controller. The board
is twenty-five coupled methods over 1300 lines of three.js and publishing it as
a plugin API would freeze hud/scene.js the day somebody wrote against it — a
widget that wants to know what the lab is doing reads the feed, which is what
the feed is for.

Where a card goes is the theme's business. theme.json grows a layout, the server
prunes it against the registry at startup — unknown widget, or a dock the widget
does not fit — and each drop says why at the console, so the page only mounts
placements that can be built. warroom docks three; chapter docks none, for the
same reason its stylesheet overrides nothing, and a test fails if either starts
to.

Three widgets to have something real in the docks, all three reading the feed
and nothing else. wire is the feed reading itself — open or not, how far the
sequence has got, and what the server threw away because a reader could not keep
up, because a recording made through a gap is worth knowing about before it is
posted. tape is every frame written down, which is the closest thing on the page
to what a replay will work from. tally counts verdicts by the rung that decided
them, allowed and denied apart: "rung 3 allowed eleven and denied none" is a
finding and "rung 3 answered eleven times" is not.

The board is not a widget and does not pretend to be. app.js still creates it
and drives it; the docks' part is reporting what they cover so the camera frames
the diagram in the gap the cards leave. Making it a real widget under the
contract above means the board reading the feed instead of being called, which
is the next piece of work and not this one — a privileged contract that handed
one widget the board object would be the plugin API this change exists to avoid.

One trap, found by driving it. --pad-* is measured against the canvas and
consumed against the window, and those differ by the height of the rung ladder,
which sits below the viewport rather than over it. A card measured one way and
positioned the other is short by exactly one ladder, which put the verdict 40px
inside the bottom card. So the docks are measured twice, against both
rectangles, and --rail-* is split out from --pad-*: the docks are positioned
from the furniture alone, because a dock positioned from a number that includes
its own size is a layout that never settles.

Measured against the running lab on 2026-09-12, Chromium 1600x1000. chapter: no
layer in the DOM, no cards, --rail-* and --pad-* equal. warroom: three cards in
three docks, three stylesheets loaded, --pad-r 616 against --rail-r 346. A probe
from lab-ubuntu reached rung 5 and the tally read 1/0; the attacker to :22 was
denied at rung 4 and the tally read 0/1, which is a denial and not a tick.
Folding the readouts rail moved the tape card 984..1244 -> 1276..1536 and the
board re-framed. At 820px the cards stack at the foot of the page with no
horizontal scroll. Verdict, view controls and the board's gauges clear every
card: no overlapping rectangles. The only console error on the page is the
favicon 404 it already had.

make -C lab check passes: ten new Go tests, and the JavaScript parse check now
finds the widget folders rather than carrying a list of them, since a list here
would have been the third place to edit and the one nobody remembers.
@aenawi
aenawi merged commit 346c551 into develop Sep 12, 2026
1 check passed
@aenawi
aenawi deleted the feat/hud-widgets-and-slots branch September 12, 2026 18:12
aenawi added a commit that referenced this pull request Sep 13, 2026
…rd (#83)

The HUD overlay insets itself by whatever the furniture is covering on
three sides and by a flat 12px on the fourth. Since #79 gave a theme a
bottom dock, that fourth side has something under it: on Warroom the
verdict panel was drawn 30px behind the tally card and the transcript —
the one line on the board that is a command rather than a rendering of a
number — was inside it completely.

--pad-b could not simply be added to the padding. It is measured against
the window, because the page verdict and the view controls have to clear
the rung ladder, and the ladder sits below the viewport rather than over
it. The overlay is laid out inside the viewport, so reading --pad-b there
clears the ladder a second time and lifts the whole bottom stack a ladder
higher than the card it only had to clear by a gap.

So publish the number the camera already gets — what covers the board,
not what is occupied at the foot of the window — as --pad-b-canvas, and
read that. The other three sides need no second token: the viewport
starts at the top-left corner of the window, so there the two rectangles
agree.

Measured in Chrome 147 at 1440x760 on 2026-09-13, Warroom, after a probe:

                    before            after       tally card
  #hud-verdict      393-514           393-514     597-700
  #hud-transcript   670-698  (inside) 557-585
  overlay pad-b     12px              125px

The default page is untouched: with nothing docked --pad-b-canvas is 0px
and the padding stays at 12px, the transcript at 670-698 exactly as
before. replay.html gets the same treatment, where the thing below the
board is the transport rather than a card.

The gauges do not move. The top row is an `auto` row above a `1fr`, so
the padding comes out of the gap; at 625px — one pixel above the height
where the layout floats and the question stops applying — the overlay
already overflows with the old rule and both rules put the bottom stack
in the same place.

Closes #81
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.

1 participant