[IMP] script todo: suivi install — fenêtre des lignes d'erreurs/avertissements

La colonne ⚠ donne le NOMBRE ; on peut désormais LIRE les lignes associées.
Touche « d » (ou indice dans la barre) : ouvre une petite fenêtre modale
(ErrorLinesScreen) listant, pour la VM sélectionnée, les lignes d'erreurs
puis d'avertissements (numéro de ligne + texte), défilable. Échap/q ferme.

- scan_log_error_lines() : mêmes détection + listes d'ignore que la suite de
  tests ERPLibre, mais retient les lignes (bornées à 500) avec leur numéro.
- Scan à la demande à l'ouverture -> toujours à jour (marche même pendant
  l'installation).

Validé headless : modale s'ouvre avec les lignes, se ferme à Échap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-30 02:16:35 +00:00
parent f7df2310b3
commit 59bf857558
2 changed files with 104 additions and 2 deletions

View file

@ -199,6 +199,34 @@ _LST_IGNORE_ERROR = (
)
def scan_log_error_lines(log_path: str, cap: int = 500) -> tuple[list, list]:
"""(lignes_erreur, lignes_avertissement) d'un log, même détection que
scan_log_errors mais on RETIENT les lignes (bornées à `cap`) pour les
afficher. Chaque ligne est préfixée de son numéro (1-indexé)."""
try:
text = Path(log_path).read_text(errors="replace")
except OSError:
return [], []
errs, warns = [], []
for i, line in enumerate(text.splitlines(), 1):
if EXIT_MARKER in line:
continue
low = line.lower()
if (
"error" in low
and not any(ig in line for ig in _LST_IGNORE_ERROR)
and len(errs) < cap
):
errs.append(f"{i}: {line}")
if (
"warning" in low
and not any(ig in line for ig in _LST_IGNORE_WARNING)
and len(warns) < cap
):
warns.append(f"{i}: {line}")
return errs, warns
def scan_log_errors(log_path: str) -> tuple[int, int]:
"""(nb_erreurs, nb_avertissements) dans un log d'installation, en
réutilisant la détection de la suite de tests ERPLibre : sous-chaîne
@ -454,13 +482,57 @@ def run_monitor(manifest_path: str, run_app: bool = True):
"""Ouvre le dashboard Textual sur un manifeste d'installation. `run_app`
à False renvoie l'instance sans la lancer (tests headless)."""
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.containers import Horizontal, Vertical
from textual.screen import ModalScreen
from textual.widgets import DataTable, Footer, Header, RichLog, Static
manifest = json.loads(Path(manifest_path).read_text())
started = manifest.get("started", time.time())
vms = manifest["vms"]
class ErrorLinesScreen(ModalScreen):
"""Petite fenêtre modale : liste les LIGNES d'erreurs/avertissements
d'une VM pour les lire (défilable). Échap / q pour fermer."""
BINDINGS = [
("escape", "dismiss", "Fermer"),
("q", "dismiss", "Fermer"),
]
def __init__(self, vm_name, errs, warns):
super().__init__()
self._vm = vm_name
self._errs = errs
self._warns = warns
def compose(self) -> ComposeResult:
with Vertical(id="errbox"):
yield Static(
f" {self._vm} — ⚠ {len(self._errs)} "
f"{t('errors')} · ⚡ {len(self._warns)} {t('warnings')}"
f" ({t('Esc to close')})",
id="errtitle",
)
yield RichLog(
id="errlog", highlight=False, markup=False, wrap=True
)
def on_mount(self) -> None:
log = self.query_one("#errlog", RichLog)
if self._errs:
log.write(f"── {t('errors').capitalize()} ──")
for line in self._errs:
log.write(line)
if self._warns:
log.write(f"── {t('warnings').capitalize()} ──")
for line in self._warns:
log.write(line)
if not self._errs and not self._warns:
log.write(t("No error detected."))
def action_dismiss(self) -> None:
self.dismiss()
ICON = {
"pending": "",
"running": "",
@ -476,6 +548,13 @@ def run_monitor(manifest_path: str, run_app: bool = True):
#stats { height: 1; color: $accent; }
#statsdetail { display: none; height: auto; color: $text-muted; }
#sshbar { height: 2; color: $text-muted; }
ErrorLinesScreen { align: center middle; }
#errbox {
width: 80%; height: 70%;
border: thick $accent; background: $surface;
}
#errtitle { height: 1; color: $accent; text-style: bold; }
#errlog { height: 1fr; border: solid $accent; }
"""
BINDINGS = [
("q", "quit", "Quitter (détaché)"),
@ -483,6 +562,7 @@ def run_monitor(manifest_path: str, run_app: bool = True):
("w", "web", "Web (navigateur CLI)"),
("f", "follow", "Suivre"),
("c", "copy_log", "Copier log"),
("d", "details", "Détails erreurs"),
("p", "pause_all", "Pause tout"),
("o", "resume_all", "Reprendre tout"),
]
@ -580,7 +660,8 @@ def run_monitor(manifest_path: str, run_app: bool = True):
if vm:
bar.update(
f" {vm['ssh']} (s = SSH · w = web :8069 · "
"c = copier le log · Maj+glisser = sélectionner)\n"
"c = copier le log · d = détails erreurs · "
"Maj+glisser = sélectionner)\n"
f" Log : {vm['log']}"
)
@ -945,6 +1026,15 @@ def run_monitor(manifest_path: str, run_app: bool = True):
title="Presse-papiers",
)
def action_details(self) -> None:
"""Ouvre une petite fenêtre avec les LIGNES d'erreurs/avertissements
de la VM sélectionnée (scan à la demande -> toujours à jour)."""
vm = self._vm_by_name(self._selected)
if not vm:
return
errs, warns = scan_log_error_lines(vm["log"])
self.push_screen(ErrorLinesScreen(vm["name"], errs, warns))
def on_click(self, event) -> None:
# Clic sur le sommaire de stats -> déplie / replie le détail.
w = getattr(event, "widget", None)

View file

@ -1432,6 +1432,18 @@ TRANSLATIONS = {
"fr": "Aucune erreur détectée.",
"en": "No error detected.",
},
"errors": {
"fr": "erreurs",
"en": "errors",
},
"warnings": {
"fr": "avertissements",
"en": "warnings",
},
"Esc to close": {
"fr": "Échap pour fermer",
"en": "Esc to close",
},
"No running VM to pause.": {
"fr": "Aucune VM en cours à mettre en pause.",
"en": "No running VM to pause.",