From e67ede881ca99ddc4a7a8a9c2a3c6f0fdae5fa76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Sun, 19 Jul 2026 02:23:17 +0200 Subject: [PATCH] release 1.12.0: collapsible monitoring env context Agent run header (env_file, suite pin, target domain, hosts, flags) is shown in a
block above the console, collapsed by default. --- README.md | 2 + VERSION | 2 +- VERSIONS.md | 1 + site-gen/console_to_html.py | 251 +++++++++++++++++++++++++++++++++++- 4 files changed, 251 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5f21082..9693623 100644 --- a/README.md +++ b/README.md @@ -425,4 +425,6 @@ If pages are missing publicly: `sudo /home/hernani/koopa-caddy/apply-monitoring- The sticky bar shows the suite **tag** (e.g. `v1.11.0`) and links to that tag on Forgejo. +**v1.12.0:** Run header (**monitoring env context**: env_file, suite pin, domain/hosts/flags) is a collapsed `
` block above the console log — expand to inspect agent context. + **v1.11.0:** Click the sticky **error** or **warning** count to filter the console log to only those lines (other log lines hide; sticky bar, stats, and top overviews stay). Same via `#filter-error` / `#filter-warn` (or `#first-error` / `#first-warn`). Click again or **Show all** to clear. diff --git a/VERSION b/VERSION index 1cac385..0eed1a2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.11.0 +1.12.0 diff --git a/VERSIONS.md b/VERSIONS.md index 9ee308c..6b9c2a7 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -17,6 +17,7 @@ Git tags: `vMAJOR.FEATURE.FIX` (e.g. `v1.8.0`). File `VERSION` omits the `v` pre | Tag | Date (UTC) | Notes | |-----|------------|--------| +| **v1.12.0** | 2026-07-19 | **Feature:** collapsible **monitoring env context** (agent header: env_file, suite pin, domain/hosts/flags) — `
` default closed; summary shows agent · domain · suite. | | **v1.11.0** | 2026-07-19 | **Feature:** sticky **error/warn filter** — click counts to show only error or only warn log lines; sticky bar + overviews stay; `#filter-error` / `#filter-warn` (+ `#first-*` still works). | | **v1.10.11** | 2026-07-19 | **Bugfix:** surface/aptdeploy wrappers **force** MON_HOSTS+PHASES+MONPAGES_INVENTORY (env cannot expand job to bank/exchange); wrap more mon knobs. | | **v1.10.10** | 2026-07-19 | **Bugfix:** monpages bare code 21 is WARN again by default (slash form remains ERROR); bare redir still needs live Caddy `redir *` — set MONPAGES_BARE_STRICT=1 to enforce. | diff --git a/site-gen/console_to_html.py b/site-gen/console_to_html.py index 3428b34..79d0617 100755 --- a/site-gen/console_to_html.py +++ b/site-gen/console_to_html.py @@ -37,6 +37,9 @@ def ui(lang: str, key: str, **kwargs) -> str: "filter_active_err": "Filter: errors only", "filter_active_warn": "Filter: warnings only", "filter_show_all": "Show all", + "env_context": "Monitoring env context", + "env_context_title": "Expand: agent env, suite pin, domain hosts & flags (from run header)", + "env_context_empty": "No run header in log", "generated": "generated", "version": "version", @@ -100,7 +103,7 @@ def ui(lang: str, key: str, **kwargs) -> str: "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_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 (other lines hidden; sticky + overviews stay). 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 counts to filter the log. Monitoring env context (agent header) is collapsed by default. Commit pins the exact tree used for this run.", "redirect_fail": "has failures.", "redirect_see": "See {link} for the full console log, error index, and sticky status bar.", "ago_s": "{n}s ago", @@ -125,6 +128,9 @@ def ui(lang: str, key: str, **kwargs) -> str: "filter_active_err": "Filtre : erreurs seulement", "filter_active_warn": "Filtre : avertissements seulement", "filter_show_all": "Tout afficher", + "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_empty": "Pas d'en-tête d'exécution dans le journal", "generated": "généré", "version": "version", @@ -188,7 +194,7 @@ def ui(lang: str, key: str, **kwargs) -> str: "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_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. Cliquer sur les compteurs erreur/avertissement filtre le journal (le reste est masqué ; barre et vues d'ensemble restent). 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 = filtre. Contexte d'env. (en-tête agent) replié par défaut. Le commit fixe l'arbre exact de cette exécution.", "redirect_fail": "a des échecs.", "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", @@ -649,6 +655,172 @@ def monitoring_scope( } +def is_env_context_start(ln: str) -> bool: + """Host-agent / suite banner that opens the monitoring env header.""" + s = ln.strip() + if re.match(r"^={4,}\s*.+\s*={4,}$", s): + return True + if s.startswith("env_file="): + return True + return False + + +def is_env_context_line(ln: str) -> bool: + """ + Lines belonging to the pre-check monitoring env header + (agent banner, suite pin, target domain / flags / timeout). + """ + s = ln.rstrip("\n") + if not s.strip(): + return True # blank lines inside header block + t = s.strip() + if is_env_context_start(s): + return True + # agent header keys + if t.startswith( + ( + "env_file=", + "domain=", + "hosts=", + "flags ", + "suite ", + "using suite", + "commit=", + "target domain=", + "note:", + "WARN: Forgejo", + "WARN suite-update", + ) + ): + return True + if t.startswith(" dir=") or t.startswith("dir="): + return True + # indented target-domain block from taler-monitoring.sh + if re.match( + r"^\s+(bank|exchange|merchant|currency|phases|flags|access|run_timeout|progress)\b", + s, + ): + return True + # re-applying overlay chatter still part of startup + if "suite-overlay" in t.lower() and ( + t.startswith("WARN") or t.startswith(" ") or "re-applying" in t.lower() + ): + return True + return False + + +def is_env_context_end(ln: str) -> bool: + """First line that is clearly past the env header (phases / checks start).""" + s = ln.strip() + if not s: + return False + # box headers / section titles for real check phases + if s.startswith("╔") or s.startswith("║") or s.startswith("╚"): + return True + if s.startswith("┌") or s.startswith("└") or s.startswith("["): + return True + if re.match(r"^==\s+", s) or re.match(r"^--\s+", s): + return True + if "numbered checks" in s.lower(): + return True + if re.match(r"^\s*\d+%\s", s): # bare progress + return True + return False + + +def extract_env_context(lines: list[str]) -> tuple[list[str], list[str], str]: + """ + Split leading monitoring env header from the rest of the log. + Returns (env_lines, body_lines, hint_summary). + """ + if not lines: + return [], [], "" + # Find start: first env-context start within the first ~40 lines + start = None + for i, ln in enumerate(lines[:40]): + if is_env_context_start(ln): + start = i + break + if start is None: + # no banner — maybe only "target domain=" block at top (CLI without agent) + for i, ln in enumerate(lines[:15]): + if ln.strip().startswith("target domain=") or ln.strip().startswith( + "env_file=" + ): + start = i + break + if start is None: + return [], lines, "" + + end = start + for j in range(start, len(lines)): + ln = lines[j] + if j > start and is_env_context_end(ln): + end = j + break + if j > start and not is_env_context_line(ln) and ln.strip(): + # non-empty line that doesn't look like env → end before it + end = j + break + end = j + 1 + else: + end = len(lines) + + env = lines[start:end] + # trim trailing blanks from env + while env and not env[-1].strip(): + env.pop() + body = lines[:start] + lines[end:] + # hint: suite + domain + phases + hint_parts: list[str] = [] + for ln in env: + t = ln.strip() + if t.startswith("suite "): + hint_parts.append(t.split(" ")[0][:48]) + elif t.startswith("domain="): + hint_parts.append(t.replace("domain=", "").split("·")[0].strip()[:40]) + elif t.startswith("target domain="): + if not any("domain" in h for h in hint_parts): + hint_parts.append(t.split("=", 1)[1].strip()[:40]) + elif re.match(r"^={4,}", t): + # agent label between equals + m = re.match(r"^={4,}\s*(.+?)\s*={4,}$", t) + if m: + lab = m.group(1) + # drop timestamp prefix if present + lab2 = re.sub(r"^\d{4}-\d{2}-\d{2}T\S+\s+", "", lab) + hint_parts.insert(0, lab2[:40]) + hint = " · ".join(hint_parts[:3]) if hint_parts else f"{len(env)} lines" + return env, body, hint + + +def env_context_html( + env_lines: list[str], + *, + tid_to_slug: dict[str, str], + lang: str, + hint: str, +) -> str: + """Collapsible monitoring env context (default closed).""" + if not env_lines: + return "" + body = "".join(render_line(ln, tid_to_slug, None) for ln in env_lines) + label = html.escape(ui(lang, "env_context")) + title = html.escape(ui(lang, "env_context_title")) + hint_esc = html.escape(hint or "") + # native
: collapsed by default (no open attr) + return f"""
+ + + {label} + {hint_esc} + +
+{body}
+
+""" + + def is_log_noise_line(ln: str) -> bool: """Drop git/update chatter that must not appear in public mon HTML.""" s = ln.strip() @@ -909,7 +1081,70 @@ body.filter-warn .filter-chip { border-color: #a68b2d; background: #1a1608; colo /* Console filter: sticky + overviews stay; hide non-matching log lines */ body.filter-error #mon-console .line:not(.error):not(.blocker) { display: none !important; } body.filter-warn #mon-console .line:not(.warn) { display: none !important; } +body.filter-error .env-context, +body.filter-warn .env-context { display: none !important; } body.filter-warn .err-top { display: none; } +/* v1.12.0: collapsible monitoring env context (default closed) */ +.env-context { + margin: 0 0 12px; + max-width: 1200px; + border: 1px solid #333; + border-radius: 4px; + background: #0a0a0a; +} +.env-context-summary { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + list-style: none; + padding: 8px 12px; + font-size: 12px; + color: #ccc; + user-select: none; +} +.env-context-summary::-webkit-details-marker { display: none; } +.env-context-summary:hover { background: #121212; color: #fff; } +.env-context[open] .env-context-summary { + border-bottom: 1px solid #2a2a2a; + background: #101010; +} +.env-context-chevron { + display: inline-block; + width: 0; height: 0; + border-left: 5px solid #888; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + transition: transform 0.12s ease; + flex-shrink: 0; +} +.env-context[open] .env-context-chevron { + transform: rotate(90deg); + border-left-color: #ccc; +} +.env-context-label { + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; + font-size: 11px; + color: #9ab; + white-space: nowrap; +} +.env-context-hint { + color: #777; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} +.env-context-body { + padding: 8px 12px 10px; + white-space: pre-wrap; + word-break: break-word; + font-size: 12px; + max-height: min(50vh, 28rem); + overflow: auto; +} .version-link { font-weight: 700; font-size: 12px; color: var(--info); text-decoration: none; padding: 1px 8px; border-radius: 3px; @@ -1206,7 +1441,11 @@ def build_html( while raw_lines and not raw_lines[-1].strip(): raw_lines.pop() - n_err, n_warn, first_err_i, first_warn_i = count_status(raw_lines) + # Split monitoring env context (collapsible, default closed) from check log + env_lines, body_raw, env_hint = extract_env_context(raw_lines) + # Sticky counts from full log; first-error anchors index into body only + n_err, n_warn, _, _ = count_status(raw_lines) + _, _, first_err_i, first_warn_i = count_status(body_raw) errors = extract_errors(raw_lines) err_slugs: dict[str, str] = {} @@ -1224,7 +1463,7 @@ def build_html( # Build body with anchors for first error/warn + per-error slugs body_lines: list[str] = [] claimed_err: set[str] = set() - for idx, ln in enumerate(raw_lines): + for idx, ln in enumerate(body_raw): extra: list[str] = [] if first_err_i is not None and idx == first_err_i: extra.append("first-error") @@ -1315,6 +1554,9 @@ def build_html( page_lang = lang footer_txt = ui(lang, "footer") + env_html = env_context_html( + env_lines, tid_to_slug=tid_to_slug, lang=lang, hint=env_hint + ) bar = sticky_bar_html( generated_at=generated_at, generated_iso=generated_iso, @@ -1447,6 +1689,7 @@ footer {{ {bar}
{err_nav} +{env_html}
{"".join(body_lines)}