release 1.10.0: aptdeploy container overview board (active/errors)

This commit is contained in:
Hernâni Marques 2026-07-19 01:24:36 +02:00
parent 229ef9383e
commit 321df897ef
No known key found for this signature in database
GPG key ID: CB5738652768F7E9
4 changed files with 282 additions and 23 deletions

View file

@ -53,6 +53,9 @@ def ui(lang: str, key: str, **kwargs) -> str:
"apt_i5": "Checks: packages, taler-merchant-httpd --version / ldd, systemd unit, optional local /config probe — not public nginx/HTTPS",
"apt_i6": "HTML published only on taler.hacktivism.ch (this page host: {host})",
"apt_i7": "Timer: taler-monitoring-aptdeploy.timer (4h)",
"apt_i8": "Top board: per-container podman state, merchant version, httpd, ldd, OK/ERROR (see CONTAINER OVERVIEW)",
"apt_board_title": "Container overview — active pods & failures",
"apt_board_empty": "No board · lines in log (re-run aptdeploy with suite ≥ v1.10.0).",
"focus_bank": "Libeufin bank public HTTPS + container inside checks",
"focus_exchange": "Exchange public HTTPS + container inside checks",
"focus_merchant": "Merchant backend / SPA public HTTPS + container inside checks",
@ -125,6 +128,9 @@ def ui(lang: str, key: str, **kwargs) -> str:
"apt_i5": "Contrôles : paquets, taler-merchant-httpd --version / ldd, unité systemd, sonde locale /config optionnelle — pas de nginx/HTTPS public",
"apt_i6": "HTML publié uniquement sur taler.hacktivism.ch (hôte de cette page : {host})",
"apt_i7": "Minuterie : taler-monitoring-aptdeploy.timer (4 h)",
"apt_i8": "Tableau haut : état podman, version merchant, httpd, ldd, OK/ERROR par conteneur",
"apt_board_title": "Vue conteneurs — pods actifs et erreurs",
"apt_board_empty": "Pas de lignes board · dans le journal (relancer aptdeploy suite ≥ v1.10.0).",
"focus_bank": "Banque Libeufin HTTPS public + contrôles inside conteneur",
"focus_exchange": "Exchange HTTPS public + contrôles inside conteneur",
"focus_merchant": "Backend marchand / SPA HTTPS public + contrôles inside conteneur",
@ -210,6 +216,90 @@ def is_run_timeout_text(text: str) -> bool:
return "RUN.TIMEOUT" in u or "RUN_TIMEOUT" in u or "RUN TIMEOUT" in u
def extract_aptdeploy_board(log_text: str) -> list[dict[str, str]]:
"""Parse `board · short=… suite=… verdict=…` lines from check_apt_deploy."""
rows: list[dict[str, str]] = []
seen: set[str] = set()
for ln in strip_ansi(log_text or "").splitlines():
if "board ·" not in ln and " board · " not in ln:
# also match "board ·" after tid
if not re.search(r"\bboard\b.*\bshort=", ln):
continue
# detail after last "board ·" or "board "
m = re.search(r"\bboard\b\s*·\s*(.+)$", ln)
if not m:
m = re.search(r"\bboard\b\s+(.+)$", ln)
if not m:
continue
detail = m.group(1).strip()
if "short=" not in detail:
continue
rec: dict[str, str] = {}
for part in detail.split():
if "=" in part:
k, v = part.split("=", 1)
rec[k] = v
short = rec.get("short", "")
if not short or short in seen:
continue
seen.add(short)
rows.append(rec)
return rows
def aptdeploy_board_html(log_text: str, lang: str = "en") -> str:
"""Compact HTML table: which containers are active and which fail."""
rows = extract_aptdeploy_board(log_text)
title = ui(lang, "apt_board_title")
if not rows:
return (
f'<section class="apt-board empty" id="container-overview">\n'
f"<h2>{html.escape(title)}</h2>\n"
f"<p class=\"muted\">{html.escape(ui(lang, 'apt_board_empty'))}</p>\n"
f"</section>\n"
)
head = (
"<tr><th>short</th><th>suite</th><th>mode</th><th>podman</th>"
"<th>merchant</th><th>httpd</th><th>ldd</th><th>verdict</th><th>note</th></tr>"
)
body_parts = []
n_ok = n_err = n_miss = 0
for r in rows:
v = (r.get("verdict") or "?").upper()
if v == "OK":
n_ok += 1
vclass = "v-ok"
elif v == "MISSING":
n_miss += 1
vclass = "v-miss"
else:
n_err += 1
vclass = "v-err"
body_parts.append(
f'<tr class="{vclass}">'
f'<td class="short">{html.escape(r.get("short", ""))}</td>'
f'<td>{html.escape(r.get("suite", ""))}</td>'
f'<td>{html.escape(r.get("mode", ""))}</td>'
f'<td>{html.escape(r.get("state", ""))}</td>'
f'<td class="mono">{html.escape(r.get("merchant", ""))}</td>'
f'<td>{html.escape(r.get("httpd", ""))}</td>'
f'<td>{html.escape(r.get("ldd", ""))}</td>'
f'<td class="verdict">{html.escape(r.get("verdict", ""))}</td>'
f'<td class="note">{html.escape(r.get("note", ""))}</td>'
f"</tr>"
)
summary = f"{len(rows)} containers · {n_ok} OK · {n_err} ERROR · {n_miss} missing"
return (
f'<section class="apt-board" id="container-overview">\n'
f"<h2>{html.escape(title)}</h2>\n"
f'<p class="board-summary">{html.escape(summary)}</p>\n'
f'<div class="board-scroll"><table class="apt-board-table">\n'
f"<thead>{head}</thead>\n<tbody>\n"
+ "\n".join(body_parts)
+ "\n</tbody></table></div>\n</section>\n"
)
def is_summary_error_line(ln: str) -> bool:
"""Skip summary/header lines that contain ERROR but are not checks."""
low = ln.lower()
@ -412,7 +502,21 @@ def monitoring_scope(
ui(lang, "apt_i5"),
ui(lang, "apt_i6", host=host),
ui(lang, "apt_i7"),
ui(lang, "apt_i8"),
] + _pages_items(lang, host, label)
# Prefer live board verdicts in the scope list when log has board · lines
board = extract_aptdeploy_board(log_text or "")
if board:
live = []
for r in board:
v = r.get("verdict", "?")
mark = "OK" if v.upper() == "OK" else ("MISS" if v.upper() == "MISSING" else "ERR")
live.append(
f"{mark} {r.get('short', '?')}: pod={r.get('state', '?')} "
f"merchant={r.get('merchant', '?')} httpd={r.get('httpd', '?')} "
f"ldd={r.get('ldd', '?')}"
)
items = live + items
return {
"kind": "aptdeploy",
"lang": lang,
@ -902,6 +1006,19 @@ def build_html(
body_lines.append(render_line(ln, tid_to_slug, extra or None))
err_nav = ""
# Aptdeploy: container board first (active pods + per-pod verdict)
_is_apt = "aptdeploy" in (page_label or "").lower() or "apt-deploy" in (
page_label or ""
).lower()
if _is_apt:
_lang_board = ui_lang()
if re.search(r"lefrancpaysan|francpaysan", hostname + "\n" + log_text, re.I):
import os
if not (os.environ.get("TALER_MON_LANG") or "").strip():
_lang_board = "fr"
err_nav += aptdeploy_board_html(log_text, lang=_lang_board)
if errors and mode == "err":
items = []
timeout_items = []
@ -971,7 +1088,13 @@ def build_html(
suite_version_url=suite_version_url,
)
kind = "surface" if "surface" in (page_label or "") else "monitoring"
pl = (page_label or "").lower()
if "aptdeploy" in pl or "apt-deploy" in pl:
kind = "aptdeploy"
elif "surface" in pl:
kind = "surface"
else:
kind = "monitoring"
return f"""<!DOCTYPE html>
<html lang="{html.escape(lang if "lang" in dir() else "en")}">
<head>
@ -1023,6 +1146,36 @@ main {{ padding: 12px 14px 48px; max-width: 1200px; }}
outline: 1px solid #a44; background: #1a0808;
padding: 4px 6px; margin: 4px 0; border-radius: 3px;
}}
/* aptdeploy container overview board */
.apt-board {{
background: var(--panel); border: 1px solid #345;
padding: 10px 12px; margin-bottom: 14px; border-radius: 4px;
}}
.apt-board h2 {{ margin: 0 0 6px; font-size: 13px; color: var(--info); }}
.apt-board .board-summary {{ margin: 0 0 8px; color: var(--dim); font-size: 12px; }}
.apt-board.empty p.muted {{ color: var(--dim); margin: 0; font-size: 12px; }}
.board-scroll {{ overflow-x: auto; }}
.apt-board-table {{
width: 100%; border-collapse: collapse; font-size: 12px;
min-width: 720px;
}}
.apt-board-table th {{
text-align: left; color: #9ab; font-weight: 700; font-size: 11px;
border-bottom: 1px solid var(--border); padding: 4px 8px;
}}
.apt-board-table td {{
padding: 5px 8px; border-bottom: 1px solid #1a1a1a; vertical-align: top;
}}
.apt-board-table td.mono {{ font-variant-numeric: tabular-nums; }}
.apt-board-table td.short {{ font-weight: 700; color: #eee; }}
.apt-board-table td.verdict {{ font-weight: 800; letter-spacing: 0.04em; }}
.apt-board-table tr.v-ok td.verdict {{ color: var(--ok); }}
.apt-board-table tr.v-err td.verdict {{ color: var(--err); }}
.apt-board-table tr.v-miss td.verdict {{ color: var(--warn); }}
.apt-board-table tr.v-err {{ background: #1a0a0a; }}
.apt-board-table tr.v-ok {{ background: #0a120e; }}
.apt-board-table tr.v-miss {{ background: #1a1608; }}
.apt-board-table td.note {{ color: var(--dim); max-width: 14rem; }}
.console {{
background: #0a0a0a; border: 1px solid var(--border);
border-radius: 4px; padding: 8px 10px;