release 1.15.7: mon HTML SUMMARY classify + no converter noise

This commit is contained in:
Hernâni Marques 2026-07-19 05:20:41 +02:00
parent a5562cf5fb
commit f7ddf783fe
No known key found for this signature in database
GPG key ID: CB5738652768F7E9
4 changed files with 84 additions and 19 deletions

View file

@ -1 +1 @@
1.15.6 1.15.7

View file

@ -17,6 +17,7 @@ Git tags: `vMAJOR.FEATURE.FIX` (e.g. `v1.8.0`). File `VERSION` omits the `v` pre
| Tag | Date (UTC) | Notes | | Tag | Date (UTC) | Notes |
|-----|------------|--------| |-----|------------|--------|
| **v1.15.7** | 2026-07-19 | **Bugfix:** mon HTML — do not paint «perf/ladder summary» as SUMMARY header (restore 2 INFO lines); drop converter SyntaxWarning/noise from public log; «wrote» on stderr; host-agent PYTHONWARNINGS for convert. |
| **v1.15.6** | 2026-07-19 | **Bugfix:** aptdeploy ERROR jump list is self-contained — systemd httpd failures include before/after state, Result/SubState, failed units, and journal snippets (no more vague «see systemd lines»); ldd/--version errors embed concrete missing libs / exit output. | | **v1.15.6** | 2026-07-19 | **Bugfix:** aptdeploy ERROR jump list is self-contained — systemd httpd failures include before/after state, Result/SubState, failed units, and journal snippets (no more vague «see systemd lines»); ldd/--version errors embed concrete missing libs / exit output. |
| **v1.15.5** | 2026-07-19 | **Bugfix:** devtesting CHF ladder — `ssh` was draining ladder stdin (only rung 1/23 ran → progress 45/98 then snap 52/52); use `ssh -n` + `mapfile`; incomplete ladder is WARN not false complete. | | **v1.15.5** | 2026-07-19 | **Bugfix:** devtesting CHF ladder — `ssh` was draining ladder stdin (only rung 1/23 ran → progress 45/98 then snap 52/52); use `ssh -n` + `mapfile`; incomplete ladder is WARN not false complete. |
| **v1.15.4** | 2026-07-19 | **Bugfix:** French sticky titles — page label *surveillance*, stack summary *public + interne*, SUMMARY verdict/totals via i18n (`tout est clair`, `totaux :`). | | **v1.15.4** | 2026-07-19 | **Bugfix:** French sticky titles — page label *surveillance*, stack summary *public + interne*, SUMMARY verdict/totals via i18n (`tout est clair`, `totaux :`). |

View file

