From e8aba1ab74a355a04247c1ff2d0962d45d7c51fb Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Tue, 7 Jul 2026 13:31:02 -0400 Subject: [PATCH] =?UTF-8?q?GUI=20:=20erreurs=20de=20d=C3=A9ploiement=20en?= =?UTF-8?q?=20langage=20clair?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quand une action échoue, un encadré résume les tâches en erreur (étape · hôte · type · message clair), avec un bouton Copier pour transmettre à un humain — au lieu d'obliger à lire le dump Ansible. Backend _extraire_echec : parse fatal:/UNREACHABLE!, privilégie stderr sur le générique 'non-zero return code', gère no_log (secret masqué) et injoignables. Marqueur __RESUME__ streamé, rendu côté JS. Éprouvé sur les 4 échecs réels du from-zero du jour. node --check OK, smoke serveur OK. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 7 ++++ scripts/inventory_gui.py | 85 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 738ef11..9f23793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ ## 2026-07-07 (soir) ### Ajouté +- **GUI : erreurs de déploiement en LANGAGE CLAIR.** Quand une action (créer/vérifier/déployer) + échoue, le GUI n'oblige plus à lire le dump Ansible : un encadré résume les tâches en erreur — + **étape · hôte · type · message clair** — avec un bouton **« Copier »** (pour transmettre le + résumé à un humain / au mainteneur). Le backend (`_extraire_echec`) parse les `fatal:`/`UNREACHABLE!`, + privilégie la vraie cause (`stderr` plutôt que le générique « non-zero return code »), gère + `no_log` (« sortie masquée — secret ») et les injoignables. Éprouvé sur les 4 échecs réels du + from-zero du jour. `node --check` OK. - **GUI : éditeur « Domaines » (6ᵉ onglet éditable).** Le registre des domaines publics — le seul sans édition dans le GUI — a désormais son onglet : ajouter/retirer une zone DNS, **autorité** (sélecteur `primaire-cache`/`auto-heberge`/`delegue`), **edge**, **DNSSEC**, **mail**, secondaires, diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 570e336..6ffbe43 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -523,6 +523,41 @@ def ecrire_domaines(path: Path, registre: dict) -> None: fichier, default_flow_style=False, sort_keys=False, allow_unicode=True) +def _extraire_echec(ligne: str, tache: str) -> dict | None: + """D'une ligne Ansible fatal/unreachable, tire un échec en LANGAGE CLAIR + (tâche · hôte · type · message) — pour communiquer entre humains.""" + m = re.search(r'(?:fatal|failed):\s*\[([^\]]+)\]:\s*(FAILED!|UNREACHABLE!)?\s*=>\s*(\{.*\})', ligne) + if not m: + return None + hote = m.group(1) + typ = "injoignable (SSH)" if m.group(2) == "UNREACHABLE!" else "échec" + detail: dict = {} + try: + detail = json.loads(m.group(3)) + except Exception: + pass + if detail.get("censored"): + message = "sortie masquée (no_log) — souvent une tâche qui manipule un secret" + else: + msg = str(detail.get("msg", "")).strip() + # Pour un échec de commande, msg est générique ; la vraie cause est dans stderr. + stderr = "" + for cle in ("stderr", "module_stderr", "stdout"): + valeur = str(detail.get(cle, "")).strip() + if valeur: + lignes = [x for x in valeur.splitlines() if x.strip()] + stderr = lignes[0] if lignes else valeur # l'erreur mène, l'indice suit + break + if msg and msg.lower() not in ("non-zero return code", "module failure", ""): + message = msg + elif stderr: + message = stderr + else: + message = msg or "échec sans message explicite (voir le journal complet)" + message = " ".join(str(message).split()) # une ligne propre + return {"tache": (tache or "?").strip(), "hote": hote, "type": typ, "message": message[:400]} + + def donnees_hote(hote: dict) -> dict: variables: dict = {} for cle, var, typ in CHAMPS_PROVISION: @@ -872,6 +907,15 @@ HTML = r""" .couche-fleche { text-align: center; color: var(--muted); font-size: 14px; line-height: 1.5; } .legende { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: var(--muted); } .legende .chip { margin-right: 4px; } + /* Résumé d'erreur en langage clair (console) */ + #console-resume { padding: 12px 16px; background: color-mix(in srgb, var(--rouge) 10%, var(--surface)); border-bottom: 1px solid var(--ligne); } + .re-tete { display: flex; align-items: center; gap: 10px; font-weight: 800; color: var(--rouge); font-size: 13px; margin-bottom: 6px; } + .re-tete button { margin-left: auto; } + .re-liste { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 7px; } + .re-liste li { font-size: 12.5px; } + .re-hote { color: var(--teal); font-weight: 700; } + .re-type { color: var(--muted); font-size: 11px; } + .re-msg { background: var(--surface-3); border-radius: 6px; padding: 4px 8px; margin-top: 3px; font-family: ui-monospace, monospace; font-size: 11.5px; white-space: pre-wrap; word-break: break-word; } @@ -939,6 +983,7 @@ HTML = r""" +