diff --git a/app/main.py b/app/main.py index ecc94a8..0254243 100644 --- a/app/main.py +++ b/app/main.py @@ -320,12 +320,17 @@ async def partial_pipeline(request: Request): counts = {} for j in jobs: counts[j["status"]] = counts.get(j["status"], 0) + 1 + auvs: list[str] = [] + for j in jobs: + if j["auv_id"] and j["auv_id"] not in auvs: + auvs.append(j["auv_id"]) data["missions"].append({ "id": m["id"], "name": m["name"], "status": m["status"], "jobs": [dict(j) for j in jobs], "counts": counts, + "auvs": auvs, }) except Exception as e: data["error"] = str(e)[:200] @@ -571,6 +576,86 @@ async def live_job(job_id: int): return {"url": row["viser_url"]} +VISER_AUV_BASE = 9300 +PIPELINE_DATA_BASE = Path(os.environ.get("COSMA_PIPELINE_DATA", "/cosma-pipeline/data")) + + +@app.post("/pipeline/missions/{mission_id}/auvs/{auv_id}/view") +async def view_auv(mission_id: int, auv_id: str): + """Launch viser showing all PLYs for one AUV from a mission.""" + if not PIPELINE_DB.exists(): + raise HTTPException(404, "state.db introuvable") + import hashlib + import shutil + import tempfile + + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: + tmp_path = tmp.name + shutil.copy2(str(PIPELINE_DB), tmp_path) + try: + with sqlite3.connect(tmp_path) as conn: + conn.row_factory = sqlite3.Row + m = conn.execute( + "SELECT name FROM missions WHERE id=?", (mission_id,) + ).fetchone() + finally: + try: + os.unlink(tmp_path) + except Exception: + pass + if not m: + raise HTTPException(404, "mission introuvable") + mission_name = m["name"] + ply_dir_local = PIPELINE_DATA_BASE / mission_name / "ply" / auv_id + if not ply_dir_local.exists(): + return JSONResponse( + {"ok": False, "error": f"PLY dir {ply_dir_local} pas encore (stitch pas done)"}, + status_code=409, + ) + + h = int(hashlib.md5(f"{mission_id}-{auv_id}".encode()).hexdigest()[:6], 16) + port = VISER_AUV_BASE + (h % 100) + worker = WORKERS[1] if len(WORKERS) > 1 else WORKERS[0] + alias = worker["ssh_alias"] + host = worker["host"] + worker_dir = f"/tmp/cosma-viser-auv/{mission_name}/{auv_id}" + + rsync = await asyncio.create_subprocess_exec( + "rsync", "-az", "--delete", + "-e", "ssh -o BatchMode=yes -o StrictHostKeyChecking=no", + f"{ply_dir_local}/", f"{alias}:{worker_dir}/", + stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE, + ) + _, err = await rsync.communicate() + if rsync.returncode != 0: + raise HTTPException(500, f"rsync failed: {err.decode()[:200]}") + + local_script = Path(__file__).parent.parent / "scripts" / "viser_auv.py" + scp = await asyncio.create_subprocess_exec( + "scp", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", + str(local_script), f"{alias}:/tmp/viser_auv.py", + stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE, + ) + _, err = await scp.communicate() + if scp.returncode != 0: + raise HTTPException(500, f"scp failed: {err.decode()[:200]}") + + venv_py = f"{worker.get('lingbot_path', '/root/ai-video/lingbot-map')}/.venv/bin/python" + launch_cmd = ( + f"pkill -f 'viser_auv.py.*--port {port}' 2>/dev/null ; sleep 1 ; " + f"setsid nohup {venv_py} /tmp/viser_auv.py --ply-dir {worker_dir} --port {port} " + f"/tmp/viser_auv_{port}.log 2>&1 & disown ; sleep 0.3" + ) + launch = await asyncio.create_subprocess_exec( + "ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", alias, launch_cmd, + stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE, + ) + await launch.communicate() + await asyncio.sleep(4) + + return {"ok": True, "url": f"http://{host}:{port}/", "auv_id": auv_id, "port": port} + + @app.post("/stitches/{stitch_id}/view") async def view_stitch(stitch_id: int): with closing(db()) as conn: diff --git a/app/static/style.css b/app/static/style.css index cd86bdc..e3d3920 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -220,3 +220,12 @@ code { background: rgba(255,255,255,0.05); padding: 0 0.25rem; border-radius: 3p .status-degraded, .pj-badge.status-degraded { color: var(--warn); background: rgba(245,197,24,0.1); } .status-error, .pj-badge.status-error { color: var(--err); background: rgba(255,92,122,0.1); } .status-ingested, .pm-status.status-ingested { color: var(--accent); background: rgba(95,208,255,0.12); } + +/* AUV viser buttons (per-mission) */ +.pm-auvs { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 0.3rem 0 0.5rem; align-items: center; } +.pm-auvs-label { color: var(--muted, #888); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; } +.btn-viser-auv { font-size: 0.72rem; padding: 2px 8px; background: transparent; + border: 1px solid var(--accent, #4af); color: var(--accent, #4af); border-radius: 3px; + cursor: pointer; font-family: inherit; } +.btn-viser-auv:hover { background: var(--accent, #4af); color: #062036; } +.btn-viser-auv:disabled { opacity: 0.5; cursor: wait; } diff --git a/app/templates/_pipeline.html b/app/templates/_pipeline.html index c3d5d20..d74e7bf 100644 --- a/app/templates/_pipeline.html +++ b/app/templates/_pipeline.html @@ -16,6 +16,16 @@ {% if m.counts.get('error') %}{{ m.counts.error }} error{% endif %} + {% if m.auvs %} +
| AUV | Segment | Stage | Status | Worker | Duree |
|---|