dashboard — thumb 48x27, step live, spin busy, live thumbnail refresh

- dispatcher: col step ajoutee (migration). set a chaque phase:
  scp N/M, ffmpeg N/M, trimming hors-eau, reconstruct demo.py
- _refresh_thumbnail() scp la DERNIERE frame extraite toutes les ~15s
  pendant ffmpeg pour que le preview colle a la progression live
- template: cache-bust thumbnail via ?t=mtime, step affiche sous
  progress bar, thumb revenue a 48x27
- CSS: .badge.busy -> animation spin (rotation infinie) au lieu de
  juste une couleur, .step-text italique mute
This commit is contained in:
Flag
2026-04-22 23:00:11 +00:00
parent f13d27b997
commit 194c94b872
5 changed files with 56 additions and 28 deletions

View File

@@ -195,7 +195,15 @@ def _build_acquisitions():
d["gp_label"] = gp_label.get((j["acquisition_id"], j["auv"], j["gopro_serial"]), "?")
d["video_duration_fmt"] = _fmt_dur(int(j["video_duration_s"] or 0)) if (j["video_duration_s"] or 0) > 0 else ""
d["trimmed_total"] = (j["trimmed_head"] or 0) + (j["trimmed_tail"] or 0)
d["has_thumbnail"] = (DB_PATH.parent / "thumbnails" / f"job_{j['id']}.jpg").exists()
thumb_path = DB_PATH.parent / "thumbnails" / f"job_{j['id']}.jpg"
d["has_thumbnail"] = thumb_path.exists()
# Bust the browser cache on the mtime so the preview refreshes as the dispatcher re-copies it.
d["thumb_ts"] = int(thumb_path.stat().st_mtime) if d["has_thumbnail"] else 0
# Try the new column; fall back silently on old rows.
try:
d["step"] = j["step"]
except (KeyError, IndexError):
d["step"] = None
# Mask the viser link when the demo.py that was serving it has since died.
if j["status"] == "done" and j["viser_url"] and not _viser_alive(j["viser_url"]):
d["viser_url"] = None

View File

@@ -126,9 +126,9 @@ code { background: rgba(255,255,255,0.05); padding: 0 0.25rem; border-radius: 3p
.jobs-table tbody tr:hover { background: rgba(255,255,255,0.02); }
.col-status { width: 24px; text-align: center; }
.col-thumb { width: 80px; }
.col-thumb img { width: 72px; height: 40px; object-fit: cover; border-radius: 4px; display: block; }
.thumb-placeholder { display: inline-block; width: 72px; height: 40px; background: rgba(255,255,255,0.04); border-radius: 4px; }
.col-thumb { width: 56px; }
.col-thumb img { width: 48px; height: 27px; object-fit: cover; border-radius: 3px; display: block; }
.thumb-placeholder { display: inline-block; width: 48px; height: 27px; background: rgba(255,255,255,0.04); border-radius: 3px; }
.col-id { font-family: ui-monospace, monospace; color: var(--muted); font-size: 0.75rem; }
.col-auv { min-width: 110px; }
.col-seg { font-variant-numeric: tabular-nums; color: var(--muted); }
@@ -157,3 +157,7 @@ code { background: rgba(255,255,255,0.05); padding: 0 0.25rem; border-radius: 3p
.stitch-list { list-style: none; padding: 0; margin: 0; }
.stitch-item { display: flex; align-items: center; gap: 0.5rem; padding: 3px 0; font-size: 0.82rem; }
.stitch-item .err-line { flex-basis: 100%; margin-left: 26px; color: var(--err, #d44); font-size: 0.72rem; }
.badge.busy { display: inline-block; animation: spin 1.2s linear infinite; transform-origin: 50% 50%; }
.progress-wrap { display: flex; align-items: center; gap: 6px; }
.step-text { margin-top: 2px; color: var(--muted); font-size: 0.7rem; font-style: italic; font-variant-numeric: tabular-nums; }

View File

@@ -35,7 +35,7 @@
{% else %}<span class="badge muted"></span>{% endif %}
</td>
<td class="col-thumb">
{% if j.has_thumbnail %}<img src="jobs/{{ j.id }}/thumbnail" alt="" loading="lazy">{% else %}<span class="thumb-placeholder"></span>{% endif %}
{% if j.has_thumbnail %}<img src="jobs/{{ j.id }}/thumbnail?t={{ j.thumb_ts }}" alt="" loading="lazy">{% else %}<span class="thumb-placeholder"></span>{% endif %}
</td>
<td class="col-id">#{{ j.id }}</td>
<td class="col-auv"><strong>{{ j.auv }}</strong> · <span title="{{ j.gopro_serial }}">{{ j.gp_label }}</span></td>
@@ -45,8 +45,11 @@
<td class="col-trim">{% if j.trimmed_total %}{{ j.trimmed_total }}{% else %}—{% endif %}</td>
<td class="col-progress">
{% if j.status in ('extracting','running') %}
<span class="prog-bar"><span class="prog-fill" style="width:{{ j.progress }}%"></span></span>
<span class="prog-text">{{ j.progress }}%</span>
<div class="progress-wrap">
<span class="prog-bar"><span class="prog-fill" style="width:{{ j.progress }}%"></span></span>
<span class="prog-text">{{ j.progress }}%</span>
</div>
{% if j.step %}<div class="step-text">{{ j.step }}</div>{% endif %}
{% elif j.status == 'done' and j.viser_url %}
<a class="btn-viser" href="{{ j.viser_url }}" target="_blank" rel="noopener">viser ↗</a>
{% elif j.status == 'skipped' %}