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

View File

@@ -140,27 +140,18 @@ class NotificationManager:
Les messages BLOCAGE bypass le rate limit pour garantir que
l'utilisateur voit qu'on a besoin de lui.
Démo GHT 8 mai 2026 : pour les BLOCAGE, on déclenche en complément
un toast Tkinter custom topmost (paused_toast). Plyer est silencieux
sur Windows 11 quand Focus Assist / Quiet Hours / app-id manquante
bloquent les balloons. Le toast custom est 100 % autonome et garantit
que Dom voit le message en démo.
UX fix 8 mai 2026 (démo GHT) : la bulle ChatWindow Léa V1 (Tkinter
topmost + bell + force-show) est désormais l'affichage canonique pour
les BLOCAGE de pause supervisée. On NE déclenche PLUS show_paused_toast
depuis ici — Dom rapportait 3 popups en parallèle (toast executor,
toast bubble, toast notifications). Plyer reste actif comme
notification système discrète. Le toast Tkinter custom est conservé
pour les fallbacks sans ChatWindow (cf. executor.Plan B).
"""
bypass = msg.niveau == NiveauMessage.BLOCAGE
# Log aussi pour tracer dans les logs fichiers
self._log_message(msg)
# Toast Tkinter custom — uniquement BLOCAGE pour ne pas spammer
if msg.niveau == NiveauMessage.BLOCAGE:
try:
from .paused_toast import show_paused_toast
show_paused_toast(
title=str(msg.titre)[:80] or "Léa a besoin de votre aide",
message=str(msg.corps)[:300],
)
except Exception:
logger.debug("paused_toast (BLOCAGE) silenced", exc_info=True)
return self.notify(
title=msg.titre,
message=msg.corps,