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:
parent
341504f1ad
commit
df2c2de8fa
5 changed files with 213 additions and 50 deletions
|
|
@ -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.
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1.10.11
|
||||
1.11.0
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue