release 1.11.0: sticky error/warn log filter

Click error or warning counts in the sticky bar to show only matching
console lines; sticky stats and top overviews remain. Hashes
#filter-error / #filter-warn (and #first-*) activate the filter.
This commit is contained in:
Hernâni Marques 2026-07-19 02:16:39 +02:00
parent 341504f1ad
commit db6722e40f
No known key found for this signature in database
GPG key ID: CB5738652768F7E9
5 changed files with 213 additions and 50 deletions

View file

@ -423,5 +423,6 @@ If pages are missing publicly: `sudo /home/hernani/koopa-caddy/apply-monitoring-
## Version on monitoring pages (v1.3+ / sticky bar)
The sticky bar shows the suite **tag** (e.g. `v1.4.1`) and links to that tag on Forgejo.
Planned **v1.4.0**: fuller L10n (fr-CH for FrancPaysan, de-CH for hacktivism/Taler CH).
The sticky bar shows the suite **tag** (e.g. `v1.11.0`) and links to that tag on Forgejo.
**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.10.11
1.11.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.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. |
| **v1.10.9** | 2026-07-19 | **Bugfix:** stats policy INFO no longer says bare “ERROR” (false #first-error on www.stats-01); extract_errors only from `┌ ERROR` badges. |

View file

@ -1,23 +1,27 @@
# Host Caddy (root service). Place BEFORE catch-all reverse_proxy in each site block.
#
# CRITICAL (v1.7.2): root must be the *host directory* (…/monitoring-sites/{host}),
# and the matcher must be a path *prefix* (`/monitoring*`), so that URI
# /monitoring/index.html maps to {root}/monitoring/index.html.
# CRITICAL: bare paths (no trailing slash) must redirect to slash, or the request
# falls through to merchant/bank/exchange API → Taler JSON code 21.
#
# WRONG (causes empty HTTP 404 from Caddy file_server):
# handle /monitoring/ {
# root * …/monitoring-sites/{host}/monitoring
# file_server
# }
# → looks for …/monitoring/monitoring/index.html
# CORRECT (site-level named matcher — preferred):
# @mon_bare path /monitoring /monitoring_err \
# /taler-monitoring-surface /taler-monitoring-surface_err \
# /taler-monitoring-aptdeploy /taler-monitoring-aptdeploy_err
# redir @mon_bare {path}/ 302
#
# CRITICAL (v1.9.2): bare-path redir inside handle MUST use matcher `*`:
# ALSO OK inside handle (matcher must be `*`):
# handle /taler-monitoring-surface {
# redir * /taler-monitoring-surface/ 302
# }
# WRONG: `redir /taler-monitoring-surface/ 302` is parsed as
# matcher=/taler-monitoring-surface/ to="302" → Location: 302, merchant code 21
# (same footgun for /monitoring, aptdeploy, mail, mattermost bare paths).
#
# WRONG (Caddy footgun — causes merchant code 21 on bare URLs):
# redir /taler-monitoring-surface/ 302
# → parsed as matcher=/taler-monitoring-surface/ to="302"
# → Location: 302, or no-op then fallthrough to reverse_proxy
#
# root must be the *host directory* (…/monitoring-sites/{host}), and the
# file_server matcher a path *prefix* (`/monitoring*`), so
# /monitoring/index.html maps to {root}/monitoring/index.html.
#
# Disk layout:
# /var/www/monitoring-sites/{host}/monitoring/index.html
@ -25,49 +29,44 @@
#
# Live apply on koopa (absolute path — ~ as root is /root):
# sudo /home/hernani/koopa-caddy/apply-monitoring-live.sh
# Runtime-only (no sudo; hernani admin API):
# curl -g -X POST http://[::1]:2019/load -H 'Content-Type: text/caddyfile' \
# --data-binary @/home/hernani/koopa-caddy/Caddyfile
# --- only inside taler.hacktivism.ch { ... } ---
handle /taler-monitoring-surface {
redir * /taler-monitoring-surface/ 302
@mon_bare path /monitoring /monitoring_err \
/taler-monitoring-surface /taler-monitoring-surface_err \
/taler-monitoring-aptdeploy /taler-monitoring-aptdeploy_err \
/taler-monitoring-mail /taler-monitoring-mail_err \
/taler-monitoring-mattermost /taler-monitoring-mattermost_err
redir @mon_bare {path}/ 302
handle /taler-monitoring-surface_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-surface* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-surface_err {
redir * /taler-monitoring-surface_err/ 302
}
handle /taler-monitoring-surface_err* {
handle /taler-monitoring-aptdeploy_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-aptdeploy {
redir * /taler-monitoring-aptdeploy/ 302
}
handle /taler-monitoring-aptdeploy* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-aptdeploy_err {
redir * /taler-monitoring-aptdeploy_err/ 302
}
handle /taler-monitoring-aptdeploy_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
# --- bank + exchange + taler (each site block; set root host dir) ---
handle /monitoring {
redir * /monitoring/ 302
@mon_bare path /monitoring /monitoring_err
redir @mon_bare {path}/ 302
handle /monitoring_err* {
root * /var/www/monitoring-sites/{host}
file_server
}
handle /monitoring* {
root * /var/www/monitoring-sites/{host}
file_server
}
handle /monitoring_err {
redir * /monitoring_err/ 302
}
handle /monitoring_err* {
root * /var/www/monitoring-sites/{host}
file_server
}

View file

@ -31,6 +31,12 @@ def ui(lang: str, key: str, **kwargs) -> str:
"warn_many": "{n} warnings",
"jump_first_error": "Jump to first error",
"jump_first_warn": "Jump to first warning",
"filter_errors": "Filter: show all errors (hide other log lines)",
"filter_warns": "Filter: show all warnings (hide other log lines)",
"filter_clear": "Clear filter · show all lines",
"filter_active_err": "Filter: errors only",
"filter_active_warn": "Filter: warnings only",
"filter_show_all": "Show all",
"generated": "generated",
"version": "version",
@ -94,7 +100,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. 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 (other lines hidden; sticky + overviews stay). 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",
@ -113,6 +119,12 @@ def ui(lang: str, key: str, **kwargs) -> str:
"warn_many": "{n} avertissements",
"jump_first_error": "Aller à la première erreur",
"jump_first_warn": "Aller au premier avertissement",
"filter_errors": "Filtrer : toutes les erreurs (masquer le reste du journal)",
"filter_warns": "Filtrer : tous les avertissements (masquer le reste du journal)",
"filter_clear": "Effacer le filtre · tout afficher",
"filter_active_err": "Filtre : erreurs seulement",
"filter_active_warn": "Filtre : avertissements seulement",
"filter_show_all": "Tout afficher",
"generated": "généré",
"version": "version",
@ -176,7 +188,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. Le commit fixe l'arbre exact de cette exécution.",
"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.",
"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",
@ -701,26 +713,50 @@ def sticky_bar_html(
else:
status_txt = ui(lang, "ok")
if first_err_href and n_err:
# Click error/warn stats → filter console to matching lines only (v1.11.0).
# Hash #filter-error / #filter-warn; #first-error / #first-warn also activate filter.
if n_err:
err_lbl = ui(lang, "error_one", n=n_err) if n_err == 1 else ui(lang, "error_many", n=n_err)
err_href = first_err_href or "#filter-error"
# Prefer filter hash; keep first-error as secondary scroll target via JS
err_stat = (
f'<a class="stat err" href="{html.escape(first_err_href)}" '
f'title="{html.escape(ui(lang, "jump_first_error"))}">{html.escape(err_lbl)}</a>'
f'<a class="stat err" href="#filter-error" data-filter="error" '
f'role="button" aria-pressed="false" '
f'data-first="{html.escape(err_href)}" '
f'title="{html.escape(ui(lang, "filter_errors"))}">{html.escape(err_lbl)}</a>'
)
else:
err_lbl = ui(lang, "error_many", n=n_err)
err_stat = f'<span class="stat err muted">{html.escape(err_lbl)}</span>'
if first_warn_href and n_warn:
if n_warn:
warn_lbl = ui(lang, "warn_one", n=n_warn) if n_warn == 1 else ui(lang, "warn_many", n=n_warn)
warn_href = first_warn_href or "#filter-warn"
warn_stat = (
f'<a class="stat warn" href="{html.escape(first_warn_href)}" '
f'title="{html.escape(ui(lang, "jump_first_warn"))}">{html.escape(warn_lbl)}</a>'
f'<a class="stat warn" href="#filter-warn" data-filter="warn" '
f'role="button" aria-pressed="false" '
f'data-first="{html.escape(warn_href)}" '
f'title="{html.escape(ui(lang, "filter_warns"))}">{html.escape(warn_lbl)}</a>'
)
else:
warn_lbl = ui(lang, "warn_many", n=n_warn)
warn_stat = f'<span class="stat warn muted">{html.escape(warn_lbl)}</span>'
filter_chip = (
f'<span class="filter-chip" id="filter-chip" hidden>'
f'<span class="filter-chip-label" id="filter-chip-label"></span>'
f'<button type="button" class="filter-clear" id="filter-clear" '
f'title="{html.escape(ui(lang, "filter_clear"))}">'
f'{html.escape(ui(lang, "filter_show_all"))}</button>'
f"</span>"
)
# data attributes for JS i18n
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-clear="{html.escape(ui(lang, "filter_clear"))}"'
)
other = ""
if link_other:
other = (
@ -758,7 +794,7 @@ def sticky_bar_html(
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-scope-kind="{scope_kind}">
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="host">{html.escape(page_label)} · {html.escape(hostname)}</span>
@ -766,6 +802,7 @@ def sticky_bar_html(
{err_stat}
<span class="sep">·</span>
{warn_stat}
{filter_chip}
<span class="sep">·</span>
{version_html}
<span class="sep">·</span>
@ -849,6 +886,30 @@ STICKY_CSS = """
.stat.warn { color: var(--warn); border-color: #664; background: #1a1608; }
.stat.muted { opacity: 0.55; font-weight: 600; }
a.stat:hover { text-decoration: underline; filter: brightness(1.15); }
a.stat[aria-pressed="true"] {
box-shadow: 0 0 0 1px currentColor inset;
filter: brightness(1.2);
}
.filter-chip {
display: inline-flex; align-items: center; gap: 6px;
font-size: 11px; font-weight: 600;
padding: 1px 4px 1px 8px; border-radius: 3px;
border: 1px solid #456; background: #0a1520; color: #9cf;
}
.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); }
.filter-clear {
font: inherit; font-size: 11px; font-weight: 700;
cursor: pointer; color: inherit;
background: #0006; border: 1px solid #555; border-radius: 3px;
padding: 1px 8px;
}
.filter-clear:hover { border-color: #aaa; background: #222; }
/* 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-warn .err-top { display: none; }
.version-link {
font-weight: 700; font-size: 12px; color: var(--info);
text-decoration: none; padding: 1px 8px; border-radius: 3px;
@ -1015,6 +1076,107 @@ STICKY_JS = """
if (localStorage.getItem(key) === "1") setOpen(true);
} catch (e) {}
}
// v1.11.0: click error/warn stats show only those log lines; sticky + overviews stay
var bar = document.getElementById("status-bar");
var chip = document.getElementById("filter-chip");
var chipLabel = document.getElementById("filter-chip-label");
var clearBtn = document.getElementById("filter-clear");
var consoleEl = document.getElementById("mon-console");
function currentFilter() {
if (document.body.classList.contains("filter-error")) return "error";
if (document.body.classList.contains("filter-warn")) return "warn";
return "";
}
function setFilter(mode, opts) {
opts = opts || {};
document.body.classList.remove("filter-error", "filter-warn");
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");
if (mode === "error") {
document.body.classList.add("filter-error");
if (errA) errA.setAttribute("aria-pressed", "true");
} else if (mode === "warn") {
document.body.classList.add("filter-warn");
if (warnA) warnA.setAttribute("aria-pressed", "true");
}
if (chip && chipLabel && bar) {
if (mode === "error") {
chip.hidden = false;
chipLabel.textContent = bar.getAttribute("data-i18n-filter-err") || "Filter: errors only";
} else if (mode === "warn") {
chip.hidden = false;
chipLabel.textContent = bar.getAttribute("data-i18n-filter-warn") || "Filter: warnings only";
} else {
chip.hidden = true;
chipLabel.textContent = "";
}
}
if (opts.updateHash !== false) {
var want = mode === "error" ? "#filter-error"
: mode === "warn" ? "#filter-warn" : "";
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") {
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);
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);
}
}
}
}
function toggleFilter(mode) {
if (currentFilter() === mode) setFilter("", { updateHash: true });
else setFilter(mode, { updateHash: true, scrollFirst: true });
}
document.querySelectorAll("a.stat[data-filter]").forEach(function (a) {
a.addEventListener("click", function (ev) {
ev.preventDefault();
var mode = a.getAttribute("data-filter");
if (mode === "error" || mode === "warn") toggleFilter(mode);
});
});
if (clearBtn) {
clearBtn.addEventListener("click", function () {
setFilter("", { updateHash: true });
});
}
function applyHash() {
var h = (location.hash || "").replace(/^#/, "");
if (h === "filter-error" || h === "first-error") {
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 (currentFilter()) {
// keep filter if hash is something else (e.g. err-slug)
}
}
applyHash();
window.addEventListener("hashchange", applyHash);
})();
</script>
"""