dashboard — colonnes par acquisition, checklist verticale hierarchique
This commit is contained in:
92
app/main.py
92
app/main.py
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user