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:
parent
db6722e40f
commit
e67ede881c
4 changed files with 251 additions and 5 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue