webpage_generator: report blocked on interlock even when scan is interrupted

A block interrupts the running scan, clearing active_request_block, so
the old _derive_status() (which only returned 'blocked' inside the
queue_has_active_scan branch) fell through to 'idle' with the blocked
LED off, despite watched beamline states being out of spec.

- _derive_status: return 'blocked' on beamline_blocking OR queue_locks,
  independent of active scan; only fresh-heartbeat 'scanning' outranks it.
- _cycle: beamline_blocking = enabled is True AND any(state mismatched).
  Gated on enabled so a disabled/unknown interlock with an out-of-spec
  state does not raise BLOCKED. Also counted as activity so the idle
  clock restarts once cleared.
- beamline-states card summary updated in lock-step: mismatched states are
  only called "currently blocking" when enabled, else "out of spec
  (interlock disabled/unknown, not blocking)", so pill and card can't
  disagree.
- status-detail JS: describe blocking beamline states when no explicit
  queue lock is present. Update help text.
This commit is contained in:
x12sa
2026-07-07 10:41:40 +02:00
committed by holler
co-authored by holler
parent 524c26f706
commit c50ac5c30c
@@ -1028,12 +1028,16 @@ class WebpageGeneratorBase:
f"beamline_states read error: {exc}", level="warning")
beamline_states_info = {"enabled": None, "states": []}
# True if any watched state is out of spec. Defined identically to the
# "N watched states currently blocking" count shown on the beamline
# states card (renderBeamlineStates: states.filter(s => s.mismatched)),
# True if the scan interlock is enabled AND at least one watched state
# is out of its accepted range. Gated on `enabled`: a mismatched state
# while the interlock is disabled does not actually block a scan, so it
# must not raise the 'blocked' status. Kept in lock-step with the
# beamline-states card summary (renderBeamlineStates), which likewise
# only calls mismatched states "blocking" when the interlock is enabled,
# so the top status pill and that card can never disagree.
beamline_blocking = any(
s.get("mismatched") for s in beamline_states_info.get("states", [])
beamline_blocking = bool(
beamline_states_info.get("enabled") is True
and any(s.get("mismatched") for s in beamline_states_info.get("states", []))
)
# Current scan number (the BEC scan in progress right now, e.g. for
@@ -2684,10 +2688,19 @@ function renderBeamlineStates(bl){{
return;
}}
summary.innerHTML = 'Scan interlock <strong>'+enabledTxt+'</strong>'
+ (mismatched.length>0
? ' &middot; <strong>'+mismatched.length+'</strong> watched state'+(mismatched.length>1?'s':'')+' currently blocking'
: ' &middot; all watched states OK');
// "Blocking" only applies when the interlock is enabled — a mismatched
// state while disabled/unknown is out of spec but does not block a scan.
// Kept in lock-step with the Python beamline_blocking gate so this card and
// the top status pill never disagree.
let blockNote;
if(mismatched.length===0){{
blockNote=' &middot; all watched states OK';
}} else if(bl&&bl.enabled===true){{
blockNote=' &middot; <strong>'+mismatched.length+'</strong> watched state'+(mismatched.length>1?'s':'')+' currently blocking';
}} else {{
blockNote=' &middot; <strong>'+mismatched.length+'</strong> watched state'+(mismatched.length>1?'s':'')+' out of spec (interlock '+enabledTxt+', not blocking)';
}}
summary.innerHTML = 'Scan interlock <strong>'+enabledTxt+'</strong>'+blockNote;
let html='';
states.forEach(s=>{{