backup: snapshot post-démo GHT 2026-05-19
Some checks failed
tests / Lint (ruff + black) (push) Successful in 1m50s
tests / Tests unitaires (sans GPU) (push) Failing after 1m50s
tests / Tests sécurité (critique) (push) Has been skipped

Backup état complet après enregistrement vidéo démo de bout en bout.
À utiliser comme point de référence pour la consolidation post-démo.

Changements majeurs de la session 18-19 mai :
- AIVA-URGENCE : page autonome avec preset URL + auto-focus chain
- Workflow Demo_urgence_3_db : merge linux_db + steps AIVA + pause humaine NoMachine
- Bypass LLM (static_result / static_text) dans replay_engine
  pour démos déterministes sans appel Ollama
- Fix api_stream:3013 — replay_paused au premier polling /next
- dag_execute : lift duration_ms vers top-level pour wait runtime
- NPM bypass auth /aiva-urgence/ via location ^~ (proxy_host/10.conf hors git)
- scripts/cancel-replays.sh — workaround Stop VWB qui ne purge pas la queue

Anchors visuels (468) forcés dans le commit pour garantir restorabilité.
DB workflows actuelle + ~12 .bak DB de la journée incluses.

Sujets identifiés pour consolidation post-démo (TODO) :
1. Bug VWB recapture anchor ne régénère pas le PNG
2. Léa client accumule état mémoire (restart périodique requis)
3. Stop VWB ne purge pas la queue serveur (lien manquant vers /replay/cancel)
4. Bug coord client mss tronqué 2560x60 → mapping Y cassé
5. delay_before/delay_after ignorés au runtime (fix partiel duration_ms)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-05-19 14:55:06 +02:00
parent f2212e77e3
commit 5ea4960e65
627 changed files with 211348 additions and 169 deletions

View File

