i18n: English default, French for FrancPaysan (v1.1)
This commit is contained in:
parent
a2afe690b7
commit
334594c423
13 changed files with 483 additions and 86 deletions
|
|
@ -8,6 +8,129 @@ import re
|
|||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
def ui_lang(explicit: str = "") -> str:
|
||||
"""en (default) or fr. Env: TALER_MON_LANG; auto fr for lefrancpaysan hosts."""
|
||||
import os
|
||||
lang = (explicit or os.environ.get("TALER_MON_LANG") or "").strip().lower()
|
||||
if lang in ("fr", "fra", "french"):
|
||||
return "fr"
|
||||
if lang in ("en", "eng", "english"):
|
||||
return "en"
|
||||
return "en"
|
||||
|
||||
|
||||
def ui(lang: str, key: str, **kwargs) -> str:
|
||||
"""UI chrome strings for sticky bar (English default; French for FrancPaysan)."""
|
||||
en = {
|
||||
"errors": "ERRORS",
|
||||
"warnings": "WARNINGS",
|
||||
"ok": "OK",
|
||||
"error_one": "{n} error",
|
||||
"error_many": "{n} errors",
|
||||
"warn_one": "{n} warning",
|
||||
"warn_many": "{n} warnings",
|
||||
"jump_first_error": "Jump to first error",
|
||||
"jump_first_warn": "Jump to first warning",
|
||||
"generated": "generated",
|
||||
"source": "source",
|
||||
"what_monitors": "What this monitors",
|
||||
"what_monitors_title": "Expand: what this page checks",
|
||||
"surface_title": "Remote ecosystem surface inventory",
|
||||
"surface_summary": "Public catalog hosts · DNS / TCP / HTTPS / TLS · Server version · CVE",
|
||||
"surface_i1": "Outside-in only: no SSH and no podman exec on remote targets",
|
||||
"surface_i2": "Catalog (surface-catalog.conf): taler.net, demo/test, gnunet.org, taler-ops.ch, taler-systems.com, deb.taler.net, ftp.gnu.org, …",
|
||||
"surface_i3": "Per host: DNS resolve, ICMP (info), open ports, protocol probes, TLS cert expiry, Server-header software version, optional CVE check",
|
||||
"surface_i4": "HTML published only on taler.hacktivism.ch (this page host: {host})",
|
||||
"surface_i5": "Timer: taler-monitoring-surface.timer (hourly)",
|
||||
"apt_title": "apt-src merchant deploy tests (podman on koopa)",
|
||||
"apt_summary": "4 containers · fresh + upgrade tracks · trixie & trixie-testing",
|
||||
"apt_i1": "koopa-taler-deploy-test-apt-src-trixie (fresh, suite trixie)",
|
||||
"apt_i2": "koopa-taler-deploy-test-apt-src-trixie-testing (fresh, trixie-testing)",
|
||||
"apt_i3": "koopa-taler-deploy-test-apt-src-trixie-upgrade (upgrade track)",
|
||||
"apt_i4": "koopa-taler-deploy-test-apt-src-trixie-testing-upgrade (upgrade track)",
|
||||
"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)",
|
||||
"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",
|
||||
"focus_stack": "Taler stack public + inside checks",
|
||||
"stack_title": "Stack monitoring · {host}",
|
||||
"stack_summary": "{phases} · public + inside",
|
||||
"stack_i1": "Focus: {focus}",
|
||||
"stack_i2": "Phases this run: {phases}",
|
||||
"stack_i3": "Typical: public URLs (HTTPS, QR, perf), inside (podman/ssh), package versions",
|
||||
"stack_i4": "HTML host: {host}",
|
||||
"stack_i5": "Timer: host-agent path + 4h · /monitoring on bank, exchange, merchant fronts",
|
||||
"stack_i_phases": "Log header phases= {phases}",
|
||||
"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.",
|
||||
"redirect_fail": "has failures.",
|
||||
"redirect_see": "See {link} for the full console log, error index, and sticky status bar.",
|
||||
"ago_s": "{n}s ago",
|
||||
"ago_m": "{n}m ago",
|
||||
"ago_h": "{n}h ago",
|
||||
"ago_d": "{n}d ago",
|
||||
"age_title": "page age · updates every second",
|
||||
}
|
||||
fr = {
|
||||
"errors": "ERREURS",
|
||||
"warnings": "AVERTISSEMENTS",
|
||||
"ok": "OK",
|
||||
"error_one": "{n} erreur",
|
||||
"error_many": "{n} erreurs",
|
||||
"warn_one": "{n} avertissement",
|
||||
"warn_many": "{n} avertissements",
|
||||
"jump_first_error": "Aller à la première erreur",
|
||||
"jump_first_warn": "Aller au premier avertissement",
|
||||
"generated": "généré",
|
||||
"source": "source",
|
||||
"what_monitors": "Ce que cette page contrôle",
|
||||
"what_monitors_title": "Déplier : ce que cette page vérifie",
|
||||
"surface_title": "Inventaire de surface de l'écosystème (distant)",
|
||||
"surface_summary": "Hôtes du catalogue public · DNS / TCP / HTTPS / TLS · version serveur · CVE",
|
||||
"surface_i1": "Uniquement de l'extérieur : pas de SSH ni de podman exec sur les cibles distantes",
|
||||
"surface_i2": "Catalogue (surface-catalog.conf) : taler.net, demo/test, gnunet.org, taler-ops.ch, taler-systems.com, deb.taler.net, ftp.gnu.org, …",
|
||||
"surface_i3": "Par hôte : résolution DNS, ICMP (info), ports ouverts, sondes de protocole, expiration du certificat TLS, version logicielle (en-tête Server), CVE optionnel",
|
||||
"surface_i4": "HTML publié uniquement sur taler.hacktivism.ch (hôte de cette page : {host})",
|
||||
"surface_i5": "Minuterie : taler-monitoring-surface.timer (horaire)",
|
||||
"apt_title": "Tests de déploiement apt-src marchand (podman sur koopa)",
|
||||
"apt_summary": "4 conteneurs · pistes fresh + upgrade · trixie et trixie-testing",
|
||||
"apt_i1": "koopa-taler-deploy-test-apt-src-trixie (fresh, suite trixie)",
|
||||
"apt_i2": "koopa-taler-deploy-test-apt-src-trixie-testing (fresh, trixie-testing)",
|
||||
"apt_i3": "koopa-taler-deploy-test-apt-src-trixie-upgrade (piste upgrade)",
|
||||
"apt_i4": "koopa-taler-deploy-test-apt-src-trixie-testing-upgrade (piste upgrade)",
|
||||
"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)",
|
||||
"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",
|
||||
"focus_stack": "Pile Taler public + inside",
|
||||
"stack_title": "Surveillance de pile · {host}",
|
||||
"stack_summary": "{phases} · public + inside",
|
||||
"stack_i1": "Focus : {focus}",
|
||||
"stack_i2": "Phases de cette exécution : {phases}",
|
||||
"stack_i3": "Typique : URL publiques (HTTPS, QR, perf), inside (podman/ssh), versions des paquets",
|
||||
"stack_i4": "Hôte HTML : {host}",
|
||||
"stack_i5": "Minuterie : host-agent path + 4 h · /monitoring sur bank, exchange, fronts marchands",
|
||||
"stack_i_phases": "En-tête de journal phases= {phases}",
|
||||
"footer": "Rendu console de taler-monitoring. Barre collante : vert = OK · jaune = avertissements · rouge = erreurs. 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",
|
||||
"ago_m": "il y a {n} min",
|
||||
"ago_h": "il y a {n} h",
|
||||
"ago_d": "il y a {n} j",
|
||||
"age_title": "âge de la page · mise à jour chaque seconde",
|
||||
}
|
||||
table = fr if lang == "fr" else en
|
||||
s = table.get(key) or en.get(key) or key
|
||||
try:
|
||||
return s.format(**kwargs)
|
||||
except Exception:
|
||||
return s
|
||||
|
||||
|
||||
# ANSI strip
|
||||
ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
|
||||
ERR_BULLET_RE = re.compile(r"^\s*[•·*-]\s+(?P<body>.+)$")
|
||||
|
|
@ -192,82 +315,84 @@ def extract_phases(log_text: str) -> str:
|
|||
return ""
|
||||
|
||||
|
||||
|
||||
def monitoring_scope(
|
||||
page_label: str,
|
||||
hostname: str,
|
||||
log_text: str = "",
|
||||
lang: str = "en",
|
||||
) -> dict[str, object]:
|
||||
"""
|
||||
Human-readable description of what this monitoring page covers.
|
||||
Returned keys: title, summary, items (list[str]), kind
|
||||
"""
|
||||
"""Human-readable description of what this page monitors (en default; fr for FP)."""
|
||||
label = (page_label or "monitoring").lower().replace("_", "-")
|
||||
phases = extract_phases(log_text)
|
||||
host = hostname or "?"
|
||||
# Auto-fr for FrancPaysan hosts if lang not forced
|
||||
if lang == "en" and re.search(r"lefrancpaysan|francpaysan", host + " " + log_text, re.I):
|
||||
import os
|
||||
if not (os.environ.get("TALER_MON_LANG") or "").strip():
|
||||
lang = "fr"
|
||||
|
||||
if "surface" in label:
|
||||
return {
|
||||
"kind": "surface",
|
||||
"title": "Remote ecosystem surface inventory",
|
||||
"summary": "Public catalog hosts · DNS / TCP / HTTPS / TLS · Server version · CVE",
|
||||
"lang": lang,
|
||||
"title": ui(lang, "surface_title"),
|
||||
"summary": ui(lang, "surface_summary"),
|
||||
"items": [
|
||||
"Outside-in only: no SSH and no podman exec on remote targets",
|
||||
"Catalog (surface-catalog.conf): taler.net, demo/test, gnunet.org, "
|
||||
"taler-ops.ch, taler-systems.com, deb.taler.net, ftp.gnu.org, …",
|
||||
"Per host: DNS resolve, ICMP (info), open ports, protocol probes, "
|
||||
"TLS cert expiry, Server-header software version, optional CVE check",
|
||||
f"HTML published only on taler.hacktivism.ch "
|
||||
f"(this page host: {host})",
|
||||
"Timer: taler-monitoring-surface.timer (hourly)",
|
||||
ui(lang, "surface_i1"),
|
||||
ui(lang, "surface_i2"),
|
||||
ui(lang, "surface_i3"),
|
||||
ui(lang, "surface_i4", host=host),
|
||||
ui(lang, "surface_i5"),
|
||||
],
|
||||
}
|
||||
|
||||
if "aptdeploy" in label or "apt-deploy" in label or "apt_src" in label:
|
||||
return {
|
||||
"kind": "aptdeploy",
|
||||
"title": "apt-src merchant deploy tests (podman on koopa)",
|
||||
"summary": "4 containers · fresh + upgrade tracks · trixie & trixie-testing",
|
||||
"lang": lang,
|
||||
"title": ui(lang, "apt_title"),
|
||||
"summary": ui(lang, "apt_summary"),
|
||||
"items": [
|
||||
"koopa-taler-deploy-test-apt-src-trixie (fresh, suite trixie)",
|
||||
"koopa-taler-deploy-test-apt-src-trixie-testing (fresh, trixie-testing)",
|
||||
"koopa-taler-deploy-test-apt-src-trixie-upgrade (upgrade track)",
|
||||
"koopa-taler-deploy-test-apt-src-trixie-testing-upgrade (upgrade track)",
|
||||
"Checks: packages, taler-merchant-httpd --version / ldd, systemd unit, "
|
||||
"optional local /config probe — not public nginx/HTTPS",
|
||||
f"HTML published only on taler.hacktivism.ch (this page host: {host})",
|
||||
"Timer: taler-monitoring-aptdeploy.timer (4h)",
|
||||
ui(lang, "apt_i1"),
|
||||
ui(lang, "apt_i2"),
|
||||
ui(lang, "apt_i3"),
|
||||
ui(lang, "apt_i4"),
|
||||
ui(lang, "apt_i5"),
|
||||
ui(lang, "apt_i6", host=host),
|
||||
ui(lang, "apt_i7"),
|
||||
],
|
||||
}
|
||||
|
||||
# Default: stack /monitoring pages (bank, exchange, merchant)
|
||||
phase_txt = phases or "urls · inside · versions"
|
||||
role = "stack"
|
||||
if host.startswith("bank."):
|
||||
if host.startswith("bank.") or "bank." in host:
|
||||
role = "bank"
|
||||
focus = "Libeufin bank public HTTPS + container inside checks"
|
||||
elif host.startswith("exchange.") or host.startswith("stage.exchange."):
|
||||
focus = ui(lang, "focus_bank")
|
||||
elif "exchange." in host:
|
||||
role = "exchange"
|
||||
focus = "Exchange public HTTPS + container inside checks"
|
||||
elif host.startswith("taler.") or "merchant" in host or host.startswith("monnaie."):
|
||||
focus = ui(lang, "focus_exchange")
|
||||
elif host.startswith("taler.") or "merchant" in host or "monnaie." in host:
|
||||
role = "merchant"
|
||||
focus = "Merchant backend / SPA public HTTPS + container inside checks"
|
||||
focus = ui(lang, "focus_merchant")
|
||||
else:
|
||||
focus = "Taler stack public + inside checks"
|
||||
focus = ui(lang, "focus_stack")
|
||||
|
||||
items = [
|
||||
f"Focus: {focus}",
|
||||
f"Phases this run: {phase_txt}",
|
||||
"Typical: public URLs (HTTPS, QR, perf), inside (podman/ssh), package versions",
|
||||
f"HTML host: {host}",
|
||||
"Timer: taler-monitoring-hacktivism (path + 4h) · /monitoring on bank, exchange, taler",
|
||||
ui(lang, "stack_i1", focus=focus),
|
||||
ui(lang, "stack_i2", phases=phase_txt),
|
||||
ui(lang, "stack_i3"),
|
||||
ui(lang, "stack_i4", host=host),
|
||||
ui(lang, "stack_i5"),
|
||||
]
|
||||
if phases:
|
||||
items.insert(2, f"Log header phases= {phases}")
|
||||
items.insert(2, ui(lang, "stack_i_phases", phases=phases))
|
||||
|
||||
return {
|
||||
"kind": role,
|
||||
"title": f"Stack monitoring · {host}",
|
||||
"summary": f"{phase_txt} · public + inside",
|
||||
"lang": lang,
|
||||
"title": ui(lang, "stack_title", host=host),
|
||||
"summary": ui(lang, "stack_summary", phases=phase_txt),
|
||||
"items": items,
|
||||
}
|
||||
|
||||
|
|
@ -289,28 +414,41 @@ def sticky_bar_html(
|
|||
scope: dict[str, object] | None = None,
|
||||
) -> str:
|
||||
level = status_level(n_err, n_warn)
|
||||
if level == "red":
|
||||
status_txt = "ERRORS"
|
||||
elif level == "yellow":
|
||||
status_txt = "WARNINGS"
|
||||
lang = "en"
|
||||
if scope and scope.get("lang"):
|
||||
lang = str(scope.get("lang"))
|
||||
else:
|
||||
status_txt = "OK"
|
||||
lang = ui_lang()
|
||||
# auto-fr for FP hostnames
|
||||
if lang == "en" and re.search(r"lefrancpaysan|francpaysan", hostname or "", re.I):
|
||||
lang = "fr"
|
||||
|
||||
if level == "red":
|
||||
status_txt = ui(lang, "errors")
|
||||
elif level == "yellow":
|
||||
status_txt = ui(lang, "warnings")
|
||||
else:
|
||||
status_txt = ui(lang, "ok")
|
||||
|
||||
if first_err_href and n_err:
|
||||
err_lbl = ui(lang, "error_one", n=n_err) if n_err == 1 else ui(lang, "error_many", n=n_err)
|
||||
err_stat = (
|
||||
f'<a class="stat err" href="{html.escape(first_err_href)}" '
|
||||
f'title="Jump to first error">{n_err} error{"s" if n_err != 1 else ""}</a>'
|
||||
f'title="{html.escape(ui(lang, "jump_first_error"))}">{html.escape(err_lbl)}</a>'
|
||||
)
|
||||
else:
|
||||
err_stat = f'<span class="stat err muted">{n_err} errors</span>'
|
||||
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:
|
||||
warn_lbl = ui(lang, "warn_one", n=n_warn) if n_warn == 1 else ui(lang, "warn_many", n=n_warn)
|
||||
warn_stat = (
|
||||
f'<a class="stat warn" href="{html.escape(first_warn_href)}" '
|
||||
f'title="Jump to first warning">{n_warn} warning{"s" if n_warn != 1 else ""}</a>'
|
||||
f'title="{html.escape(ui(lang, "jump_first_warn"))}">{html.escape(warn_lbl)}</a>'
|
||||
)
|
||||
else:
|
||||
warn_stat = f'<span class="stat warn muted">{n_warn} warnings</span>'
|
||||
warn_lbl = ui(lang, "warn_many", n=n_warn)
|
||||
warn_stat = f'<span class="stat warn muted">{html.escape(warn_lbl)}</span>'
|
||||
|
||||
other = ""
|
||||
if link_other:
|
||||
|
|
@ -320,7 +458,7 @@ def sticky_bar_html(
|
|||
)
|
||||
|
||||
if scope is None:
|
||||
scope = monitoring_scope(page_label, hostname, "")
|
||||
scope = monitoring_scope(page_label, hostname, "", lang=ui_lang())
|
||||
scope_title = html.escape(str(scope.get("title") or "Monitoring"))
|
||||
scope_summary = html.escape(str(scope.get("summary") or ""))
|
||||
scope_kind = html.escape(str(scope.get("kind") or "monitoring"))
|
||||
|
|
@ -347,21 +485,21 @@ def sticky_bar_html(
|
|||
<span class="generated"
|
||||
data-generated-iso="{html.escape(generated_iso)}"
|
||||
title="{html.escape(generated_at)}">
|
||||
generated <time datetime="{html.escape(generated_iso)}">{html.escape(generated_at)}</time>
|
||||
{html.escape(ui(lang, "generated"))} <time datetime="{html.escape(generated_iso)}">{html.escape(generated_at)}</time>
|
||||
<span class="age" id="generated-age"></span>
|
||||
</span>
|
||||
<button type="button" class="scope-toggle" id="scope-toggle"
|
||||
aria-expanded="false" aria-controls="sticky-scope-panel"
|
||||
title="Aufklappen: was diese Seite prüft">
|
||||
title="{html.escape(ui(lang, "what_monitors_title"))}">
|
||||
<span class="scope-chevron" aria-hidden="true"></span>
|
||||
<span class="scope-toggle-label">Was geprüft wird</span>
|
||||
<span class="scope-toggle-label">{html.escape(ui(lang, "what_monitors"))}</span>
|
||||
<span class="scope-toggle-hint">{scope_summary}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sticky-scope-panel" id="sticky-scope-panel" hidden>
|
||||
<div class="sticky-bar-row secondary">
|
||||
<span class="badge mode-{html.escape(mode)}">{html.escape(mode.upper())}</span>
|
||||
source {commit_html}{suite_bit}
|
||||
{html.escape(ui(lang, "source"))} {commit_html}{suite_bit}
|
||||
{(" · " + other) if other else ""}
|
||||
</div>
|
||||
<div class="scope-body">
|
||||
|
|
@ -531,10 +669,22 @@ STICKY_JS = """
|
|||
(function () {
|
||||
function fmtAge(sec) {
|
||||
if (sec < 0) sec = 0;
|
||||
if (sec < 60) return sec + "s ago";
|
||||
if (sec < 3600) return Math.floor(sec / 60) + "m ago";
|
||||
if (sec < 86400) return Math.floor(sec / 3600) + "h ago";
|
||||
return Math.floor(sec / 86400) + "d ago";
|
||||
var lang = (document.documentElement.getAttribute("lang") || "en").toLowerCase();
|
||||
var n;
|
||||
if (sec < 60) {
|
||||
n = sec;
|
||||
return lang === "fr" ? ("il y a " + n + "s") : (n + "s ago");
|
||||
}
|
||||
if (sec < 3600) {
|
||||
n = Math.floor(sec / 60);
|
||||
return lang === "fr" ? ("il y a " + n + " min") : (n + "m ago");
|
||||
}
|
||||
if (sec < 86400) {
|
||||
n = Math.floor(sec / 3600);
|
||||
return lang === "fr" ? ("il y a " + n + " h") : (n + "h ago");
|
||||
}
|
||||
n = Math.floor(sec / 86400);
|
||||
return lang === "fr" ? ("il y a " + n + " j") : (n + "d ago");
|
||||
}
|
||||
function tick() {
|
||||
var el = document.querySelector("[data-generated-iso]");
|
||||
|
|
@ -546,7 +696,9 @@ STICKY_JS = """
|
|||
if (isNaN(t)) return;
|
||||
var sec = Math.floor((Date.now() - t) / 1000);
|
||||
age.textContent = fmtAge(sec);
|
||||
age.title = "page age · updates every second";
|
||||
age.title = (document.documentElement.getAttribute("lang") || "en").toLowerCase() === "fr"
|
||||
? "âge de la page · mise à jour chaque seconde"
|
||||
: "page age · updates every second";
|
||||
}
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
|
|
@ -679,8 +831,15 @@ def build_html(
|
|||
|
||||
first_err_href = "#first-error" if first_err_i is not None else None
|
||||
first_warn_href = "#first-warn" if first_warn_i is not None else None
|
||||
scope = monitoring_scope(page_label, hostname, log_text)
|
||||
lang = 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 = "fr"
|
||||
scope = monitoring_scope(page_label, hostname, log_text, lang=lang)
|
||||
|
||||
page_lang = lang
|
||||
footer_txt = ui(lang, "footer")
|
||||
bar = sticky_bar_html(
|
||||
generated_at=generated_at,
|
||||
generated_iso=generated_iso,
|
||||
|
|
@ -698,7 +857,7 @@ def build_html(
|
|||
)
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="{html.escape(lang if "lang" in dir() else "en")}">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
|
|
@ -775,9 +934,7 @@ footer {{
|
|||
{"".join(body_lines)}
|
||||
</div>
|
||||
<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.
|
||||
{html.escape(footer_txt)}
|
||||
</footer>
|
||||
</main>
|
||||
{STICKY_JS}
|
||||
|
|
@ -830,7 +987,7 @@ def build_redirect_html(
|
|||
scope=scope,
|
||||
)
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="{html.escape(lang if "lang" in dir() else "en")}">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="refresh" content="2; url={html.escape(link)}"/>
|
||||
|
|
@ -882,6 +1039,11 @@ def main() -> None:
|
|||
default="/monitoring_err/",
|
||||
help="URL path for error page (redirect target)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--lang",
|
||||
default="",
|
||||
help="UI language en|fr (default: env TALER_MON_LANG or auto)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--page-label",
|
||||
default="monitoring",
|
||||
|
|
@ -889,6 +1051,9 @@ def main() -> None:
|
|||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
if getattr(args, "lang", ""):
|
||||
import os
|
||||
os.environ["TALER_MON_LANG"] = args.lang
|
||||
log_text = args.log.read_text(errors="replace") if args.log.is_file() else ""
|
||||
now = datetime.now(timezone.utc)
|
||||
generated_iso = now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue