Backend: galerie session /s/{code} + upload API

Ajoute deux routes FastAPI :
- GET /s/{code} → galerie HTML mobile avec auto-refresh 15s
- POST /api/sessions/{code}/upload → stockage photo dans session_{code}/

Corrige layout admin : titre sm au lieu de lg, compteur sans chevauchement.
Wizard step 2 : code affiché en lg (96px) au lieu de xxl (300px).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 14:33:35 +02:00
parent a1a0ce0503
commit 8ae42e1bc6
2 changed files with 1972 additions and 1 deletions

View File

@@ -10,7 +10,7 @@ from contextlib import asynccontextmanager
import cv2
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File
from fastapi.responses import FileResponse, JSONResponse
from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from backend.config import (
@@ -155,6 +155,63 @@ async def page_principale():
return FileResponse(str(RACINE / "frontend" / "index.html"))
@app.post("/api/sessions/{code}/upload")
async def api_session_upload(code: str, fichier: UploadFile = File(...)):
"""Upload d'une photo dans une session identifiée par code à 9 chiffres."""
if not code.isdigit() or len(code) != 9:
return JSONResponse({"erreur": "Code invalide"}, status_code=400)
dossier = DOSSIER_PHOTOS / f"session_{code}"
dossier.mkdir(parents=True, exist_ok=True)
dest = dossier / fichier.filename
dest.write_bytes(await fichier.read())
return {"ok": True, "fichier": fichier.filename}
@app.get("/s/{code}", response_class=HTMLResponse)
async def galerie_session(code: str):
"""Galerie mobile pour une session photo (QR code)."""
if not code.isdigit() or len(code) != 9:
return HTMLResponse("<h1>Code invalide</h1>", status_code=404)
dossier = DOSSIER_PHOTOS / f"session_{code}"
cfg = charger_config()
nom_event = cfg.get("evenement", {}).get("nom", "Photobooth")
photos = sorted(dossier.glob("*.jpg"), key=lambda p: p.stat().st_mtime, reverse=True) if dossier.exists() else []
items = ""
for p in photos:
items += f'<div class="photo"><img src="/data/photos/session_{code}/{p.name}" loading="lazy" onclick="agrandir(this)"></div>\n'
if not items:
items = '<p class="vide">Les photos apparaîtront ici après chaque prise 📷</p>'
html = f"""<!DOCTYPE html>
<html lang="fr"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{nom_event} — Galerie</title>
<style>
*{{margin:0;padding:0;box-sizing:border-box}}
body{{background:#0a0a1a;color:#fff;font-family:system-ui,sans-serif;min-height:100vh}}
header{{background:#111;padding:1rem;text-align:center;border-bottom:2px solid #e91e63}}
header h1{{font-size:1.4rem;color:#e91e63}}
header p{{font-size:.85rem;color:#aaa;margin-top:.3rem}}
.grille{{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:6px;padding:8px}}
.photo img{{width:100%;aspect-ratio:3/2;object-fit:cover;border-radius:6px;cursor:pointer;transition:transform .2s}}
.photo img:active{{transform:scale(.97)}}
.vide{{text-align:center;padding:3rem;color:#666;font-size:1.1rem}}
.lightbox{{display:none;position:fixed;inset:0;background:#000d;align-items:center;justify-content:center;z-index:99}}
.lightbox.open{{display:flex}}
.lightbox img{{max-width:95vw;max-height:95vh;border-radius:8px}}
.lightbox::after{{content:"✕";position:absolute;top:1rem;right:1.2rem;font-size:2rem;cursor:pointer;color:#fff}}
</style></head><body>
<header><h1>📷 {nom_event}</h1><p>Code session : {code}</p></header>
<div class="grille">{items}</div>
<div class="lightbox" id="lb" onclick="this.classList.remove('open')"><img id="lb-img" src=""></div>
<script>
function agrandir(el){{document.getElementById('lb-img').src=el.src;document.getElementById('lb').classList.add('open')}}
// Rafraîchissement auto toutes les 15s
setTimeout(()=>location.reload(), 15000)
</script>
</body></html>"""
return HTMLResponse(html)
# --- API Config ---
@app.get("/api/config")

1914
photobooth_pygame.py Normal file

File diff suppressed because it is too large Load Diff