Skip to content

feat(web): the side panel's headline and lane ledger (#40) - #63

Merged
bgent19 merged 1 commit into
mainfrom
feat/40-side-panel
Aug 14, 2026
Merged

feat(web): the side panel's headline and lane ledger (#40)#63
bgent19 merged 1 commit into
mainfrom
feat/40-side-panel

Conversation

@bgent19

@bgent19 bgent19 commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Closes #40.

The instructor's dashboard beside the graph: a headline and a lane ledger that lists every lane, always, in a fixed order, so a lane's numbers are in the same place on every beat of all four days and can be read aloud without hunting.

What's here

  • Headline — six fixed slots (Supply, Demand, Flow, Cut, Struck, Over cap) plus one A/B pair line.
  • Ledger — one row per stored lane, always, in fixed order, keyed by edge id: Lane / From / To / Cap / Cost / Risk / Flow. Columns and frame are generated from one list, so a header cannot exist without its cell.
  • Row states.removed, .in-cut, .violating, .on-path, .hot as classes on that one row. No separate DOM for any lane subset.
  • Click-to-pin — a ledger row pins that lane hot; R clears it.

Both slots are built once per dataset in the same topology pass as the canvas and thereafter mutated only by class and text content. No innerHTML after build, in any slot — a slot rebuilt from a string loses its handles, and the handle is what makes a row and its lane demonstrably the same object.

Decisions worth reading

  • The cut's certifying lane list and the interdicted subset were never components. They are subsets of the lanes, so they are row states rather than separate DOM that could disagree with the row. The struck and violation counts are counted from the very flags that class those rows.
  • Throughput and cut capacity are not derived here. They are solver scalars the server computed. Summing cut capacity in the browser would put two numbers that max-flow/min-cut says must be equal on the dashboard by two different routes, one slot apart. Flow and Cut view (Days 2-3) #42 must set state.cut and marks.inCut from one /maxflow response.
  • The ledger is never filtered to the cold lanes. Filtering reflows on every keypress, and it empties itself on the pristine network, where an empty hot set means every lane is hot. Showing everything gives the ledger the job it needed anyway: it is the printed key to the canvas.
  • No reflow, structurally. The tick's width is reserved transparently from the first paint, and the table is laid out fixed — an auto table re-measures its columns from their content, so a two-digit flow becoming three-digit would shift every column on a beat that changed nothing else.
  • A pointer may never reach a state the keyboard cannot, which is why R ships alongside the click rather than after it.

Known follow-ups

  • R clears the pin, but "until the next beat" cannot be honoured until the beat engine (The beat engine, keyboard controls and layer catalog #41) lands. clearPin() documents that the beat mutator must call it.
  • Tick colours for cut, path and violation are provisional stand-ins: they are meant to be low-chroma echoes of canvas hues that Flow and Cut view (Days 2-3) #42Cost and Risk view (Day 1) #44 have not authored yet. Only removed and hot echo a colour that exists today. The stylesheet flags them for re-derivation.
  • A pinned lane also brightens on the canvas, so the click is visible in the room and not only on the instructor's display. It spends no hue — weight and brightness only — and is scoped away from removed lanes so it cannot overpaint a struck-out reading.

Verification

tests/test_web_assets.py gains four checks, written before the implementation:

  • no asset writes markup as a string (innerHTML and friends);
  • every row state is both painted by the stylesheet and computed by state.js;
  • every row state has a --panel-tick-* token;
  • the ticks are strictly less saturated than every categorical role on the canvas — a relationship rather than a value, since the palette is expected to be retuned against the real projector.

Full suite: 294 passed. No JavaScript test runner is introduced; the frontend stays verified from Python or not at all.

🤖 Generated with Claude Code

The instructor's dashboard beside the graph: a headline and a lane ledger
that lists every lane, always, in a fixed order, so a lane's numbers are in
the same place on every beat of all four days and can be read aloud without
hunting.

Both slots are built once per dataset in the same topology pass as the canvas
and thereafter mutated only by class and text content. No innerHTML after
build, in any slot -- a slot rebuilt from a string loses its handles, and the
handle is what makes a row and its lane demonstrably the same object.

Three of the panel's apparent content shapes were never components. The cut's
certifying lane list and the interdicted subset are subsets of the lanes, so
they are row states on the one ledger row -- .in-cut, .removed, .violating,
.on-path, .hot -- never separate DOM that could disagree with it. The struck
and violation counts are counted from the very flags that class those rows.
Throughput and cut capacity are not: they are solver scalars the server
computed, and re-deriving either here would put two numbers that must be
equal on the dashboard by two different routes.

The ledger shows every lane always, with hot rows marked, rather than
filtering to the cold ones. Filtering reflows on every keypress and empties
itself on the pristine network, where an empty hot set means every lane is
hot. Showing everything gives the ledger the job it needed anyway: it is the
printed key to the canvas.

Row-state colour is a low-chroma left-edge tick whose width is reserved
transparently from the first paint -- a border that appeared would move the
row's text sideways on the beat it appeared. The table is laid out `fixed`
for the same reason: an auto table re-measures its columns from their
content, so a two-digit flow becoming three-digit would shift every column on
a beat that changed nothing else.

Clicking a row pins that lane hot until the next beat or R. That is the whole
of what a pointer may do here: duplicate a keyboard jump, or produce
transient emphasis no later state depends on. R ships alongside it because
the rule is that a pointer may never reach a state the keyboard cannot.

tests/test_web_assets.py gains four checks: no asset writes markup as a
string; every row state is both painted and computed; every row state has a
tick token; and the ticks are strictly less saturated than every categorical
role on the canvas -- a relationship rather than a value, since the palette
is expected to be retuned against the real projector.

Closes #40
@bgent19
bgent19 merged commit ecb3a87 into main Aug 14, 2026
1 check passed
@bgent19
bgent19 deleted the feat/40-side-panel branch August 14, 2026 22:43
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.

Side panel: headline and lane ledger

1 participant