movable cards and more
All checks were successful
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 3s
CI for csaxs_bec / test (push) Successful in 1m55s

This commit was merged in pull request #174.
This commit is contained in:
x12sa
2026-03-26 16:20:21 +01:00
committed by holler
parent 55531c8a65
commit f92db3f169

View File

@@ -164,8 +164,10 @@ def _derive_status(
) -> str:
"""
Returns one of:
scanning -- tomo heartbeat fresh (< _TOMO_HEARTBEAT_STALE_S)
running -- queue currently has an active scan
scanning -- tomo heartbeat fresh (< _TOMO_HEARTBEAT_STALE_S), OR
heartbeat recently seen AND queue still has active scan
(handles long individual projections > heartbeat timeout)
running -- queue has active scan but heartbeat has never been seen
idle -- not scanning, last_active_time known
unknown -- no activity ever seen since generator started
@@ -177,6 +179,11 @@ def _derive_status(
hb_age = _heartbeat_age_s(progress.get("heartbeat"))
if hb_age < _TOMO_HEARTBEAT_STALE_S:
return "scanning"
# Heartbeat stale but queue still active and heartbeat was seen recently
# enough (within 10× the stale window) — likely a long projection, not idle.
# Report as 'scanning' so audio system does not trigger a false alarm.
if queue_has_active_scan and hb_age < _TOMO_HEARTBEAT_STALE_S * 10:
return "scanning"
if queue_has_active_scan:
return "running"
if last_active_time is not None or had_activity:
@@ -625,10 +632,16 @@ class WebpageGeneratorBase:
if not scan_id:
return {}
# If scan unchanged and all output files exist, skip all work
thumb_key = f"{scan_id}_phase_thumb.jpg"
if (scan_id == self._last_ptycho_scan
and (self._output_dir / thumb_key).exists()):
# If scan unchanged, thumbs exist, and source hasn't changed, skip all work
thumb_key = f"{scan_id}_phase_thumb.jpg"
thumb_path = self._output_dir / thumb_key
src_phase = found.get("phase")
thumb_fresh = (
thumb_path.exists()
and src_phase is not None
and thumb_path.stat().st_mtime >= src_phase.stat().st_mtime
)
if (scan_id == self._last_ptycho_scan and thumb_fresh):
# Rebuild result dict from existing files without doing I/O
images = []
for role, _pat, primary in _ROLES:
@@ -1211,6 +1224,22 @@ def _render_html(phone_numbers: list) -> str:
}}
.phone-num {{ font-size: 0.9rem; font-weight: 600; color: var(--text); }}
/* ── Drag-and-drop card ordering ── */
#card-container {{ display: grid; gap: 1.25rem; }}
.draggable-card {{ position: relative; }}
.drag-handle {{
position: absolute; top: 0.9rem; right: 0.9rem;
font-size: 1rem; color: var(--border); cursor: grab;
line-height: 1; letter-spacing: -2px; user-select: none;
transition: color 0.2s;
}}
.drag-handle:active {{ cursor: grabbing; }}
.draggable-card:hover .drag-handle {{ color: var(--text-dim); }}
.draggable-card.drag-over {{
outline: 2px dashed var(--status-color); outline-offset: 3px;
}}
.draggable-card.dragging {{ opacity: 0.4; }}
/* ── Footer ── */
footer {{
font-family: var(--mono); font-size: 0.65rem; color: var(--text-dim);
@@ -1295,37 +1324,17 @@ def _render_html(phone_numbers: list) -> str:
</div>
</div>
<!-- 2. Reconstruction status -->
<div class="card">
<div class="card-title">Reconstruction queue</div>
<div class="recon-stats">
<div class="recon-stat"><span class="label">Waiting</span><span class="value" id="recon-waiting">-</span></div>
<div class="recon-stat"><span class="label">Failed</span><span class="value" id="recon-failed">-</span></div>
<div class="recon-stat"><span class="label">Queue name</span><span class="value" style="font-size:0.9rem" id="recon-name">-</span></div>
</div>
<div class="recon-path" id="recon-path"></div>
</div>
<!-- Lightbox overlay -->
<div id="ptycho-lightbox" onclick="closeLightbox()">
<img id="ptycho-lightbox-img" src="" alt="">
</div>
<div class="card" id="ptycho-card">
<div class="card-title">Ptychography reconstructions</div>
<div id="ptycho-content">
<div class="ptycho-none">No reconstruction found</div>
</div>
</div>
<!-- Draggable cards container -->
<div id="card-container">
<!-- 3. Instrument details -->
<div class="card" id="instrument-card" style="display:none">
<div class="card-title">Instrument details</div>
<div class="instrument-grid" id="instrument-grid"></div>
</div>
<!-- Audio -->
<div class="card audio-card">
<div class="card audio-card draggable-card" data-card-id="audio">
<div class="drag-handle" title="Drag to reorder">&#8942;&#8942;</div>
<div class="audio-info">
<div class="audio-leds">
<div class="led-group">
@@ -1351,14 +1360,44 @@ def _render_html(phone_numbers: list) -> str:
</div>
</div>
<!-- 2. Reconstruction queue -->
<div class="card draggable-card" data-card-id="recon-queue">
<div class="drag-handle" title="Drag to reorder">&#8942;&#8942;</div>
<div class="card-title">Reconstruction queue</div>
<div class="recon-stats">
<div class="recon-stat"><span class="label">Waiting</span><span class="value" id="recon-waiting">-</span></div>
<div class="recon-stat"><span class="label">Failed</span><span class="value" id="recon-failed">-</span></div>
<div class="recon-stat"><span class="label">Queue name</span><span class="value" style="font-size:0.9rem" id="recon-name">-</span></div>
</div>
<div class="recon-path" id="recon-path"></div>
</div>
<div class="card draggable-card" id="ptycho-card" data-card-id="ptycho">
<div class="drag-handle" title="Drag to reorder">&#8942;&#8942;</div>
<div class="card-title">Ptychography reconstructions</div>
<div id="ptycho-content">
<div class="ptycho-none">No reconstruction found</div>
</div>
</div>
<!-- 3. Instrument details -->
<div class="card draggable-card" id="instrument-card" data-card-id="instrument" style="display:none">
<div class="drag-handle" title="Drag to reorder">&#8942;&#8942;</div>
<div class="card-title">Instrument details</div>
<div class="instrument-grid" id="instrument-grid"></div>
</div>
<!-- 4. Contacts -->
<div class="card">
<div class="card draggable-card" data-card-id="contacts">
<div class="drag-handle" title="Drag to reorder">&#8942;&#8942;</div>
<div class="card-title">Contacts</div>
<div class="phones">
{phones_html}
</div>
</div>
</div><!-- /card-container -->
<footer>
<span id="footer-gen">generator: -</span>
<span id="footer-hb">tomo_heartbeat: -</span>
@@ -1384,6 +1423,69 @@ function setTheme(t) {{
setTheme(saved);
}})();
// ── Drag-and-drop card ordering ──────────────────────────────────────────
// Default order stored as card-id array; persisted in localStorage.
const CARD_ORDER_KEY = 'cardOrder';
const DEFAULT_ORDER = ['audio','recon-queue','ptycho','instrument','contacts'];
let _dragSrc = null;
function savedOrder() {{
try {{
const s = localStorage.getItem(CARD_ORDER_KEY);
return s ? JSON.parse(s) : null;
}} catch(e) {{ return null; }}
}}
function applyOrder(order) {{
const container = document.getElementById('card-container');
order.forEach(id => {{
const el = container.querySelector('[data-card-id="'+id+'"]');
if(el) container.appendChild(el);
}});
}}
function initDrag() {{
// Restore saved order (runs before first render so no visible reorder flash)
const order = savedOrder();
if(order) applyOrder(order);
document.querySelectorAll('.draggable-card').forEach(card => {{
card.setAttribute('draggable', 'true');
card.addEventListener('dragstart', e => {{
_dragSrc = card;
card.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}});
card.addEventListener('dragend', () => {{
card.classList.remove('dragging');
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
// Save new order
const ids = [...document.querySelectorAll('[data-card-id]')].map(el => el.dataset.cardId);
localStorage.setItem(CARD_ORDER_KEY, JSON.stringify(ids));
}});
card.addEventListener('dragover', e => {{
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if(card !== _dragSrc) card.classList.add('drag-over');
}});
card.addEventListener('dragleave', () => card.classList.remove('drag-over'));
card.addEventListener('drop', e => {{
e.preventDefault();
card.classList.remove('drag-over');
if(_dragSrc && _dragSrc !== card) {{
const container = document.getElementById('card-container');
const cards = [...container.querySelectorAll('.draggable-card')];
const srcIdx = cards.indexOf(_dragSrc);
const tgtIdx = cards.indexOf(card);
if(srcIdx < tgtIdx) container.insertBefore(_dragSrc, card.nextSibling);
else container.insertBefore(_dragSrc, card);
}}
}});
}});
}}
initDrag();
// ── Audio ─────────────────────────────────────────────────────────────────
// Two independent warning channels:
//