Ajoute vue détail par passage (clip + galerie frames)

- Sauvegarde le clip MP4 et les top 10 frames par événement dans /data/events/{id}/
- Nouvelle route GET /event/{id} avec page détail : info, vignette interactive, lecteur vidéo, galerie frames
- Les cartes de l'index sont maintenant cliquables
- Migration DB : ajout colonne clip_path sur les BDD existantes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
perco
2026-06-02 15:52:12 +02:00
co-authored by Claude Sonnet 4.6
parent 8507987ea9
commit 528a158c42
6 changed files with 261 additions and 83 deletions
+32 -5
View File
@@ -1,9 +1,10 @@
import os
import glob
import logging
import threading
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Query
from fastapi.responses import HTMLResponse, FileResponse
from fastapi import FastAPI, Request, Query, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from datetime import datetime
@@ -13,8 +14,10 @@ import watcher
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
DATA_DIR = os.environ.get("SNAPSHOTS_DIR", "/data/snapshots")
os.makedirs(DATA_DIR, exist_ok=True)
SNAPSHOTS_DIR = os.environ.get("SNAPSHOTS_DIR", "/data/snapshots")
EVENTS_DIR = os.environ.get("EVENTS_DIR", "/data/events")
os.makedirs(SNAPSHOTS_DIR, exist_ok=True)
os.makedirs(EVENTS_DIR, exist_ok=True)
@asynccontextmanager
@@ -26,7 +29,8 @@ async def lifespan(app: FastAPI):
app = FastAPI(lifespan=lifespan)
app.mount("/snapshots", StaticFiles(directory="/data/snapshots"), name="snapshots")
app.mount("/snapshots", StaticFiles(directory=SNAPSHOTS_DIR), name="snapshots")
app.mount("/events", StaticFiles(directory=EVENTS_DIR), name="events")
templates = Jinja2Templates(directory="/app/templates")
@@ -74,6 +78,29 @@ async def index(
})
@app.get("/event/{event_id}", response_class=HTMLResponse)
async def event_detail(request: Request, event_id: str):
ev = database.get_event(event_id)
if not ev:
raise HTTPException(status_code=404, detail="Événement introuvable")
ev["time_str"] = ts_to_str(ev["start_time"])
# List frames for this event
event_dir = os.path.join(EVENTS_DIR, event_id)
frame_paths = sorted(glob.glob(os.path.join(event_dir, "frame_*.jpg")))
frames = [f"events/{event_id}/frame_{i+1:04d}.jpg" for i in range(len(frame_paths))]
has_clip = ev.get("clip_path") and os.path.exists(os.path.join("/data", ev["clip_path"]))
return templates.TemplateResponse("event_detail.html", {
"request": request,
"ev": ev,
"frames": frames,
"has_clip": has_clip,
})
@app.get("/api/events")
async def api_events(
page: int = Query(1, ge=1),