From 334594c423e98be4fc9e008f180e30bb51fe2ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Sat, 18 Jul 2026 14:57:14 +0200 Subject: [PATCH] i18n: English default, French for FrancPaysan (v1.1) --- README.md | 4 + VERSION | 2 +- VERSIONS.md | 30 ++- host-agent/env.example | 1 + host-agent/env/francpaysan.env.example | 3 + host-agent/env/hacktivism.env.example | 2 + host-agent/env/stagepaysan.env.example | 3 + host-agent/run-host-report.sh | 12 + i18n.sh | 101 +++++++++ lib.sh | 61 +++-- site-gen/console_to_html.py | 295 +++++++++++++++++++------ site-gen/settings.conf | 51 +++++ taler-monitoring.sh | 4 + 13 files changed, 483 insertions(+), 86 deletions(-) create mode 100644 i18n.sh create mode 100644 site-gen/settings.conf diff --git a/README.md b/README.md index b8125f0..63871da 100644 --- a/README.md +++ b/README.md @@ -360,3 +360,7 @@ LADDER_LOAD=0 ./taler-monitoring.sh ladder - SSH `koopa` or `koopa-external` (inside / load / sanity server bits) - secrets under `koopa-admin-secrets/...` for e2e - `taler-wallet-cli` for e2e + +## Languages (i18n) + +Default **English**. FrancPaysan stacks use **French** (`TALER_MON_LANG=fr`, auto for `*lefrancpaysan*`). See `VERSIONS.md` and `i18n.sh`. diff --git a/VERSION b/VERSION index afaf360..1cc5f65 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 \ No newline at end of file +1.1.0 \ No newline at end of file diff --git a/VERSIONS.md b/VERSIONS.md index baf7929..c53eac5 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -1,14 +1,36 @@ # VERSIONS — taler-monitoring +Release history for the standalone **taler-monitoring** suite +(https://git.hacktivism.ch/hernani/taler-monitoring). + | Tag | Date (UTC) | Notes | |-----|------------|--------| -| **v1.0** | 2026-07-18 | Initial standalone release. Split from `koopa-admin-log` at `scripts/taler-monitoring/` (tip after monitoring host-agent/site-gen split commits). | +| **v1.1** | 2026-07-18 | **i18n**: default UI/console language **English**; **French** for FrancPaysan (`TALER_MON_LANG=fr`, auto on `*lefrancpaysan*`). Sticky bar “what this monitors” EN/FR; badge/summary chrome + message translation via `i18n.sh`. | +| **v1.0** | 2026-07-18 | Initial standalone release. Split from `koopa-admin-log` (`scripts/taler-monitoring/`). Host-agent HTML, surface, aptdeploy, sticky bar. | -Source: https://git.hacktivism.ch/hernani/taler-monitoring +## Language (v1.1+) + +| Context | Language | +|---------|----------| +| Default / GOA / hacktivism | **English** | +| FrancPaysan prod + stage (`*lefrancpaysan*`) | **French** (auto or `TALER_MON_LANG=fr`) | + +```bash +# force +TALER_MON_LANG=fr ./taler-monitoring.sh -d lefrancpaysan.ch urls +TALER_MON_LANG=en ./taler-monitoring.sh -d lefrancpaysan.ch urls +``` + +Host-agent env examples: +- `host-agent/env/francpaysan.env.example` → `TALER_MON_LANG=fr` +- `host-agent/env/stagepaysan.env.example` → `TALER_MON_LANG=fr` +- `host-agent/env/hacktivism.env.example` → `TALER_MON_LANG=en` + +## Pin a release ```bash git clone https://git.hacktivism.ch/hernani/taler-monitoring.git ~/src/taler-monitoring -cd ~/src/taler-monitoring && git checkout v1.0 # or stay on main +cd ~/src/taler-monitoring && git checkout v1.1 ``` -Default host-agent tracks `origin/main` (`SUITE_UPDATE_MODE=reset`). Pin with `SUITE_GIT_REF=v1.0` in `~/.config/taler-monitoring/env`. +Default host-agent tracks `origin/main`. Pin with `SUITE_GIT_REF=v1.1` in `~/.config/taler-monitoring/env`. diff --git a/host-agent/env.example b/host-agent/env.example index b7aec8b..2826f83 100644 --- a/host-agent/env.example +++ b/host-agent/env.example @@ -24,3 +24,4 @@ # HTML_OUT=$HOME/monitoring-sites-staging # MON_HOSTS="bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch" # SOURCE_REPO_WEB=https://git.hacktivism.ch/hernani/taler-monitoring +# TALER_MON_LANG=en # en default; fr for FrancPaysan (auto if domain *lefrancpaysan*) diff --git a/host-agent/env/francpaysan.env.example b/host-agent/env/francpaysan.env.example index d3132d8..2f5019d 100644 --- a/host-agent/env/francpaysan.env.example +++ b/host-agent/env/francpaysan.env.example @@ -22,3 +22,6 @@ SUITE_UPDATE_MODE=reset SOURCE_REPO_WEB=https://git.hacktivism.ch/hernani/taler-monitoring # DEPLOY_WWW_ROOT=/var/www/monitoring-sites + +# UI language for console + HTML sticky bar (en|fr) +TALER_MON_LANG=fr diff --git a/host-agent/env/hacktivism.env.example b/host-agent/env/hacktivism.env.example index 32c8e00..ebdcb8f 100644 --- a/host-agent/env/hacktivism.env.example +++ b/host-agent/env/hacktivism.env.example @@ -22,3 +22,5 @@ SUITE_UPDATE_MODE=reset SOURCE_REPO_WEB=https://git.hacktivism.ch/hernani/taler-monitoring DEPLOY_WWW_ROOT=/var/www/monitoring-sites + +TALER_MON_LANG=en diff --git a/host-agent/env/stagepaysan.env.example b/host-agent/env/stagepaysan.env.example index e21180a..f1510f2 100644 --- a/host-agent/env/stagepaysan.env.example +++ b/host-agent/env/stagepaysan.env.example @@ -25,3 +25,6 @@ SOURCE_REPO_WEB=https://git.hacktivism.ch/hernani/taler-monitoring # Optional public docroot if this user can write it # DEPLOY_WWW_ROOT=/var/www/monitoring-sites + +# UI language for console + HTML sticky bar (en|fr) +TALER_MON_LANG=fr diff --git a/host-agent/run-host-report.sh b/host-agent/run-host-report.sh index c38cdcf..16857c1 100755 --- a/host-agent/run-host-report.sh +++ b/host-agent/run-host-report.sh @@ -72,6 +72,15 @@ export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}" export AUTH401_CONTINUE="${AUTH401_CONTINUE:-${CONTINUE_ON_ERROR}}" export RUN_TIMEOUT="${RUN_TIMEOUT:-600}" export TALER_DOMAIN="${TALER_DOMAIN:-hacktivism.ch}" +# UI language: en default; fr for FrancPaysan (override with TALER_MON_LANG) +if [ -z "${TALER_MON_LANG:-}" ]; then + case "$TALER_DOMAIN" in + *lefrancpaysan*|*francpaysan*) export TALER_MON_LANG=fr ;; + *) export TALER_MON_LANG=en ;; + esac +fi +export TALER_MON_LANG + PHASES="${PHASES:-urls inside versions}" HTML_BASE="${HTML_OUT:-$HOME/monitoring-sites-staging}" MON_HOSTS="${MON_HOSTS:-}" @@ -285,6 +294,7 @@ htmlify_host() { if [ -n "$SITE_GEN" ] && [ -f "$SITE_GEN/console_to_html.py" ]; then python3 "$SITE_GEN/console_to_html.py" \ + --lang "${TALER_MON_LANG:-en}" \ --log "$LOG" \ --out "$mon_err" \ --hostname "$host" \ @@ -297,6 +307,7 @@ htmlify_host() { --link-other "$HTML_URL_OK" if [ "$ec" -eq 0 ]; then python3 "$SITE_GEN/console_to_html.py" \ + --lang "${TALER_MON_LANG:-en}" \ --log "$LOG" \ --out "$mon" \ --hostname "$host" \ @@ -311,6 +322,7 @@ htmlify_host() { echo "html $host → ${HTML_URL_OK} only (clean · ${COMMIT_SHORT:-?})" else python3 "$SITE_GEN/console_to_html.py" \ + --lang "${TALER_MON_LANG:-en}" \ --log "$LOG" \ --out "$mon" \ --hostname "$host" \ diff --git a/i18n.sh b/i18n.sh new file mode 100644 index 0000000..45dd1b7 --- /dev/null +++ b/i18n.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# i18n.sh — language for taler-monitoring console + HTML chrome +# +# TALER_MON_LANG=en|fr (default en) +# Auto: domains *lefrancpaysan* → fr unless TALER_MON_LANG is set explicitly. + +: "${TALER_MON_LANG:=}" +: "${TALER_MON_LANG_SET:=0}" + +i18n_init() { + if [ -n "${TALER_MON_LANG:-}" ]; then + TALER_MON_LANG_SET=1 + fi + if [ "${TALER_MON_LANG_SET}" != "1" ]; then + case "${TALER_DOMAIN:-}" in + *lefrancpaysan*|*francpaysan*) + TALER_MON_LANG=fr + ;; + *) + TALER_MON_LANG=en + ;; + esac + fi + case "${TALER_MON_LANG}" in + fr|FR|fra|french) TALER_MON_LANG=fr ;; + *) TALER_MON_LANG=en ;; + esac + export TALER_MON_LANG +} + +i18n_tag() { + local t="$1" + if [ "${TALER_MON_LANG:-en}" != "fr" ]; then + printf '%s' "$t" + return 0 + fi + case "$t" in + OK) printf 'OK' ;; + ERROR) printf 'ERREUR' ;; + WARN) printf 'AVERT' ;; + INFO) printf 'INFO' ;; + BLOCKER) printf 'BLOCAGE' ;; + BLOCK) printf 'BLOC' ;; + SUMMARY) printf 'RESUME' ;; + PROG|PROGRESS) printf 'PROG' ;; + *) printf '%s' "$t" ;; + esac +} + +i18n_text() { + local s="$*" + if [ "${TALER_MON_LANG:-en}" != "fr" ] || [ -z "$s" ]; then + printf '%s' "$s" + return 0 + fi + case "$s" in + "numbered checks this run:") printf 'controles numerotes de cette execution :' ;; + "failed — see list above"|"failed - see list above") printf 'echec — voir la liste ci-dessus' ;; + "BLOCKERS · pay/withdraw cannot finish") printf 'BLOCAGES · paiement/retrait impossible' ;; + "BLOCKERS (pay/withdraw cannot finish)") printf 'BLOCAGES (paiement/retrait impossible)' ;; + "ERRORS · failed checks") printf 'ERREURS · controles en echec' ;; + "ERRORS (failed checks)") printf 'ERREURS (controles en echec)' ;; + "pay/withdraw cannot finish") printf 'paiement/retrait impossible a terminer' ;; + "SUMMARY") printf 'RESUME' ;; + "totals:") printf 'totaux :' ;; + "container") printf 'conteneur' ;; + "disk") printf 'disque' ;; + "package") printf 'paquet' ;; + "version") printf 'version' ;; + "reachability") printf 'accessibilite' ;; + "server-header") printf 'en-tete Server' ;; + "inventory") printf 'inventaire' ;; + "flags") printf 'options' ;; + "target domain") printf 'domaine cible' ;; + "currency") printf 'devise' ;; + "phases") printf 'phases' ;; + "access") printf 'acces' ;; + *) + local out="$s" + out=${out//shared library missing/bibliotheque partagee manquante} + out=${out//shared library error/erreur de bibliotheque partagee} + out=${out//not active/non actif} + out=${out//after_start/apres_demarrage} + out=${out//catalogued but not reachable/catalogue mais inaccessible} + out=${out//does not resolve/ne resout pas} + out=${out//protocol confirmed/protocole confirme} + out=${out//catalogued service OK/service catalogue OK} + out=${out//from Server header/depuis en-tete Server} + out=${out//OSV clean/OSV sans vulnerabilite} + out=${out//actionable vuln/vuln. actionnable} + out=${out//header version · not Debian pkg/version en-tete · pas paquet Debian} + out=${out//RUN_TIMEOUT exceeded/RUN_TIMEOUT depasse} + out=${out//public HTTPS/HTTPS public} + out=${out//container status/etat du conteneur} + out=${out//remote-only/uniquement distant} + out=${out//no SSH/sans SSH} + out=${out//failed — see list above/echec — voir la liste ci-dessus} + printf '%s' "$out" + ;; + esac +} diff --git a/lib.sh b/lib.sh index 8455c6b..27bbca0 100755 --- a/lib.sh +++ b/lib.sh @@ -2,6 +2,18 @@ # Shared helpers for taler-monitoring (laptop or koopa). # Default stack = GOA / hacktivism (overridden by TALER_DOMAIN / --domain) +# i18n (en default; fr for FrancPaysan — see i18n.sh / TALER_MON_LANG) +_I18N_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +if [ -f "$_I18N_DIR/i18n.sh" ]; then + # shellcheck source=i18n.sh + source "$_I18N_DIR/i18n.sh" + i18n_init +else + i18n_tag() { printf '%s' "$1"; } + i18n_text() { printf '%s' "$*"; } + i18n_init() { :; } +fi + : "${TALER_DOMAIN:=hacktivism.ch}" : "${BANK_PUBLIC:=https://bank.hacktivism.ch}" : "${EXCHANGE_PUBLIC:=https://exchange.hacktivism.ch}" @@ -114,6 +126,11 @@ set_taler_stack() { BANK_PUBLIC=${BANK_PUBLIC%/} EXCHANGE_PUBLIC=${EXCHANGE_PUBLIC%/} MERCHANT_PUBLIC=${MERCHANT_PUBLIC%/} + # re-evaluate language after domain profile (FrancPaysan → fr) + if [ "${TALER_MON_LANG_SET:-0}" != "1" ]; then + TALER_MON_LANG= + i18n_init + fi } # Load first matching profile from domains.conf. @@ -265,6 +282,11 @@ apply_taler_domain() { BANK_PUBLIC=${BANK_PUBLIC%/} EXCHANGE_PUBLIC=${EXCHANGE_PUBLIC%/} MERCHANT_PUBLIC=${MERCHANT_PUBLIC%/} + # re-evaluate language after domain profile (FrancPaysan → fr) + if [ "${TALER_MON_LANG_SET:-0}" != "1" ]; then + TALER_MON_LANG= + i18n_init + fi # Koopa SSH only for LOCAL_STACK=1. Stage LFP uses INSIDE_SSH (stagepaysan) separately. if [ "${LOCAL_STACK}" != "1" ]; then @@ -679,6 +701,10 @@ _fmt_badge() { # $1=badge style $2=tag text $3=label colour $4=label $5=detail _msg_line() { local badge="$1" tag="$2" lcol="$3" label="$4" detail="${5:-}" + # i18n: tags + free text (en default; fr for FrancPaysan) + tag=$(i18n_tag "$tag") + label=$(i18n_text "$label") + [ -n "$detail" ] && detail=$(i18n_text "$detail") if [ -n "$detail" ]; then printf -- '%s %s%s%s%s %s·%s %s%s%s\n' \ "$(_fmt_badge "$badge" "$tag")" "$(_fmt_tid)" "$lcol" "$label" "$N" "$D" "$N" "$D" "$detail" "$N" @@ -749,7 +775,8 @@ blocker() { } section() { # Boxed section header (3 lines when colour on) - local title="$*" + local title + title=$(i18n_text "$*") local w=${#title} [ "$w" -lt 24 ] && w=24 [ "$w" -gt 56 ] && w=56 @@ -783,13 +810,13 @@ summary() { _progress_bar_line "$PROGRESS_DONE" "$PROGRESS_TOTAL" fi if [ "$GLOBAL_N" -gt 0 ]; then - printf -- '%s numbered checks this run: #001…#%03d%s\n' "$D" "$GLOBAL_N" "$N" + printf -- '%s %s #001…#%03d%s\n' "$D" "$(i18n_text 'numbered checks this run:')" "$GLOBAL_N" "$N" fi if [ "$blk_n" -gt 0 ]; then if [ "${BOX:-0}" = "1" ]; then - printf -- '%s┌ BLOCKERS · pay/withdraw cannot finish ┐%s\n' "$BG_BLK" "$N" + printf -- '%s┌ %s ┐%s\n' "$BG_BLK" "$(i18n_text 'BLOCKERS · pay/withdraw cannot finish')" "$N" else - printf -- '%s--- BLOCKERS (pay/withdraw cannot finish) ---%s\n' "$M" "$N" + printf -- '%s--- %s ---%s\n' "$M" "$(i18n_text 'BLOCKERS (pay/withdraw cannot finish)')" "$N" fi local b for b in "${BLOCKERS[@]}"; do @@ -798,9 +825,9 @@ summary() { fi if [ "${#ERRORS[@]}" -gt 0 ] && [ "$blk_n" -lt "${#ERRORS[@]}" ]; then if [ "${BOX:-0}" = "1" ]; then - printf -- '%s┌ ERRORS · failed checks ┐%s\n' "$BG_ERR" "$N" + printf -- '%s┌ %s ┐%s\n' "$BG_ERR" "$(i18n_text 'ERRORS · failed checks')" "$N" else - printf -- '%s--- ERRORS (failed checks) ---%s\n' "$R" "$N" + printf -- '%s--- %s ---%s\n' "$R" "$(i18n_text 'ERRORS (failed checks)')" "$N" fi local e for e in "${ERRORS[@]}"; do @@ -811,9 +838,11 @@ summary() { # Coloured totals — same severity badges as check lines (always; NO_COLOR blanks ANSI) if [ "${BOX:-0}" = "1" ]; then - printf -- '\n%s┌ SUMMARY ┐%s\n' "$BG_SEC" "$N" + printf -- '\n%s┌ %s ┐%s\n' "$BG_SEC" "$(i18n_tag SUMMARY)" "$N" _sum_badge() { # $1=bg $2=tag $3=fg $4=n - printf -- ' %s┌ %-5s┐%s %s%4d%s\n' "$1" "$2" "$N" "$3" "$4" "$N" + local _tg + _tg=$(i18n_tag "$2") + printf -- ' %s┌ %-5s┐%s %s%4d%s\n' "$1" "$_tg" "$N" "$3" "$4" "$N" } _sum_badge "$BG_OK" "OK" "$G" "$PASS_N" [ "$FAIL_N" -gt 0 ] && _sum_badge "$BG_ERR" "ERROR" "$R" "$FAIL_N" @@ -821,12 +850,12 @@ summary() { [ "$INFO_N" -gt 0 ] && _sum_badge "$BG_INFO" "INFO" "$C" "$INFO_N" [ "$blk_n" -gt 0 ] && _sum_badge "$BG_BLK" "BLOCK" "$M" "$blk_n" else - printf -- '\n[ SUMMARY ]\n' - printf -- ' [ OK ] %s%4d%s\n' "$G" "$PASS_N" "$N" - [ "$FAIL_N" -gt 0 ] && printf -- ' [ ERROR ] %s%4d%s\n' "$R" "$FAIL_N" "$N" - [ "$WARN_N" -gt 0 ] && printf -- ' [ WARN ] %s%4d%s\n' "$Y" "$WARN_N" "$N" - [ "$INFO_N" -gt 0 ] && printf -- ' [ INFO ] %s%4d%s\n' "$C" "$INFO_N" "$N" - [ "$blk_n" -gt 0 ] && printf -- ' [ BLOCK ] %s%4d%s\n' "$M" "$blk_n" "$N" + printf -- '\n[ %s ]\n' "$(i18n_tag SUMMARY)" + printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag OK)" "$G" "$PASS_N" "$N" + [ "$FAIL_N" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag ERROR)" "$R" "$FAIL_N" "$N" + [ "$WARN_N" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag WARN)" "$Y" "$WARN_N" "$N" + [ "$INFO_N" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag INFO)" "$C" "$INFO_N" "$N" + [ "$blk_n" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag BLOCK)" "$M" "$blk_n" "$N" fi # one-line rollup (bold label, severity-coloured counts) printf -- ' %stotals:%s %s%d OK%s' "$B" "$N" "$G" "$PASS_N" "$N" @@ -850,9 +879,9 @@ summary() { fi else if [ "${BOX:-0}" = "1" ]; then - printf -- ' %s┌ ERROR ┐%s %sfailed — see list above%s\n' "$BG_ERR" "$N" "$R" "$N" + printf -- ' %s┌ ERROR ┐%s %s"$(i18n_text "failed — see list above")"%s\n' "$BG_ERR" "$N" "$R" "$N" else - printf -- ' %s[ ERROR ] failed — see list above%s\n' "$R" "$N" + printf -- ' %s[ ERROR ] "$(i18n_text "failed — see list above")"%s\n' "$R" "$N" fi fi [ "$FAIL_N" -eq 0 ] diff --git a/site-gen/console_to_html.py b/site-gen/console_to_html.py index 75db298..4c4927f 100755 --- a/site-gen/console_to_html.py +++ b/site-gen/console_to_html.py @@ -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.+)$") @@ -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'{n_err} error{"s" if n_err != 1 else ""}' + f'title="{html.escape(ui(lang, "jump_first_error"))}">{html.escape(err_lbl)}' ) else: - err_stat = f'{n_err} errors' + err_lbl = ui(lang, "error_many", n=n_err) + err_stat = f'{html.escape(err_lbl)}' 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'{n_warn} warning{"s" if n_warn != 1 else ""}' + f'title="{html.escape(ui(lang, "jump_first_warn"))}">{html.escape(warn_lbl)}' ) else: - warn_stat = f'{n_warn} warnings' + warn_lbl = ui(lang, "warn_many", n=n_warn) + warn_stat = f'{html.escape(warn_lbl)}' 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( - generated + {html.escape(ui(lang, "generated"))}