movable cards and more
This commit was merged in pull request #174.
This commit is contained in:
@@ -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">⋮⋮</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">⋮⋮</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">⋮⋮</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">⋮⋮</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">⋮⋮</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:
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user