From 4f0c3cd2175827e2faa297caa19b6b32977e27cd Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 30 Jul 2026 00:14:58 -0400 Subject: [PATCH] =?UTF-8?q?[IMP]=20script=20todo:=20suivi=20install=20?= =?UTF-8?q?=E2=80=94=20auto-refresh=20coup=C3=A9=20par=20d=C3=A9faut=20(mo?= =?UTF-8?q?ins=20de=20lag)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sur un gros parc, les ticks périodiques (logs 1s / table 2s / domstate 10s) causaient du lag à la lecture des logs. Nouveau bouton « a » pour couper/ activer le rafraîchissement automatique, COUPÉ par défaut ; « r » pour rafraîchir une seule fois à la demande. - Chaque tick est scindé en wrapper _tick_* (ne fait rien si auto coupé) + corps _do_* (le travail réel), réutilisable pour les rafraîchissements forcés (init à l'ouverture, toggle, refresh manuel, après pause/reprise). - Un relevé initial unique (table + domstate) à l'ouverture affiche l'état courant même auto coupé ; changer de VM recharge son log (synchrone). - Indicateur #autobar : « ⏸ Rafraîchissement auto : COUPÉ (a=activer · r=rafraîchir) » / « ▶ … : ACTIVÉ (a=couper) ». Validé headless : défaut OFF, toggle a, refresh r, table peuplée à l'init. Co-Authored-By: Claude Opus 4.8 (1M context) --- script/todo/qemu_install_monitor.py | 70 +++++++++++++++++++++++++---- script/todo/todo_i18n.py | 24 ++++++++++ 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/script/todo/qemu_install_monitor.py b/script/todo/qemu_install_monitor.py index 2bb52b7..8f89266 100644 --- a/script/todo/qemu_install_monitor.py +++ b/script/todo/qemu_install_monitor.py @@ -545,6 +545,7 @@ def run_monitor(manifest_path: str, run_app: bool = True): DataTable { width: 74; height: 1fr; overflow-x: auto; border: solid $accent; } RichLog { border: solid $accent; } #telemetry { height: 1; color: $text-muted; } + #autobar { height: 1; color: $warning; } #stats { height: 1; color: $accent; } #statsdetail { display: none; height: auto; color: $text-muted; } #sshbar { height: 2; color: $text-muted; } @@ -563,6 +564,8 @@ def run_monitor(manifest_path: str, run_app: bool = True): ("f", "follow", "Suivre"), ("c", "copy_log", "Copier log"), ("d", "details", "Détails erreurs"), + ("a", "toggle_auto", "Auto-refresh on/off"), + ("r", "refresh_now", "Rafraîchir"), ("p", "pause_all", "Pause tout"), ("o", "resume_all", "Reprendre tout"), ] @@ -588,6 +591,11 @@ def run_monitor(manifest_path: str, run_app: bool = True): self._errcount = {} # Sommaire de stats déplié (clic) ou non. self._stats_open = False + # Rafraîchissement automatique COUPÉ par défaut : sur un gros parc, + # les ticks périodiques (logs/table/domstate) causent du lag quand + # on lit les logs. On l'active à la demande (touche « a ») ; sinon + # « r » rafraîchit une seule fois. + self._auto_refresh = False @staticmethod def _fmt(secs): @@ -625,6 +633,8 @@ def run_monitor(manifest_path: str, run_app: bool = True): with Horizontal(): yield table yield self._log + # Indicateur d'auto-refresh (a = on/off · r = rafraîchir). + yield Static("", id="autobar") # Barre de télémétrie hôte (CPU, disque, ETA parc). yield Static("", id="telemetry") # Sommaire de stats en CHIFFRES (cliquable -> détail). @@ -640,15 +650,17 @@ def run_monitor(manifest_path: str, run_app: bool = True): self.sub_title = f"0/{len(vms)} {t('completed')}" self._refresh_ssh() self._load_selected_log(reset=True) - # Table toutes les 2 s (30 lectures de fin de log), suivi du log - # sélectionné toutes les 1 s (une seule lecture incrémentale). + self._update_autobar() + # Ticks périodiques : ils ne font RIEN tant que l'auto-refresh est + # coupé (wrappers gardés). On garde les intervalles en place pour + # ne pas avoir à les (re)créer au moment du toggle. self.set_interval(2.0, self._tick_table) self.set_interval(1.0, self._tick_log) - # État libvirt (pause / effacée) : check LENT (appel virsh) toutes - # les 10 s ; le tableau applique le cache à chaque tick (2 s). self.set_interval(10.0, self._tick_domstate) - # Premier relevé domstate immédiat (async -> via worker). - self.run_worker(self._tick_domstate(), exclusive=False) + # Un SEUL relevé initial (table + domstate) pour afficher l'état + # courant à l'ouverture, même auto-refresh coupé. + self.run_worker(self._do_domstate(), exclusive=False) + self.run_worker(self._do_table(), exclusive=False) # -- helpers -------------------------------------------------------- # def _vm_by_name(self, name): @@ -661,6 +673,7 @@ def run_monitor(manifest_path: str, run_app: bool = True): bar.update( f" {vm['ssh']} (s = SSH · w = web :8069 · " "c = copier le log · d = détails erreurs · " + "a = auto-refresh · r = rafraîchir · " "Maj+glisser = sélectionner)\n" f" Log : {vm['log']}" ) @@ -719,6 +732,10 @@ def run_monitor(manifest_path: str, run_app: bool = True): return disks, status, self._collect_tele(), errors async def _tick_table(self): + if self._auto_refresh: + await self._do_table() + + async def _do_table(self): # I/O (lectures de logs, stat disque, /proc) DÉPORTÉES en thread -> # la boucle d'événements Textual reste fluide même sous forte # charge ou disque lent. Les mises à jour d'UI restent sur la boucle. @@ -866,6 +883,10 @@ def run_monitor(manifest_path: str, run_app: bool = True): return "\n".join(lines) async def _tick_log(self): + if self._auto_refresh: + await self._do_log() + + async def _do_log(self): if not self._follow: return vm = self._vm_by_name(self._selected) @@ -884,6 +905,10 @@ def run_monitor(manifest_path: str, run_app: bool = True): self._log.write(line) async def _tick_domstate(self): + if self._auto_refresh: + await self._do_domstate() + + async def _do_domstate(self): # Relevé LENT (subprocess virsh) DÉPORTÉ en thread : ne bloque plus # la boucle. Une VM absente de « virsh list » est EFFACÉE (« gone »). try: @@ -893,6 +918,33 @@ def run_monitor(manifest_path: str, run_app: bool = True): for vm in vms: self._domstate[vm["name"]] = states.get(vm["name"], "gone") + # -- auto-refresh (a = on/off · r = rafraîchir une fois) ------------- # + def _update_autobar(self): + if self._auto_refresh: + msg = f" ▶ {t('Auto-refresh: ON')} (a = {t('turn off')})" + else: + msg = ( + f" ⏸ {t('Auto-refresh: OFF')} " + f"(a = {t('turn on')} · r = {t('refresh once')})" + ) + self.query_one("#autobar", Static).update(msg) + + def action_toggle_auto(self) -> None: + self._auto_refresh = not self._auto_refresh + self._update_autobar() + if self._auto_refresh: + # Rafraîchit tout de suite en activant. + self.run_worker(self._do_domstate(), exclusive=False) + self.run_worker(self._do_table(), exclusive=False) + self.run_worker(self._do_log(), exclusive=False) + + def action_refresh_now(self) -> None: + # Une seule actualisation, même auto-refresh coupé. + self.run_worker(self._do_domstate(), exclusive=False) + self.run_worker(self._do_table(), exclusive=False) + self.run_worker(self._do_log(), exclusive=False) + self.notify(t("Refreshed.")) + # -- events --------------------------------------------------------- # def on_data_table_row_highlighted(self, event) -> None: name = event.row_key.value @@ -1084,8 +1136,10 @@ def run_monitor(manifest_path: str, run_app: bool = True): await asyncio.to_thread(self._virsh_bulk, action, targets) verb = t("paused") if action == "suspend" else t("resumed") self.notify(f"{len(targets)} VM {verb}.") - # Rafraîchit tout de suite l'état libvirt (pause/reprise visible). - self.run_worker(self._tick_domstate(), exclusive=False) + # Rafraîchit tout de suite l'état libvirt (pause/reprise visible), + # même si l'auto-refresh est coupé -> _do_domstate (forcé). + self.run_worker(self._do_domstate(), exclusive=False) + self.run_worker(self._do_table(), exclusive=False) app = Monitor() if run_app: diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index bb418c5..bd1e830 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1460,6 +1460,30 @@ TRANSLATIONS = { "fr": "Aucune erreur détectée.", "en": "No error detected.", }, + "Auto-refresh: ON": { + "fr": "Rafraîchissement auto : ACTIVÉ", + "en": "Auto-refresh: ON", + }, + "Auto-refresh: OFF": { + "fr": "Rafraîchissement auto : COUPÉ", + "en": "Auto-refresh: OFF", + }, + "turn off": { + "fr": "couper", + "en": "turn off", + }, + "turn on": { + "fr": "activer", + "en": "turn on", + }, + "refresh once": { + "fr": "rafraîchir", + "en": "refresh once", + }, + "Refreshed.": { + "fr": "Rafraîchi.", + "en": "Refreshed.", + }, "Test a VM (open Odoo in a CLI browser)": { "fr": "🧪 Tester une VM (ouvrir Odoo dans un navigateur CLI)", "en": "🧪 Test a VM (open Odoo in a CLI browser)",