release 1.16.0: mon sticky info/ok filters + suite version under pill

This commit is contained in:
Hernâni Marques 2026-07-19 05:49:42 +02:00
parent 6dd30ce4b8
commit 2d7af7a960
No known key found for this signature in database
GPG key ID: CB5738652768F7E9
3 changed files with 149 additions and 43 deletions

View file

@ -1 +1 @@
1.15.8
1.16.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.16.0** | 2026-07-19 | **Feature:** mon sticky — blue **info** + green **ok** filter badges; suite **version tag under status pill** (not blue badge); filters `#filter-info` / `#filter-ok`. |
| **v1.15.8** | 2026-07-19 | **Bugfix:** mon HTML filter JS — drop literal box-drawing / `\-` regex that caused Python SyntaxWarning (and weird glyphs in suite logs); use Unicode code-point loop instead. |
| **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. |

View file

@ -1035,6 +1035,8 @@ def sticky_bar_html(
scope: dict[str, object] | None = None,
suite_version: str = "",
suite_version_url: str = "",
n_info: int = 0,
n_ok: int = 0,
) -> str:
level = status_level(n_err, n_warn)
lang = "en"
@ -1082,6 +1084,30 @@ def sticky_bar_html(
warn_lbl = ui(lang, "warn_many", n=n_warn)
warn_stat = f'<span class="stat warn muted">{html.escape(warn_lbl)}</span>'
# Blue badge = INFO filter (v1.16.0)
if n_info:
info_lbl = ui(lang, "info_one", n=n_info) if n_info == 1 else ui(lang, "info_many", n=n_info)
info_stat = (
f'<a class="stat info" href="#filter-info" data-filter="info" '
f'role="button" aria-pressed="false" '
f'title="{html.escape(ui(lang, "filter_infos"))}">{html.escape(info_lbl)}</a>'
)
else:
info_lbl = ui(lang, "info_many", n=n_info)
info_stat = f'<span class="stat info muted">{html.escape(info_lbl)}</span>'
# Green count badge = OK-only filter
if n_ok:
ok_lbl = ui(lang, "ok_one", n=n_ok) if n_ok == 1 else ui(lang, "ok_many", n=n_ok)
ok_stat = (
f'<a class="stat ok" href="#filter-ok" data-filter="ok" '
f'role="button" aria-pressed="false" '
f'title="{html.escape(ui(lang, "filter_oks"))}">{html.escape(ok_lbl)}</a>'
)
else:
ok_lbl = ui(lang, "ok_many", n=n_ok)
ok_stat = f'<span class="stat ok muted">{html.escape(ok_lbl)}</span>'
filter_chip = (
f'<span class="filter-chip" id="filter-chip" hidden>'
f'<span class="filter-chip-label" id="filter-chip-label"></span>'
@ -1094,6 +1120,8 @@ def sticky_bar_html(
filter_i18n = (
f'data-i18n-filter-err="{html.escape(ui(lang, "filter_active_err"))}" '
f'data-i18n-filter-warn="{html.escape(ui(lang, "filter_active_warn"))}" '
f'data-i18n-filter-info="{html.escape(ui(lang, "filter_active_info"))}" '
f'data-i18n-filter-ok="{html.escape(ui(lang, "filter_active_ok"))}" '
f'data-i18n-filter-clear="{html.escape(ui(lang, "filter_clear"))}"'
)
@ -1116,22 +1144,24 @@ def sticky_bar_html(
f" · <code>{html.escape(suite_path)}</code>" if suite_path else ""
)
# Version badge (linked to Forgejo tag when available)
# Suite version tag under status pill (not the blue filter badge) — v1.16.0
ver = (suite_version or "").strip()
ver_url = (suite_version_url or "").strip()
if ver:
ver_cls = "suite-ver-tag" + ("" if level == "green" else " dim")
ver_title = html.escape(
ui(lang, "suite_version_title") + " · " + ver
+ (" · " + ui(lang, "version_tree_link") if ver_url else "")
)
if ver_url:
# href is usually src/commit/<sha> (exact run tree); label stays vX.Y.Z
version_html = (
f'<a class="version-link" href="{html.escape(ver_url)}" '
f'title="{html.escape(ui(lang, "version"))} {html.escape(ver)}'
f' · {html.escape(ui(lang, "version_tree_link"))}" '
f'rel="noopener noreferrer">{html.escape(ver)}</a>'
version_tag = (
f'<a class="{ver_cls}" href="{html.escape(ver_url)}" '
f'title="{ver_title}" rel="noopener noreferrer">{html.escape(ver)}</a>'
)
else:
version_html = f'<span class="version-link muted">{html.escape(ver)}</span>'
version_tag = f'<span class="{ver_cls}" title="{ver_title}">{html.escape(ver)}</span>'
else:
version_html = ""
version_tag = ""
# Localize page label for sticky host title (monitoring → surveillance in FR)
pl = (page_label or "monitoring").lower().replace("_", "-")
if "surface" in pl:
@ -1142,21 +1172,27 @@ def sticky_bar_html(
pl_disp = ui(lang, "page_label_monitoring")
mode_key = f"mode_{mode}" if mode in ("ok", "err", "redirect") else ""
mode_disp = ui(lang, mode_key) if mode_key and ui(lang, mode_key) != mode_key else mode.upper()
# Compact sticky row always visible; "Was geprüft wird" expands inside sticky-bar
# Compact sticky row: status stack (pill + suite version under OK/WARN/ERRORS)
return f"""
<div class="sticky-bar sticky-{level}" id="status-bar" role="status"
data-errors="{n_err}" data-warnings="{n_warn}" data-level="{level}"
data-errors="{n_err}" data-warnings="{n_warn}" data-info="{n_info}" data-ok="{n_ok}"
data-level="{level}"
data-scope-kind="{scope_kind}" {filter_i18n}>
<div class="sticky-bar-row primary">
<span class="status-pill sticky-{level}">{html.escape(status_txt)}</span>
<span class="status-stack">
<span class="status-pill sticky-{level}">{html.escape(status_txt)}</span>
{version_tag}
</span>
<span class="host">{html.escape(pl_disp)} · {html.escape(hostname)}</span>
<span class="sep">·</span>
{err_stat}
<span class="sep">·</span>
{warn_stat}
{filter_chip}
<span class="sep">·</span>
{version_html}
{info_stat}
<span class="sep">·</span>
{ok_stat}
{filter_chip}
<span class="sep">·</span>
<span class="generated"
data-generated-iso="{html.escape(generated_iso)}"
@ -1228,6 +1264,18 @@ STICKY_CSS = """
.status-pill.sticky-green { color: var(--ok); border-color: #1f6b3a; background: #0a1a10; }
.status-pill.sticky-yellow { color: var(--warn); border-color: #a68b2d; background: #1a1608; }
.status-pill.sticky-red { color: var(--err); border-color: #a33; background: #1a0a0a; }
.status-stack {
display: inline-flex; flex-direction: column; align-items: flex-start;
gap: 2px; margin-right: 2px;
}
.suite-ver-tag {
display: inline-block; font-size: 10px; font-weight: 700;
letter-spacing: 0.04em; padding: 0 6px; border-radius: 2px;
color: var(--ok); border: 1px solid #1f6b3a; background: #0a1a10;
text-decoration: none; line-height: 1.4;
}
.suite-ver-tag.dim { color: #9ab; border-color: #345; background: #0a1018; opacity: 0.9; }
.suite-ver-tag:hover { filter: brightness(1.15); text-decoration: underline; }
.host { font-weight: 600; color: #eee; font-size: 13px; }
.sep { color: var(--dim); }
.stat {
@ -1236,6 +1284,8 @@ STICKY_CSS = """
}
.stat.err { color: var(--err); border-color: #522; background: #1a0a0a; }
.stat.warn { color: var(--warn); border-color: #664; background: #1a1608; }
.stat.info { color: var(--info); border-color: #234; background: #0a1520; }
.stat.ok { color: var(--ok); border-color: #1f6b3a; background: #0a1a10; }
.stat.muted { opacity: 0.55; font-weight: 600; }
a.stat:hover { text-decoration: underline; filter: brightness(1.15); }
a.stat[aria-pressed="true"] {
@ -1251,6 +1301,8 @@ a.stat[aria-pressed="true"] {
.filter-chip[hidden] { display: none !important; }
body.filter-error .filter-chip { border-color: #a33; background: #1a0a0a; color: var(--err); }
body.filter-warn .filter-chip { border-color: #a68b2d; background: #1a1608; color: var(--warn); }
body.filter-info .filter-chip { border-color: #246; background: #0a1520; color: var(--info); }
body.filter-ok .filter-chip { border-color: #1f6b3a; background: #0a1a10; color: var(--ok); }
.filter-clear {
font: inherit; font-size: 11px; font-weight: 700;
cursor: pointer; color: inherit;
@ -1261,7 +1313,9 @@ body.filter-warn .filter-chip { border-color: #a68b2d; background: #1a1608; colo
/* Console filter: sticky + overviews stay; match lines + dimmed neighbours (v1.13.11+)
v1.15.2: stamp out black voids hide blanks, zero-size non-matches, compact console */
body.filter-error #mon-console,
body.filter-warn #mon-console {
body.filter-warn #mon-console,
body.filter-info #mon-console,
body.filter-ok #mon-console {
padding: 6px 8px;
min-height: 0;
display: flex;
@ -1269,7 +1323,9 @@ body.filter-warn #mon-console {
gap: 1px;
}
body.filter-error #mon-console .line:not(.error):not(.blocker):not(.filter-ctx),
body.filter-warn #mon-console .line:not(.warn):not(.filter-ctx) {
body.filter-warn #mon-console .line:not(.warn):not(.filter-ctx),
body.filter-info #mon-console .line:not(.info):not(.filter-ctx),
body.filter-ok #mon-console .line:not(.ok):not(.filter-ctx) {
display: none !important;
height: 0 !important;
margin: 0 !important;
@ -1281,13 +1337,19 @@ body.filter-warn #mon-console .line:not(.warn):not(.filter-ctx) {
/* empty / whitespace-only lines never take space in filter mode */
body.filter-error #mon-console .line:empty,
body.filter-warn #mon-console .line:empty,
body.filter-info #mon-console .line:empty,
body.filter-ok #mon-console .line:empty,
body.filter-error #mon-console .line.line-blank,
body.filter-warn #mon-console .line.line-blank {
body.filter-warn #mon-console .line.line-blank,
body.filter-info #mon-console .line.line-blank,
body.filter-ok #mon-console .line.line-blank {
display: none !important;
}
/* Context lines: grayed / muted so the warn|error still stands out */
/* Context lines: grayed / muted so the match still stands out */
body.filter-error #mon-console .line.filter-ctx,
body.filter-warn #mon-console .line.filter-ctx {
body.filter-warn #mon-console .line.filter-ctx,
body.filter-info #mon-console .line.filter-ctx,
body.filter-ok #mon-console .line.filter-ctx {
opacity: 0.55;
color: #8a8a8a !important;
filter: grayscale(0.85);
@ -1297,19 +1359,25 @@ body.filter-warn #mon-console .line.filter-ctx {
background: transparent !important;
}
body.filter-error #mon-console .line.filter-ctx a.jump,
body.filter-warn #mon-console .line.filter-ctx a.jump {
body.filter-warn #mon-console .line.filter-ctx a.jump,
body.filter-info #mon-console .line.filter-ctx a.jump,
body.filter-ok #mon-console .line.filter-ctx a.jump {
color: #6a6a6a !important;
}
/* Match lines stay tight */
body.filter-error #mon-console .line.error,
body.filter-error #mon-console .line.blocker,
body.filter-warn #mon-console .line.warn {
body.filter-warn #mon-console .line.warn,
body.filter-info #mon-console .line.info,
body.filter-ok #mon-console .line.ok {
margin: 1px 0 !important;
padding: 3px 6px !important;
}
/* Compact ellipsis between distant clusters (not a black slab) */
body.filter-error #mon-console .filter-gap,
body.filter-warn #mon-console .filter-gap {
body.filter-warn #mon-console .filter-gap,
body.filter-info #mon-console .filter-gap,
body.filter-ok #mon-console .filter-gap {
display: block;
color: #666;
text-align: center;
@ -1323,10 +1391,14 @@ body.filter-warn #mon-console .filter-gap {
background: transparent;
height: auto;
}
body:not(.filter-error):not(.filter-warn) #mon-console .filter-gap { display: none !important; }
body:not(.filter-error):not(.filter-warn):not(.filter-info):not(.filter-ok) #mon-console .filter-gap { display: none !important; }
body.filter-error .env-context,
body.filter-warn .env-context { display: none !important; }
body.filter-warn .err-top { display: none; }
body.filter-warn .env-context,
body.filter-info .env-context,
body.filter-ok .env-context { display: none !important; }
body.filter-warn .err-top,
body.filter-info .err-top,
body.filter-ok .err-top { display: none; }
/* secondary jump anchors: never affect layout */
#mon-console .anchor-only {
display: inline;
@ -1576,6 +1648,8 @@ STICKY_JS = """
function currentFilter() {
if (document.body.classList.contains("filter-error")) return "error";
if (document.body.classList.contains("filter-warn")) return "warn";
if (document.body.classList.contains("filter-info")) return "info";
if (document.body.classList.contains("filter-ok")) return "ok";
return "";
}
@ -1601,6 +1675,8 @@ STICKY_JS = """
return el.classList.contains("error") || el.classList.contains("blocker");
}
if (mode === "warn") return el.classList.contains("warn");
if (mode === "info") return el.classList.contains("info");
if (mode === "ok") return el.classList.contains("ok");
return false;
}
function isBlankLine(el) {
@ -1676,11 +1752,14 @@ STICKY_JS = """
function setFilter(mode, opts) {
opts = opts || {};
document.body.classList.remove("filter-error", "filter-warn");
document.body.classList.remove("filter-error", "filter-warn", "filter-info", "filter-ok");
var errA = document.querySelector('a.stat.err[data-filter="error"]');
var warnA = document.querySelector('a.stat.warn[data-filter="warn"]');
if (errA) errA.setAttribute("aria-pressed", "false");
if (warnA) warnA.setAttribute("aria-pressed", "false");
var infoA = document.querySelector('a.stat.info[data-filter="info"]');
var okA = document.querySelector('a.stat.ok[data-filter="ok"]');
[errA, warnA, infoA, okA].forEach(function (a) {
if (a) a.setAttribute("aria-pressed", "false");
});
if (mode === "error") {
document.body.classList.add("filter-error");
@ -1688,11 +1767,19 @@ STICKY_JS = """
} else if (mode === "warn") {
document.body.classList.add("filter-warn");
if (warnA) warnA.setAttribute("aria-pressed", "true");
} else if (mode === "info") {
document.body.classList.add("filter-info");
if (infoA) infoA.setAttribute("aria-pressed", "true");
} else if (mode === "ok") {
document.body.classList.add("filter-ok");
if (okA) okA.setAttribute("aria-pressed", "true");
}
// Mark dimmed neighbours (or clear when filter off)
if (mode === "error" || mode === "warn") markFilterContext(mode);
else clearFilterContext();
if (mode === "error" || mode === "warn" || mode === "info" || mode === "ok") {
markFilterContext(mode);
} else {
clearFilterContext();
}
if (chip && chipLabel && bar) {
if (mode === "error") {
@ -1701,6 +1788,12 @@ STICKY_JS = """
} else if (mode === "warn") {
chip.hidden = false;
chipLabel.textContent = bar.getAttribute("data-i18n-filter-warn") || "Filter: warnings + context";
} else if (mode === "info") {
chip.hidden = false;
chipLabel.textContent = bar.getAttribute("data-i18n-filter-info") || "Filter: info + context";
} else if (mode === "ok") {
chip.hidden = false;
chipLabel.textContent = bar.getAttribute("data-i18n-filter-ok") || "Filter: OK only + context";
} else {
chip.hidden = true;
chipLabel.textContent = "";
@ -1709,24 +1802,28 @@ STICKY_JS = """
if (opts.updateHash !== false) {
var want = mode === "error" ? "#filter-error"
: mode === "warn" ? "#filter-warn" : "";
: mode === "warn" ? "#filter-warn"
: mode === "info" ? "#filter-info"
: mode === "ok" ? "#filter-ok" : "";
if (want) {
if (location.hash !== want) {
try { history.replaceState(null, "", want); } catch (e) { location.hash = want; }
}
} else if (location.hash === "#filter-error" || location.hash === "#filter-warn"
|| location.hash === "#first-error" || location.hash === "#first-warn") {
} else if (/^#(filter-error|filter-warn|filter-info|filter-ok|first-error|first-warn)$/.test(location.hash || "")) {
try { history.replaceState(null, "", location.pathname + location.search); } catch (e) {}
}
}
if (opts.scrollFirst && mode) {
var id = mode === "error" ? "first-error" : "first-warn";
var el = document.getElementById(id);
var sel = mode === "error" ? ".line.error, .line.blocker"
: mode === "warn" ? ".line.warn"
: mode === "info" ? ".line.info"
: ".line.ok";
var id = mode === "error" ? "first-error" : mode === "warn" ? "first-warn" : "";
var el = id ? document.getElementById(id) : null;
if (el && el.scrollIntoView) {
setTimeout(function () { el.scrollIntoView({ block: "start", behavior: "smooth" }); }, 30);
} else if (consoleEl) {
var sel = mode === "error" ? ".line.error, .line.blocker" : ".line.warn";
var first = consoleEl.querySelector(sel);
if (first && first.scrollIntoView) {
setTimeout(function () { first.scrollIntoView({ block: "start", behavior: "smooth" }); }, 30);
@ -1744,7 +1841,9 @@ STICKY_JS = """
a.addEventListener("click", function (ev) {
ev.preventDefault();
var mode = a.getAttribute("data-filter");
if (mode === "error" || mode === "warn") toggleFilter(mode);
if (mode === "error" || mode === "warn" || mode === "info" || mode === "ok") {
toggleFilter(mode);
}
});
});
if (clearBtn) {
@ -1759,6 +1858,10 @@ STICKY_JS = """
setFilter("error", { updateHash: false, scrollFirst: h === "first-error" });
} else if (h === "filter-warn" || h === "first-warn") {
setFilter("warn", { updateHash: false, scrollFirst: h === "first-warn" });
} else if (h === "filter-info") {
setFilter("info", { updateHash: false, scrollFirst: true });
} else if (h === "filter-ok") {
setFilter("ok", { updateHash: false, scrollFirst: true });
} else if (currentFilter()) {
// keep filter if hash is something else (e.g. err-slug)
}
@ -1797,9 +1900,9 @@ def build_html(
# 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, _n_info, _n_ok, _, _, _, _ = count_status(raw_lines)
n_err, n_warn, n_info, n_ok, _, _, _, _ = count_status(raw_lines)
_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
del _e2, _w2, _i2, _o2, _fi, _fo
errors = extract_errors(raw_lines)
err_slugs: dict[str, str] = {}
@ -1916,6 +2019,8 @@ def build_html(
generated_iso=generated_iso,
n_err=n_err,
n_warn=n_warn,
n_info=n_info,
n_ok=n_ok,
hostname=hostname,
page_label=page_label,
mode=mode,
@ -2286,8 +2391,8 @@ def main() -> None:
for l in log_text.splitlines()
if not is_log_noise_line(strip_ansi(l))
]
n_err, n_warn, _n_info, _n_ok, _, _, _, _ = count_status(raw_for_counts)
del _n_info, _n_ok
n_err, n_warn, n_info, n_ok, _, _, _, _ = count_status(raw_for_counts)
# n_info/n_ok used by sticky filters when build_html is called below
if args.mode == "redirect":
html_out = build_redirect_html(