Skip to content

refactor(ui): one roster component, mounted twice - #157

Merged
pskeshu merged 1 commit into
gently-project:developmentfrom
pskeshu:feat/one-roster-component
Sep 5, 2026
Merged

refactor(ui): one roster component, mounted twice#157
pskeshu merged 1 commit into
gently-project:developmentfrom
pskeshu:feat/one-roster-component

Conversation

@pskeshu

@pskeshu pskeshu commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Judgement call, flagged

I gave Acquisition a delete it did not have. #113 complained that delete sat next to the select target in a mostly-empty list; the wide roster has room to separate them, and being unable to remove a false positive from the surface you review before a run was the odder half of the old split. Reverse it by dropping 'remove' from that mount.


Audit finding 1. Three renderings of one embryo list: renderEmbryoRail and renderRoster in operate.js (~80% the same code, each with its own count element — the root of #129), plus a badge in embryos.js.

Worse than the duplication, the action sets differed arbitrarily:

before now
rail (beside the frame) delete only remove
roster (pre-run review) role, Centre role, centre, remove

Nothing about either pane justified the split — it is where each button happened to be added. Actions are declared per mount now, so the difference is a decision.

panels/roster.js renders SharedState.embryos and dispatches to OperateManager.roster.*. The list, selection and endpoints stay in operate.js next to the frame geometry — same split as the Marking panel, and the reason both can mount in the Atrium's EMBRYOS window where there is no canvas.

One row style replaces .op-erow and .op-rrow, which were near-identical grids differing only in border and column count. 111 lines of CSS out, and the 12 rules that became unreachable are deleted rather than left to rot. US-13's selectors moved with the markup; .op-empty stays because the tactic library and run spine still use it.

A bug this introduced, then fixed

SharedState.set emits only on a real change, and compares by value. toggleRole mutates emb.role in place, so publishing _embryos.slice() stored an array of the very objects being compared against — the change was invisible and the role button silently did nothing.

Caught by driving the toggle in a browser, not by reading it. structuredClone now, with a test pinning it. Same lesson as #126: never hand out a reference to mutable state.

Verified live

Both mounts render one list with their declared actions · counts agree · selecting in one reflects in the other · an action click does not also select · the reference embryo is marked · the toggle round-trips · the empty state names the fix at both mounts with the CTA opt-in.

There were three renderings of one embryo list: `renderEmbryoRail` and
`renderRoster` in operate.js, plus a count badge in embryos.js. The two in
Operate were ~80% the same code, each keeping its own count element — the root
of gently-project#129, counts disagreeing across surfaces.

Worse than the duplication, the action sets differed **arbitrarily**. On Bottom
cam you could delete an embryo but not centre on it; on Acquisition you could
centre and assign a role but not delete. Nothing about either pane justified
the split; it is where each button happened to be added.

`panels/roster.js` renders `SharedState.embryos` and dispatches to
`OperateManager.roster.*`. The list, the selection and the endpoints stay in
operate.js, next to the frame geometry they depend on — same split as the
Marking panel, and the reason both can mount in the Atrium's EMBRYOS window
where there is no canvas.

Actions are declared per mount, so the difference is now a decision:

    rail       remove                    beside the frame, where you spot a
                                         false positive
    roster     role, centre, remove      the pre-run review surface

JUDGEMENT CALL: I gave Acquisition a delete it did not have. gently-project#113 complained
that delete sat next to the select target in a mostly-empty list; the wide
roster has room to separate them, and being unable to remove a false positive
from the surface you review before a run was the odder half of the split.

One row style replaces `.op-erow` and `.op-rrow` — near-identical grids
differing only in border and column count, because each grew its own actions.
111 lines of CSS out, and the 12 rules that became unreachable are deleted
rather than left to rot. `US-13`'s selectors moved with the markup;
`.op-empty` stays because the tactic library and run spine still use it.

## A bug this introduced, then fixed

`SharedState.set` emits only on a real change, and compares by value.
`toggleRole` mutates `emb.role` in place, so publishing `_embryos.slice()`
stored an array of the very objects being compared against — the change was
invisible and the role button silently did nothing. Caught by driving the
toggle in a browser, not by reading it.

`structuredClone` now, and a test pins it. Same lesson as gently-project#126: never hand out
a reference to mutable state.

Verified live: both mounts render one list with their declared actions, counts
agree, selecting in one reflects in the other, an action click does not also
select, the reference embryo is marked, the toggle round-trips, and the empty
state names the fix at both mounts with the CTA opt-in.

Audit finding 1 in docs/devices-tab-audit.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pskeshu
pskeshu merged commit 0d3187d into gently-project:development Sep 5, 2026
2 checks passed
pskeshu added a commit that referenced this pull request Sep 6, 2026
…thing

My regression, shipped in #157.

Consolidating the two roster renderers into one panel, the roster's own
click/keydown listeners had to go — the panel owns the markup, so it owns the
handlers. I removed them by slicing text between two landmarks, and the end
landmark matched too late. The slice took eight unrelated handlers with it:

    op-spim-toggle    start/stop the light-sheet view
    op-calibrate      run the calibration
    [data-gv]         galvo nudges
    [data-pz]         piezo nudges
    [data-backoff]    the interlock banner's "Back off 100 µm"
    op-modes          the run-mode chooser
    op-tl-stop        the stop-condition select
    op-lib-list       pick a saved tactic

Every one still rendered and still looked enabled. `[data-backoff]` is the
worst of them: a dead button on an interlock banner is pressed at exactly the
moment something is already wrong.

Nothing caught it. CI runs no JavaScript, and no test asserted that a control
was connected to a function, so there was no signal — I found it only by
noticing that clicking a run mode did not change the run mode.

`tests/test_operate_controls_are_wired.py` closes that. It checks every control
inside `wire()` specifically, not merely somewhere in the file: several of these
ids are also read elsewhere (the SPIM toggle's label is updated in
`applySpim`), so presence proves the control is mentioned, not that anything
listens to it. Verified against the broken file — it flags all eight.

The helper anchors on `_wired`, which only the top-level `wire()` uses. My
first attempt matched "function wire() {" and found the gauge factory's inner
one — landmark-matching text is the mistake this file is about, so it is not
repeated in it.

Verified live: the mode chooser switches panel and run verb, the stop-condition
select reveals its value field, and the back-off, galvo and piezo controls are
present and bound.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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