@@ -833,6 +833,9 @@ class ChatWindow:
font=FONT_TIMESTAMP, anchor="w",
).pack(fill=tk.X, anchor=tk.W, pady=(2, 0))
# UX fix 8 mai 2026 : auto-scroll après chaque bulle d'action
self._scroll_to_bottom()
# ------------------------------------------------------------------
# Bulle paused_need_help interactive (J3.5)
# ------------------------------------------------------------------
@@ -844,6 +847,9 @@ class ChatWindow:
(`root.withdraw()`). Il FAUT la rendre visible et la forcer au premier
plan, sinon Dom ne voit jamais la bulle. On exécute dans le thread
tkinter via `root.after(0, ...)`.
UX fix 8 mai 2026 : un seul affichage (la bulle chat). Plus de toast
en double — on force juste la chat window au premier plan.
"""
if self._root is None:
return
@@ -854,23 +860,40 @@ class ChatWindow:
# Re-pin topmost pour passer devant les apps actives
self._root.attributes("-topmost", True)
self._root.lift()
# Toast topmost en complément (visible même si la chat est
# masquée par une fenêtre d'app)
# Bell sonore léger pour attirer l'attention (Tkinter natif)
try:
from .paused_toast import show_paused_toast
reason = payload.get("reason") or "Action en attente."
show_paused_toast(
title="Léa a besoin de votre aide",
message=str(reason)[:300],
)
self._root.bell()
except Exception:
logger.debug("paused_toast launch silenced", exc_info=True)
pass
except Exception:
logger.debug("force-show chat_window silenced", exc_info=True)
# UX fix mai 2026 : repartir d'un chat vide pour focaliser
# l'attention sur la question (clear visuel uniquement,
# self._messages reste intact pour la traçabilité debug).
self._clear_chat_history()
self._render_paused_bubble(payload)
self._root.after(0, _show_and_render)
def _clear_chat_history(self) -> None:
"""Vide la zone d'affichage du chat (widgets enfants de _msg_frame).
Détruit uniquement les widgets visuels — self._messages (liste mémoire)
reste intact pour la traçabilité debug. Cohérent avec _do_remove_typing
qui détruit aussi le widget sans toucher à l'historique.
"""
if getattr(self, "_msg_frame", None) is None:
return
try:
for child in list(self._msg_frame.winfo_children()):
child.destroy()
self._active_paused_bubble = None
if hasattr(self, "_typing_frame"):
self._typing_frame = None
self._scroll_to_bottom()
except Exception:
logger.debug("clear chat history silenced", exc_info=True)
def _render_paused_bubble(self, payload: Dict[str, Any]) -> None:
tk = self._tk
if getattr(self, "_msg_frame", None) is None:
@@ -898,11 +921,30 @@ class ChatWindow:
font=("Segoe UI", 12, "bold"), anchor="w",
).pack(fill=tk.X, anchor=tk.W)
tk.Label(
inner, text=reason, bg=PAUSED_BG, fg=PAUSED_FG,
font=FONT_MSG, wraplength=MSG_WRAP_WIDTH - 30,
anchor="w", justify=tk.LEFT,
).pack(fill=tk.X, anchor=tk.W, pady=(6, 0))
# Message scrollable pour les longs reasons (ex: 200+ chars depuis le serveur).
# On utilise un Text en mode read-only avec hauteur calculée selon la longueur.
# Au-delà de 280 chars, scrollbar interne ; sinon Text auto-fitté.
reason_str = str(reason)
# Estimation simple : ~70 chars/ligne avec wraplength
approx_lines = max(2, min(8, (len(reason_str) // 60) + 1))
msg_frame = tk.Frame(inner, bg=PAUSED_BG)
msg_frame.pack(fill=tk.X, anchor=tk.W, pady=(6, 0))
reason_text = tk.Text(
msg_frame, bg=PAUSED_BG, fg=PAUSED_FG,
font=FONT_MSG, wrap=tk.WORD, bd=0, height=approx_lines,
highlightthickness=0, relief=tk.FLAT, cursor="arrow",
)
reason_text.insert("1.0", reason_str)
reason_text.configure(state="disabled")
reason_text.pack(side=tk.LEFT, fill=tk.X, expand=True)
# Scrollbar interne uniquement si le contenu déborde (long messages)
if len(reason_str) > 280:
reason_scroll = tk.Scrollbar(
msg_frame, orient=tk.VERTICAL,
command=reason_text.yview, width=8,
)
reason_text.configure(yscrollcommand=reason_scroll.set)
reason_scroll.pack(side=tk.RIGHT, fill=tk.Y)
tk.Label(
inner, text=f"{workflow} — étape {completed}/{total}",
@@ -930,12 +972,23 @@ class ChatWindow:
)
btn_abort.pack(side=tk.LEFT)
# Zone de feedback (mise à jour après clic, avant l'ack du bus)
feedback_label = tk.Label(
inner, text="", bg=PAUSED_BG, fg=PAUSED_FG,
font=FONT_TIMESTAMP, anchor="w",
)
feedback_label.pack(fill=tk.X, anchor=tk.W, pady=(6, 0))
self._active_paused_bubble = {
"container": container, "inner": inner,
"btn_resume": btn_resume, "btn_abort": btn_abort,
"feedback_label": feedback_label,
"replay_id": replay_id,
}
# Scroll automatique vers la nouvelle bulle (visible immédiatement)
self._scroll_to_bottom()
def _close_active_paused_bubble(self, reason: str) -> None:
if self._active_paused_bubble is None or self._root is None:
return
@@ -961,28 +1014,79 @@ class ChatWindow:
self._active_paused_bubble = None
def _on_paused_resume(self, replay_id: str) -> None:
if not replay_id or self._bus is None or not self._bus.connected:
self._add_lea_message("⚠ Bus indisponible — impossible de relancer")
"""Bouton Continuer : émettre lea:replay_resume + feedback immédiat UI.
UX fix 8 mai 2026 : on désactive les 2 boutons et on affiche un message
de feedback dès le clic, sans attendre l'ack serveur. Le bus émet en
arrière-plan ; si la connexion est tombée, on log un warning visible.
"""
if not replay_id:
self._update_paused_feedback("⚠ replay_id manquant — impossible de relancer")
return
self._bus.resume_replay(replay_id)
if self._active_paused_bubble:
try:
self._active_paused_bubble["btn_resume"].config(state="disabled")
self._active_paused_bubble["btn_abort"].config(state="disabled")
except Exception:
pass
emitted = False
if self._bus is not None and self._bus.connected:
emitted = self._bus.resume_replay(replay_id)
# Feedback immédiat : disable boutons + message
self._disable_paused_buttons()
if emitted:
self._update_paused_feedback("→ Reprise demandée…")
logger.info("paused_bubble: lea:replay_resume émis pour %s", replay_id)
else:
self._update_paused_feedback("⚠ Bus indisponible — réessayez dans 5s")
logger.warning("paused_bubble: bus déconnecté, resume non émis")
# UX fix mai 2026 : minimiser la fenêtre vers le systray après 500ms
# (laisse à l'utilisateur le temps de voir "Reprise demandée…").
try:
self._root.after(500, self._do_hide)
except Exception:
logger.debug("auto-hide on resume silenced", exc_info=True)
def _on_paused_abort(self, replay_id: str) -> None:
if self._bus is None or not self._bus.connected:
self._add_lea_message("⚠ Bus indisponible — impossible d'annuler")
"""Bouton Annuler : émettre lea:replay_abort + fermeture locale immédiate.
UX fix 8 mai 2026 : on ferme la bulle localement dès le clic (le serveur
n'envoie pas de lea:resumed pour un abort, donc sans cette fermeture
locale la bulle restait coincée — c'était la cause de "Annuler ne
fonctionne pas" rapportée par Dom).
"""
emitted = False
if self._bus is not None and self._bus.connected:
emitted = self._bus.abort_replay(replay_id)
self._disable_paused_buttons()
if emitted:
self._update_paused_feedback("✗ Annulé")
logger.info("paused_bubble: lea:replay_abort émis pour %s", replay_id)
else:
self._update_paused_feedback("✗ Annulé (bus indisponible)")
logger.warning("paused_bubble: bus déconnecté, abort non émis")
# Fermer la bulle en local (l'abort n'a pas de lea:resumed associé)
self._close_active_paused_bubble(reason="abort_local")
# UX fix mai 2026 : minimiser la fenêtre après 500ms (cohérence
# avec _on_paused_resume, demandé explicitement par Dom).
try:
self._root.after(500, self._do_hide)
except Exception:
logger.debug("auto-hide on abort silenced", exc_info=True)
def _disable_paused_buttons(self) -> None:
if not self._active_paused_bubble:
return
self._bus.abort_replay(replay_id)
if self._active_paused_bubble:
try:
self._active_paused_bubble["btn_resume"].config(state="disabled")
self._active_paused_bubble["btn_abort"].config(state="disabled")
except Exception:
pass
try:
self._active_paused_bubble["btn_resume"].config(state="disabled")
self._active_paused_bubble["btn_abort"].config(state="disabled")
except Exception:
logger.debug("disable paused buttons silenced", exc_info=True)
def _update_paused_feedback(self, text: str) -> None:
if not self._active_paused_bubble:
return
label = self._active_paused_bubble.get("feedback_label")
if label is None:
return
try:
label.config(text=text)
except Exception:
logger.debug("update paused feedback silenced", exc_info=True)
# ======================================================================
# Ajout de messages dans la zone de chat