CI's diff-coverage gate sat at 29% because the ZMQ fixes live in code no
test reached: the subscriber's socket setup, _emit_image/notify_frame_
displayed, _recv_latest and the run() frame path, plus the main window's
frame slot. The subscriber now runs against a real inproc socket (the
drain and the in-flight cap are the fix, so a mock would prove nothing);
run() is driven with a stubbed _recv_latest that hands over one JPEG and
then stops. The busy overlay gets a check that cooling is the only blue
busy state.
The subscriber fixture takes qapp so no QImage is built before the
QApplication exists.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The sample camera drew its own gold/orange/red "Session Vacant /
Baton Requested / Guest Mode" box whenever the busy overlay had nothing
to show. Since the busy-style builder already receives the session
state, that box only ever appeared while the session was still unknown
(no status yet, or one without a session), so it flashed a different
color and shape than the yellow "Viewing mode, click here to grab the
baton" badge every known not-owned state gets.
Hoist that badge into VIEWING_MODE_STYLE and paint it from both paths;
the hover fill is shared through _badge_fill. The three MARK_TOOLTIP
constants had no other users and are dropped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CI resolves aarecommon 0.7.3, where DataCollectionParameters.transmission
is a 0-to-1 fraction that rejects anything above 1.0 (the percent sheets
are converted inside AareDB >= 0.83, which main already requires). The
divide-by-100 from the earlier 'percentage' fix therefore failed the
model test on CI and would have turned 20% into 0.2% at the beamline.
Pass the fraction through and pin aarecommon>=0.7.3 so the older int
percentage model can no longer be installed; relock.
Also satisfy the diff typecheck gate: basedpyright only counts instance
variables assigned in __init__, so the Database/User toggle widgets are
created there and _build_source_toggle only lays them out.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/status cannot tell an auto centering from a manual alignment move (both
are busy + SampleAlignment, the combo the overlay deliberately hides), so
the DAQ worker emits its own auto_centering flag: True while its own
/alc/center_loop POST is in flight (the endpoint blocks until done) or
while the automation progress reports the Center step running. The
sample camera views pass it to build_busy_overlay_style, which paints a
purple AUTO CENTERING wave text like the ROBOT MOUNTING/COOLING ones.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DataCollectionParameters.transmission is a StrictInt documented as
"positive, between 0 and 100" (aareDB's own API description of the
spreadsheet payload), while every model that consumes it wants a 0-to-1
fraction. The old "divide by 100 only when above 1.0" guess therefore
turned a spreadsheet asking for 1% into a scan at 100% transmission -
a hundredfold dose on the sample. A fraction cannot be stored in that
column at all: the model rejects 0.2.
The mapping test now builds a real DataCollectionParameters instead of a
stand-in namespace, so a renamed column fails the test rather than the
beamline. That is what let 'totalrange' through.
daq.py's spreadsheet_params and get_auto_raster_params still carry both
the old column name and the same transmission guess.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHoUkj66jxByS2ypY5h9Mn
The column is totalangle, not totalrange, so reading it raised
AttributeError four times a second in the status loop.
Reading a spreadsheet cell now goes through _spreadsheet_float, which
treats a missing column, an empty cell and a cell that does not hold a
number all as "not set" and logs the column once. aareDB owns these
names; the GUI should fall back to its defaults when one moves, not die
on the status loop it is read from.
Note that the same misspelling is still in daq.py (spreadsheet_params
and get_auto_raster_params), where getattr's default hides it: automation
has never picked up the total angle from the spreadsheet either.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHoUkj66jxByS2ypY5h9Mn
The panels kept the effective detector distance, resolution and
transmission in their own attributes, and a sample change overwrote those
from the spreadsheet whatever the Database/User toggle said. The fields
went on showing the user's numbers while the scan request carried the
database ones, so a value had to be re-entered after every mount.
The fields are now the only place a value lives. Each setting is worth,
in order: what the user typed while "User values" is selected, what the
mounted sample asks for, the panel default - one rule, ScanSettingsPanel
._setting, that each panel spells out a line at a time. Overrides are per
setting, so a setting the user did not touch keeps following the sample.
Along the way:
* DbOverrideLineEdit is gone; the panels use NumberLineEdit directly, and
the per-widget database/user bookkeeping is replaced by SampleParameters,
the one place that knows the aareDB spreadsheet column names.
* The (name, widget, converter) mapping loops with getattr/hasattr are
replaced by explicit per-field commit slots.
* The omega speed cap no longer drives one field's validator from another
field's value: it is a read-out plus a check on the pair at Run time.
* _add_row/_add_pair_row build the label/field/unit rows, which also puts
the Total angle degree sign on its own row instead of the header above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHoUkj66jxByS2ypY5h9Mn
x=20 sits on a digit glyph with CI's Linux fonts, so the assert compared
an antialiased glyph blend against the pure background. The padding area
(border 1px + padding 6px) can never contain text on any font.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147jE48bQUTm9AqNQApT6b2
The movestate property sat on the spinbox's inner QLineEdit, which both
themes deliberately paint transparent (QAbstractSpinBox QLineEdit rule)
- the state machine worked but no color ever showed, and the tests only
asserted the property, not the paint. The property now lives on the
spinbox itself with QAbstractSpinBox[movestate=...] added to both
themes' rules, verified by a pixel-sampling regression test that grabs
the rendered widget through the real stylesheet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The energy setpoint applies via the Change Energy button, not Enter, so
it gets the full motor state machine instead of the typing-pending
color: neutral tracks the readback, a user edit stages pending (and is
the only thing that enables the button), the click turns moving, and
readback arrival within tol returns to neutral. New SpinMoveState in
motor_move_group.py adapts the pattern to a QDoubleSpinBox+button pair
(colors ride the movestate QSS via the spin's internal QLineEdit);
replaces the hand-rolled pending-until-click in the exp-config row and
covers the Beamline setup row that had no color at all. The server's
0.0 detector-unavailable energy is kept out of the readback feed so it
cannot clamp the spin to the range minimum.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both Set Energy rows allowed 1-30 keV, an arbitrary spinbox default;
nothing downstream validates the request (daq.change_energy forwards
straight to bec). Shared ENERGY_MIN/MAX_KEV constants now bound both
spins to 4-20 keV with a tooltip flagging the range as pending
Beamline Scientist confirmation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Values inside the Experiment configuration banner now show the same
pending color as the motor move boxes while the typed text differs from
the applied value; Enter (or focus-out) commits as before and clears it.
Opt-in via NumberLineEdit(track_pending=True) because MotorMoveGroup
owns the movestate property on its registered boxes; reuses the existing
movestate QSS, so no new styling. The Set Energy spinbox marks pending
until Change Energy is clicked, since its apply gate is the button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fix removed Moving from the motion-watch trigger but the unit test and code comment still described the old behavior, so pre-push failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Auto Centering moves out of the experiment tabs to its own row between
Manual sample and Exp. Config., with an apply-after-mount checkbox that
re-runs it on every detected sample mount (guarded against firing on GUI
startup). Draw a Grid now only shows on the Raster scan tab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Click is no longer the only way back to the "?" badge: moving the
pointer off the expanded box, or out of the widget entirely, folds it.
Checked before the interaction gate so it works in viewing mode too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bottom-right corner shows the hovered pixel coordinates with a grey
1-2-5 scale bar above them, derived from geom.pixel_in_mm and the
current zoom. Works in viewing mode (before the interaction gate),
clears on leave, and skips drawing outside the image.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
basedpyright requires _admin_tip_anchor to exist after __init__, so the
anchor is created eagerly instead of lazily. Cover the red-tip branch and
the status-bar menu gate; the menu test swaps in a QMenu subclass with a
no-op exec because PySide method lookup ignores class-attr monkeypatches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Beam location, Beamstop alignment and Flux measurement grey out for
non-staff in the state strip and the status-bar transitions menu. All
"not admin" denials now show as an auto-dismissing QToolTip on a
Catppuccin red wash instead of a click-to-close QMessageBox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Operable controls (Fast Shutter, State, p-group, Session) group right
via addPermanentWidget and share one hover affordance - hand cursor +
underline - now owned by ClickableLabel; ValueLabel opts out for the
passive readouts, and the unwired busy label becomes a plain QLabel.
Cryo sits first in the operable group but stays passive: a cryo
operation is planned (TODO in status_bar.py), the placement just
reserves its spot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same exception as the motion watch: busy in Sample alignment is the
alignment itself, and the sample motion is watched in these very
views - the curtain hid exactly what the user needed to see. Gated
at both style call sites (sample camera and the Axis views); the
session-driven 'In viewing mode' badge is unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Robot motion switches to the combined view; alignment beginning
switches straight back while busy is still set, and staying busy in
alignment never re-triggers. Piggybacks on the init test's window -
extra MainWindow constructions feed the PySide teardown flake.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_autoscale now defaults on, so the view fits from the first layout
(the constructor's placeholder already carries the camera size and
resizeEvent refits on every resize). update_pixmap additionally
refits when the frame size changes - the real stream resolution can
differ from the placeholder, and only a view resize refit before.
Right-click 'Scale to fit' still toggles back to 1:1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The queue engine keeps insertion order and Run pops its head, but the
Queued view sorted by the dewar table's header sort - the top row was
not what runs next. set_queued_ids now keeps the order it is fed (the
queue model's order) and the Queued view displays exactly that; header
clicks no longer reorder it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Status flag on the left (same rich-text scheme as the status bar,
theme-aware via set_theme), Open/Close buttons on the right, wired
to the same daq.open_shutter/close_shutter slots staff-gated in
main_window.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The status column holds no sortable data, so its header click slot
was free: it now does Ctrl+A, pairing with the Unmeasured chip to
grab everything still to be done in one motion. The sort indicator
is put back afterwards - the click drags it onto '#' even though
the model ignores sorting there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Confirmed by Dawn: any rotation data counts as measured; unmeasured
is exactly rotation count 0. The old > 1 rule left single-rotation
samples looking like still-to-do work in the Unmeasured view.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New 'Current energy / lambda' row above the setpoint, fed per DAQ
tick from diffraction.energy_keV; the setpoint row is renamed
'Set Energy (keV)' so 'what is' and 'what to set' are distinct.
Guards energy 0.0 (server's detector-unavailable fallback) because
the wavelength property divides by it. The copied energy row in the
experiment configuration is renamed to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Complement of Measured (rotation count <= 1): one click shows
everything still to be done, so it can be selected wholesale and
queued. Plain chip like All - unmeasured rows carry no tint, so
there is no legend color to wear; cross-status marks (queued/
flagged) show like in the Measured view.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
While Moving, the target state sat solid blue and read as already
reached. An opacity pulse (QGraphicsOpacityEffect + QPropertyAnimation)
marks it as in progress; a stylesheet animation would repolish the
button every frame, which the guarded updates in _apply_highlight
exist to avoid.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-window installs of the two QApplication-level filters (cursor,
wheel guard) stacked one stale copy per MainWindow; now installed once
per process, parented to the app. closeEvent overrides in the baton
dialogs replaced by finished.connect(timer.stop); keyPressEvent in
VideoGraphicsView replaced by WidgetShortcut QShortcuts; hover
enter/leave overrides in PlayPauseButton replaced by underMouse();
ValueLabel inherits ClickableLabel instead of duplicating its
mousePressEvent. Dead raster-timer branch and a PyQt5-era
event-position fallback deleted; redundant try/except and getattr
guards dropped now that class-level defaults cover pre-init reads.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The locked-banner and Beamline-tab-bar filters were installed mid
__init__, but MainWindow.eventFilter reads sample_lists_tabs, which is
created later - every event delivered in between raised AttributeError
inside the filter ('Error calling Python override of eventFilter()'
spam) and broke widget teardown, cascading errors across CI tests and
wedging the pxiii_bec job. Filters now install late, next to
installEventFilter(self), and the filter guards its attribute reads so
construction/teardown-time events can never raise.
Also fold the staff-gate assertions into test_main_window_init: every
extra MainWindow construction raises the odds of the pre-existing
PySide SystemError flake, so don't build a window just for them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diff coverage vs main now 100 percent from the gui suite alone:
non-staff locked banners and both gated-tab popups, remote-close
bookkeeping in update_daq_status, smargon staged moves, camera More
link and Alt-wheel axis fallback, number box relimits, energy row
emit, and the gui.main() banner/graceful-exit path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Promote the illumination panel's __main__ self-check to a real pytest
file so CI counts it, add busy-overlay render checks (status text
paints, video view routes through it), and cover the motor group's
incomplete-entry and below-min Enter paths. Local diff coverage from
the gui suite alone: 82 percent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename NumberLineEdit.validator to range_validator: the instance attr
shadowed QLineEdit.validator() and broke type checking at call sites.
Annotate the smargon _move_axes dict and cast the stand-in self in the
prediction subscriber test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>