Admin backoffice : rubriques General/Evenement + videos situation + surprise + rembobinage ruban

- Reorganisation menu admin en 2 rubriques (General / Evenement)
- Destinations deplace dans Evenement
- Nouveau panneau Videos : upload par situation (montage, depannage, bourrage, rechargement)
- Nouveau panneau Surprise : photo/video affichee ~1s avant capture pour provoquer sourire
- Toggle rembobinage ruban Mitsubishi (StpiDecklist) pour economiser le ruban sur petits formats
- Historique email cloisonne par evenement
- Memoire projet consolidee dans memoire.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 12:31:27 +02:00
parent e5e9f763ab
commit 44a9992557
10 changed files with 731 additions and 32 deletions

View File

@@ -195,28 +195,47 @@ async def tache_spool_demarrage():
FICHIER_EMAILS = RACINE / "data" / "emails_history.json"
def _event_id_actif() -> str:
return charger_config().get("evenement", {}).get("event_id") or "_sans_evenement"
def _charger_toutes_historiques() -> dict:
if not FICHIER_EMAILS.exists():
return {}
try:
with open(FICHIER_EMAILS, "r", encoding="utf-8") as f:
data = json.load(f)
# Compatibilite avec l'ancien format (liste globale non cloisonnee)
if isinstance(data, list):
return {"_sans_evenement": data}
return data
except (json.JSONDecodeError, OSError):
return {}
def _sauvegarder_email_historique(email: str):
historique = charger_emails_historique()
toutes = _charger_toutes_historiques()
event_id = _event_id_actif()
historique = toutes.setdefault(event_id, [])
email_lower = email.lower().strip()
if email_lower not in historique:
historique.append(email_lower)
try:
with open(FICHIER_EMAILS, "w", encoding="utf-8") as f:
json.dump(historique, f, ensure_ascii=False)
json.dump(toutes, f, ensure_ascii=False)
except OSError:
pass
def charger_emails_historique() -> list:
if not FICHIER_EMAILS.exists():
return []
try:
with open(FICHIER_EMAILS, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return []
return _charger_toutes_historiques().get(_event_id_actif(), [])
def effacer_emails_historique():
if FICHIER_EMAILS.exists():
FICHIER_EMAILS.unlink()
toutes = _charger_toutes_historiques()
toutes.pop(_event_id_actif(), None)
try:
with open(FICHIER_EMAILS, "w", encoding="utf-8") as f:
json.dump(toutes, f, ensure_ascii=False)
except OSError:
pass

View File

@@ -1072,19 +1072,28 @@ async def api_obtenir_evenement(event_id: str):
return event
def _event_id_actif() -> str | None:
return charger_config().get("evenement", {}).get("event_id")
@app.put("/api/evenements/{event_id}")
async def api_modifier_evenement(event_id: str, donnees: dict):
event = modifier_evenement(event_id, donnees)
if not event:
return JSONResponse({"erreur": "Evenement introuvable"}, status_code=404)
if event_id == _event_id_actif():
await diffuser_ws({"type": "config_maj", "config": charger_config()})
return event
@app.delete("/api/evenements/{event_id}")
async def api_supprimer_evenement(event_id: str):
etait_actif = event_id == _event_id_actif()
ok = supprimer_evenement(event_id)
if not ok:
return JSONResponse({"erreur": "Evenement introuvable"}, status_code=404)
if etait_actif:
await diffuser_ws({"type": "config_maj", "config": charger_config()})
return {"succes": True}
@@ -1093,6 +1102,7 @@ async def api_activer_evenement(event_id: str):
event = activer_evenement(event_id)
if not event:
return JSONResponse({"erreur": "Evenement introuvable"}, status_code=404)
await diffuser_ws({"type": "config_maj", "config": charger_config()})
return event
@@ -1113,6 +1123,8 @@ async def api_set_cadre_event(event_id: str, format_papier: str, donnees: dict):
mode = donnees.get("mode", "aucun")
cadre = donnees.get("cadre")
set_cadre_event(event_id, format_papier, mode, cadre)
if event_id == _event_id_actif():
await diffuser_ws({"type": "config_maj", "config": charger_config()})
return {"succes": True}
@@ -1384,6 +1396,87 @@ async def _lancer_photostation():
FLAG_PHOTOSTATION.unlink(missing_ok=True)
# --- API Videos de situation ---
DOSSIER_VIDEOS = RACINE / "data" / "videos"
DOSSIER_VIDEOS.mkdir(parents=True, exist_ok=True)
@app.get("/api/videos")
async def api_videos():
videos = {}
for f in DOSSIER_VIDEOS.iterdir():
if f.is_file() and f.suffix.lower() in ('.mp4', '.webm', '.mov'):
videos[f.stem] = f.name
return {"videos": videos}
@app.post("/api/videos/upload")
async def api_upload_video(video: UploadFile = File(...), situation: str = ""):
if not situation:
return JSONResponse({"erreur": "situation requise"}, 400)
ext = Path(video.filename).suffix.lower() or '.mp4'
dest = DOSSIER_VIDEOS / f"{situation}{ext}"
for old in DOSSIER_VIDEOS.glob(f"{situation}.*"):
old.unlink()
with open(dest, "wb") as f:
while chunk := await video.read(1024 * 1024):
f.write(chunk)
return {"ok": True, "fichier": dest.name}
@app.delete("/api/videos/{situation}")
async def api_delete_video(situation: str):
for f in DOSSIER_VIDEOS.glob(f"{situation}.*"):
f.unlink()
return {"ok": True}
@app.get("/api/videos/{situation}/stream")
async def api_stream_video(situation: str):
for f in DOSSIER_VIDEOS.glob(f"{situation}.*"):
media = "video/mp4" if f.suffix == ".mp4" else "video/webm"
return FileResponse(f, media_type=media)
return JSONResponse({"erreur": "video introuvable"}, 404)
# --- API Surprise ---
DOSSIER_SURPRISE = RACINE / "data" / "surprise"
DOSSIER_SURPRISE.mkdir(parents=True, exist_ok=True)
@app.post("/api/surprise/upload")
async def api_upload_surprise(media: UploadFile = File(...)):
for old in DOSSIER_SURPRISE.iterdir():
old.unlink()
ext = Path(media.filename).suffix.lower() or '.jpg'
dest = DOSSIER_SURPRISE / f"surprise{ext}"
with open(dest, "wb") as f:
while chunk := await media.read(1024 * 1024):
f.write(chunk)
fichier_type = "video" if ext in ('.mp4', '.webm', '.mov') else "photo"
mettre_a_jour_config({"surprise": {"fichier": dest.name, "type": fichier_type}})
return {"ok": True, "fichier": dest.name, "type": fichier_type}
@app.delete("/api/surprise/media")
async def api_delete_surprise():
for f in DOSSIER_SURPRISE.iterdir():
f.unlink()
mettre_a_jour_config({"surprise": {"fichier": None}})
return {"ok": True}
@app.get("/api/surprise/media")
async def api_get_surprise():
for f in DOSSIER_SURPRISE.iterdir():
if f.is_file():
ext = f.suffix.lower()
if ext in ('.mp4', '.webm', '.mov'):
return FileResponse(f, media_type="video/mp4")
return FileResponse(f, media_type="image/jpeg")
return JSONResponse({"erreur": "aucun media"}, 404)
# --- API Systeme ---
@app.post("/api/systeme/redemarrer")

View File

@@ -299,6 +299,8 @@ def imprimer(
}
time.sleep(1)
rembobinage = conf_imp.get("rembobinage_ruban", False)
try:
for tentative in range(1, 4):
cmd = [
@@ -307,8 +309,10 @@ def imprimer(
"-n", str(copies),
"-o", f"PageSize={page_size}",
"-o", "StpiShrinkOutput=Crop",
str(chemin_print),
]
if rembobinage:
cmd.extend(["-o", "StpiDecklist=true"])
cmd.append(str(chemin_print))
code, out, err = _run(cmd, timeout=30)