dashboard — colonnes par acquisition, checklist verticale hierarchique

This commit is contained in:
2026-04-21 10:08:38 +00:00
parent 47b082fd9d
commit 3b005a4994
3 changed files with 174 additions and 75 deletions

View File

@@ -5,10 +5,47 @@ import json
import os
import sqlite3
from contextlib import asynccontextmanager, closing
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def _fmt_dur(seconds: float) -> str:
if seconds is None or seconds < 0:
return ""
s = int(seconds)
if s < 60:
return f"{s}s"
m, s = divmod(s, 60)
if m < 60:
return f"{m}m{s:02d}s" if s else f"{m}m"
h, m = divmod(m, 60)
if h < 24:
return f"{h}h{m:02d}m" if m else f"{h}h"
d, h = divmod(h, 24)
return f"{d}d{h:02d}h"
def _parse_ts(s: str | None) -> datetime | None:
if not s:
return None
try:
return datetime.fromisoformat(s.replace("Z", "+00:00"))
except Exception:
return None
def _job_duration_s(job: sqlite3.Row) -> int:
start = _parse_ts(job["started_at"])
end = _parse_ts(job["finished_at"]) or datetime.now(timezone.utc)
if not start:
return 0
if start.tzinfo is None:
start = start.replace(tzinfo=timezone.utc)
if end.tzinfo is None:
end = end.replace(tzinfo=timezone.utc)
return int((end - start).total_seconds())
from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
@@ -79,19 +116,41 @@ templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
def _build_acquisitions():
with closing(db()) as conn:
acqs = conn.execute(
"SELECT * FROM acquisitions ORDER BY created_at DESC"
).fetchall()
jobs = conn.execute(
"SELECT * FROM jobs ORDER BY auv, gopro_serial, segment_label"
).fetchall()
by_acq: dict[int, list[dict]] = {}
by_acq_total: dict[int, int] = {}
for j in jobs:
d = dict(j)
dur_s = _job_duration_s(j)
d["_duration"] = _fmt_dur(dur_s)
by_acq.setdefault(j["acquisition_id"], []).append(d)
by_acq_total[j["acquisition_id"]] = by_acq_total.get(j["acquisition_id"], 0) + dur_s
return [
{
"id": acq["id"],
"name": acq["name"],
"source_path": acq["source_path"],
"jobs": by_acq.get(acq["id"], []),
"total_duration": _fmt_dur(by_acq_total.get(acq["id"], 0)),
}
for acq in acqs
]
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
with closing(db()) as conn:
jobs = conn.execute("""
SELECT j.*, a.name AS acquisition_name
FROM jobs j
LEFT JOIN acquisitions a ON a.id = j.acquisition_id
ORDER BY j.created_at DESC
LIMIT 200
""").fetchall()
acquisitions = _build_acquisitions()
return templates.TemplateResponse("index.html", {
"request": request,
"jobs": jobs,
"acquisitions": acquisitions,
"workers": WORKERS,
})
@@ -105,15 +164,10 @@ async def list_jobs():
@app.get("/partials/jobs", response_class=HTMLResponse)
async def partial_jobs(request: Request):
with closing(db()) as conn:
jobs = conn.execute("""
SELECT j.*, a.name AS acquisition_name
FROM jobs j
LEFT JOIN acquisitions a ON a.id = j.acquisition_id
ORDER BY j.created_at DESC
LIMIT 200
""").fetchall()
return templates.TemplateResponse("_jobs_table.html", {"request": request, "jobs": jobs})
return templates.TemplateResponse(
"_jobs_table.html",
{"request": request, "acquisitions": _build_acquisitions()},
)
@app.get("/partials/monitor", response_class=HTMLResponse)