- {html.escape(status_txt)}
+
+ {html.escape(status_txt)}
+ {version_tag}
+
{html.escape(pl_disp)} · {html.escape(hostname)}
·
{err_stat}
·
{warn_stat}
- {filter_chip}
·
- {version_html}
+ {info_stat}
+ ·
+ {ok_stat}
+ {filter_chip}
·
= 0x2500 && bc <= 0x257f) continue; // U+2500–U+257F box drawing
+ return false;
+ }
+ return true;
}
var keep = new Array(lines.length);
var i, j, k, n, idx;
@@ -1618,11 +1987,14 @@ STICKY_JS = """
function setFilter(mode, opts) {
opts = opts || {};
- document.body.classList.remove("filter-error", "filter-warn");
+ document.body.classList.remove("filter-error", "filter-warn", "filter-info", "filter-ok");
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");
+ var infoA = document.querySelector('a.stat.info[data-filter="info"]');
+ var okA = document.querySelector('a.stat.ok[data-filter="ok"]');
+ [errA, warnA, infoA, okA].forEach(function (a) {
+ if (a) a.setAttribute("aria-pressed", "false");
+ });
if (mode === "error") {
document.body.classList.add("filter-error");
@@ -1630,11 +2002,19 @@ STICKY_JS = """
} else if (mode === "warn") {
document.body.classList.add("filter-warn");
if (warnA) warnA.setAttribute("aria-pressed", "true");
+ } else if (mode === "info") {
+ document.body.classList.add("filter-info");
+ if (infoA) infoA.setAttribute("aria-pressed", "true");
+ } else if (mode === "ok") {
+ document.body.classList.add("filter-ok");
+ if (okA) okA.setAttribute("aria-pressed", "true");
}
- // Mark dimmed neighbours (or clear when filter off)
- if (mode === "error" || mode === "warn") markFilterContext(mode);
- else clearFilterContext();
+ if (mode === "error" || mode === "warn" || mode === "info" || mode === "ok") {
+ markFilterContext(mode);
+ } else {
+ clearFilterContext();
+ }
if (chip && chipLabel && bar) {
if (mode === "error") {
@@ -1643,6 +2023,12 @@ STICKY_JS = """
} else if (mode === "warn") {
chip.hidden = false;
chipLabel.textContent = bar.getAttribute("data-i18n-filter-warn") || "Filter: warnings + context";
+ } else if (mode === "info") {
+ chip.hidden = false;
+ chipLabel.textContent = bar.getAttribute("data-i18n-filter-info") || "Filter: info + context";
+ } else if (mode === "ok") {
+ chip.hidden = false;
+ chipLabel.textContent = bar.getAttribute("data-i18n-filter-ok") || "Filter: OK only + context";
} else {
chip.hidden = true;
chipLabel.textContent = "";
@@ -1651,24 +2037,28 @@ STICKY_JS = """
if (opts.updateHash !== false) {
var want = mode === "error" ? "#filter-error"
- : mode === "warn" ? "#filter-warn" : "";
+ : mode === "warn" ? "#filter-warn"
+ : mode === "info" ? "#filter-info"
+ : mode === "ok" ? "#filter-ok" : "";
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") {
+ } else if (/^#(filter-error|filter-warn|filter-info|filter-ok|first-error|first-warn)$/.test(location.hash || "")) {
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);
+ var sel = mode === "error" ? ".line.error, .line.blocker"
+ : mode === "warn" ? ".line.warn"
+ : mode === "info" ? ".line.info"
+ : ".line.ok";
+ var id = mode === "error" ? "first-error" : mode === "warn" ? "first-warn" : "";
+ var el = id ? document.getElementById(id) : null;
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);
@@ -1686,7 +2076,9 @@ STICKY_JS = """
a.addEventListener("click", function (ev) {
ev.preventDefault();
var mode = a.getAttribute("data-filter");
- if (mode === "error" || mode === "warn") toggleFilter(mode);
+ if (mode === "error" || mode === "warn" || mode === "info" || mode === "ok") {
+ toggleFilter(mode);
+ }
});
});
if (clearBtn) {
@@ -1701,6 +2093,10 @@ STICKY_JS = """
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 (h === "filter-info") {
+ setFilter("info", { updateHash: false, scrollFirst: true });
+ } else if (h === "filter-ok") {
+ setFilter("ok", { updateHash: false, scrollFirst: true });
} else if (currentFilter()) {
// keep filter if hash is something else (e.g. err-slug)
}
@@ -1739,8 +2135,9 @@ def build_html(
# Split monitoring env context (collapsible, default closed) from check log
env_lines, body_raw, env_hint = extract_env_context(raw_lines)
# Sticky counts from full log; first-error anchors index into body only
- n_err, n_warn, _, _ = count_status(raw_lines)
- _, _, first_err_i, first_warn_i = count_status(body_raw)
+ n_err, n_warn, n_info, n_ok, _, _, _, _ = count_status(raw_lines)
+ _e2, _w2, _i2, _o2, first_err_i, first_warn_i, _fi, _fo = count_status(body_raw)
+ del _e2, _w2, _i2, _o2, _fi, _fo
errors = extract_errors(raw_lines)
err_slugs: dict[str, str] = {}
@@ -1790,12 +2187,7 @@ def build_html(
page_label or ""
).lower()
if _is_apt:
- _lang_board = 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_board = "fr"
+ _lang_board = ui_lang(host_hint=hostname + "\n" + log_text)
err_nav += aptdeploy_board_html(log_text, lang=_lang_board)
if errors and mode == "err":
@@ -1840,11 +2232,7 @@ 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
- 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"
+ lang = ui_lang(host_hint=hostname + "\n" + log_text)
scope = monitoring_scope(page_label, hostname, log_text, lang=lang)
page_lang = lang
@@ -1857,6 +2245,8 @@ def build_html(
generated_iso=generated_iso,
n_err=n_err,
n_warn=n_warn,
+ n_info=n_info,
+ n_ok=n_ok,
hostname=hostname,
page_label=page_label,
mode=mode,
@@ -2206,7 +2596,7 @@ def main() -> None:
ap.add_argument(
"--lang",
default="",
- help="UI language en|fr (default: env TALER_MON_LANG or auto)",
+ help="UI language en|fr|de (default: domains.conf lang / env; de never stock default)",
)
ap.add_argument(
"--page-label",
@@ -2227,7 +2617,8 @@ def main() -> None:
for l in log_text.splitlines()
if not is_log_noise_line(strip_ansi(l))
]
- n_err, n_warn, _, _ = count_status(raw_for_counts)
+ n_err, n_warn, n_info, n_ok, _, _, _, _ = count_status(raw_for_counts)
+ # n_info/n_ok used by sticky filters when build_html is called below
if args.mode == "redirect":
html_out = build_redirect_html(
@@ -2262,7 +2653,12 @@ def main() -> None:
)
atomic_write_text(args.out, html_out, encoding="utf-8")
- print(f"wrote {args.out} (errors={n_err} warnings={n_warn} level={status_level(n_err, n_warn)})")
+ # stderr only — stdout is often teed into the suite mon log
+ print(
+ f"wrote {args.out} (errors={n_err} warnings={n_warn} "
+ f"level={status_level(n_err, n_warn)})",
+ file=sys.stderr,
+ )
if __name__ == "__main__":
diff --git a/surface-catalog.conf b/surface-catalog.conf
index cae2a14..3caa703 100644
--- a/surface-catalog.conf
+++ b/surface-catalog.conf
@@ -46,7 +46,10 @@ bugs.gnunet.org 443 https gnunet-bugs
# --- taler-systems.com ---
www.taler-systems.com 443 https tsa-www
taler-systems.com 443 https tsa-apex
-anastasis.taler-systems.com 443,25,465,587,143,993 https anastasis-mail
+# Anastasis / TSA mail plane (MX mail.anastasis.lu → pixel). Do NOT list
+# anastasis.taler-systems.com — it does not resolve (was a catalog typo).
+mail.anastasis.lu 443,25,465,587,143,993 https anastasis-mail
+pixel.taler-systems.com 443,25,465,587,143,993 https pixel-mail
# --- mail (also covered by phase mail; listed for surface nmap/ports) ---
firefly.gnunet.org 443,25,465,587,143,993 https firefly-mail
diff --git a/taler-monitoring.sh b/taler-monitoring.sh
index 67ddf76..5379f3f 100755
--- a/taler-monitoring.sh
+++ b/taler-monitoring.sh
@@ -93,14 +93,16 @@ Phases:
sanity public + optional server
server server-side only (SSH)
e2e withdraw + pay (small amounts; remote aborts on login/KYC)
- ladder withdraw/pay amount ladder (GOA ceiling or stage TESTPAYSAN max_wire)
+ ladder amount ladder classic (default 23-rung withdraw + pay; any currency)
+ max-ladder max-search: highest withdrawable + highest payable (≤ available)
+ goa-ladder alias of ladder (legacy phase name; same check_amount_ladder.sh)
auth401 merchant Basic-auth / case matrix (HTTP 401 paths; may create throwaway instance)
aptdeploy koopa podman apt-src smoke: taler-merchant in
koopa-taler-deploy-test-apt-src-trixie{,-testing}
surface REMOTE-ONLY public inventory (NOT in default/all/full):
ecosystem hosts (taler.net, gnunet.org, taler-systems.com, mattermost, …)
or -d DOMAIN → that domain’s surface; port/protocol/TLS/CVE (OSV)
- mattermost Mattermost chat health (default mattermost.taler.net; SPA + /api/v4/system/ping)
+ mattermost Mattermost chat health (SPA + ping + client floors: mobile/desktop/support ≥10.11)
mail MX/SMTP/IMAP for Taler mail (firefly, pixel/TSA, catalogued domains)
monpages public monitoring HTML via FQDN (obligatory ERROR; GOA full inventory, FP only FP)
uses MON_HOSTS + HTML_URL_OK (same as host-agent); v1.3.1+
@@ -122,7 +124,7 @@ Options:
--merchant URL merchant-backend base — overrides profile
--currency CODE expected currency (GOA, KUDOS, CHF, …); empty = report only
--no-probe do not probe alternate hosts (unknown domains only)
- --lang en|fr UI/console language (sticky bar + badges)
+ --lang en|fr|de UI/console language (sticky bar + badges; de available, not a profile default)
overrides auto (FrancPaysan→fr, else en)
--ver, --version, -V print suite version, git commit, and repo URLs
-h, --help
@@ -165,7 +167,8 @@ Env (same meaning):
SURFACE_CVE_LEVEL=warn|error bare Server-header versions default warn
(Debian package versions default error)
SURFACE_CATALOG=path override surface-catalog.conf
- TALER_MON_LANG=en|fr same as --lang (default en; auto fr for *lefrancpaysan*)
+ TALER_MON_LANG=en|fr|de same as --lang (profile lang from domains.conf; de never stock default)
+ TALER_MON_LOCALE=fr-CH|de-CH regional l10n from domains.conf locale column
Full load (GOA / hacktivism):
./taler-monitoring.sh -d hacktivism.ch full
@@ -225,12 +228,13 @@ NO_PROBE=0
while [ $# -gt 0 ]; do
case "$1" in
--lang)
- [ $# -ge 2 ] || { echo "error: --lang needs en|fr" >&2; exit 2; }
+ [ $# -ge 2 ] || { echo "error: --lang needs en|fr|de" >&2; exit 2; }
_lang="$2"
case "$_lang" in
fr|FR|fra|french) TALER_MON_LANG=fr ;;
+ de|DE|deu|ger|german|deutsch) TALER_MON_LANG=de ;;
en|EN|eng|english) TALER_MON_LANG=en ;;
- *) echo "error: unknown language '$_lang' (use en or fr)" >&2; exit 2 ;;
+ *) echo "error: unknown language '$_lang' (use en, fr, or de)" >&2; exit 2 ;;
esac
TALER_MON_LANG_SET=1
export TALER_MON_LANG TALER_MON_LANG_SET
@@ -263,7 +267,7 @@ while [ $# -gt 0 ]; do
CURRENCY_OVERRIDE="$2"; shift 2
;;
--no-probe) NO_PROBE=1; shift ;;
- urls|inside|versions|sanity|server|e2e|ladder|goa-ladder|auth401|aptdeploy|apt-deploy|apt_src|surface|ecosystem|mattermost|mail|monpages|pages|devtesting|franken|fake-franken|fake_franken|all|full) PHASES+=("$1"); shift ;;
+ urls|inside|versions|sanity|server|e2e|ladder|goa-ladder|max-ladder|maxladder|auth401|aptdeploy|apt-deploy|apt_src|surface|ecosystem|mattermost|mail|monpages|pages|devtesting|franken|fake-franken|fake_franken|all|full) PHASES+=("$1"); shift ;;
*)
# bare domain shorthand: ./taler-monitoring.sh taler.net
if [[ "$1" == *.* && "$1" != *://* && "$1" != -* ]]; then
@@ -293,7 +297,7 @@ if [ "${LOCAL_STACK:-1}" != "1" ]; then
SKIP_SSH=1
fi
-# Remote: no SSH; ATM withdraw ladder set in check_e2e (smaller notes)
+# Remote: no SSH; bank withdraw ladder set in check_e2e (smaller notes)
if [ "${LOCAL_STACK}" != "1" ]; then
E2E_FAKE_INCOMING=0
E2E_REMOTE=1
@@ -307,14 +311,15 @@ export EXPECT_CURRENCY SKIP_SSH LOCAL_STACK TALER_DOMAIN_PROBE CHECK_LANDING
export TALER_DOMAIN_FROM_CLI="${TALER_DOMAIN_FROM_CLI:-0}"
export DOMAIN_SET="${DOMAIN_SET:-0}"
export WITHDRAW_AMT PAY_AMT CREDIT_AMT MERCHANT_INSTANCE
-export E2E_FAKE_INCOMING E2E_REMOTE E2E_VARIABLE E2E_ATM_MAX
+export E2E_FAKE_INCOMING E2E_REMOTE E2E_VARIABLE E2E_ATM_MAX E2E_BANK_WITHDRAW_MAX
export E2E_WITHDRAW_VALUES E2E_PAY_VALUES E2E_USE_TEMPLATES E2E_TEMPLATE_MAP
export PAIVANA_PUBLIC E2E_PAIVANA E2E_PAIVANA_TEMPLATE E2E_PAIVANA_AMOUNT E2E_PAIVANA_INSTANCE
export INSIDE_PROFILE INSIDE_SSH
export INSIDE_BANK_CTR INSIDE_EXCHANGE_CTR INSIDE_MERCHANT_CTR
export INSIDE_BANK_PORT INSIDE_EXCHANGE_PORT INSIDE_MERCHANT_PORT
export INSIDE_DNS_BANK INSIDE_DNS_EXCHANGE INSIDE_DNS_MERCHANT
-# Ladder: withdraw then pay — 0 + random mids + max-1 + max (see check_goa_ladder.sh).
+# Amount ladder — check_amount_ladder.sh + ladder/lib_pay.sh + ladder/extract_rpubs.py
+# Phases: ladder|goa-ladder (classic), max-ladder|maxladder (max-search).
# Defaults so set -u export is safe when vars were never set by caller.
: "${LADDER_STEPS:=23}"
: "${LADDER_MIN_AMOUNT:=0.000001}"
@@ -328,16 +333,23 @@ export INSIDE_DNS_BANK INSIDE_DNS_EXCHANGE INSIDE_DNS_MERCHANT
: "${LADDER_INCLUDE_ZERO:=1}"
: "${LADDER_INCLUDE_MAX:=1}"
: "${LADDER_HIGH_FROM:=1000000}"
-: "${LADDER_HIGH_RUNGS:=12}"
+: "${LADDER_HIGH_RUNGS:=4}"
: "${LADDER_LOAD:=1}"
: "${LADDER_PAY:=1}"
: "${LADDER_WITHDRAW_SCALE:=1.5}"
: "${LADDER_PAY_SETTLE_ROUNDS:=6}"
+: "${LADDER_MODE:=classic}"
+: "${LADDER_MAX_PROBES:=32}"
+: "${LADDER_MAX_TOL_REL:=0.001}"
+: "${LADDER_MAX_SEED:=}"
+: "${LADDER_PAY_WITH_MAX:=0}"
+: "${LADDER_HIGH_RUNGS:=6}"
export LADDER_STEPS LADDER_MIN_AMOUNT LADDER_CONFIRM_POLLS LADDER_MAX_RUNGS LADDER_TIMEOUT_S LADDER_REPORT_DIR
export LADDER_SETTLE_ROUNDS LADDER_SETTLE_SLEEP EXP_PW_FILE EXP_USER
export LADDER_MAX_AMOUNT LADDER_INCLUDE_ZERO LADDER_INCLUDE_MAX
export LADDER_HIGH_FROM LADDER_HIGH_RUNGS LADDER_LOAD
export LADDER_PAY LADDER_WITHDRAW_SCALE LADDER_PAY_SETTLE_ROUNDS
+export LADDER_MODE LADDER_MAX_PROBES LADDER_MAX_TOL_REL LADDER_MAX_SEED LADDER_PAY_WITH_MAX
export TALER_DOMAIN_APPLIED=1
i18n_init 2>/dev/null || true
# Optional: auth401 and other phases may honor CONTINUE_ON_ERROR
@@ -431,9 +443,9 @@ _progress_estimate_phase() {
versions) n=35 ;;
sanity) n=35 ;;
server) n=20 ;;
- # e2e: ATM ladder emits many INFO lines (coins before/after each note)
+ # e2e: bank withdraw ladder emits many INFO lines (coins before/after each note)
e2e) n=240 ;;
- ladder|goa-ladder) n=120 ;;
+ ladder|goa-ladder|max-ladder|maxladder) n=120 ;;
auth401) n=70 ;;
aptdeploy) n=40 ;;
surface|ecosystem) n=120 ;;
@@ -547,6 +559,8 @@ elif [ "${LOCAL_STACK:-0}" = "1" ] && command -v podman >/dev/null 2>&1 \
fi
printf 'target domain=%s\n' "${TALER_DOMAIN}"
+printf ' ui_lang %s\n' "${TALER_MON_LANG:-en}"
+printf ' locale %s\n' "${TALER_MON_LOCALE:-${TALER_DOMAIN_LOCALE:-?}}"
printf ' bank %s\n' "$BANK_PUBLIC"
printf ' exchange %s\n' "$EXCHANGE_PUBLIC"
printf ' merchant %s\n' "$MERCHANT_PUBLIC"
@@ -607,7 +621,15 @@ while [ "$_phase_idx" -lt "$_n_phases" ]; do
sanity) run_phase sanity "$ROOT/check_sanity.sh" || ec=1 ;;
server) run_phase server "$ROOT/check_server.sh" || ec=1 ;;
e2e) run_phase e2e "$ROOT/check_e2e.sh" || ec=1 ;;
- ladder|goa-ladder) run_phase ladder "$ROOT/check_goa_ladder.sh" || ec=1 ;;
+ ladder|goa-ladder)
+ run_phase ladder "$ROOT/check_amount_ladder.sh" || ec=1
+ ;;
+ max-ladder|maxladder)
+ # Feature: hunt highest mint+confirm withdrawable amount (any currency)
+ LADDER_MODE=max
+ export LADDER_MODE
+ run_phase ladder "$ROOT/check_amount_ladder.sh" || ec=1
+ ;;
auth401) run_phase auth401 "$ROOT/check_auth401.sh" || ec=1 ;;
aptdeploy|apt-deploy|apt_src) run_phase aptdeploy "$ROOT/check_apt_deploy.sh" || ec=1 ;;
surface|ecosystem) run_phase surface "$ROOT/check_surface.sh" || ec=1 ;;
diff --git a/tests/test_normalize_taler_uri.sh b/tests/test_normalize_taler_uri.sh
new file mode 100755
index 0000000..1db180b
--- /dev/null
+++ b/tests/test_normalize_taler_uri.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+# Unit tests for normalize_taler_uri / taler_uri_has_default_port (lib.sh).
+# Run: ./tests/test_normalize_taler_uri.sh
+set -euo pipefail
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+# shellcheck source=../lib.sh
+source "$ROOT/lib.sh"
+
+pass=0
+fail=0
+assert_eq() {
+ local name="$1" got="$2" want="$3"
+ if [ "$got" = "$want" ]; then
+ pass=$((pass + 1))
+ else
+ echo "FAIL $name" >&2
+ echo " got: $got" >&2
+ echo " want: $want" >&2
+ fail=$((fail + 1))
+ fi
+}
+assert_true() {
+ local name="$1"
+ if "$@"; then
+ # shift name
+ :
+ fi
+}
+
+# --- strip :443 / :80 ---
+assert_eq "withdraw :443/" \
+ "$(normalize_taler_uri 'taler://withdraw/bank.hacktivism.ch:443/taler-integration/abc-123')" \
+ "taler://withdraw/bank.hacktivism.ch/taler-integration/abc-123"
+
+assert_eq "withdraw :443 end" \
+ "$(normalize_taler_uri 'taler://withdraw/bank.example:443')" \
+ "taler://withdraw/bank.example"
+
+assert_eq "pay :443" \
+ "$(normalize_taler_uri 'taler://pay/taler.hacktivism.ch:443/instances/x/orders/y?c=z')" \
+ "taler://pay/taler.hacktivism.ch/instances/x/orders/y?c=z"
+
+assert_eq "pay-template :80" \
+ "$(normalize_taler_uri 'taler://pay-template/shop.example:80/instances/i/t')" \
+ "taler://pay-template/shop.example/instances/i/t"
+
+assert_eq "already clean" \
+ "$(normalize_taler_uri 'taler://withdraw/bank.hacktivism.ch/taler-integration/uuid')" \
+ "taler://withdraw/bank.hacktivism.ch/taler-integration/uuid"
+
+assert_eq "empty" "$(normalize_taler_uri '')" ""
+
+# non-default port kept
+assert_eq "keep :8443" \
+ "$(normalize_taler_uri 'taler://withdraw/bank.example:8443/taler-integration/x')" \
+ "taler://withdraw/bank.example:8443/taler-integration/x"
+
+# has_default_port detector
+if taler_uri_has_default_port 'taler://withdraw/h:443/p'; then
+ pass=$((pass + 1))
+else
+ echo "FAIL detector should match :443" >&2
+ fail=$((fail + 1))
+fi
+if taler_uri_has_default_port 'taler://withdraw/h/p'; then
+ echo "FAIL detector should not match clean URI" >&2
+ fail=$((fail + 1))
+else
+ pass=$((pass + 1))
+fi
+
+echo "normalize_taler_uri tests: pass=$pass fail=$fail"
+[ "$fail" -eq 0 ]