[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:
parent
f7df2310b3
commit
59bf857558
2 changed files with 104 additions and 2 deletions
|
|
@ -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]:
|
def scan_log_errors(log_path: str) -> tuple[int, int]:
|
||||||
"""(nb_erreurs, nb_avertissements) dans un log d'installation, en
|
"""(nb_erreurs, nb_avertissements) dans un log d'installation, en
|
||||||
réutilisant la détection de la suite de tests ERPLibre : sous-chaîne
|
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`
|
"""Ouvre le dashboard Textual sur un manifeste d'installation. `run_app`
|
||||||
à False renvoie l'instance sans la lancer (tests headless)."""
|
à False renvoie l'instance sans la lancer (tests headless)."""
|
||||||
from textual.app import App, ComposeResult
|
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
|
from textual.widgets import DataTable, Footer, Header, RichLog, Static
|
||||||
|
|
||||||
manifest = json.loads(Path(manifest_path).read_text())
|
manifest = json.loads(Path(manifest_path).read_text())
|
||||||
started = manifest.get("started", time.time())
|
started = manifest.get("started", time.time())
|
||||||
vms = manifest["vms"]
|
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 = {
|
ICON = {
|
||||||
"pending": "⏳",
|
"pending": "⏳",
|
||||||
"running": "⏳",
|
"running": "⏳",
|
||||||
|
|
@ -476,6 +548,13 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
#stats { height: 1; color: $accent; }
|
#stats { height: 1; color: $accent; }
|
||||||
#statsdetail { display: none; height: auto; color: $text-muted; }
|
#statsdetail { display: none; height: auto; color: $text-muted; }
|
||||||
#sshbar { height: 2; 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 = [
|
BINDINGS = [
|
||||||
("q", "quit", "Quitter (détaché)"),
|
("q", "quit", "Quitter (détaché)"),
|
||||||
|
|
@ -483,6 +562,7 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
("w", "web", "Web (navigateur CLI)"),
|
("w", "web", "Web (navigateur CLI)"),
|
||||||
("f", "follow", "Suivre"),
|
("f", "follow", "Suivre"),
|
||||||
("c", "copy_log", "Copier log"),
|
("c", "copy_log", "Copier log"),
|
||||||
|
("d", "details", "Détails erreurs"),
|
||||||
("p", "pause_all", "Pause tout"),
|
("p", "pause_all", "Pause tout"),
|
||||||
("o", "resume_all", "Reprendre tout"),
|
("o", "resume_all", "Reprendre tout"),
|
||||||
]
|
]
|
||||||
|
|
@ -580,7 +660,8 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
if vm:
|
if vm:
|
||||||
bar.update(
|
bar.update(
|
||||||
f" {vm['ssh']} (s = SSH · w = web :8069 · "
|
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']}"
|
f" Log : {vm['log']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -945,6 +1026,15 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
title="Presse-papiers",
|
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:
|
def on_click(self, event) -> None:
|
||||||
# Clic sur le sommaire de stats -> déplie / replie le détail.
|
# Clic sur le sommaire de stats -> déplie / replie le détail.
|
||||||
w = getattr(event, "widget", None)
|
w = getattr(event, "widget", None)
|
||||||
|
|
|
||||||
|
|
@ -1432,6 +1432,18 @@ TRANSLATIONS = {
|
||||||
"fr": "Aucune erreur détectée.",
|
"fr": "Aucune erreur détectée.",
|
||||||
"en": "No error detected.",
|
"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.": {
|
"No running VM to pause.": {
|
||||||
"fr": "Aucune VM en cours à mettre en pause.",
|
"fr": "Aucune VM en cours à mettre en pause.",
|
||||||
"en": "No running VM to pause.",
|
"en": "No running VM to pause.",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue