refactor(ui): one marking surface, invoked two ways - #162
Merged
pskeshu merged 1 commit intoSep 6, 2026
Conversation
There were two complete implementations, and the question "why are there two tabs of embryos" is what surfaced it. `static/js/marking.js` — 452 lines with its own canvas, its own hit-test, its own marker list — lived in the Embryos tab behind a "Marking" subtab, and `websocket.js` switched the operator into it when the agent sent a `marking_image` frame. `operate.js` had the other, on the bottom-camera pane, for an operator marking unprompted. So every improvement to marking landed on exactly one of them. The zoom, the display range, gently-project#105's hit-test radius and gently-project#126's roster aliasing were all on the Operate side and none of it existed on the agent's side. Whether an operator got the corrected behaviour depended on who had asked them to mark. The agent's request lands on the Operate pane. The pushed image is ADAPTED into the payload shape a live camera frame already has — `shape`, `downsample`, `stage_position`, `jpeg_b64` — so `frameOf`, `stageOf`, `drawMarkers`, `onCanvasClick` and the corrected hit-test all apply with no special cases. That is the point: the agent's request inherits zoom, geometry and the display range because it is the same surface, not because anything was reimplemented. Initial markers arrive in pixels and are converted to stage µm once, at the boundary, because this pane keeps markers in stage coordinates so they stay attached to the sample under zoom and stage motion. The answer converts back. `pixel_size_um` is a session parameter rather than the rig default, threaded through the geometry by `pxBase()` — a session declaring a different scale would otherwise place every marker at 0.65 µm/px. It is a websocket request/response, not an HTTP call: the agent blocks on `session["complete"]` in `routes/websocket.py` and reads a role per marker out of the answer. So `marking_done` goes back with the session id, pixel coordinates and a role each, `marking_redetect` still works, and the session panel carries a per-marker role toggle — registered embryos get their roles in the Acquisition roster, but these are not registered yet. A reference is also tagged on the glyph, since it must be distinguishable on the image and not only in the panel. The session UI is present only while the agent is waiting. marking.js, the `embryos-marking` markup, the subtab bar (one subtab left is chrome for a choice that no longer exists), and 222 lines of CSS that became unreachable — 31 rules, removed rather than left to rot. The agent's frame lands on Devices → Operate → Bottom cam with the image shown, the session panel seated, and two initial markers carrying the roles they arrived with. Cycling a role, asking for a re-detect and pressing Done all answer over the websocket, and pixel coordinates round-trip exactly: (100,80) and (220,140) in, the same out. And the claim that motivated it — zoomed to 1.75x, clicking the frame centre put a marker back at exactly (200,150), the frame centre, with the display histogram available on the pushed image too. Neither was possible on the old surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pskeshu
force-pushed
the
feat/one-marking-surface
branch
from
September 6, 2026 02:42
1fcee0e to
19de950
Compare
pskeshu
added a commit
that referenced
this pull request
Sep 6, 2026
The Light panel showed everything at once, and with no device layer that meant
four disabled power sliders reading an em dash — maximum clutter for zero
information.
Beam and power are now revealed under the Laser row when the config routes
something, and hidden when it does not.
## Scope, not dependency
The nesting groups the laser's own settings. It deliberately does NOT imply
`BeamEnabled` follows from the config, because it does not — emission is
conjunctive:
emitting = armed AND routed AND power > 0
#106 is exactly what happens when someone assumes otherwise: a correctly
configured laser that emits nothing because the beam is off. So the indent
carries the grouping and the contradiction gets a line of its own rather than
being softened by it. Routed lines with the beam off now reads "this
configuration will not emit", and names the cause — every volume acquisition
leaves BeamEnabled at No.
The mirror case is handled too: an armed beam with nothing routed is safe,
surprising, and the state the rig is left in, so a note on the Laser row keeps
that fact visible. Hiding the detail must not hide the fact, or the disclosure
would have made the panel less honest than the flat version it replaced.
`routedLines()` is for display and answers "which lines do we KNOW are routed".
`wavelengthsOf()` is unchanged and still answers "which could be involved",
returning every line for an unknown config — right for `emitting()`, because an
unread config with unread power must come out unknown rather than safe.
The config select now always includes what is actually set. The preset list
comes from an endpoint that 503s with the device layer down, so the select read
an em dash while the detail below it showed routed lines and live power — two
halves of one panel contradicting each other.
## A regression fixed on the way, unrelated to the above
The device-layer strip's Start/Stop/Log buttons had been borrowing
`.marking-action-btn` from the old marking surface. Deleting that surface
(#162) took their styling with them, and they have been rendering unstyled
since.
My liveness check behind that deletion used a regex requiring a character
before "marking", so class names STARTING with it were never recognised as
live. `.marking-action-btn` was in use by markup that had nothing to do with
marking, and its rules went anyway. Second over-deletion this session with the
same root cause: bulk removal on an imperfect liveness test.
Re-homed onto `.devices-layer-btn`, which the buttons already carried, so the
name now says what it styles. The borrowed class names are gone from the
markup, and a test pins both.
Verified live across all four states: unknown reveals nothing (0 sliders, was
4), ALL OFF with an armed beam reveals nothing but shows the note, routed with
the beam off reveals the detail and states the contradiction, routed and armed
reveals it and reads EMITTING. Start is blue again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Net −339 lines. The question "why are there two tabs of embryos" surfaced this.
There were two complete marking implementations:
static/js/marking.js(452 lines)operate.jsbottom-cam panemarking-canvasop-mark-canvasmarking_imageswitched you there)So every improvement to marking landed on exactly one of them. The zoom, the display range, #105's hit-test radius and #126's roster aliasing were all on the Operate side; none of it existed on the agent's side. Whether an operator got the corrected behaviour depended on who had asked them to mark.
One surface
The agent's request now lands on the Operate pane. The pushed image is adapted into the payload shape a live camera frame already has —
shape,downsample,stage_position,jpeg_b64— soframeOf,stageOf,drawMarkers,onCanvasClickand the corrected hit-test apply with no special cases.That's the point: the agent's request inherits zoom, geometry and the display range because it is the same surface, not because anything was reimplemented.
Initial markers arrive in pixels and convert to stage µm once, at the boundary — this pane keeps markers in stage coordinates so they stay attached to the sample under zoom and stage motion. The answer converts back.
pixel_size_umis a session parameter, not the rig default, threaded bypxBase(). A session declaring a different scale would otherwise place every marker at 0.65 µm/px.The contract is preserved
It's a websocket request/response, not an HTTP call — the agent blocks on
session["complete"]inroutes/websocket.pyand reads a role per marker. Somarking_donegoes back with the session id, pixel coordinates and a role each;marking_redetectstill works; and the session panel carries a per-marker role toggle, because registered embryos get roles in the Acquisition roster but these aren't registered yet. A reference is tagged on the glyph too — it has to be distinguishable on the image, not only in the panel.The session UI is present only while the agent is waiting.
Deleted
marking.js, theembryos-markingmarkup, the subtab bar (one subtab left is chrome for a choice that no longer exists), and 222 lines of CSS across 31 rules that became unreachable.Verified live
The agent's frame lands on Devices → Operate → Bottom cam, image shown, session panel seated, two initial markers carrying the roles they arrived with. Cycling a role, re-detect and Done all answer over the websocket, and pixel coordinates round-trip exactly —
(100,80)and(220,140)in, the same out.And the motivating claim: zoomed to 1.75×, clicking the frame centre put a marker back at exactly
(200,150), with the display histogram available on the pushed image. Neither was possible on the old surface.