@ -353,7 +353,13 @@ htmlify_host() {
mkdir -p "$HTML_BASE/$host/${HTML_OK_DIR}" "$HTML_BASE/$host/${HTML_ERR_DIR}" mkdir -p "$HTML_BASE/$host/${HTML_OK_DIR}" "$HTML_BASE/$host/${HTML_ERR_DIR}"
if [ -n "$SITE_GEN" ] && [ -f "$SITE_GEN/console_to_html.py" ]; then if [ -n "$SITE_GEN" ] && [ -f "$SITE_GEN/console_to_html.py" ]; then
python3 "$SITE_GEN/console_to_html.py" \ # PYTHONWARNINGS: never let SyntaxWarning leak into the suite log (tee).
# stderr → agent log file only, not the mon console stream.
_mon_html_py() {
PYTHONWARNINGS=ignore::SyntaxWarning \
python3 "$SITE_GEN/console_to_html.py" "$@" 2>>"${LOG_DIR:-/tmp}/console_to_html.err"
}
_mon_html_py \
--lang "${TALER_MON_LANG:-en}" \ --lang "${TALER_MON_LANG:-en}" \
--log "$LOG" \ --log "$LOG" \
--out "$mon_err" \ --out "$mon_err" \
@ -368,7 +374,7 @@ htmlify_host() {
--path-err "$HTML_URL_ERR" \ --path-err "$HTML_URL_ERR" \
--link-other "$HTML_URL_OK" --link-other "$HTML_URL_OK"
if [ "$ec" -eq 0 ]; then if [ "$ec" -eq 0 ]; then
python3 "$SITE_GEN/console_to_html.py" \ _mon_html_py \
--lang "${TALER_MON_LANG:-en}" \ --lang "${TALER_MON_LANG:-en}" \
--log "$LOG" \ --log "$LOG" \
--out "$mon" \ --out "$mon" \
@ -385,7 +391,7 @@ htmlify_host() {
rm -rf "$HTML_BASE/$host/${HTML_ERR_DIR}" rm -rf "$HTML_BASE/$host/${HTML_ERR_DIR}"
echo "html $host${HTML_URL_OK} only (clean · ${COMMIT_SHORT:-?})" echo "html $host${HTML_URL_OK} only (clean · ${COMMIT_SHORT:-?})"
else else
python3 "$SITE_GEN/console_to_html.py" \ _mon_html_py \
--lang "${TALER_MON_LANG:-en}" \ --lang "${TALER_MON_LANG:-en}" \
--log "$LOG" \ --log "$LOG" \
--out "$mon" \ --out "$mon" \

View file

@ -4,8 +4,9 @@ from __future__ import annotations
import argparse import argparse
import html import html
import re
import os import os
import re
import sys
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@ -69,16 +70,25 @@ def ui(lang: str, key: str, **kwargs) -> str:
"jump_first_warn": "Jump to first warning", "jump_first_warn": "Jump to first warning",
"filter_errors": "Filter: errors + a few dimmed context lines", "filter_errors": "Filter: errors + a few dimmed context lines",
"filter_warns": "Filter: warnings + a few dimmed context lines", "filter_warns": "Filter: warnings + a few dimmed context lines",
"filter_infos": "Filter: info lines + a few dimmed context lines",
"filter_oks": "Filter: OK lines + a few dimmed context lines",
"filter_clear": "Clear filter · show all lines", "filter_clear": "Clear filter · show all lines",
"filter_active_err": "Filter: errors + context", "filter_active_err": "Filter: errors + context",
"filter_active_warn": "Filter: warnings + context", "filter_active_warn": "Filter: warnings + context",
"filter_active_info": "Filter: info + context",
"filter_active_ok": "Filter: OK only + context",
"filter_show_all": "Show all", "filter_show_all": "Show all",
"info_one": "{n} info",
"info_many": "{n} info",
"ok_one": "{n} ok",
"ok_many": "{n} ok",
"env_context": "Monitoring env context", "env_context": "Monitoring env context",
"env_context_title": "Expand: agent env, suite pin, domain hosts & flags (from run header)", "env_context_title": "Expand: agent env, suite pin, domain hosts & flags (from run header)",
"env_context_empty": "No run header in log", "env_context_empty": "No run header in log",
"generated": "generated", "generated": "generated",
"version": "version", "version": "version",
"version_tree_link": "open this runs git tree (commit)", "version_tree_link": "open this runs git tree (commit)",
"suite_version_title": "taler-monitoring suite version · exact run tree",
"source": "source", "source": "source",
"what_monitors": "What this monitors", "what_monitors": "What this monitors",
@ -147,7 +157,7 @@ def ui(lang: str, key: str, **kwargs) -> str:
"mail_i1": "Outside-in phase mail: MX, SMTP/IMAP ports and handshakes, SPF/DMARC", "mail_i1": "Outside-in phase mail: MX, SMTP/IMAP ports and handshakes, SPF/DMARC",
"mail_i2": "Mail hosts: firefly.gnunet.org (taler.net/gnunet.org) and anastasis.taler-systems.com", "mail_i2": "Mail hosts: firefly.gnunet.org (taler.net/gnunet.org) and anastasis.taler-systems.com",
"mail_i3": "Reported on taler-monitoring-surface HTML (no separate /taler-monitoring-mail* page)", "mail_i3": "Reported on taler-monitoring-surface HTML (no separate /taler-monitoring-mail* page)",
"footer": "Console-style render of taler-monitoring output. Sticky bar: green = clean · yellow = warnings · red = errors. Click error/warning counts to filter the log (match lines plus a few dimmed neighbours for context). Monitoring env context (agent header) is collapsed by default. Commit pins the exact tree used for this run.", "footer": "Console-style render of taler-monitoring output. Sticky bar: green = clean · yellow = warnings · red = errors. Click error / warning / info (blue) / ok counts to filter the log (match lines plus dimmed neighbours). Suite version sits under the status pill. Monitoring env context is collapsed by default.",
"redirect_fail": "has failures.", "redirect_fail": "has failures.",
"redirect_see": "See {link} for the full console log, error index, and sticky status bar.", "redirect_see": "See {link} for the full console log, error index, and sticky status bar.",
"ago_s": "{n}s ago", "ago_s": "{n}s ago",
@ -168,16 +178,25 @@ def ui(lang: str, key: str, **kwargs) -> str:
"jump_first_warn": "Aller au premier avertissement", "jump_first_warn": "Aller au premier avertissement",
"filter_errors": "Filtrer : erreurs + quelques lignes de contexte grisées", "filter_errors": "Filtrer : erreurs + quelques lignes de contexte grisées",
"filter_warns": "Filtrer : avertissements + quelques lignes de contexte grisées", "filter_warns": "Filtrer : avertissements + quelques lignes de contexte grisées",
"filter_infos": "Filtrer : infos + quelques lignes de contexte grisées",
"filter_oks": "Filtrer : lignes OK + quelques lignes de contexte grisées",
"filter_clear": "Effacer le filtre · tout afficher", "filter_clear": "Effacer le filtre · tout afficher",
"filter_active_err": "Filtre : erreurs + contexte", "filter_active_err": "Filtre : erreurs + contexte",
"filter_active_warn": "Filtre : avertissements + contexte", "filter_active_warn": "Filtre : avertissements + contexte",
"filter_active_info": "Filtre : infos + contexte",
"filter_active_ok": "Filtre : OK seulement + contexte",
"filter_show_all": "Tout afficher", "filter_show_all": "Tout afficher",
"info_one": "{n} info",
"info_many": "{n} infos",
"ok_one": "{n} ok",
"ok_many": "{n} ok",
"env_context": "Contexte d'env. de monitoring", "env_context": "Contexte d'env. de monitoring",
"env_context_title": "Déplier : env agent, pin de suite, hôtes domaine et flags (en-tête d'exécution)", "env_context_title": "Déplier : env agent, pin de suite, hôtes domaine et flags (en-tête d'exécution)",
"env_context_empty": "Pas d'en-tête d'exécution dans le journal", "env_context_empty": "Pas d'en-tête d'exécution dans le journal",
"generated": "généré", "generated": "généré",
"version": "version", "version": "version",
"version_tree_link": "ouvrir l'arbre git de cette exécution (commit)", "version_tree_link": "ouvrir l'arbre git de cette exécution (commit)",
"suite_version_title": "version de la suite taler-monitoring · arbre exact de l'exécution",
"source": "source", "source": "source",
"what_monitors": "Ce que cette page contrôle", "what_monitors": "Ce que cette page contrôle",
@ -246,7 +265,7 @@ def ui(lang: str, key: str, **kwargs) -> str:
"mail_i1": "Phase outside-in mail : MX, ports SMTP/IMAP et handshakes, SPF/DMARC", "mail_i1": "Phase outside-in mail : MX, ports SMTP/IMAP et handshakes, SPF/DMARC",
"mail_i2": "Hôtes mail : firefly.gnunet.org (taler.net/gnunet.org) et anastasis.taler-systems.com", "mail_i2": "Hôtes mail : firefly.gnunet.org (taler.net/gnunet.org) et anastasis.taler-systems.com",
"mail_i3": "Rapporté dans le HTML taler-monitoring-surface (pas de page /taler-monitoring-mail*)", "mail_i3": "Rapporté dans le HTML taler-monitoring-surface (pas de page /taler-monitoring-mail*)",
"footer": "Rendu console de taler-monitoring. Barre collante : vert = OK · jaune = avertissements · rouge = erreurs. Compteurs = filtre. Contexte d'env. (en-tête agent) replié par défaut. Le commit fixe l'arbre exact de cette exécution.", "footer": "Rendu console de taler-monitoring. Barre collante : vert = OK · jaune = avertissements · rouge = erreurs. Compteurs erreur / avertissement / info (bleu) / ok = filtres. Version de la suite sous le badge d'état. Contexte d'env. replié par défaut.",
"redirect_fail": "a des échecs.", "redirect_fail": "a des échecs.",
"redirect_see": "Voir {link} pour le journal console complet, l'index d'erreurs et la barre de statut.", "redirect_see": "Voir {link} pour le journal console complet, l'index d'erreurs et la barre de statut.",
"ago_s": "il y a {n}s", "ago_s": "il y a {n}s",
@ -289,10 +308,14 @@ def is_numbered_check_line(line: str) -> bool:
def classify(line: str) -> str: def classify(line: str) -> str:
u = line.upper() u = line.upper()
low = line.lower() low = line.lower()
# SUMMARY box header only when the word SUMMARY is present (not phase section() boxes). # Real SUMMARY *box* only (╔/║/╚ frames or bare [ SUMMARY ] / [ RESUME ]).
# Phase section() also uses ╔═║╚ with blue BG_SEC — those must stay class "section". # Never match free text like "perf summary" / "ladder summary" (those are INFO checks).
if "SUMMARY" in u: if re.search(r"\bSUMMARY\b", u) or re.search(r"\bRESUME\b", u):
if re.search(r"[╔║╚]", line) or re.match(
r"^\s*\[\s*(SUMMARY|RESUME)\s*\]", u
):
return "sum-header" return "sum-header"
# else fall through to badge classifiers (INFO/OK/…)
# Double-line box frames from section() → section (no sum-* paint) # Double-line box frames from section() → section (no sum-* paint)
if re.match(r"^\s*[╔╚].*═", line) or re.match(r"^\s*║\s", line): if re.match(r"^\s*[╔╚].*═", line) or re.match(r"^\s*║\s", line):
return "section" return "section"
@ -534,12 +557,18 @@ def extract_errors(lines: list[str]) -> list[str]:
return out return out
def count_status(lines: list[str]) -> tuple[int, int, int | None, int | None]: def count_status(
"""Return (error_count, warn_count, first_error_idx, first_warn_idx).""" lines: list[str],
) -> tuple[int, int, int, int, int | None, int | None, int | None, int | None]:
"""Return (n_err, n_warn, n_info, n_ok, first_err, first_warn, first_info, first_ok)."""
n_err = 0 n_err = 0
n_warn = 0 n_warn = 0
n_info = 0
n_ok = 0
first_err: int | None = None first_err: int | None = None
first_warn: int | None = None first_warn: int | None = None
first_info: int | None = None
first_ok: int | None = None
for i, ln in enumerate(lines): for i, ln in enumerate(lines):
kind = classify(ln) kind = classify(ln)
if kind in ("error", "blocker"): if kind in ("error", "blocker"):
@ -552,7 +581,15 @@ def count_status(lines: list[str]) -> tuple[int, int, int | None, int | None]:
n_warn += 1 n_warn += 1
if first_warn is None: if first_warn is None:
first_warn = i first_warn = i
return n_err, n_warn, first_err, first_warn elif kind == "info":
n_info += 1
if first_info is None:
first_info = i
elif kind == "ok":
n_ok += 1
if first_ok is None:
first_ok = i
return n_err, n_warn, n_info, n_ok, first_err, first_warn, first_info, first_ok
def slug_error(i: int, text: str) -> str: def slug_error(i: int, text: str) -> str:
@ -965,6 +1002,19 @@ def is_log_noise_line(ln: str) -> bool:
return True return True
if "ihr branch ist" in low or "bereits auf" in low: if "ihr branch ist" in low or "bereits auf" in low:
return True return True
# Converter / host-agent noise that tee appends into the run log after SUMMARY
if "syntaxwarning" in low or "invalid escape sequence" in low:
return True
if "console_to_html.py:" in low:
return True
if re.match(r"^\s*if\s*\(/\^\[╔", s):
return True
if low.lstrip().startswith("wrote ") and "errors=" in low:
return True
if re.match(r"^html\s+\S+", low) and ("" in s or "->" in s):
return True
if "public url (expected)" in low:
return True
return False return False
@ -1567,7 +1617,8 @@ STICKY_JS = """
return true; return true;
} }
var t = (el.textContent || "").replace(/\u00a0/g, " ").trim(); var t = (el.textContent || "").replace(/\u00a0/g, " ").trim();
if (/^[\-\s]+$/.test(t)) return true; // box-drawing / pure chrome only (JS char class; escapes doubled for Python).
if (/^[\\s-]+$/.test(t)) return true;
return false; return false;
} }
var keep = new Array(lines.length); var keep = new Array(lines.length);
@ -1739,8 +1790,9 @@ def build_html(
# Split monitoring env context (collapsible, default closed) from check log # Split monitoring env context (collapsible, default closed) from check log
env_lines, body_raw, env_hint = extract_env_context(raw_lines) env_lines, body_raw, env_hint = extract_env_context(raw_lines)
# Sticky counts from full log; first-error anchors index into body only # Sticky counts from full log; first-error anchors index into body only
n_err, n_warn, _, _ = count_status(raw_lines) n_err, n_warn, _n_info, _n_ok, _, _, _, _ = count_status(raw_lines)
_, _, first_err_i, first_warn_i = count_status(body_raw) _e2, _w2, _i2, _o2, first_err_i, first_warn_i, _fi, _fo = count_status(body_raw)
del _e2, _w2, _i2, _o2, _fi, _fo, _n_info, _n_ok
errors = extract_errors(raw_lines) errors = extract_errors(raw_lines)
err_slugs: dict[str, str] = {} err_slugs: dict[str, str] = {}
@ -2227,7 +2279,8 @@ def main() -> None:
for l in log_text.splitlines() for l in log_text.splitlines()
if not is_log_noise_line(strip_ansi(l)) if not is_log_noise_line(strip_ansi(l))
] ]
n_err, n_warn, _, _ = count_status(raw_for_counts) n_err, n_warn, _n_info, _n_ok, _, _, _, _ = count_status(raw_for_counts)
del _n_info, _n_ok
if args.mode == "redirect": if args.mode == "redirect":
html_out = build_redirect_html( html_out = build_redirect_html(
@ -2262,7 +2315,12 @@ def main() -> None:
) )
atomic_write_text(args.out, html_out, encoding="utf-8") atomic_write_text(args.out, html_out, encoding="utf-8")
print(f"wrote {args.out} (errors={n_err} warnings={n_warn} level={status_level(n_err, n_warn)})") # stderr only — stdout is often teed into the suite mon log
print(
f"wrote {args.out} (errors={n_err} warnings={n_warn} "
f"level={status_level(n_err, n_warn)})",
file=sys.stderr,
)
if __name__ == "__main__": if __name__ == "__main__":