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
+1 -1
View File
@@ -60,7 +60,7 @@
{% if previews %} {% if previews %}
<div class="gallery"> <div class="gallery">
{% for n in previews %} {% for n in previews %}
<figure><img src="/p/{{ p.slug }}/previews/{{ n }}" loading="lazy"> <figure><img src="/p/{{ p.slug }}/previews/{{ n }}?v={{ cache_v }}" loading="lazy">
<figcaption>{{ n }}</figcaption></figure> <figcaption>{{ n }}</figcaption></figure>
{% endfor %} {% endfor %}
</div> </div>
+68 -31
View File
@@ -191,15 +191,16 @@ async def project_page(request):
entry = NARRATORS.get(slug) entry = NARRATORS.get(slug)
chat = entry["chat"] if entry else [] chat = entry["chat"] if entry else []
# previews existantes ? # previews existantes ?
previews = [] previews, cache_v = [], 0
pptx = info.get("dernier_pptx") pptx = _pptx_path(slug)
if pptx: 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(): if prev_dir.is_dir():
previews = sorted(p.name for p in previews = sorted(p.name for p in
prev_dir.glob("slide-*.png")) prev_dir.glob("slide-*.png"))
return render("project.html", p=info, chat_html=_chat_html(chat), return render("project.html", p=info, chat_html=_chat_html(chat),
previews=previews) previews=previews, cache_v=cache_v)
# ── routes : chat Narrator ─────────────────────────────────────────────────── # ── routes : chat Narrator ───────────────────────────────────────────────────
@@ -285,28 +286,45 @@ async def formalise(request):
entry = _narrator(slug) entry = _narrator(slug)
if not entry["started"]: if not entry["started"]:
return HTMLResponse('<p class="err">Aucune conversation ' return HTMLResponse('<p class="err">Aucune conversation '
'Narrator — travaille le plan d\'abord.' 'Narrator — travaille le plan d\'abord, '
'</p>') 'puis sauvegarde-le.</p>')
proj = fac.Project(slug)
try: try:
md = entry["agent"].send( # Message IDENTIQUE à /formalise du facilitator (le Narrator
"formalise — produis la formalisation complète slide par " # produit un Markdown structuré #/##/### — PAS des "SLIDE N —
"slide (SLIDE N — titre, contenus, Notes éventuelles), " # layout" : ce sont le Designer puis l'Encoder qui les créent).
"sans commentaire autour.") md = entry["agent"].feedback(
entry["chat"].append(("system", "Formalisation demandée…")) "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) r = api.formalise(slug, md)
except Exception as e: except Exception as e:
return HTMLResponse(f'<p class="err">Échec : ' return HTMLResponse(f'<p class="err">Échec : '
f'{html.escape(str(e))}</p>') f'{html.escape(str(e))}</p>')
if r.get("error"): if r.get("error"):
return HTMLResponse(f'<p class="err">' return HTMLResponse(
f'{html.escape(str(r))}</p>') 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( return HTMLResponse(
f'<p class="ok">Deck généré : {r["slides"]} slides — ' f'<p class="ok">Deck généré : {r["slides"]} slides — '
f'{html.escape(r["validation"])}<br>' f'{html.escape(r["validation"])}<br>'
f'<a href="/p/{slug}/download">Télécharger le PPTX</a> · ' f'<a class="btn" href="/p/{slug}/download">Télécharger le '
f'<button hx-post="/p/{slug}/preview" ' f'PPTX</a> <span class="hint">puis « Générer les aperçus » '
f'hx-target="#gallery" hx-indicator="#busy">Générer les ' f'ci-dessus pour les voir.</span></p>')
f'aperçus</button></p>')
async def preview(request): async def preview(request):
@@ -325,6 +343,8 @@ async def preview(request):
f'</figcaption></figure>' f'</figcaption></figure>'
for i, n in enumerate(r["slides"])) for i, n in enumerate(r["slides"]))
return HTMLResponse( 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'<form hx-post="/p/{slug}/revise" hx-target="#pipeline-out" '
f'hx-indicator="#busy"><div class="gallery">{cells}</div>' f'hx-indicator="#busy"><div class="gallery">{cells}</div>'
f'<input name="instruction" placeholder="Instruction de ' f'<input name="instruction" placeholder="Instruction de '
@@ -349,33 +369,45 @@ async def revise(request):
f'{html.escape(str(r))}</p>') f'{html.escape(str(r))}</p>')
return HTMLResponse( return HTMLResponse(
f'<p class="ok">Slides {r["slides_revisees"]} révisées, deck ' 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'complet régénéré.<br><a class="btn" href="/p/{slug}/'
f'Télécharger</a> · <button hx-post="/p/{slug}/preview" ' f'download">Télécharger</a> <span class="hint">puis '
f'hx-target="#gallery" hx-indicator="#busy">Re-générer les ' f'« Générer les aperçus » pour revoir le deck.</span></p>')
f'aperçus</button></p>')
# ── routes : fichiers ──────────────────────────────────────────────────────── # ── 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): async def preview_png(request):
guard = require_login(request) guard = require_login(request)
if guard: if guard:
return guard return guard
slug = request.path_params["slug"] slug = request.path_params["slug"]
name = request.path_params["name"] name = request.path_params["name"]
proj = fac.Project(slug) pptx = _pptx_path(slug)
state = proj.load_state()
pptx = state.get("dernier_pptx")
if not pptx: if not pptx:
return Response(status_code=404) return Response(status_code=404)
prev_dir = Path(pptx).parent / f"{Path(pptx).stem}_previews" prev_dir = pptx.parent / f"{pptx.stem}_previews"
try: try:
png = _safe_child(prev_dir, name) png = _safe_child(prev_dir, name)
except ValueError: except ValueError:
return Response(status_code=403) return Response(status_code=403)
if not png.exists(): if not png.exists():
return Response(status_code=404) 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): async def download(request):
@@ -383,11 +415,16 @@ async def download(request):
if guard: if guard:
return guard return guard
slug = request.path_params["slug"] slug = request.path_params["slug"]
proj = fac.Project(slug) pptx = _pptx_path(slug)
pptx = proj.load_state().get("dernier_pptx") if not pptx:
if not pptx or not Path(pptx).exists(): return HTMLResponse(
return Response(status_code=404) '<p class="err">Aucun PPTX disponible pour ce projet — '
return FileResponse(pptx, filename=Path(pptx).name) '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): async def health(request):