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)
|
## 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.
|
The sticky bar shows the suite **tag** (e.g. `v1.11.0`) and links to that tag on Forgejo.
|
||||||
Planned **v1.4.0**: fuller L10n (fr-CH for FrancPaysan, de-CH for hacktivism/Taler CH).
|
|
||||||
|
**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 |
|
| 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.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.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. |
|
| **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.
|
# 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}),
|
# CRITICAL: bare paths (no trailing slash) must redirect to slash, or the request
|
||||||
# and the matcher must be a path *prefix* (`/monitoring*`), so that URI
|
# falls through to merchant/bank/exchange API → Taler JSON code 21.
|
||||||
# /monitoring/index.html maps to {root}/monitoring/index.html.
|
|
||||||
#
|
#
|
||||||
# WRONG (causes empty HTTP 404 from Caddy file_server):
|
# CORRECT (site-level named matcher — preferred):
|
||||||
# handle /monitoring/ {
|
# @mon_bare path /monitoring /monitoring_err \
|
||||||
# root * …/monitoring-sites/{host}/monitoring
|
# /taler-monitoring-surface /taler-monitoring-surface_err \
|
||||||
# file_server
|
# /taler-monitoring-aptdeploy /taler-monitoring-aptdeploy_err
|
||||||
# }
|
# redir @mon_bare {path}/ 302
|
||||||
# → looks for …/monitoring/monitoring/index.html
|
|
||||||
#
|
#
|
||||||
# CRITICAL (v1.9.2): bare-path redir inside handle MUST use matcher `*`:
|
# ALSO OK inside handle (matcher must be `*`):
|
||||||
# handle /taler-monitoring-surface {
|
# handle /taler-monitoring-surface {
|
||||||
# redir * /taler-monitoring-surface/ 302
|
# 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
|
# WRONG (Caddy footgun — causes merchant code 21 on bare URLs):
|
||||||
# (same footgun for /monitoring, aptdeploy, mail, mattermost bare paths).
|
# 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:
|
# Disk layout:
|
||||||
# /var/www/monitoring-sites/{host}/monitoring/index.html
|
# /var/www/monitoring-sites/{host}/monitoring/index.html
|
||||||
|
|
@ -25,49 +29,44 @@
|
||||||
#
|
#
|
||||||
# Live apply on koopa (absolute path — ~ as root is /root):
|
# Live apply on koopa (absolute path — ~ as root is /root):
|
||||||
# sudo /home/hernani/koopa-caddy/apply-monitoring-live.sh
|
# 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 { ... } ---
|
# --- only inside taler.hacktivism.ch { ... } ---
|
||||||
handle /taler-monitoring-surface {
|
@mon_bare path /monitoring /monitoring_err \
|
||||||
redir * /taler-monitoring-surface/ 302
|
/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* {
|
handle /taler-monitoring-surface* {
|
||||||
root * /var/www/monitoring-sites/taler.hacktivism.ch
|
root * /var/www/monitoring-sites/taler.hacktivism.ch
|
||||||
file_server
|
file_server
|
||||||
}
|
}
|
||||||
handle /taler-monitoring-surface_err {
|
handle /taler-monitoring-aptdeploy_err* {
|
||||||
redir * /taler-monitoring-surface_err/ 302
|
|
||||||
}
|
|
||||||
handle /taler-monitoring-surface_err* {
|
|
||||||
root * /var/www/monitoring-sites/taler.hacktivism.ch
|
root * /var/www/monitoring-sites/taler.hacktivism.ch
|
||||||
file_server
|
file_server
|
||||||
}
|
}
|
||||||
handle /taler-monitoring-aptdeploy {
|
|
||||||
redir * /taler-monitoring-aptdeploy/ 302
|
|
||||||
}
|
|
||||||
handle /taler-monitoring-aptdeploy* {
|
handle /taler-monitoring-aptdeploy* {
|
||||||
root * /var/www/monitoring-sites/taler.hacktivism.ch
|
root * /var/www/monitoring-sites/taler.hacktivism.ch
|
||||||
file_server
|
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) ---
|
# --- bank + exchange + taler (each site block; set root host dir) ---
|
||||||
handle /monitoring {
|
@mon_bare path /monitoring /monitoring_err
|
||||||
redir * /monitoring/ 302
|
redir @mon_bare {path}/ 302
|
||||||
|
|
||||||
|
handle /monitoring_err* {
|
||||||
|
root * /var/www/monitoring-sites/{host}
|
||||||
|
file_server
|
||||||
}
|
}
|
||||||
handle /monitoring* {
|
handle /monitoring* {
|
||||||
root * /var/www/monitoring-sites/{host}
|
root * /var/www/monitoring-sites/{host}
|
||||||
file_server
|
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",
|
"warn_many": "{n} warnings",
|
||||||
"jump_first_error": "Jump to first error",
|
"jump_first_error": "Jump to first error",
|
||||||
"jump_first_warn": "Jump to first warning",
|
"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",
|
"generated": "generated",
|
||||||
"version": "version",
|
"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_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_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)",
|
"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_fail": "has failures.",
|
||||||
"redirect_see": "See {link} for the full console log, error index, and sticky status bar.",
|
"redirect_see": "See {link} for the full console log, error index, and sticky status bar.",
|
||||||
"ago_s": "{n}s ago",
|
"ago_s": "{n}s ago",
|
||||||
|
|
@ -113,6 +119,12 @@ def ui(lang: str, key: str, **kwargs) -> str:
|
||||||
"warn_many": "{n} avertissements",
|
"warn_many": "{n} avertissements",
|
||||||
"jump_first_error": "Aller à la première erreur",
|
"jump_first_error": "Aller à la première erreur",
|
||||||
"jump_first_warn": "Aller au premier avertissement",
|
"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é",
|
"generated": "généré",
|
||||||
"version": "version",
|
"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_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_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*)",
|
"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_fail": "a des échecs.",
|
||||||
"redirect_see": "Voir {link} pour le journal console complet, l'index d'erreurs et la barre de statut.",
|
"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",
|
"ago_s": "il y a {n}s",
|
||||||
|
|
@ -701,26 +713,50 @@ def sticky_bar_html(
|
||||||
else:
|
else:
|
||||||
status_txt = ui(lang, "ok")
|
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_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 = (
|
err_stat = (
|
||||||
f'<a class="stat err" href="{html.escape(first_err_href)}" '
|
f'<a class="stat err" href="#filter-error" data-filter="error" '
|
||||||
f'title="{html.escape(ui(lang, "jump_first_error"))}">{html.escape(err_lbl)}</a>'
|
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:
|
else:
|
||||||
err_lbl = ui(lang, "error_many", n=n_err)
|
err_lbl = ui(lang, "error_many", n=n_err)
|
||||||
err_stat = f'<span class="stat err muted">{html.escape(err_lbl)}</span>'
|
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_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 = (
|
warn_stat = (
|
||||||
f'<a class="stat warn" href="{html.escape(first_warn_href)}" '
|
f'<a class="stat warn" href="#filter-warn" data-filter="warn" '
|
||||||
f'title="{html.escape(ui(lang, "jump_first_warn"))}">{html.escape(warn_lbl)}</a>'
|
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:
|
else:
|
||||||
warn_lbl = ui(lang, "warn_many", n=n_warn)
|
warn_lbl = ui(lang, "warn_many", n=n_warn)
|
||||||
warn_stat = f'<span class="stat warn muted">{html.escape(warn_lbl)}</span>'
|
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 = ""
|
other = ""
|
||||||
if link_other:
|
if link_other:
|
||||||
other = (
|
other = (
|
||||||
|
|
@ -758,7 +794,7 @@ def sticky_bar_html(
|
||||||
return f"""
|
return f"""
|
||||||
<div class="sticky-bar sticky-{level}" id="status-bar" role="status"
|
<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-level="{level}"
|
||||||
data-scope-kind="{scope_kind}">
|
data-scope-kind="{scope_kind}" {filter_i18n}>
|
||||||
<div class="sticky-bar-row primary">
|
<div class="sticky-bar-row primary">
|
||||||
<span class="status-pill sticky-{level}">{html.escape(status_txt)}</span>
|
<span class="status-pill sticky-{level}">{html.escape(status_txt)}</span>
|
||||||
<span class="host">{html.escape(page_label)} · {html.escape(hostname)}</span>
|
<span class="host">{html.escape(page_label)} · {html.escape(hostname)}</span>
|
||||||
|
|
@ -766,6 +802,7 @@ def sticky_bar_html(
|
||||||
{err_stat}
|
{err_stat}
|
||||||
<span class="sep">·</span>
|
<span class="sep">·</span>
|
||||||
{warn_stat}
|
{warn_stat}
|
||||||
|
{filter_chip}
|
||||||
<span class="sep">·</span>
|
<span class="sep">·</span>
|
||||||
{version_html}
|
{version_html}
|
||||||
<span class="sep">·</span>
|
<span class="sep">·</span>
|
||||||
|
|
@ -849,6 +886,30 @@ STICKY_CSS = """
|
||||||
.stat.warn { color: var(--warn); border-color: #664; background: #1a1608; }
|
.stat.warn { color: var(--warn); border-color: #664; background: #1a1608; }
|
||||||
.stat.muted { opacity: 0.55; font-weight: 600; }
|
.stat.muted { opacity: 0.55; font-weight: 600; }
|
||||||
a.stat:hover { text-decoration: underline; filter: brightness(1.15); }
|
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 {
|
.version-link {
|
||||||
font-weight: 700; font-size: 12px; color: var(--info);
|
font-weight: 700; font-size: 12px; color: var(--info);
|
||||||
text-decoration: none; padding: 1px 8px; border-radius: 3px;
|
text-decoration: none; padding: 1px 8px; border-radius: 3px;
|
||||||
|
|
@ -1015,6 +1076,107 @@ STICKY_JS = """
|
||||||
if (localStorage.getItem(key) === "1") setOpen(true);
|
if (localStorage.getItem(key) === "1") setOpen(true);
|
||||||
} catch (e) {}
|
} 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>
|
</script>
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue