fix: cache-busting de la galerie d apercus (C9)
This commit is contained in:
@@ -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
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user