release 1.12.0: collapsible monitoring env context

Agent run header (env_file, suite pin, target domain, hosts, flags) is
shown in a <details> block above the console, collapsed by default.
This commit is contained in:
Hernâni Marques 2026-07-19 02:23:17 +02:00
parent db6722e40f
commit 284fe34bc7
No known key found for this signature in database
GPG key ID: CB5738652768F7E9
4 changed files with 251 additions and 5 deletions

View file

@ -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 `<details>` 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.

View file

@ -1 +1 @@
1.11.0
1.12.0

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 |
|-----|------------|--------|
| **v1.12.0** | 2026-07-19 | **Feature:** collapsible **monitoring env context** (agent header: env_file, suite pin, domain/hosts/flags) — `<details>` 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. |

View file

@ -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 <details>: collapsed by default (no open attr)
return f"""<details class="env-context" id="mon-env-context">
<summary class="env-context-summary" title="{title}">
<span class="env-context-chevron" aria-hidden="true"></span>
<span class="env-context-label">{label}</span>
<span class="env-context-hint">{hint_esc}</span>
</summary>
<div class="env-context-body" role="region" aria-label="{label}">
{body} </div>
</details>
"""
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}
<main>
{err_nav}
{env_html}
<div class="console" id="mon-console">
{"".join(body_lines)}
</div>