fix: cache-busting de la galerie d apercus (C9)

This commit is contained in:
2026-07-14 16:10:04 +02:00
parent dc92171f82
commit f17b1696d5
2 changed files with 69 additions and 32 deletions
+68 -31
View File
@@ -191,15 +191,16 @@ async def project_page(request):
entry = NARRATORS.get(slug)
chat = entry["chat"] if entry else []
# previews existantes ?
previews = []
pptx = info.get("dernier_pptx")
previews, cache_v = [], 0
pptx = _pptx_path(slug)
if pptx:
prev_dir = Path(pptx).parent / f"{Path(pptx).stem}_previews"
prev_dir = pptx.parent / f"{pptx.stem}_previews"
cache_v = int(pptx.stat().st_mtime)
if prev_dir.is_dir():
previews = sorted(p.name for p in
prev_dir.glob("slide-*.png"))
return render("project.html", p=info, chat_html=_chat_html(chat),
previews=previews)
previews=previews, cache_v=cache_v)
# ── routes : chat Narrator ───────────────────────────────────────────────────
@@ -285,28 +286,45 @@ async def formalise(request):
entry = _narrator(slug)
if not entry["started"]:
return HTMLResponse('<p class="err">Aucune conversation '
'Narrator — travaille le plan d\'abord.'
'</p>')
'Narrator — travaille le plan d\'abord, '
'puis sauvegarde-le.</p>')
proj = fac.Project(slug)
try:
md = entry["agent"].send(
"formalise — produis la formalisation complète slide par "
"slide (SLIDE N — titre, contenus, Notes éventuelles), "
"sans commentaire autour.")
entry["chat"].append(("system", "Formalisation demandée…"))
# Message IDENTIQUE à /formalise du facilitator (le Narrator
# produit un Markdown structuré #/##/### — PAS des "SLIDE N —
# layout" : ce sont le Designer puis l'Encoder qui les créent).
md = entry["agent"].feedback(
"Formalise maintenant le plan compact validé en Markdown "
"structuré complet pour le Designer (format #, ##, ###), "
"en développant le contenu de chaque slide. Inclus les "
"slides tiroir en les marquant [OPT-XXX].")
entry["chat"].append(("system", "Formalisation demandée."))
entry["chat"].append(("narrator", md))
# Garde : le Narrator a-t-il vraiment produit un plan ?
if md.count("#") < 3 or len(md) < 300:
return HTMLResponse(
'<p class="err">Le Narrator n\'a pas renvoyé un plan '
'formalisé exploitable (réponse trop courte ou non '
'structurée). Vérifie le plan dans le chat, ajuste, '
'puis relance.</p>')
proj.mark_formalised(md)
r = api.formalise(slug, md)
except Exception as e:
return HTMLResponse(f'<p class="err">Échec : '
f'{html.escape(str(e))}</p>')
if r.get("error"):
return HTMLResponse(f'<p class="err">'
f'{html.escape(str(r))}</p>')
return HTMLResponse(
f'<p class="err">{html.escape(str(r.get("error")))}'
f'<br><span class="hint">Le Designer/Encoder n\'a pas su '
f'transcrire ce plan. Regarde la formalisation dans le '
f'chat : elle doit lister les slides une à une.</span>'
f'</p>')
return HTMLResponse(
f'<p class="ok">Deck généré : {r["slides"]} slides — '
f'{html.escape(r["validation"])}<br>'
f'<a href="/p/{slug}/download">Télécharger le PPTX</a> · '
f'<button hx-post="/p/{slug}/preview" '
f'hx-target="#gallery" hx-indicator="#busy">Générer les '
f'aperçus</button></p>')
f'<a class="btn" href="/p/{slug}/download">Télécharger le '
f'PPTX</a> <span class="hint">puis « Générer les aperçus » '
f'ci-dessus pour les voir.</span></p>')
async def preview(request):
@@ -325,6 +343,8 @@ async def preview(request):
f'</figcaption></figure>'
for i, n in enumerate(r["slides"]))
return HTMLResponse(
f'<p class="ok">{len(r["slides"])} aperçus générés. Coche des '
f'slides pour les réviser.</p>'
f'<form hx-post="/p/{slug}/revise" hx-target="#pipeline-out" '
f'hx-indicator="#busy"><div class="gallery">{cells}</div>'
f'<input name="instruction" placeholder="Instruction de '
@@ -349,33 +369,45 @@ async def revise(request):
f'{html.escape(str(r))}</p>')
return HTMLResponse(
f'<p class="ok">Slides {r["slides_revisees"]} révisées, deck '
f'complet régénéré.<br><a href="/p/{slug}/download">'
f'Télécharger</a> · <button hx-post="/p/{slug}/preview" '
f'hx-target="#gallery" hx-indicator="#busy">Re-générer les '
f'aperçus</button></p>')
f'complet régénéré.<br><a class="btn" href="/p/{slug}/'
f'download">Télécharger</a> <span class="hint">puis '
f'« Générer les aperçus » pour revoir le deck.</span></p>')
# ── routes : fichiers ────────────────────────────────────────────────────────
def _pptx_path(slug):
"""Chemin ABSOLU du dernier PPTX. project_state.json stocke un
chemin relatif à la racine du pipeline — la webapp peut avoir un
cwd différent, on résout donc depuis BASE."""
proj = fac.Project(slug)
raw = (proj.load_state().get("dernier_pptx") or "").strip()
if not raw:
return None
p = Path(raw)
if not p.is_absolute():
p = BASE / p
return p if p.exists() else None
async def preview_png(request):
guard = require_login(request)
if guard:
return guard
slug = request.path_params["slug"]
name = request.path_params["name"]
proj = fac.Project(slug)
state = proj.load_state()
pptx = state.get("dernier_pptx")
pptx = _pptx_path(slug)
if not pptx:
return Response(status_code=404)
prev_dir = Path(pptx).parent / f"{Path(pptx).stem}_previews"
prev_dir = pptx.parent / f"{pptx.stem}_previews"
try:
png = _safe_child(prev_dir, name)
except ValueError:
return Response(status_code=403)
if not png.exists():
return Response(status_code=404)
return FileResponse(str(png), media_type="image/png")
return FileResponse(str(png), media_type="image/png",
headers={"Cache-Control": "no-store"})
async def download(request):
@@ -383,11 +415,16 @@ async def download(request):
if guard:
return guard
slug = request.path_params["slug"]
proj = fac.Project(slug)
pptx = proj.load_state().get("dernier_pptx")
if not pptx or not Path(pptx).exists():
return Response(status_code=404)
return FileResponse(pptx, filename=Path(pptx).name)
pptx = _pptx_path(slug)
if not pptx:
return HTMLResponse(
'<p class="err">Aucun PPTX disponible pour ce projet — '
'lance « Formaliser le deck » d\'abord.</p>',
status_code=404)
return FileResponse(
str(pptx), filename=pptx.name,
media_type="application/vnd.openxmlformats-officedocument."
"presentationml.presentation")
async def health(request):