diff --git a/app/static/style.css b/app/static/style.css index 794a57f..92c5260 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -174,6 +174,8 @@ code { background: rgba(255,255,255,0.05); padding: 0 0.25rem; border-radius: 3p .docs-link:hover { background: #2d2f31; } .btn-glb, .btn-ply-dl { display: inline-block; text-decoration: none; padding: 3px 10px; border: 1px solid #8bc34a; border-radius: 3px; color: #8bc34a; font-size: 0.72rem; background: transparent; cursor: pointer; font-family: inherit; } .btn-glb:hover, .btn-ply-dl:hover { background: #8bc34a; color: #000; } +.btn-qc { display: inline-block; text-decoration: none; padding: 3px 10px; border: 1px solid #29b6f6; border-radius: 3px; color: #29b6f6; font-size: 0.72rem; background: transparent; cursor: pointer; font-family: inherit; } +.btn-qc:hover { background: #29b6f6; color: #000; } /* Section évolutions */ #evolutions { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--border, #333); } @@ -192,3 +194,7 @@ code { background: rgba(255,255,255,0.05); padding: 0 0.25rem; border-radius: 3p .pipeline-box ol { margin: 0; padding-left: 1.4rem; } .pipeline-box li { padding: 0.18rem 0; font-size: 0.78rem; color: var(--muted, #888); } .pipeline-box code { font-size: 0.73rem; background: rgba(255,255,255,0.07); padding: 1px 5px; border-radius: 3px; color: #cef; } + +.viewer-btn { background: #1a3a2a; color: #4ade80; border: 1px solid #4ade80; border-radius: 3px; padding: 2px 8px; cursor: pointer; font-size: 0.8rem; } +.viewer-btn:hover { background: #4ade80; color: #0a1a10; } +.viewer-btn:disabled { opacity: 0.5; cursor: wait; } diff --git a/app/templates/index.html b/app/templates/index.html index 351c94e..c51659c 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -75,16 +75,26 @@ document.addEventListener('click', async (e) => { const btn = e.target.closest('.viewer-btn'); if (!btn) return; e.preventDefault(); - const url = btn.dataset.viewUrl; + const liveUrl = btn.dataset.liveUrl; + const viewUrl = btn.dataset.viewUrl; btn.textContent = '…'; btn.disabled = true; - try { - const res = await fetch(url, { method: 'POST' }); - const data = await res.json(); - if (res.ok && data.url) window.open(data.url, '_blank'); - else alert(data.detail || 'Erreur lancement viewer'); - } catch (err) { alert('Erreur réseau: ' + err); } - btn.textContent = 'viser'; + let url = null; + if (liveUrl) { + try { + const res = await fetch(liveUrl, { method: 'POST' }); + if (res.ok) { const d = await res.json(); url = d.url; } + } catch {} + } + if (!url && viewUrl) { + try { + const res = await fetch(viewUrl, { method: 'POST' }); + if (res.ok) { const d = await res.json(); url = d.url; } + else { const d = await res.json(); alert(d.detail || 'Erreur lancement viewer'); } + } catch (err) { alert('Erreur réseau: ' + err); } + } + if (url) window.open(url, '_blank'); + btn.textContent = 'viser ↗'; btn.disabled = false; }); diff --git a/docker-compose.yml b/docker-compose.yml index ec94b89..e37e9d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,8 @@ services: ports: - "3849:8000" volumes: - - /home/floppyrj45/cosma-qc-data:/var/lib/cosma-qc - - /home/floppyrj45/.ssh:/ssh-in:ro + - /home/cosma/cosma-qc-data:/var/lib/cosma-qc + - /home/cosma/.ssh:/ssh-in:ro environment: COSMA_QC_WORKERS: | [ diff --git a/scripts/dispatcher.py b/scripts/dispatcher.py index a8ba997..6fc2484 100644 --- a/scripts/dispatcher.py +++ b/scripts/dispatcher.py @@ -806,6 +806,43 @@ def run_one_stitch(stitch: sqlite3.Row): finished_at=_now_iso()) + +ML_STACK_HOST = "192.168.0.84" +ML_STACK_ALIAS = "ml-stack" +_PRE_DECIMATE = "/root/cosma-nav/scripts/pre_decimate.py" +_ARCHIVE_SH = "/root/cosma-nav/scripts/archive_job.sh" + + +def _post_job_qc_sync(job_id: int, worker: dict, frames_dir: str): + """Fire-and-forget: decimate PLY + archive to NAS after a successful job. + Only runs when the worker is ml-stack (.84) where the scripts live. + """ + if worker["host"] != ML_STACK_HOST: + print(f" post_job #{job_id}: worker={worker['host']} != ml-stack, skip QC sync", flush=True) + return + alias = ML_STACK_ALIAS + parent = str(Path(frames_dir).parent) + pre_cmd = ( + f"python3 {_PRE_DECIMATE} {job_id} " + f"--frames-dir {shlex.quote(parent)} " + f"> /tmp/pre_decimate_{job_id}.log 2>&1" + ) + rc_pre, _, _ = ssh(alias, pre_cmd, timeout=600) + if rc_pre == 0: + print(f" post_job #{job_id}: pre_decimate OK", flush=True) + else: + tail = ssh(alias, f"tail -5 /tmp/pre_decimate_{job_id}.log")[1] + print(f" post_job #{job_id}: pre_decimate FAIL: {tail[:300]}", flush=True) + + arc_cmd = f"bash {_ARCHIVE_SH} {job_id} > /tmp/archive_{job_id}.log 2>&1" + rc_arc, _, _ = ssh(alias, arc_cmd, timeout=600) + if rc_arc == 0: + print(f" post_job #{job_id}: archive OK", flush=True) + else: + tail = ssh(alias, f"tail -5 /tmp/archive_{job_id}.log")[1] + print(f" post_job #{job_id}: archive FAIL: {tail[:300]}", flush=True) + + def run_one(job: sqlite3.Row) -> bool: """Returns True if a worker was picked and work started.""" job_id = job["id"] @@ -825,6 +862,7 @@ def run_one(job: sqlite3.Row) -> bool: set_status(job_id, status="done", viser_url=viser_url, ply_path=ply_path, progress=100, log_tail=log, finished_at=_now_iso()) _maybe_create_per_auv_stitch(job_id) + threading.Thread(target=_post_job_qc_sync, args=(job_id, worker, frames_dir), daemon=True).start() except Exception as e: # do_extract raises "skipped_short" after flagging status='skipped' — don't override. if "skipped_short" not in str(e):