diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_webpage_generator.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_webpage_generator.py index 9e1f9ac..b00622e 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_webpage_generator.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_webpage_generator.py @@ -103,10 +103,8 @@ VERBOSITY_VERBOSE = 2 VERBOSITY_DEBUG = 3 _PHONE_NUMBERS = [ - ("Beamline", "+41 56 310 5845"), - ("Beamline mobile", "+41 56 310 5844"), - ("Local contact", "+41 79 343 9230"), - ("Control room", "+41 56 310 5503"), + ("Beamline", "+41 56 310 5845"), + ("Control room", "+41 56 310 5503"), ] @@ -1030,6 +1028,12 @@ class WebpageGeneratorBase: else max(0.0, _epoch() - self._last_active_time) ) + # ── Tomo queue ─────────────────────────────────────────────── + # Persisted list of parameter-snapshot jobs (global var "tomo_queue"). + # Each job: {"label": str, "params": {...}, + # "status": pending|running|incomplete|done, "added_at": ISO str} + tomo_queue = self._bec.get_global_var("tomo_queue") or [] + # ── Reconstruction queue ────────────────────────────────────── recon = self._collect_recon_data() @@ -1061,6 +1065,7 @@ class WebpageGeneratorBase: "idle_for_human": _format_duration(idle_for_s), "queue_locks": queue_locks, "beamline_states": beamline_states_info, + "tomo_queue": tomo_queue, "progress": { "tomo_type": progress.get("tomo_type", "N/A"), "projection": progress.get("projection", 0), @@ -1806,6 +1811,52 @@ def _render_html(phone_numbers: list) -> str: .bl-badge.bl-watched-no {{ background: var(--surface2); color: var(--text-dim); }} .blstates-none {{ font-family: var(--mono); font-size: 0.75rem; color: var(--text-dim); padding: 0.5rem 0; }} + /* ── Tomo queue card ── */ + .tq-empty {{ + font-family: var(--mono); font-size: 0.78rem; color: var(--text-dim); + padding: 0.25rem 0; + }} + .tq-job {{ border-bottom: 1px solid var(--border); }} + .tq-job:last-child {{ border-bottom: none; }} + .tq-summary {{ + display: flex; align-items: center; gap: 0.75rem; + padding: 0.55rem 0; cursor: pointer; list-style: none; + font-size: 0.85rem; + }} + .tq-summary::-webkit-details-marker {{ display: none; }} + .tq-summary::before {{ + content: '▶'; font-family: var(--mono); font-size: 0.55rem; + color: var(--text-dim); transition: transform 0.2s; flex-shrink: 0; + }} + details.tq-job[open] > .tq-summary::before {{ transform: rotate(90deg); }} + .tq-num {{ + font-family: var(--mono); font-size: 0.65rem; color: var(--text-dim); + min-width: 1.8rem; flex-shrink: 0; + }} + .tq-label {{ flex: 1; font-weight: 600; color: var(--text); }} + .tq-badge {{ + font-family: var(--mono); font-size: 0.58rem; font-weight: 700; + letter-spacing: 0.06em; text-transform: uppercase; + padding: 0.12rem 0.45rem; border-radius: 100px; white-space: nowrap; + flex-shrink: 0; + }} + .tq-badge.tq-pending {{ background: var(--surface2); color: var(--text-dim); }} + .tq-badge.tq-running {{ background: color-mix(in srgb, var(--c-scanning) 20%, transparent); color: var(--c-scanning); }} + .tq-badge.tq-incomplete {{ background: color-mix(in srgb, var(--c-blocked) 20%, transparent); color: var(--c-blocked); }} + .tq-badge.tq-done {{ background: color-mix(in srgb, var(--c-running) 20%, transparent); color: var(--c-running); }} + .tq-details {{ padding: 0.4rem 0 0.7rem 2.4rem; }} + .tq-params {{ width: 100%; border-collapse: collapse; }} + .tq-params td {{ padding: 0.18rem 0; font-size: 0.78rem; vertical-align: top; }} + .tq-params td:first-child {{ + font-family: var(--mono); font-size: 0.65rem; color: var(--text-dim); + padding-right: 1rem; white-space: nowrap; width: 40%; + }} + .tq-params td:last-child {{ color: var(--text); font-weight: 600; }} + .tq-added {{ + font-family: var(--mono); font-size: 0.6rem; color: var(--text-dim); + margin-top: 0.4rem; + }} + /* ── Audio card ── */ .audio-card {{ display: flex; align-items: center; @@ -1964,6 +2015,7 @@ def _render_html(phone_numbers: list) -> str:
Remaining-
Finish time-
Started-
+ @@ -2081,7 +2133,14 @@ def _render_html(phone_numbers: list) -> str: - + +
+
⋮⋮
+
Tomo queue
+
+
+ +
⋮⋮
Contacts
@@ -2119,7 +2178,7 @@ function setTheme(t) {{ // ── Drag-and-drop card ordering ────────────────────────────────────────── const CARD_ORDER_KEY = 'cardOrder'; -const DEFAULT_ORDER = ['audio','recon-queue','ptycho','instrument','blstates','contacts']; +const DEFAULT_ORDER = ['audio','recon-queue','ptycho','instrument','blstates','tomo-queue','contacts']; let _dragSrc = null; function savedOrder() {{ @@ -2555,6 +2614,61 @@ function renderBeamlineStates(bl){{ tbody.innerHTML=html; }} +// Key params to show in expanded job detail, in display order. +const TQ_PARAM_DISPLAY = [ + ['tomo_type', 'Type'], + ['tomo_countingtime', 'Exposure (s)'], + ['fovx', 'FOV x (\u00b5m)'], + ['fovy', 'FOV y (\u00b5m)'], + ['tomo_angle_stepsize', 'Angle step (\u00b0)'], + ['tomo_angle_range', 'Angle range (\u00b0)'], + ['tomo_shellstep', 'Shell step (\u00b5m)'], + ['stitch_x', 'Stitch x'], + ['stitch_y', 'Stitch y'], + ['ptycho_reconstruct_foldername','Recon folder'], +]; + +function fmtTqParam(key, val){{ + if(val==null) return '-'; + if(typeof val==='number') return Number.isInteger(val)?String(val):val.toFixed(3).replace(/\\.?0+$/,''); + return String(val); +}} + +function renderTomoQueue(jobs){{ + const el=document.getElementById('tomo-queue-content'); + if(!jobs||jobs.length===0){{ + el.innerHTML='
No jobs queued
'; + return; + }} + let html=''; + jobs.forEach((job,i)=>{{ + const status=job.status||'pending'; + const label=job.label||('Job '+(i+1)); + const params=job.params||{{}}; + let paramRows=''; + TQ_PARAM_DISPLAY.forEach(([key,dispLabel])=>{{ + if(key in params){{ + paramRows+=''+dispLabel+''+esc(fmtTqParam(key,params[key]))+''; + }} + }}); + const addedAt=job.added_at + ?'Added '+new Date(job.added_at).toLocaleString([],{{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}}) + :''; + html+='
' + +'' + +'#'+(i+1)+'' + +''+esc(label)+'' + +''+esc(status)+'' + +'' + +'
' + +(paramRows?''+paramRows+'
':'') + +(addedAt?'
'+esc(addedAt)+'
':'') + +'
' + +'
'; + }}); + el.innerHTML=html; +}} + function render(d){{ const s=d.experiment_status||'unknown',p=d.progress||{{}}; document.body.className=s; @@ -2576,6 +2690,14 @@ function render(d){{ document.getElementById('pi-remaining').textContent=p.estimated_remaining_human||'-'; document.getElementById('pi-finish').textContent=fmtTime(p.estimated_finish_time); document.getElementById('pi-start').textContent=fmtTime(p.tomo_start_time); + const runningJob=(d.tomo_queue||[]).find(j=>j.status==='running'); + const queueItem=document.getElementById('pi-queue-item'); + if(runningJob){{ + document.getElementById('pi-queue-label').textContent=runningJob.label||'-'; + queueItem.style.display=''; + }}else{{ + queueItem.style.display='none'; + }} if(d.recon){{ document.getElementById('recon-waiting').textContent=d.recon.waiting; @@ -2586,6 +2708,7 @@ function render(d){{ }} renderInstrument(d.setup); renderBeamlineStates(d.beamline_states); + renderTomoQueue(d.tomo_queue); renderPtycho(d.ptycho); document.getElementById('last-update').textContent='updated '+new Date(d.generated_at).toLocaleTimeString(); const ageS=(Date.now()/1000)-d.generated_at_epoch;