diff --git a/configs/systemd/user/taler-landing-stats.service b/configs/systemd/user/taler-landing-stats.service index cc6bd88..6273a72 100644 --- a/configs/systemd/user/taler-landing-stats.service +++ b/configs/systemd/user/taler-landing-stats.service @@ -14,14 +14,12 @@ TimeoutStartSec=480 # Do not keep "active" after run — timer may fire again cleanly RemainAfterExit=no Environment=TZ=Europe/Zurich -# Stack overrides (goa / stage-testpaysan / custom) — optional -EnvironmentFile=-%h/.config/taler-landing/stack.env -# GOA defaults if stack.env absent Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log Environment=SECRETS_BANK=%h/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank Environment=BANK_URL=http://127.0.0.1:9012 Environment=BANK_PUBLIC_URL=https://bank.hacktivism.ch Environment=EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config +# All accounts except exchange (double-count of withdraw credits on exchange ledger) Environment=SCAN_SKIP=exchange Environment=TX_PAGE=500 Environment=MAX_TX_PAGES=0 diff --git a/scripts/taler-exchange/landing-stats-exchange.sh b/scripts/taler-exchange/landing-stats-exchange.sh index 987e631..35452d4 100644 --- a/scripts/taler-exchange/landing-stats-exchange.sh +++ b/scripts/taler-exchange/landing-stats-exchange.sh @@ -238,7 +238,6 @@ fi GEN=$(now_iso) HUMAN=$(now_human) -GEN_UNIX=$(date +%s) num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; } MEM_JSON='"container_rss_human": "—"' @@ -258,7 +257,6 @@ cat >"$TMP" </dev/null; then - if cp -f "$src_stats" "${d}/stats.json" 2>/dev/null; then - chmod a+r "${d}/stats.json" 2>/dev/null || true - [ -n "$src_run" ] && [ -f "$src_run" ] && cp -f "$src_run" "${d}/stats-run.json" 2>/dev/null || true - log "host: published ${d}/stats.json" - continue - fi - fi - if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then - sudo mkdir -p "$d" 2>/dev/null || true - sudo cp -f "$src_stats" "${d}/stats.json" - sudo chmod a+r "${d}/stats.json" 2>/dev/null || true - [ -n "$src_run" ] && [ -f "$src_run" ] && sudo cp -f "$src_run" "${d}/stats-run.json" 2>/dev/null || true - log "host: published ${d}/stats.json (sudo)" - else - log "WARN: cannot write ${d}/stats.json (need write or passwordless sudo)" - fi - done <<<"$paths" -} - read_pass() { local name="$1" f for f in \ @@ -175,25 +107,11 @@ read_pass() { return 0 fi done - # container secrets: /root/… (GOA) or /data/secrets/… (FrancPaysan stage) - if [ -n "${BANK_CTR:-}" ] && podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then - for path in "/root/${name}" "/data/secrets/${name}" "/data/secrets/bank-admin-password.txt"; do - case "$path" in - *bank-admin-password.txt) - [ "$name" = "bank-admin-password.txt" ] || continue - ;; - esac - if podman exec "$BANK_CTR" test -r "$path" 2>/dev/null; then - podman exec "$BANK_CTR" cat "$path" 2>/dev/null | tr -d '\n\r' - return 0 - fi - done - # stage explorer secret naming - if [ "$name" = "bank-explorer-password.txt" ]; then - if podman exec "$BANK_CTR" test -r /data/secrets/bank-explorer-password.txt 2>/dev/null; then - podman exec "$BANK_CTR" cat /data/secrets/bank-explorer-password.txt 2>/dev/null | tr -d '\n\r' - return 0 - fi + # container (hernani can podman exec root files inside owned containers) + if podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then + if podman exec "$BANK_CTR" test -r "/root/${name}" 2>/dev/null; then + podman exec "$BANK_CTR" cat "/root/${name}" 2>/dev/null | tr -d '\n' + return 0 fi fi return 1 @@ -205,8 +123,6 @@ ctr_running() { publish_json() { local ctr="$1" dest_dir="$2" src_stats="$3" src_run="$4" - [ "${PUBLISH_PODMAN}" = "1" ] || return 0 - [ -n "$ctr" ] || return 0 if ! ctr_running "$ctr"; then log "WARN: $ctr not running — skip publish $dest_dir" return 1 @@ -250,7 +166,6 @@ collect_bank() { BANK_URL="$BANK_URL" \ BANK_PUBLIC_URL="$BANK_PUBLIC_URL" \ EXCHANGE_CONFIG_URL="$EXCHANGE_CONFIG_URL" \ - BANK_CURRENCY="${BANK_CURRENCY:-}" \ BANK_ADMIN_PASS="$admin_pass" \ BANK_EXPLORER_PASS="$explorer_pass" \ DEMO_DIR="${demo_dir}" \ @@ -258,7 +173,6 @@ collect_bank() { TX_PAGE="${TX_PAGE:-500}" \ MAX_TX_PAGES="${MAX_TX_PAGES:-0}" \ ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}" \ - STATS_SOURCE_LABEL="$STATS_SOURCE_LABEL" \ "$PY" "$LIB/collect_bank_stats.py" \ --out "$WORKDIR/bank-stats.json" \ --run-out "$WORKDIR/bank-stats-run.json" \ @@ -267,13 +181,11 @@ collect_bank() { set -e if [ "$ec_bank" -eq 0 ] && [ -f "$WORKDIR/bank-stats.json" ]; then - # Optional: attach in-container RSS/loadavg (not host /proc) + # Always attach in-container RSS/loadavg (not host /proc) merge_container_resources bank "$BANK_CTR" "$WORKDIR/bank-stats.json" || true publish_json "$BANK_CTR" "$BANK_LANDING_IN" \ "$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json" - # Shared public feed: same bank stats on all HOST_STATS_DIRS (bank/ex/merchant landings) - publish_host_stats "$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json" - log "bank: OK (podman=${BANK_CTR:-none} host_dirs=${HOST_STATS_DIRS:-none})" + log "bank: OK → ${BANK_CTR}:${BANK_LANDING_IN}/stats.json" else log "ERROR: bank collect failed (ec=$ec_bank) — previous stats.json kept" [ -f "$WORKDIR/bank-stats-run.json" ] || \ @@ -360,26 +272,13 @@ collect_merchant() { # --------------------------------------------------------------------------- main() { - log "=== taler-landing-stats start (user=$(id -un) lib=$LIB currency=${BANK_CURRENCY:-auto}) ===" - log "phases bank=${COLLECT_BANK} exchange=${COLLECT_EXCHANGE} merchant=${COLLECT_MERCHANT} resources=${COLLECT_RESOURCES}" - if [ "${COLLECT_BANK}" = "1" ]; then - collect_bank || ec_bank=$? - else - log "skip bank collect" - fi - if [ "${COLLECT_EXCHANGE}" = "1" ]; then - collect_exchange || ec_ex=$? - else - log "skip exchange collect" - fi - if [ "${COLLECT_MERCHANT}" = "1" ]; then - collect_merchant || ec_mer=$? - else - log "skip merchant collect" - fi + log "=== taler-landing-stats start (user=$(id -un) lib=$LIB) ===" + collect_bank || ec_bank=$? + collect_exchange || ec_ex=$? + collect_merchant || ec_mer=$? log "=== done bank=$ec_bank exchange=$ec_ex merchant=$ec_mer ===" # non-zero if bank failed (primary public flow numbers); soft on ex/mer - if [ "${COLLECT_BANK}" = "1" ] && [ "$ec_bank" -ne 0 ]; then + if [ "$ec_bank" -ne 0 ]; then return 1 fi return 0 diff --git a/scripts/taler-landing/collect_bank_stats.py b/scripts/taler-landing/collect_bank_stats.py index b8d4398..096eae7 100755 --- a/scripts/taler-landing/collect_bank_stats.py +++ b/scripts/taler-landing/collect_bank_stats.py @@ -1,22 +1,23 @@ #!/usr/bin/env python3 """Full-scan bank landing stats (all accounts, all ledger money). -Generic for any currency stack (GOA, TESTPAYSAN, KUDOS, …). Run on the ops host -(with admin API access). Writes stats.json for public landings + amount_alt. +Run on koopa as hernani (or anywhere with admin API access). Writes stats.json +compatible with bank.hacktivism.ch/intro/ plus amount_alt fields for compact UI. Env (selected): BANK_URL default http://127.0.0.1:9012 - BANK_CURRENCY optional; else from exchange /config or first ledger amount BANK_ADMIN_USER default admin BANK_ADMIN_PASS or BANK_ADMIN_PASS_FILE / pass via --admin-pass-file BANK_EXPLORER_USER default explorer BANK_EXPLORER_PASS optional (balance_explorer) - EXCHANGE_CONFIG_URL for alt_unit_names + currency - BANK_PUBLIC_URL public base for latency probes + EXCHANGE_CONFIG_URL for alt_unit_names (default https://exchange.hacktivism.ch/config) OUT output path (default stdout if -) SCAN_SKIP comma usernames excluded from flow (default: exchange) - TX_PAGE / MAX_TX_PAGES / ACCOUNTS_DELTA / RECENT_WD_N / TZ - STATS_SOURCE_LABEL optional string in stats.source field + TX_PAGE page size for transactions delta (default 500) + MAX_TX_PAGES 0 = unlimited pages per account (default 0) + ACCOUNTS_DELTA GET /accounts?delta= (default -10000) + RECENT_WD_N recent withdraws (default 10) + TZ default Europe/Zurich """ from __future__ import annotations @@ -342,11 +343,6 @@ def main() -> int: "--exchange-config", default=env("EXCHANGE_CONFIG_URL", "https://exchange.hacktivism.ch/config"), ) - ap.add_argument( - "--currency", - default=env("BANK_CURRENCY", ""), - help="ledger currency code (optional; else exchange /config or first amount)", - ) ap.add_argument("--out", default=env("OUT", "-")) ap.add_argument("--run-out", default=env("RUN_OUT", "")) ap.add_argument( @@ -368,11 +364,6 @@ def main() -> int: ) ap.add_argument("--recent", type=int, default=int(env("RECENT_WD_N", "10") or "10")) ap.add_argument("--public-base", default=env("BANK_PUBLIC_URL", "https://bank.hacktivism.ch")) - ap.add_argument( - "--source-label", - default=env("STATS_SOURCE_LABEL", "host collect_bank_stats.py"), - help="stats.source field (identify stack / collector)", - ) args = ap.parse_args() tz_name = env("TZ", "Europe/Zurich") or "Europe/Zurich" @@ -387,7 +378,6 @@ def main() -> int: bank = args.bank.rstrip("/") skip = {s.strip() for s in (args.skip or "").split(",") if s.strip()} - currency_hint = (args.currency or "").strip().upper() def write_run(ok: bool, err: Optional[str] = None) -> None: if not args.run_out: @@ -407,24 +397,6 @@ def main() -> int: raise RuntimeError("no admin password (BANK_ADMIN_PASS or --admin-pass-file)") token = get_token(bank, args.admin_user, admin_pass) alt = load_alt_from_config(args.exchange_config) - # Currency: explicit → exchange /config → first ledger amount → GOA - currency = currency_hint - if not currency: - try: - code_c, cfg_d, _ = http_json(args.exchange_config, timeout=12) - if code_c == 200 and isinstance(cfg_d, dict) and cfg_d.get("currency"): - currency = str(cfg_d["currency"]).strip().upper() - except Exception: - pass - if not currency: - try: - code_c, cfg_d, _ = http_json(f"{bank}/config", timeout=12) - if code_c == 200 and isinstance(cfg_d, dict) and cfg_d.get("currency"): - currency = str(cfg_d["currency"]).strip().upper() - except Exception: - pass - if not currency: - currency = "GOA" if not alt: alt = dict(DEFAULT_ALT) @@ -470,24 +442,13 @@ def main() -> int: except Exception: continue low = (subject or "").lower() - # Normalise bare numbers to stack currency - if ":" not in str(amount): - amount = f"{currency}:{amount}" - else: - # adopt ledger currency if we only had a hint - try: - cur0, _n0 = parse_amount(amount) - if cur0 and currency_hint == "" and cur0.upper() != "GOA": - currency = cur0.upper() - except Exception: - pass if direction == "credit": total_in += n n_incoming += 1 incomings.append( { "kind": "incoming", - "amount": amount, + "amount": amount if ":" in amount else f"GOA:{amount}", "at_unix": ts, "account": uname, "subject": subject, @@ -499,7 +460,7 @@ def main() -> int: withdraws.append( { "kind": "withdraw", - "amount": amount, + "amount": amount if ":" in amount else f"GOA:{amount}", "at_unix": ts, "account": uname, "subject": subject, @@ -542,7 +503,7 @@ def main() -> int: users_n = sum(1 for u in accounts if u not in ("admin", "exchange")) def pack_amt(d: Decimal) -> Dict[str, Any]: - f = format_amount_alt(f"{currency}:{d}", alt) + f = format_amount_alt(f"GOA:{d}", alt) return { "amount": f["amount"], "amount_alt": f["amount_alt"], @@ -606,11 +567,10 @@ def main() -> int: } ) - # explorer / pool balance (shared demo account when present) - zero = f"{currency}:0" - balance = zero - bal_alt = zero - bal_full = zero + # explorer balance + balance = "GOA:0" + bal_alt = "GOA:0" + bal_full = "GOA:0" if explorer_pass: try: etok = get_token(bank, args.explorer_user, explorer_pass) @@ -623,7 +583,7 @@ def main() -> int: balance = str( data.get("balance", {}).get("amount") if isinstance(data.get("balance"), dict) - else data.get("amount") or data.get("balance") or zero + else data.get("amount") or data.get("balance") or "GOA:0" ) # sometimes balance is object with amount if isinstance(data.get("balance"), dict) and data["balance"].get( @@ -648,7 +608,7 @@ def main() -> int: ) if code == 200 and isinstance(data, dict): if isinstance(data.get("balance"), dict): - balance = str(data["balance"].get("amount") or zero) + balance = str(data["balance"].get("amount") or "GOA:0") elif data.get("amount"): balance = str(data["amount"]) bf = format_amount_alt(balance, alt) @@ -677,12 +637,12 @@ def main() -> int: stats = { "ok": True, - "currency": currency, + "currency": "GOA", "timezone": tz_name, "generated_at": gen_iso, "generated_at_human": gen_human, "generated_at_unix": now_u, - "source": str(args.source_label or "host collect_bank_stats.py"), + "source": "host collect_bank_stats.py (hernani systemd)", "bank_url": bank, "scan": { "tx_page": args.tx_page, diff --git a/scripts/taler-landing/install-landing-stats-host.sh b/scripts/taler-landing/install-landing-stats-host.sh index 4a26217..7c572f0 100755 --- a/scripts/taler-landing/install-landing-stats-host.sh +++ b/scripts/taler-landing/install-landing-stats-host.sh @@ -1,41 +1,35 @@ #!/usr/bin/env bash -# Install generic landing-stats collector as user systemd timer. +# Install central landing-stats collector as hernani user systemd timer. # -# GOA / koopa (default): +# On koopa: # cd ~/src/koopa/koopa-admin-log # ./scripts/taler-landing/install-landing-stats-host.sh +# systemctl --user start taler-landing-stats.service # one-shot now +# systemctl --user status taler-landing-stats.timer # -# Stage TESTPAYSAN (stagepaysan on francpaysan host): -# STACK_PROFILE=stage-testpaysan ./scripts/taler-landing/install-landing-stats-host.sh -# # or from francpaysan-admin-log: ./scripts/stagepaysan/install-landing-stats.sh -# -# Optional: STACK_ENV_FILE=/path/to/profile.env -# Requires linger: sudo loginctl enable-linger $USER +# Requires: linger enabled for hernani (loginctl enable-linger hernani) +# so the timer runs without an interactive login. set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" ADMIN_LOG="${ADMIN_LOG:-$ROOT}" SRC="$ADMIN_LOG/scripts/taler-landing" UNIT_SRC="$ADMIN_LOG/configs/systemd/user" -PROFILE="${STACK_PROFILE:-goa}" BIN_DST="${HOME}/.local/bin" LIB_DST="${HOME}/.local/lib/taler-landing" UNIT_DST="${HOME}/.config/systemd/user" STATE_DST="${HOME}/.local/state/taler-landing-stats" -CFG_DST="${HOME}/.config/taler-landing" -mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST" "$CFG_DST" +mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST" install -m 0755 "$SRC/collect-landing-stats.sh" "$BIN_DST/collect-landing-stats.sh" install -m 0755 "$SRC/collect_bank_stats.py" "$LIB_DST/collect_bank_stats.py" install -m 0755 "$SRC/enrich_stats_alt.py" "$LIB_DST/enrich_stats_alt.py" install -m 0755 "$SRC/merge_resources.py" "$LIB_DST/merge_resources.py" install -m 0755 "$SRC/collect_container_resources.sh" "$LIB_DST/collect_container_resources.sh" -if [ -f "$SRC/test-landing-stats.sh" ]; then - install -m 0755 "$SRC/test-landing-stats.sh" "$BIN_DST/test-landing-stats.sh" 2>/dev/null || \ - install -m 0755 "$SRC/test-landing-stats.sh" "$LIB_DST/test-landing-stats.sh" -fi +install -m 0755 "$SRC/test-landing-stats.sh" "$BIN_DST/test-landing-stats.sh" 2>/dev/null || \ + install -m 0755 "$SRC/test-landing-stats.sh" "$LIB_DST/test-landing-stats.sh" install -m 0644 "$SRC/goa_amounts.py" "$LIB_DST/goa_amounts.py" # mem-snapshot helper lives in taler-shared (copied into containers at collect time) if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then @@ -43,34 +37,18 @@ if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then "$LIB_DST/mem-snapshot.sh" fi -# Stack profile → ~/.config/taler-landing/stack.env (sourced by collector) -if [ -n "${STACK_ENV_FILE:-}" ] && [ -f "${STACK_ENV_FILE}" ]; then - install -m 0644 "${STACK_ENV_FILE}" "$CFG_DST/stack.env" -elif [ -f "$SRC/profiles/${PROFILE}.env" ]; then - install -m 0644 "$SRC/profiles/${PROFILE}.env" "$CFG_DST/stack.env" -else - echo "WARN: no profile ${PROFILE}.env — collector uses built-in GOA defaults" >&2 -fi - +# Wrapper always uses installed lib next to itself when LIB discovery works; +# also point ADMIN_LOG for in-container script refresh. install -m 0644 "$UNIT_SRC/taler-landing-stats.service" "$UNIT_DST/" install -m 0644 "$UNIT_SRC/taler-landing-stats.timer" "$UNIT_DST/" -# Point unit EnvironmentFile at stack profile -if grep -q '^EnvironmentFile=' "$UNIT_DST/taler-landing-stats.service" 2>/dev/null; then - : -else - # inject after [Service] - sed -i '/^\[Service\]/a EnvironmentFile=-%h/.config/taler-landing/stack.env' \ - "$UNIT_DST/taler-landing-stats.service" -fi - +# Rewrite ExecStart to installed binary if unit uses %h path — units already do. systemctl --user daemon-reload systemctl --user enable --now taler-landing-stats.timer -echo "Installed (user=$(id -un) profile=${PROFILE}):" +echo "Installed (user=$(id -un)):" echo " $BIN_DST/collect-landing-stats.sh" echo " $LIB_DST/{collect_bank_stats,enrich_stats_alt,goa_amounts}.py" -echo " $CFG_DST/stack.env" echo " $UNIT_DST/taler-landing-stats.{service,timer} (timer enabled)" echo " logs: $STATE_DST/" echo @@ -83,6 +61,8 @@ echo "Run once now:" echo " systemctl --user start taler-landing-stats.service" echo " journalctl --user -u taler-landing-stats.service -n 40 --no-pager" echo -echo "Secrets (if not via podman exec bank):" -echo " mkdir -p ~/.config/taler-landing && chmod 700 ~/.config/taler-landing" -echo " # bank-admin-password.txt bank-explorer-password.txt" +echo "Optional secrets (if not readable via podman exec bank /root/…):" +echo " mkdir -p ~/.config/taler-landing" +echo " cp …/bank-admin-password.txt ~/.config/taler-landing/" +echo " cp …/bank-explorer-password.txt ~/.config/taler-landing/" +echo " chmod 600 ~/.config/taler-landing/*" diff --git a/scripts/taler-landing/profiles/goa.env b/scripts/taler-landing/profiles/goa.env deleted file mode 100644 index 08c357a..0000000 --- a/scripts/taler-landing/profiles/goa.env +++ /dev/null @@ -1,18 +0,0 @@ -# Stack profile: GOA / hacktivism -# systemd EnvironmentFile: KEY=value only (no export) -BANK_CTR=taler-hacktivism-bank -EX_CTR=taler-hacktivism-exchange-ansible -MER_CTR=taler-hacktivism -BANK_URL=http://127.0.0.1:9012 -BANK_PUBLIC_URL=https://bank.hacktivism.ch -EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config -BANK_CURRENCY=GOA -BANK_LANDING_IN=/var/www/bank-landing -EX_LANDING_IN=/var/www/exchange-landing -MER_LANDING_IN=/var/www/merchant-landing -COLLECT_BANK=1 -COLLECT_EXCHANGE=1 -COLLECT_MERCHANT=1 -COLLECT_RESOURCES=1 -PUBLISH_PODMAN=1 -STATS_SOURCE_LABEL="host collect_bank_stats.py goa hacktivism" diff --git a/scripts/taler-landing/profiles/stage-testpaysan.env b/scripts/taler-landing/profiles/stage-testpaysan.env deleted file mode 100644 index b63a58c..0000000 --- a/scripts/taler-landing/profiles/stage-testpaysan.env +++ /dev/null @@ -1,22 +0,0 @@ -# Stack profile: FrancPaysan stage / TESTPAYSAN (ops user stagepaysan) -# systemd EnvironmentFile: KEY=value only (no export, no shell expansion) -BANK_CTR=stage-lfp-bank -EX_CTR=stage-lfp-exchange-ansible -MER_CTR=stage-lfp-merchant -BANK_URL=http://127.0.0.1:9032 -BANK_PUBLIC_URL=https://stage.bank.lefrancpaysan.ch -EXCHANGE_CONFIG_URL=https://stage.exchange.lefrancpaysan.ch/config -BANK_CURRENCY=TESTPAYSAN -SCAN_SKIP=exchange -BANK_LANDING_IN=/data/var/www -EX_LANDING_IN=/data/var/www -MER_LANDING_IN=/data/var/www -# Shared public feed: one bank scan → same stats.json on all three Caddy landings -# Colon-separated for systemd EnvironmentFile -HOST_STATS_DIRS=/var/www/lfp-stage/bank:/var/www/lfp-stage/exchange:/var/www/lfp-stage/merchant -COLLECT_BANK=1 -COLLECT_EXCHANGE=0 -COLLECT_MERCHANT=0 -COLLECT_RESOURCES=0 -PUBLISH_PODMAN=1 -STATS_SOURCE_LABEL="host collect_bank_stats.py stage TESTPAYSAN shared" diff --git a/scripts/taler-merchant/landing-stats-merchant.sh b/scripts/taler-merchant/landing-stats-merchant.sh index 15c5f9b..cee1bc7 100644 --- a/scripts/taler-merchant/landing-stats-merchant.sh +++ b/scripts/taler-merchant/landing-stats-merchant.sh @@ -363,7 +363,6 @@ CUR_JSON="${CUR_JSON} GEN=$(now_iso) HUMAN=$(now_human) -GEN_UNIX=$(date +%s) # Live performance probes (exchange-style) measure_ms() { @@ -407,7 +406,6 @@ cat >"$TMP" <"$out" return 1 fi - # one-shot needs DB file; serve mode needs socket - if [ -z "${WSERVE_PID:-}" ] || [ ! -S "${WSOCK:-}" ]; then - if [ ! -f "${WDB:-}" ]; then - echo "(no wallet)" >"$out" - return 1 - fi - fi local err="${out}.err" if command -v perl >/dev/null 2>&1; then - if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then - perl -e 'alarm shift; exec @ARGV' 10 \ - node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \ - >"$out" 2>"$err" || true - else - perl -e 'alarm shift; exec @ARGV' 10 \ - node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \ - >"$out" 2>"$err" || true - fi - else - if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then - node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \ - >"$out" 2>"$err" || true - else + perl -e 'alarm shift; exec @ARGV' 10 \ node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \ - >"$out" 2>"$err" || true - fi + >"$out" 2>"$err" || true + else + node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \ + >"$out" 2>"$err" || true fi [ -s "$out" ] } @@ -122,9 +104,9 @@ wait_wallet_balance() { local rounds="${2:-15}" local sleep_s="${3:-3}" local r av - # Poll balance only — never run-until-done (hangs; banned in monitoring). - info "wallet settle wait" "up to ${rounds}×${sleep_s}s for avail>${min_n} ${CUR} (poll balance only; no run-until-done)" + info "wallet settle wait" "up to ${rounds}×${sleep_s}s for avail>${min_n} ${CUR} (wirewatch lag is common)" for r in $(seq 1 "$rounds"); do + wcli run-until-done >"$SCRATCH/settle-run.out" 2>&1 || true wcli_bal_snap "$SCRATCH/bal-live.out" || wcli balance >"$SCRATCH/bal-live.out" 2>&1 || true av=$(wallet_avail_num "$SCRATCH/bal-live.out") if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > float(sys.argv[2]) else 1)" "$av" "$min_n" 2>/dev/null; then @@ -200,7 +182,6 @@ e2e_skip_rest() { # Area e2e-### — withdraw + pay cycle set_area e2e -set_group prereq section "e2e · prerequisites" info "e2e budget" "${E2E_TIMEOUT}s (set E2E_TIMEOUT= to change)" @@ -239,22 +220,12 @@ except Exception: print("")' 2>/dev/null || true) if [ "$E2E_REMOTE" = "1" ]; then # remote: still ATM-shaped but smaller notes; keep cheap : "${E2E_WITHDRAW_VALUES:=10 20 50}" - if [ "$CUR" = "TESTPAYSAN" ]; then - # match fixed public templates e2e-001 / e2e-005 / e2e-1 - : "${E2E_PAY_VALUES:=0.01 0.05 1}" - else - : "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}" - fi + : "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}" else # local GOA: classic ATM notes + 4200 for paivana paywall template : "${E2E_WITHDRAW_VALUES:=20 50 100 200 4200}" : "${E2E_PAY_VALUES:=0.01 0.05 0.1 0.5 1 2 5 10}" fi -# Public template pays (landing/shop style) — default on for TESTPAYSAN stage -if [ -z "${E2E_USE_TEMPLATES:-}" ]; then - if [ "$CUR" = "TESTPAYSAN" ]; then E2E_USE_TEMPLATES=1; else E2E_USE_TEMPLATES=0; fi -fi -: "${E2E_USE_TEMPLATES:=0}" # Build CUR:amount lists build_amt_list() { @@ -328,27 +299,12 @@ BANK_HOST=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$BA SCRATCH=$(mktemp -d) WDB="$SCRATCH/wallet.sqlite3" -WSOCK="$SCRATCH/wallet.sock" -WSERVE_PID="" -# Client/server wallet (advanced serve): long-lived shepherd without run-until-done. -# Default on — wallet-cli only completes withdraw/pay while a serve process is up. -: "${E2E_WALLET_SERVE:=1}" METRICS_DIR="${METRICS_DIR:-$SCRATCH/metrics}" mkdir -p "$METRICS_DIR" export METRICS_DIR CUR="${CUR:-${EXPECT_CURRENCY:-GOA}}" WDB export PATH="${HOME}/.local/bin:${HOME}/taler/opt/taler-wallet-cli/usr/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" - -stop_wallet_serve() { - if [ -n "${WSERVE_PID:-}" ] && kill -0 "$WSERVE_PID" 2>/dev/null; then - kill "$WSERVE_PID" 2>/dev/null || true - wait "$WSERVE_PID" 2>/dev/null || true - fi - WSERVE_PID="" - rm -f "${WSOCK:-}" 2>/dev/null || true -} - # Always dump balances on any exit (timeout, blocker, signal, success) -trap 'ec=$?; e2e_finish "$ec"; stop_wallet_serve; rm -rf "$SCRATCH"; exit "$ec"' EXIT +trap 'ec=$?; e2e_finish "$ec"; rm -rf "$SCRATCH"; exit "$ec"' EXIT # Abort cleanly on login / KYC / registration barriers (esp. remote domains) e2e_abort_auth() { @@ -426,12 +382,10 @@ else fi # Host + bank/exchange/merchant RAM/CPU/load (SSH koopa or koopa-external) -set_group load section "e2e · load snapshot (before withdraw/pay)" metrics_report_load "${METRICS_DIR}/load-before.json" "e2e-start" || true # Local stack: koopa secrets. Remote: only explicit env (never leak local passwords to foreign banks). -set_group prereq if [ "$E2E_REMOTE" = "1" ]; then ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-}" MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}" @@ -448,29 +402,13 @@ else exit 1 fi if [ -n "$MPW" ]; then - # Secrets often store full "secret-token:…"; strip before re-prefixing Bearer. - MPW=$(printf '%s' "$MPW" | tr -d '\n\r') - case "$MPW" in - secret-token:*) MPW_TOKEN="$MPW" ;; - *) MPW_TOKEN="secret-token:${MPW}" ;; - esac ok "merchant secret (instance ${MERCHANT_INSTANCE})" elif [ "$E2E_REMOTE" = "1" ]; then - MPW_TOKEN="" - warn "merchant secret" "missing — private orders fail unless E2E_USE_TEMPLATES=1 (set E2E_MERCHANT_TOKEN)" + warn "merchant secret" "missing — order create may fail (set E2E_MERCHANT_TOKEN)" else blocker "prereq" "merchant instance password missing" exit 1 fi -# Authorization header for merchant private API (never double secret-token:) -merchant_auth_header() { - local t="${1:-${MPW_TOKEN:-}}" - [ -n "$t" ] || return 1 - case "$t" in - secret-token:*) printf 'Authorization: Bearer %s' "$t" ;; - *) printf 'Authorization: Bearer secret-token:%s' "$t" ;; - esac -} # Public reachability gates (remote: soft-skip if bank/merchant absent) for pair in \ @@ -514,31 +452,7 @@ info "e2e user" "$USER withdraw=$WITHDRAW_AMT pay=$PAY_AMT" BANK="$BANK_PUBLIC" INST="$MERCHANT_INSTANCE" -start_wallet_serve() { - [ "${E2E_WALLET_SERVE}" = "1" ] || return 0 - [ -n "${WALLET_CLI:-}" ] && [ -f "${WALLET_CLI}" ] || return 1 - stop_wallet_serve - rm -f "$WSOCK" - # serve holds the DB open; clients use --wallet-connection only - node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults \ - advanced serve --unix-path "$WSOCK" \ - >"$SCRATCH/wallet-serve.log" 2>&1 & - WSERVE_PID=$! - local i - for i in $(seq 1 40); do - if [ -S "$WSOCK" ] && kill -0 "$WSERVE_PID" 2>/dev/null; then - ok "wallet serve" "pid=$WSERVE_PID sock=$WSOCK (no run-until-done)" - return 0 - fi - sleep 0.15 - done - warn "wallet serve" "socket not ready — falling back to one-shot CLI (coins may stay pending)" - stop_wallet_serve - return 1 -} - # $1 = max seconds for this call (optional); rest = wallet-cli args -# Prefer live serve socket so the shepherd stays up (alternative to run-until-done). wcli() { local maxc=12 if [[ "${1:-}" =~ ^[0-9]+$ ]]; then @@ -550,20 +464,11 @@ wcli() { cap=$(e2e_left) [ "$cap" -gt "$maxc" ] && cap=$maxc [ "$cap" -lt 3 ] && return 124 - if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then - if command -v perl >/dev/null 2>&1; then - perl -e 'alarm shift; exec @ARGV' "$cap" \ - node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@" - else - node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@" - fi - else - if command -v perl >/dev/null 2>&1; then - perl -e 'alarm shift; exec @ARGV' "$cap" \ - node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" - else + if command -v perl >/dev/null 2>&1; then + perl -e 'alarm shift; exec @ARGV' "$cap" \ node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" - fi + else + node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" fi } @@ -581,26 +486,16 @@ wcli_pay() { cap=12 fi info "pay wallet cap" "${cap}s" - if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then - if command -v perl >/dev/null 2>&1; then - perl -e 'alarm shift; exec @ARGV' "$cap" \ - node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@" - else - node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@" - fi - else - if command -v perl >/dev/null 2>&1; then - perl -e 'alarm shift; exec @ARGV' "$cap" \ - node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" - else + if command -v perl >/dev/null 2>&1; then + perl -e 'alarm shift; exec @ARGV' "$cap" \ node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" - fi + else + node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" fi } # When coins never become available: run inside + targeted bank/exchange dig dig_when_no_coins() { - set_group dig section "e2e · coins missing — inside dig" info "reason" "wallet empty after withdraw path (often wirewatch timing) — probing bank + exchange" if [ -n "${WID:-}" ]; then @@ -685,7 +580,6 @@ BAL_BEFORE=$(fmt_bal "$SCRATCH/bal-before.out") info "BALANCE before" "$BAL_BEFORE" # --------------------------------------------------------------------------- -set_group bank section "e2e · bank (account · credit · withdraw)" # --------------------------------------------------------------------------- AT="" @@ -784,11 +678,8 @@ else fi # --------------------------------------------------------------------------- -set_group wallet section "e2e · wallet setup (exchange + ToS once)" # --------------------------------------------------------------------------- -# Long-lived serve = client/server wallet (shepherd keeps running; no run-until-done). -start_wallet_serve || true if ! wcli exchanges add "${EXCHANGE_PUBLIC}/" >"$SCRATCH/ex-add.out" 2>&1; then if ! grep -qiE 'already|ok' "$SCRATCH/ex-add.out"; then blocker "wallet-exchange" "exchanges add failed — $(tail -c 160 "$SCRATCH/ex-add.out" | tr '\n' ' ')" @@ -883,13 +774,12 @@ e2e_one_withdraw() { fi cp "$SCRATCH/accept-$tag.out" "$SCRATCH/accept.out" - # Poll bank withdrawal status only (no run-until-done). force-select if stuck pending. st="" - for i in 1 2 3 4 5 6 8 10; do + for i in 1 2 3 4; do e2e_over && break + wcli run-until-done >"$SCRATCH/sel-$tag-$i.out" 2>&1 || true st=$(wd_status) case "$st" in selected|confirmed|aborted) break ;; esac - sleep 0.4 done if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then wcli transactions >"$SCRATCH/tx-pre.json" 2>&1 || true @@ -934,9 +824,10 @@ else: fi fake_incoming_speedup - # Short per-ATM settle: poll balance + bank transfer_done only (never run-until-done) - local ok_bal=0 r av xfer - for r in 1 2 3 4 5 6 8 10; do + # Short per-ATM poll; full settle wait happens after the ladder (avoids false FAIL) + local ok_bal=0 r av + for r in 1 2 3 4 5 6; do + wcli run-until-done >"$SCRATCH/run-$tag-$r.out" 2>&1 || true wcli_bal_snap "$SCRATCH/bal-$tag.out" || wcli balance >"$SCRATCH/bal-$tag.out" 2>&1 || true av=$(wallet_avail_num "$SCRATCH/bal-$tag.out") if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av" 2>/dev/null; then @@ -945,13 +836,6 @@ else: info "balance" "$(fmt_bal "$SCRATCH/bal-$tag.out")" break fi - xfer=$(curl -sS -m 8 "$BANK/taler-integration/withdrawal-operation/${WID}" 2>/dev/null \ - | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("transfer_done"), d.get("status"))' 2>/dev/null || echo "?") - if echo "$xfer" | grep -qi True; then - ok "ATM $WITHDRAW_AMT bank transfer_done" "wallet avail=${CUR}:${av} ($xfer) — no run-until-done" - ok_bal=1 - break - fi sleep 2 done metrics_report_coins "after-ATM-${tag}" || true @@ -969,28 +853,8 @@ else: return 1 } -# Map CUR:amount → public template id (TESTPAYSAN stage e2e-* templates). -# Override with E2E_TEMPLATE_MAP="0.01=e2e-001 0.05=e2e-005 1=e2e-1" -e2e_template_id_for_amount() { - local amt="$1" - local val map_entry k v - val=$(printf '%s' "$amt" | awk -F: '{print $NF}') - : "${E2E_TEMPLATE_MAP:=0.01=e2e-001 0.05=e2e-005 1=e2e-1 1.0=e2e-1}" - for map_entry in $E2E_TEMPLATE_MAP; do - k="${map_entry%%=*}" - v="${map_entry#*=}" - if [ "$k" = "$val" ]; then - printf '%s' "$v" - return 0 - fi - done - return 1 -} - # One payment: e2e_one_pay AMOUNT [SUMMARY] [TAG] # SUMMARY defaults to "monitoring pay $AMOUNT"; TAG defaults from amount. -# When E2E_USE_TEMPLATES=1 and amount maps to a public template, uses -# POST /instances/{inst}/templates/{id} (same as landings/shops). e2e_one_pay() { PAY_AMT="$1" local PAY_SUM="${2:-monitoring pay ${PAY_AMT}}" @@ -1003,28 +867,11 @@ e2e_one_pay() { section "e2e · pay $PAY_AMT · $(format_amount_alt "$PAY_AMT") · $PAY_SUM" e2e_over && { warn "pay" "budget exhausted — skip $PAY_AMT ($PAY_SUM)"; return 1; } metrics_report_coins "before-pay-${tag}" || true - - # Prefer public templates when enabled (stage TESTPAYSAN / shop-style orders) - local tpl_id="" - if [ "${E2E_USE_TEMPLATES:-0}" = "1" ]; then - tpl_id=$(e2e_template_id_for_amount "$PAY_AMT" 2>/dev/null || true) - if [ -n "$tpl_id" ]; then - info "pay $PAY_AMT" "via public template $tpl_id (E2E_USE_TEMPLATES=1)" - e2e_one_pay_public_template "$tpl_id" "$PAY_SUM" "$PAY_AMT" - return $? - fi - warn "pay $PAY_AMT" "no template map for amount — fall back to private order" - fi - - if [ -z "${MPW:-}" ] && [ -z "${MPW_TOKEN:-}" ]; then + if [ -z "${MPW:-}" ]; then warn "pay $PAY_AMT" "no merchant token" return 1 fi - local AUTH - AUTH=$(merchant_auth_header) || { - warn "pay $PAY_AMT" "no merchant token" - return 1 - } + AUTH="Authorization: Bearer secret-token:${MPW}" # JSON-escape summary for curl -d local SUM_JSON SUM_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$PAY_SUM" 2>/dev/null || printf '"%s"' "$PAY_SUM") @@ -1034,9 +881,8 @@ e2e_one_pay() { "${MERCHANT_PUBLIC}/instances/${INST}/private/orders" 2>"$SCRATCH/ord-$tag.err" || true if ! grep -q order_id "$SCRATCH/ord-$tag.json" 2>/dev/null; then if [ "$E2E_REMOTE" != "1" ] && koopa_ssh_ok; then - local mpw_raw="${MPW_TOKEN:-secret-token:${MPW}}" koopa_ssh_run 20 \ - "curl -skS -m 12 -X POST -H 'Authorization: Bearer ${mpw_raw}' -H 'Content-Type: application/json' -d '{\"order\":{\"summary\":${SUM_JSON},\"amount\":\"${PAY_AMT}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}' 'https://127.0.0.1:9010/instances/${INST}/private/orders'" \ + "curl -skS -m 12 -X POST -H 'Authorization: Bearer secret-token:${MPW}' -H 'Content-Type: application/json' -d '{\"order\":{\"summary\":${SUM_JSON},\"amount\":\"${PAY_AMT}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}' 'https://127.0.0.1:9010/instances/${INST}/private/orders'" \ >"$SCRATCH/ord-$tag.json" 2>"$SCRATCH/ord-$tag.err" || true fi fi @@ -1080,37 +926,23 @@ except Exception: print("") return 1 fi ok "wallet handle pay $PAY_AMT ($PAY_SUM)" - # Settle: poll merchant order + wallet transactions only (never run-until-done) local r - for r in 1 2 3 4 5 6; do + for r in 1 2 3; do + wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true - if [ -n "${AUTH:-}" ]; then - curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \ - "${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true - fi - if [ -n "${OTOK:-}" ]; then - curl -skS -m 8 -o "$SCRATCH/ord-pub-$tag.json" \ - "${MERCHANT_PUBLIC}/instances/${INST}/orders/$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$OID")?token=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$OTOK")" \ - 2>/dev/null || true - fi + curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \ + "${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \ || grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null \ || python3 -c 'import json,sys -for p in sys.argv[1:]: - try: - d=json.load(open(p)) - if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid": - sys.exit(0) - except Exception: - pass -sys.exit(1) -' "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null; then +d=json.load(open(sys.argv[1])) +sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1) +' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then ok "payment settled $PAY_AMT ($PAY_SUM · order $OID)" metrics_report_coins "after-pay-${tag}" || true metrics_record_flow spent "$PAY_AMT" || true return 0 fi - sleep 1 done metrics_report_coins "after-pay-fail-${tag}" || true warn "pay $PAY_AMT ($PAY_SUM)" "not settled for order $OID" @@ -1213,34 +1045,33 @@ except Exception: print("") fi ok "shop $pname" "wallet accepted pay URI" - # Settle: public order status + transactions only (never run-until-done) local r AUTH="" - AUTH=$(merchant_auth_header 2>/dev/null) || AUTH="" - for r in 1 2 3 4 5 6; do + if [ -n "${MPW:-}" ]; then + AUTH="Authorization: Bearer secret-token:${MPW}" + fi + for r in 1 2 3; do + wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true if [ -n "$AUTH" ]; then curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \ "${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true + if python3 -c 'import json,sys +d=json.load(open(sys.argv[1])) +sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1) +' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then + ok "shop $pname" "payment settled ($pamt · order $OID)" + metrics_report_coins "after-shop-${tag}" || true + metrics_record_flow spent "$pamt" || true + return 0 + fi fi - curl -skS -m 8 -o "$SCRATCH/ord-pub-$tag.json" "$statusUrl" 2>/dev/null || true - if python3 -c 'import json,sys -for p in sys.argv[1:]: - try: - d=json.load(open(p)) - if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid": - sys.exit(0) - except Exception: - pass -sys.exit(1) -' "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null \ - || grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \ + if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \ || grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null; then - ok "shop $pname" "payment settled ($pamt · order $OID)" + ok "shop $pname" "payment settled via wallet tx ($pamt · order $OID)" metrics_report_coins "after-shop-${tag}" || true metrics_record_flow spent "$pamt" || true return 0 fi - sleep 1 done metrics_report_coins "after-shop-fail-${tag}" || true warn "shop $pname ($pid)" "not settled for order $OID" @@ -1248,7 +1079,6 @@ sys.exit(1) } # --------------------------------------------------------------------------- -set_group atm section "e2e · ATM withdraw ladder" # --------------------------------------------------------------------------- WITHDRAW_OK=0 @@ -1279,14 +1109,12 @@ for WITHDRAW_AMT in $WITHDRAW_LIST; do esac done info "ATM withdraw summary" "$WITHDRAW_REPORT (ok=$WITHDRAW_OK_N lag=$WITHDRAW_LAG_N fail=$WITHDRAW_FAIL_N)" -set_group load section "e2e · load snapshot (after ATM withdraws)" metrics_report_load "${METRICS_DIR}/load-after-withdraw.json" "after-withdraw" || true cp -f "${METRICS_DIR}/load-after-withdraw.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true metrics_report_coins "after-ATM-ladder" || true # Settlement catch-up: bank may have confirmed while wallet was still empty -set_group settle section "e2e · wallet settlement (timing)" if ! wait_wallet_balance 0 "${E2E_SETTLE_ROUNDS}" "${E2E_SETTLE_SLEEP}"; then if [ "$WITHDRAW_LAG_N" -gt 0 ] || [ "$WITHDRAW_OK_N" -gt 0 ]; then @@ -1326,7 +1154,6 @@ else fi # --------------------------------------------------------------------------- -set_group pay section "e2e · variable payments (if balance allows)" # --------------------------------------------------------------------------- PAY_OK=0 @@ -1334,16 +1161,13 @@ PAY_OK_N=0 PAY_FAIL_N=0 PAY_SKIP_N=0 PAY_REPORT="" -if [ -z "${MPW:-}" ] && [ "${E2E_USE_TEMPLATES:-0}" != "1" ]; then +if [ -z "${MPW:-}" ]; then if [ "$E2E_REMOTE" = "1" ]; then - e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN (or E2E_USE_TEMPLATES=1)" + e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN" fi blocker "merchant-order" "no merchant token" exit 1 fi -if [ -z "${MPW:-}" ] && [ "${E2E_USE_TEMPLATES:-0}" = "1" ]; then - info "pay" "no merchant token — public templates only (E2E_USE_TEMPLATES=1)" -fi for PAY_AMT in $PAY_LIST; do e2e_over && { warn "pay ladder" "time budget low — stopping more payments"; break; } @@ -1369,7 +1193,6 @@ for PAY_AMT in $PAY_LIST; do fi done info "pay summary" "$PAY_REPORT (ok=$PAY_OK_N fail=$PAY_FAIL_N skip=$PAY_SKIP_N)" -set_group load section "e2e · load snapshot (after payments)" metrics_report_load "${METRICS_DIR}/load-after-pay.json" "after-pay" || true cp -f "${METRICS_DIR}/load-after-pay.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true @@ -1384,47 +1207,18 @@ if [ "$PAY_OK" != "1" ]; then fi # --------------------------------------------------------------------------- -set_group shop -section "e2e · shop products (public templates)" +section "e2e · GOA shop products (merchant landing catalog)" # --------------------------------------------------------------------------- -# Public template POST + wallet pay — catalog random pick of N products. -# GOA: goa-shop on hacktivism. TESTPAYSAN: farmer shops (jardin / fermes). +# Public template POST + wallet pay — full catalog list, random pick of N products. SHOP_OK=0 SHOP_OK_N=0 SHOP_FAIL_N=0 SHOP_SKIP_N=0 SHOP_REPORT="" -# Auto catalog for stage TESTPAYSAN when not overridden -if [ "${CUR:-}" = "TESTPAYSAN" ] && [ -z "${E2E_SHOP_PRODUCTS_SET:-}" ] && [ -z "${E2E_SHOP_FORCE_GOA:-}" ]; then - # id|name|amount|instance — force stage catalog (GOA default is set earlier) - E2E_SHOP_PRODUCTS="oeufs-6|Œufs plein air × 6|TESTPAYSAN:5|jardin-du-creux -pain-seigle|Pain de seigle 800 g|TESTPAYSAN:4.5|jardin-du-creux -jus-pomme|Jus de pomme 1 L|TESTPAYSAN:6|jardin-du-creux -panier-legumes|Panier légumes|TESTPAYSAN:25|fermes-des-collines -fromage-chevre|Fromage de chèvre 200 g|TESTPAYSAN:8.5|fermes-des-collines -miel-printemps|Miel de printemps 250 g|TESTPAYSAN:12|fermes-des-collines" - # Force stage defaults (GOA goa-shop is already set earlier via :=) - if [ -z "${E2E_SHOP_INSTANCE_SET:-}" ]; then - E2E_SHOP_INSTANCE=jardin-du-creux - fi - if [ -z "${E2E_SHOP_PAYTO_SET:-}" ]; then - E2E_SHOP_PAYTO="payto://x-taler-bank/stage.bank.lefrancpaysan.ch/jardin-du-creux" - fi - E2E_SHOP_PICK_N="${E2E_SHOP_PICK_N:-2}" - E2E_SHOP_ENABLE=1 -fi -# GOA local only by default; remote non-GOA shops need E2E_SHOP_ENABLE=1 or TESTPAYSAN auto above -if [ -z "${E2E_SHOP_ENABLE:-}" ]; then - if [ "$E2E_REMOTE" = "1" ] || [ "${CUR:-}" != "GOA" ]; then - E2E_SHOP_ENABLE=0 - else - E2E_SHOP_ENABLE=1 - fi -fi -if [ "${E2E_SHOP_ENABLE}" != "1" ]; then - info "shop" "SKIPPED (set E2E_SHOP_ENABLE=1 or use TESTPAYSAN stage catalog)" +if [ "$E2E_REMOTE" = "1" ] || [ "${CUR:-}" != "GOA" ]; then + info "goa-shop" "SKIPPED (remote or non-GOA currency)" else - # Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount[|instance] lines) + # Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount lines) SHOP_CATALOG=() while IFS= read -r line; do [ -z "$line" ] && continue @@ -1438,7 +1232,7 @@ EOF case "$PICK_N" in ''|*[!0-9]*) PICK_N=2 ;; esac if [ "$PICK_N" -lt 1 ]; then PICK_N=1; fi if [ "$SHOP_CAT_N" -eq 0 ]; then - warn "shop" "empty E2E_SHOP_PRODUCTS catalog" + warn "goa-shop" "empty E2E_SHOP_PRODUCTS catalog" else if [ "$PICK_N" -gt "$SHOP_CAT_N" ]; then PICK_N=$SHOP_CAT_N; fi # Shuffle catalog, take first PICK_N (portable: python) @@ -1451,26 +1245,24 @@ random.shuffle(lines) print('\n'.join(lines[:n])) " "$PICK_N" ) - info "shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · default instance ${E2E_SHOP_INSTANCE}" - info "shop payto" "$E2E_SHOP_PAYTO" - info "shop pick" "$(printf '%s\n' "$SHOP_PICKED" | cut -d'|' -f1,2 | tr '\n' '; ' | sed 's/; $//')" - # e2e_one_pay_public_template uses global INST — pin per product if 4th field set + info "goa-shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · instance ${E2E_SHOP_INSTANCE}" + info "goa-shop payto" "$E2E_SHOP_PAYTO" + info "goa-shop pick" "$(printf '%s\n' "$SHOP_PICKED" | cut -d'|' -f1,2 | tr '\n' '; ' | sed 's/; $//')" + # e2e_one_pay_public_template uses global INST — temporarily pin shop instance _E2E_INST_SAVE="$INST" INST="${E2E_SHOP_INSTANCE}" while IFS= read -r line; do [ -z "$line" ] && continue - e2e_over && { warn "shop" "time budget low — stopping product pays"; break; } + e2e_over && { warn "goa-shop" "time budget low — stopping product pays"; break; } pid=$(printf '%s' "$line" | cut -d'|' -f1) pname=$(printf '%s' "$line" | cut -d'|' -f2) pamt=$(printf '%s' "$line" | cut -d'|' -f3) - pinst=$(printf '%s' "$line" | cut -d'|' -f4) [ -z "$pid" ] || [ -z "$pamt" ] && continue [ -z "$pname" ] && pname="$pid" - INST="${pinst:-${E2E_SHOP_INSTANCE}}" av_now=$(wallet_avail_num) pay_n=$(python3 -c 'import sys; print(float(sys.argv[1].split(":",1)[-1]))' "$pamt" 2>/dev/null || echo 0) if ! python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) + 1e-12 >= float(sys.argv[2]) else 1)" "$av_now" "$pay_n" 2>/dev/null; then - warn "shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})" + warn "goa-shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})" SHOP_SKIP_N=$((SHOP_SKIP_N + 1)) SHOP_REPORT="${SHOP_REPORT}${SHOP_REPORT:+ }${pname}=SKIP(bal)" continue @@ -1494,9 +1286,9 @@ $SHOP_PICKED EOF INST="$_E2E_INST_SAVE" unset _E2E_INST_SAVE - info "shop summary" "$SHOP_REPORT (ok=$SHOP_OK_N fail=$SHOP_FAIL_N skip=$SHOP_SKIP_N · pick $PICK_N/$SHOP_CAT_N)" + info "goa-shop summary" "$SHOP_REPORT (ok=$SHOP_OK_N fail=$SHOP_FAIL_N skip=$SHOP_SKIP_N · pick $PICK_N/$SHOP_CAT_N)" if [ "$SHOP_OK" != "1" ] && [ "$SHOP_FAIL_N" -gt 0 ]; then - warn "shop" "no sample product payment succeeded ($SHOP_REPORT)" + warn "goa-shop" "no sample product payment succeeded ($SHOP_REPORT)" fi section "e2e · load snapshot (after shop pays)" metrics_report_load "${METRICS_DIR}/load-after-shop.json" "after-shop" || true @@ -1505,7 +1297,6 @@ EOF fi # --------------------------------------------------------------------------- -set_group paivana section "e2e · paivana paywall (template GOA:4200)" # --------------------------------------------------------------------------- # Public paywall: https://paivana.hacktivism.ch → 302 to merchant template "paivana". @@ -1597,12 +1388,11 @@ else info "paivana summary" "${PAIVANA_REPORT:-?} · instance ${E2E_PAIVANA_INSTANCE}" fi -set_group report section "e2e · report" info "user" "$USER" info "ATM withdraws" "$WITHDRAW_REPORT" info "payments" "$PAY_REPORT" -info "shop" "${SHOP_REPORT:-(n/a)}" +info "goa-shop" "${SHOP_REPORT:-(n/a)}" info "paivana" "${PAIVANA_REPORT:-(n/a)}" info "final balance" "$(fmt_bal "$SCRATCH/bal-live.out" 2>/dev/null || echo "(n/a)")" info "scratch" "$SCRATCH" diff --git a/scripts/taler-monitoring/check_goa_ladder.sh b/scripts/taler-monitoring/check_goa_ladder.sh index 8b311ce..28d71ed 100755 --- a/scripts/taler-monitoring/check_goa_ladder.sh +++ b/scripts/taler-monitoring/check_goa_ladder.sh @@ -29,10 +29,7 @@ ROOT=$(cd "$(dirname "$0")" && pwd) # shellcheck source=lib.sh source "$ROOT/lib.sh" -# Area ladder.* — GOA withdraw/pay ladder -# Groups: ladder.plan / ladder.load / ladder.withdraw / ladder.pay / ladder.report set_area ladder -set_group plan SECTION_T0=$(date +%s) now_ms() { python3 -c 'import time; print(int(time.time()*1000))'; } elapsed_ms() { @@ -264,7 +261,6 @@ info "steps" "${LADDER_STEPS} (0 + random≥${LADDER_MIN_AMOUNT} + max-1=${CUR}: info "withdraw_scale" "${LADDER_WITHDRAW_SCALE}× pay mids (fund pay ladder)" info "pay_phase" "$([ "${LADDER_PAY}" = "1" ] && echo enabled || echo disabled)" -set_group load section "ladder · load snapshot (before withdraws)" metrics_report_load "$LOAD_BEFORE" "ladder-start" || true # Fresh wallet per rung — baseline empty (or last-rung DB if re-used later) @@ -348,7 +344,6 @@ STOP_REASON="" STOP_AMOUNT="" declare -a RUNG_JSON=() -set_group withdraw section "ladder · phase A · withdraw (${LADDER_N} rungs)" rung=0 for AMT in "$@"; do @@ -663,7 +658,12 @@ else: fi } - # No run-until-done (hangs / banned). Read transactions only for reserve_pub candidates. + # Short bounded select assist (≤8s) — not a hang path; helps attach fresh reserve_pub + if command -v perl >/dev/null 2>&1 && [ -f "$CLI_JS" ]; then + perl -e 'alarm shift; exec @ARGV' 8 \ + node "$CLI_JS" --wallet-db="$WDB" --no-throttle run-until-done \ + >"$SCRATCH/select-$tag.out" 2>&1 || true + fi wcli transactions >"$SCRATCH/tx-$tag.json" 2>&1 || true t0=$(now_ms) @@ -787,7 +787,6 @@ done PAY_OK_N=0 PAY_FAIL_N=0 if [ "${LADDER_PAY}" = "1" ] && [ -n "${PAY_LIST:-}" ] && [ "$FAIL_N_L" -eq 0 ]; then - set_group pay section "ladder · phase B · pay" metrics_report_coins "before-pay-ladder" || true # Merchant secret (same as e2e) @@ -960,12 +959,19 @@ except Exception: print("") ms_handle=$(elapsed_ms "$t0") ok "handle-uri $PAMT ${ms_handle}ms" - # Short settle polls: merchant order + wallet tx only — never run-until-done + # Short settle polls (bounded — no infinite hang) t0=$(now_ms) settled=0 r=0 while [ "$r" -lt "${LADDER_PAY_SETTLE_ROUNDS}" ]; do r=$((r + 1)) + if command -v perl >/dev/null 2>&1; then + perl -e 'alarm shift; exec @ARGV' 10 \ + "$CLI_JS" --wallet-db="$WDB" --no-throttle run-until-done \ + >"$SCRATCH/pay-run-$ptag.out" 2>&1 || true + else + wcli run-until-done >"$SCRATCH/pay-run-$ptag.out" 2>&1 || true + fi wcli transactions >"$SCRATCH/tx-$ptag.out" 2>&1 || true curl -skS -m 8 -o "$SCRATCH/ord-paid-$ptag.json" -H "$AUTH" \ "${MER}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true @@ -977,7 +983,6 @@ sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="p settled=1 break fi - sleep 0.5 done ms_psettle=$(elapsed_ms "$t0") ms_total=$(elapsed_ms "$t_pay") @@ -1019,7 +1024,6 @@ fi ms_phase=$(python3 -c 'import sys,time; print(int((time.time()-float(sys.argv[1]))*1000))' "$SECTION_T0") # --- report --- -set_group report section "ladder · report" info "auto-account" "$ACCT_USER" info "ok_rungs" "$OK_N" @@ -1124,7 +1128,6 @@ PY wcli balance 2>&1 | tee "$REPORT_DIR/balance-final.out" | tail -20 || true -set_group load section "ladder · load snapshot (after withdraws)" metrics_report_load "$LOAD_AFTER" "ladder-end" || true if [ -f "$LOAD_BEFORE" ] && [ -f "$LOAD_AFTER" ]; then diff --git a/scripts/taler-monitoring/check_inside.sh b/scripts/taler-monitoring/check_inside.sh index 2d012ed..f49b4b6 100755 --- a/scripts/taler-monitoring/check_inside.sh +++ b/scripts/taler-monitoring/check_inside.sh @@ -7,10 +7,8 @@ source "$ROOT/lib.sh" # shellcheck source=metrics.sh source "$ROOT/metrics.sh" -# Area inside.* — container / process state on koopa (SSH) -# Groups: inside.ssh / inside.bank / inside.exchange / inside.merchant / inside.caddy +# Area inside-### — container / process state on koopa (SSH) set_area inside -set_group ssh section "inside · collect from koopa" if [ "${SKIP_SSH}" = "1" ]; then @@ -130,20 +128,10 @@ if [ -z "$RAW" ] || ! echo "$RAW" | grep -q '^E|'; then exit 1 fi -_last_inside_grp="" while IFS= read -r line; do case "$line" in E\|*) IFS='|' read -r _ comp level key detail <<<"$line" - # Group IDs by component so issues map cleanly: inside.bank-02, inside.exchange-04 - case "$comp" in - bank|exchange|merchant|caddy) _g="$comp" ;; - *) _g="ssh" ;; - esac - if [ "$_g" != "$_last_inside_grp" ]; then - set_group "$_g" - _last_inside_grp="$_g" - fi case "$level" in OK) ok "[$comp] $key${detail:+ ($detail)}" ;; ERROR) err "$comp" "$key" "$detail" ;; @@ -155,7 +143,6 @@ while IFS= read -r line; do done <<<"$RAW" # Host loadavg + RAM + per-container RSS/CPU (same probe as e2e/ladder) -set_group load section "inside · load / memory" METRICS_DIR="${METRICS_DIR:-$(mktemp -d)}" export METRICS_DIR diff --git a/scripts/taler-monitoring/check_sanity.sh b/scripts/taler-monitoring/check_sanity.sh index 1e3b922..1486e9b 100755 --- a/scripts/taler-monitoring/check_sanity.sh +++ b/scripts/taler-monitoring/check_sanity.sh @@ -23,12 +23,10 @@ json_currency() { python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("currency") or "")' "$1" 2>/dev/null || true } -# Area sanity.* — public + optional server-side per component -# Groups: sanity.bank / sanity.exchange / sanity.merchant +# Area sanity-### — public + optional server-side per component set_area sanity # --------------------------------------------------------------------------- -set_group bank section "sanity · bank" # --------------------------------------------------------------------------- expect_code "bank public /config" 200 "$BANK_PUBLIC/config" @@ -100,7 +98,6 @@ else fi # --------------------------------------------------------------------------- -set_group exchange section "sanity · exchange" # --------------------------------------------------------------------------- expect_code "exchange public /config" 200 "$EXCHANGE_PUBLIC/config" @@ -196,7 +193,6 @@ else fi # --------------------------------------------------------------------------- -set_group merchant section "sanity · merchant" # --------------------------------------------------------------------------- expect_code "merchant public /config" 200 "$MERCHANT_PUBLIC/config" diff --git a/scripts/taler-monitoring/check_server.sh b/scripts/taler-monitoring/check_server.sh index 8412a12..961b4c8 100755 --- a/scripts/taler-monitoring/check_server.sh +++ b/scripts/taler-monitoring/check_server.sh @@ -5,9 +5,8 @@ ROOT=$(cd "$(dirname "$0")" && pwd) # shellcheck source=lib.sh source "$ROOT/lib.sh" -# Area server.* — host/container ports via SSH +# Area server-### — host/container ports via SSH set_area server -set_group ssh section "server · ssh ${KOOPA_SSH}" if ! koopa_ssh_ok; then diff --git a/scripts/taler-monitoring/check_urls.sh b/scripts/taler-monitoring/check_urls.sh index 55ed1b8..c5cd78d 100755 --- a/scripts/taler-monitoring/check_urls.sh +++ b/scripts/taler-monitoring/check_urls.sh @@ -5,9 +5,7 @@ ROOT=$(cd "$(dirname "$0")" && pwd) # shellcheck source=lib.sh source "$ROOT/lib.sh" -# Area www.* — public HTTPS (outside-in) -# Groups: www.exchange / www.perf / www.stats / www.bank / www.merchant / -# www.paivana / www.landing +# Area www-### — public HTTPS (outside-in) set_area www section "www · public URLs · ${TALER_DOMAIN:-?} (outside-in, no SSH)" @@ -171,8 +169,7 @@ sys.exit(0) PY } -# --- exchange (core; always required) --- www.exchange-NN -set_group exchange +# --- exchange (core; always required) --- www-001 … check_url "exchange /config" 200 "$EXCHANGE_PUBLIC/config" code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ec.json") if [ "$code" = "200" ]; then @@ -222,7 +219,6 @@ fi # Performance — outside-in public HTTPS latency (this runner, not loopback) # Same spirit as bank landing-stats public probes; measured here from outside. # --------------------------------------------------------------------------- -set_group perf section "www · performance · public HTTPS latency (outside-in)" # Latency thresholds (ms), outside-in from this runner. @@ -294,18 +290,11 @@ fi info "perf note" "RTT measured from this host (outside-in)" # --------------------------------------------------------------------------- -# Landing stats.json (public outside-in) — freshness + display fields. -# Timestamps (required for external measurement): -# generated_at_unix — epoch seconds (preferred) -# generated_at — ISO-8601 -# generated_at_human — footer / “updated” UI string -# Bank collector always sets all three; exchange/merchant scripts set unix too. -# Env: -# STATS_STALE_SECS warn if age > this (default 900 = 15m; timer often 5–15m) -# STATS_FAIL_SECS hard fail if age > this (default 3600); 0 = never hard-fail on age +# Landing load stats (stats.json written *inside* containers, usually public). +# Prefer HTTPS /intro/stats.json (no SSH). If missing and SSH works, cat from +# the container. Report loadavg + RSS + in-container probe ms — one line each. # --------------------------------------------------------------------------- -: "${STATS_STALE_SECS:=900}" -: "${STATS_FAIL_SECS:=3600}" +: "${STATS_STALE_SECS:=900}" # warn if generated_at older than 15m _fetch_landing_stats_json() { # $1=name bank|exchange|merchant $2=base URL → writes $tmp/stats-$1.json, exit 0 if ok @@ -333,265 +322,81 @@ _fetch_landing_stats_json() { return 1 } -# Validate one stats.json for monitoring display + outside age measurement. -# Prints lines: OK|WARN|ERR|STALE|FAIL -# Sets $tmp/stats-meta-$name.txt with gen_unix|source for shared-feed compare. -check_landing_stats_json() { +report_landing_load_stats() { local name="$1" base="$2" local f="$tmp/stats-${name}.json" - local line hard_missing=0 + local line age_s if ! _fetch_landing_stats_json "$name" "$base"; then if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then - fail "stats ${name}" "HTTP/body missing · ${base}/intro/stats.json" - else - warn "stats ${name}" "stats.json not available" + warn "perf ${name} landing-stats" "stats.json not available (public or SSH)" fi - return 1 + return fi - ok "stats ${name} reachable" "${base}/intro/stats.json" - - # Multi-line verdict from python (STATUS detail) - while IFS= read -r line; do - case "$line" in - META\ *) - printf '%s\n' "${line#META }" >"$tmp/stats-meta-${name}.txt" - ;; - OK\ *) - ok "stats ${name}" "${line#OK }" - ;; - WARN\ *) - warn "stats ${name}" "${line#WARN }" - ;; - STALE\ *) - warn "stats ${name} freshness" "stale · ${line#STALE }" - ;; - FAIL\ *) - fail "stats ${name}" "${line#FAIL }" - hard_missing=1 - ;; - ERR\ *) - if [ "${LOCAL_STACK:-0}" = "1" ] || [ "${CHECK_LANDING:-1}" = "1" ]; then - fail "stats ${name}" "${line#ERR }" - else - warn "stats ${name}" "${line#ERR }" - fi - hard_missing=1 - ;; - *) - [ -n "$line" ] && warn "stats ${name}" "unparsed · $line" - ;; - esac - done < <(python3 - "$f" "$name" "${STATS_STALE_SECS}" "${STATS_FAIL_SECS}" <<'PY' 2>/dev/null || echo "ERR python check failed" -import json, sys, time, re -from datetime import datetime, timezone - -path, role, stale_s, fail_s = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4]) -now = int(time.time()) - -def out(kind, msg): - print("%s %s" % (kind, msg)) + line=$(python3 - "$f" "${STATS_STALE_SECS}" <<'PY' 2>/dev/null || true +import json, sys, time +path, stale = sys.argv[1], int(sys.argv[2]) try: d = json.load(open(path)) except Exception as e: - out("ERR", "JSON parse: %s" % str(e)[:100]) + print("ERR parse:" + str(e)[:80]) sys.exit(0) - -if not isinstance(d, dict): - out("ERR", "body is not a JSON object") +if not d.get("ok", True): + print("ERR ok=false " + str(d.get("error") or "")[:80]) sys.exit(0) - -if d.get("ok") is False: - out("ERR", "ok=false · %s" % str(d.get("error") or d.get("hint") or "")[:80]) +p = d.get("performance") or {} +if not isinstance(p, dict) or not p: + print("ERR no performance block") sys.exit(0) - -# --- timestamps (outside-in age measurement) --- -gu = d.get("generated_at_unix") -giso = d.get("generated_at") or d.get("generated_at_iso") -ghum = d.get("generated_at_human") -age = None -src_u = None - -if gu is not None and str(gu).strip() != "": - try: - src_u = int(float(gu)) - # tolerate ms accidental timestamps - if src_u > 10_000_000_000: - src_u //= 1000 - age = now - src_u - except Exception: - out("ERR", "generated_at_unix not numeric: %r" % gu) - src_u = None -elif giso: - # parse ISO when unix missing (legacy exchange/merchant before unix field) - try: - s = str(giso).strip() - if s.endswith("Z"): - s = s[:-1] + "+00:00" - # allow +0200 without colon - s = re.sub(r"([+-]\d{2})(\d{2})$", r"\1:\2", s) - dt = datetime.fromisoformat(s) - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - src_u = int(dt.timestamp()) - age = now - src_u - out("WARN", "no generated_at_unix — derived age from generated_at ISO (deploy collector with unix)") - except Exception as e: - out("ERR", "cannot parse generated_at=%r (%s)" % (giso, e)) -else: - out("ERR", "missing generated_at_unix and generated_at — cannot measure freshness outside") - -if not ghum and not giso: - out("ERR", "missing generated_at_human/generated_at — UI cannot show “updated” time") -elif ghum: - out("OK", "timestamp human=%s" % ghum) -else: - out("OK", "timestamp iso=%s" % giso) - -if src_u is not None: - out("META", "%s|%s|%s" % (src_u, d.get("source") or "", d.get("currency") or "")) - if age is not None and age < -120: - out("WARN", "clock skew · generated_at_unix in the future by %ss" % (-age)) - if age is not None: - out("OK", "generated_at_unix=%s age=%ss (now=%s)" % (src_u, age, now)) - if fail_s > 0 and age > fail_s: - out("FAIL", "age=%ss > STATS_FAIL_SECS=%s · collector not updating" % (age, fail_s)) - elif age > stale_s: - out("STALE", "age=%ss > STATS_STALE_SECS=%s · timer lag or stuck publish" % (age, stale_s)) - -# --- display fields for landings / monitoring --- -src = str(d.get("source") or "") -# Bank-shaped (incl. stage shared feed on all three landings) -bankish = ( - role == "bank" - or "collect_bank_stats" in src - or "shared" in src - or isinstance(d.get("withdraws"), dict) - or isinstance(d.get("bank_accounts"), dict) -) -if bankish: - missing = [] - if not isinstance(d.get("withdraws"), dict): - missing.append("withdraws") - ba = d.get("bank_accounts") - wl = d.get("wallets") - if not isinstance(ba, dict) and not isinstance(wl, dict): - missing.append("bank_accounts|wallets") - if d.get("currency") in (None, ""): - missing.append("currency") - if missing: - out("ERR", "bank-shaped display missing: %s" % ",".join(missing)) - else: - w = d.get("withdraws") or {} - ba = ba if isinstance(ba, dict) else {} - n_acc = ba.get("total") if ba.get("total") is not None else ba.get("users") - out( - "OK", - "display bank-shape currency=%s accounts=%s withdraws.count=%s source=%s" - % (d.get("currency"), n_acc, w.get("count"), (src or "?")[:48]), - ) -elif src == "exchange-db" or (role == "exchange" and "reserves" in d): - need = [] - for k in ("reserves", "wire_in_count", "known_coins"): - if k not in d and k.replace("known_coins", "coins_live") not in d: - if k == "known_coins" and ("coins_live" in d or "coins_remaining_amount" in d): - continue - need.append(k) - if need: - out("WARN", "exchange display keys missing: %s" % ",".join(need)) - else: - out( - "OK", - "display exchange-db reserves=%s wire_in=%s coins=%s" - % (d.get("reserves"), d.get("wire_in_count"), d.get("known_coins") or d.get("coins_live")), - ) -elif src == "merchant-db" or (role == "merchant" and "instances" in d): - if "instances" not in d and "orders" not in d: - out("ERR", "merchant display missing instances/orders") - else: - out( - "OK", - "display merchant-db instances=%s orders=%s paid=%s" - % (d.get("instances"), d.get("orders"), d.get("paid")), - ) -else: - out("WARN", "unknown stats schema role=%s source=%s keys=%s" % (role, src[:40], ",".join(list(d.keys())[:8]))) - -# performance block optional (stage shared bank feed may omit heavy probes) -p = d.get("performance") -if isinstance(p, dict) and p: - mem = p.get("memory") if isinstance(p.get("memory"), dict) else {} - rss = mem.get("container_rss_human") or mem.get("proc_sum_rss_human") or "—" - load = p.get("loadavg") or "—" - bits = [] - for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"): - if k in p and p[k] is not None: - bits.append("%s=%s" % (k.replace("_ms", ""), p[k])) - lat = " ".join(bits) if bits else "latency=—" - out("OK", "performance loadavg=%s RSS=%s %s" % (load, rss, lat)) -else: - out("OK", "performance block optional/absent") +mem = p.get("memory") if isinstance(p.get("memory"), dict) else {} +rss = mem.get("container_rss_human") or mem.get("proc_sum_rss_human") or "?" +load = p.get("loadavg") or "?" +# latency fields differ by site +bits = [] +for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"): + if k in p and p[k] is not None: + bits.append("%s=%sms" % (k.replace("_ms", ""), p[k])) +lat = " ".join(bits) if bits else "latency=?" +gen = d.get("generated_at_human") or d.get("generated_at") or "?" +# staleness +age = "" +try: + gu = d.get("generated_at_unix") + if gu is not None: + age_s = int(time.time()) - int(gu) + age = " age=%ss" % age_s + if age_s > stale: + print("STALE loadavg=%s RSS=%s %s gen=%s%s" % (load, rss, lat, gen, age)) + sys.exit(0) +except Exception: + pass +print("OK loadavg=%s RSS=%s %s gen=%s%s" % (load, rss, lat, gen, age)) PY ) - return 0 + case "$line" in + OK\ *) + ok "perf ${name} landing-stats" "${line#OK }" + ;; + STALE\ *) + warn "perf ${name} landing-stats" "stale · ${line#STALE }" + ;; + ERR\ *) + warn "perf ${name} landing-stats" "${line#ERR }" + ;; + *) + warn "perf ${name} landing-stats" "unreadable stats.json" + ;; + esac } if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then - set_group stats - section "www · landing stats.json (freshness + display fields · outside-in)" - info "stats policy" "STALE≥${STATS_STALE_SECS}s WARN · FAIL≥${STATS_FAIL_SECS}s ERROR (0=disable fail) · need generated_at_unix" - check_landing_stats_json "bank" "$BANK_PUBLIC" || true - check_landing_stats_json "exchange" "$EXCHANGE_PUBLIC" || true - check_landing_stats_json "merchant" "$MERCHANT_PUBLIC" || true - - # Shared feed: stage-style publish copies same bank stats to all three URLs - if [ -f "$tmp/stats-meta-bank.txt" ] && [ -f "$tmp/stats-meta-exchange.txt" ] && [ -f "$tmp/stats-meta-merchant.txt" ]; then - shared_line=$(python3 - "$tmp/stats-meta-bank.txt" "$tmp/stats-meta-exchange.txt" "$tmp/stats-meta-merchant.txt" <<'PY' 2>/dev/null || true -import sys -def parse(p): - try: - t = open(p).read().strip().split("|", 2) - return t[0], (t[1] if len(t) > 1 else ""), (t[2] if len(t) > 2 else "") - except Exception: - return "", "", "" -b, bs, bc = parse(sys.argv[1]) -e, es, ec = parse(sys.argv[2]) -m, ms, mc = parse(sys.argv[3]) -if not b or not e or not m: - print("SKIP incomplete meta") -elif b == e == m: - print("SHARED gen_unix=%s bank_src=%s" % (b, (bs or "?")[:50])) -else: - # Shared feed only when all three claim the same bank-collector source - # (e.g. stage TESTPAYSAN "… shared"). GOA uses independent exchange-db / - # merchant-db collectors — different gen_unix is expected. - def bankish(s): - s = (s or "").lower() - return "shared" in s or "collect_bank_stats" in s - if bankish(bs) and bankish(es) and bankish(ms): - print("DRIFT bank=%s exchange=%s merchant=%s (expected equal for shared feed)" % (b, e, m)) - else: - print("INDEPENDENT bank=%s exchange=%s merchant=%s" % (b, e, m)) -PY -) - case "$shared_line" in - SHARED\ *) - ok "stats shared feed" "${shared_line#SHARED }" - ;; - DRIFT\ *) - warn "stats shared feed" "timestamps differ · ${shared_line#DRIFT }" - ;; - INDEPENDENT\ *) - info "stats feeds" "independent collectors · ${shared_line#INDEPENDENT }" - ;; - *) - info "stats feeds" "${shared_line:-n/a}" - ;; - esac - fi - info "stats note" "public GET /intro/stats.json; measure age via generated_at_unix vs wall clock" + section "www · performance · landing load stats (stats.json · in-container probes)" + report_landing_load_stats "bank" "$BANK_PUBLIC" + report_landing_load_stats "exchange" "$EXCHANGE_PUBLIC" + report_landing_load_stats "merchant" "$MERCHANT_PUBLIC" + info "perf landing-stats note" "from /intro/stats.json (public); SSH container fallback if needed" fi # Rollup + optional JSON for metrics_print_overall if [ -s "$PERF_TSV" ]; then @@ -665,8 +470,7 @@ fi info "perf note" "measured from this host (outside-in); thresholds PERF_WARN_MS=${PERF_WARN_MS} PERF_FAIL_MS=${PERF_FAIL_MS}" -# Exchange terms + privacy (same www.exchange group — issues map to exchange) -set_group exchange +# Terms + privacy (legal docs) check_legal_doc "exchange /terms" "$EXCHANGE_PUBLIC/terms" "terms|GOA|exploration|FADP|revDSG|privacy" check_legal_doc "exchange /privacy" "$EXCHANGE_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|exploration" # trailing slash: 200 or redirect to bare path @@ -681,7 +485,6 @@ case "$code" in esac # --- bank --- -set_group bank if [ "${LOCAL_STACK:-1}" = "0" ]; then check_url_soft "bank /config" 200 "$BANK_PUBLIC/config" else @@ -705,7 +508,7 @@ if [ "$code" = "200" ]; then check_url_soft "bank /webui/" 200 "$BANK_PUBLIC/webui/" check_url_soft "bank /intro/" 200 "$BANK_PUBLIC/intro/" check_url_soft "bank /" 302,301,200 "$BANK_PUBLIC/" - # Auto-account: GOA shared-pool mint (hacktivism). Not used on TESTPAYSAN stage. + # Auto-account: credentials + shared-pool taler://withdraw (like step 2) aa_code=$(http_body "$BANK_PUBLIC/intro/auto-account.json" "$tmp/aa.json") case "$aa_code" in 200) @@ -715,22 +518,11 @@ if [ "$code" = "200" ]; then fail "bank /intro/auto-account.json" "invalid withdraw/login ($(tr '\n' ' ' <"$tmp/aa-val" | sed 's/[[:space:]]*$//'))" fi ;; - 404|405|501) - if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then - fail "bank /intro/auto-account.json" "HTTP $aa_code (want 200; 405/501 = broken)" - else - info "bank /intro/auto-account.json" "HTTP $aa_code — skip (not a GOA auto-account stack)" - fi - ;; - 502|503|000) - fail "bank /intro/auto-account.json" "HTTP $aa_code want 200" + 405|501|404|502|503|000) + fail "bank /intro/auto-account.json" "HTTP $aa_code (want 200; 405/501 = broken)" ;; *) - if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then - fail "bank /intro/auto-account.json" "HTTP $aa_code want 200" - else - warn "bank /intro/auto-account.json" "HTTP $aa_code (optional off-GOA)" - fi + fail "bank /intro/auto-account.json" "HTTP $aa_code want 200" ;; esac fi @@ -753,7 +545,6 @@ else fi # --- merchant --- -set_group merchant if [ "${LOCAL_STACK:-1}" = "0" ]; then check_url_soft "merchant /config" 200 "$MERCHANT_PUBLIC/config" else @@ -819,7 +610,6 @@ esac # Paivana paywall (local GOA stack) — public front only; pay path is e2e # --------------------------------------------------------------------------- if [ "${LOCAL_STACK:-1}" = "1" ] && [ "${E2E_PAIVANA:-1}" != "0" ]; then - set_group paivana section "www · paivana paywall" : "${PAIVANA_PUBLIC:=https://paivana.hacktivism.ch}" PAIVANA_PUBLIC="${PAIVANA_PUBLIC%/}" @@ -856,7 +646,6 @@ if [ "${CHECK_LANDING:-1}" != "1" ]; then exit 0 fi -set_group landing section "www · landing exposed links · bank / merchant / exchange" # Probe one URL: print code to stdout (200 after following redirects counts as 200). @@ -1014,24 +803,18 @@ check_one_landing() { return fi - # Required static: qrcode.min.js. GOA og image is soft off-GOA (stage has no og-goa-shop.png). - if _landing_probe "${base}/intro/qrcode.min.js"; then - a_ok=$((a_ok + 1)) - else - a_fail=$((a_fail + 1)) - fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} ${base}/intro/qrcode.min.js" - fi - if _landing_probe "${base}/intro/og-goa-shop.png"; then - a_ok=$((a_ok + 1)) - else - if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then - a_fail=$((a_fail + 1)) - fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} ${base}/intro/og-goa-shop.png" + # Required static assets (hard: qrcode + og; soft: qr-logo) + for url in \ + "${base}/intro/qrcode.min.js" \ + "${base}/intro/og-goa-shop.png" + do + if _landing_probe "$url"; then + a_ok=$((a_ok + 1)) else - a_soft=$((a_soft + 1)) - soft_sample="${soft_sample}${soft_sample:+; }HTTP ${_landing_code} og-goa-shop.png (GOA asset)" + a_fail=$((a_fail + 1)) + fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} $url" fi - fi + done if _landing_probe "${base}/intro/qr-logo.png"; then a_ok=$((a_ok + 1)) else @@ -1159,15 +942,7 @@ if [ "${LOCAL_STACK:-1}" = "1" ] || [ -n "${BANK_PUBLIC:-}" ]; then fail "bank /intro/demo-withdraw.json" "invalid taler://withdraw ($(tr '\n' ' ' <"$tmp/dw-val" | sed 's/[[:space:]]*$//'))" fi ;; - 404|405|501) - # GOA bank landing mints demo-withdraw.json; stage TESTPAYSAN uses ATM/wallet guide only - if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then - fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)" - else - info "landing bank demo-withdraw" "HTTP $dw_code — skip (not a GOA demo-withdraw stack)" - fi - ;; - 502|503|000) + 405|501|404|502|503|000) fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)" ;; *) diff --git a/scripts/taler-monitoring/check_versions.sh b/scripts/taler-monitoring/check_versions.sh index ea4df2f..eed9e5f 100755 --- a/scripts/taler-monitoring/check_versions.sh +++ b/scripts/taler-monitoring/check_versions.sh @@ -19,7 +19,6 @@ ROOT=$(cd "$(dirname "$0")" && pwd) source "$ROOT/lib.sh" set_area versions -set_group outside SUITE="${TALER_APT_SUITE:-trixie}" APT_BASE="${TALER_APT_BASE:-https://deb.taler.net/apt/debian}" @@ -167,7 +166,6 @@ fi # --------------------------------------------------------------------------- # 2) INSIDE — containers can reach deb.taler.net (pasta / install path) # --------------------------------------------------------------------------- -set_group inside section "versions · inside · containers → deb.taler.net" if [ "${SKIP_SSH}" = "1" ]; then @@ -268,7 +266,6 @@ ok "collected installed packages" "${n_inst} rows" # --------------------------------------------------------------------------- # 3) compare installed vs trixie (and note testing) # --------------------------------------------------------------------------- -set_group compare section "versions · compare installed vs ${SUITE}" export BEHIND_MODE HAVE_TESTING diff --git a/scripts/taler-monitoring/lib.sh b/scripts/taler-monitoring/lib.sh index 65c6005..5df0819 100755 --- a/scripts/taler-monitoring/lib.sh +++ b/scripts/taler-monitoring/lib.sh @@ -193,13 +193,6 @@ apply_taler_domain() { PAY_AMT="${PAY_AMT:-${EXPECT_CURRENCY}:0.01}" CREDIT_AMT="${CREDIT_AMT:-${EXPECT_CURRENCY}:100}" ;; - TESTPAYSAN) - # Stage FrancPaysan: default instance + public templates (e2e-001/005/1) - MERCHANT_INSTANCE="${MERCHANT_INSTANCE:-default}" - WITHDRAW_AMT="${WITHDRAW_AMT:-TESTPAYSAN:20}" - PAY_AMT="${PAY_AMT:-TESTPAYSAN:0.01}" - CREDIT_AMT="${CREDIT_AMT:-TESTPAYSAN:100}" - ;; CHF) WITHDRAW_AMT="${WITHDRAW_AMT:-CHF:20}" PAY_AMT="${PAY_AMT:-CHF:0.01}" @@ -359,30 +352,17 @@ koopa_ssh_python() { with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" python3 - } -# ANSI colours — boxed badges (fg+bg) + body (label bold, detail dim). -# Off: NO_COLOR=1 or CLICOLOR=0. +# ANSI colours for tags (green OK / yellow WARN / red ERROR·BLOCKER / cyan INFO). +# Default: always on so pasted logs keep visible tags. Disable: NO_COLOR=1 or CLICOLOR=0. if [ "${NO_COLOR:-0}" = "1" ] || [ "${CLICOLOR:-1}" = "0" ]; then - G= R= Y= C= M= D= W= B= N= - BG_OK= BG_INFO= BG_WARN= BG_ERR= BG_BLK= BG_SEC= - BOX=0 + G= R= Y= C= N= B= else - G=$'\e[1;32m' # green text (totals) - R=$'\e[1;31m' # red text - Y=$'\e[1;33m' # yellow text - C=$'\e[1;36m' # cyan text - M=$'\e[1;35m' # magenta text - D=$'\e[2m' # dim tid / detail - W=$'\e[1;37m' # bold white label - B=$'\e[1m' # bold section - N=$'\e[0m' # reset - # Badge fill: bright text on solid background - BG_OK=$'\e[1;30;42m' # black on green - BG_INFO=$'\e[1;30;46m' # black on cyan - BG_WARN=$'\e[1;30;43m' # black on yellow - BG_ERR=$'\e[1;37;41m' # white on red - BG_BLK=$'\e[1;37;45m' # white on magenta - BG_SEC=$'\e[1;37;44m' # white on blue (section) - BOX=1 + G=$'\e[1;32m' # bold green [OK] + R=$'\e[1;31m' # bold red [ERROR] [BLOCKER] + Y=$'\e[1;33m' # bold yellow [WARN] + C=$'\e[1;36m' # bold cyan [INFO] + B=$'\e[1m' + N=$'\e[0m' fi PASS_N=0 @@ -392,68 +372,17 @@ INFO_N=0 BLOCKERS=() # human-readable payment/withdraw blockers ERRORS=() # all ERROR lines (component scope) -# Grouped test IDs: area.group-NN (e.g. www.exchange-01, e2e.pay-03) -# set_area www # phase: www | e2e | inside | versions | … -# set_group exchange # resets counter + prints group box → www.exchange-01 -# set_group bank # → www.bank-01 -# Without set_group: area-01, area-02, … (flat within area) +# Test IDs by area: www-001, e2e-001, inside-001, … +# Usage: set_area www then each ok/fail/warn/info/blocker/err auto-numbers. TEST_AREA="" -TEST_GROUP="" TEST_N=0 +# Last issued id (www-001); set by _take_tid — not via $(…) so TEST_N persists. LAST_TID="" -# Per-group counters so re-entering www.exchange after perf continues NN -# (shell vars TEST_CNT__). -_tid_key() { - # $1=area $2=group → safe identifier - printf 'TEST_CNT_%s_%s' "$1" "$2" | tr -c 'A-Za-z0-9_' '_' -} -_tid_save() { - [ -z "${TEST_AREA:-}" ] || [ -z "${TEST_GROUP:-}" ] && return 0 - local k - k=$(_tid_key "$TEST_AREA" "$TEST_GROUP") - eval "$k=\$TEST_N" -} -_tid_load() { - local k - k=$(_tid_key "$TEST_AREA" "$1") - eval "TEST_N=\${$k:-0}" -} set_area() { - # leave previous group counter saved - _tid_save TEST_AREA="$1" - TEST_GROUP="" TEST_N=0 LAST_TID="" } -# Start a logical sub-group. Short stable names: -# exchange bank merchant perf stats landing paivana -# prereq wallet atm settle pay shop dig load report -# ssh caddy outside inside compare withdraw -# Counter continues if the same group is re-entered later in the area. -# Prints a compact group chip so log + issue text map cleanly (www.bank-03). -set_group() { - local g="$1" - # no-op if same group already active (e.g. inside emit loop) - if [ "$g" = "${TEST_GROUP:-}" ] && [ -n "$g" ]; then - return 0 - fi - _tid_save - TEST_GROUP="$g" - LAST_TID="" - if [ -z "${TEST_AREA:-}" ] || [ -z "$g" ]; then - TEST_N=0 - return 0 - fi - _tid_load "$g" - local tag="${TEST_AREA}.${g}" - if [ "${BOX:-0}" = "1" ]; then - # cyan-outline group chip: ┌ www.exchange ┐ - printf '%s%s┌ %s ┐%s\n' "$D" "$C" "$tag" "$N" - else - printf -- '-- %s --\n' "$tag" - fi -} # Assign next id into LAST_TID (must not run in a subshell). _take_tid() { LAST_TID="" @@ -461,69 +390,49 @@ _take_tid() { return fi TEST_N=$((TEST_N + 1)) - if [ -n "${TEST_GROUP:-}" ]; then - LAST_TID=$(printf '%s.%s-%02d' "$TEST_AREA" "$TEST_GROUP" "$TEST_N") - _tid_save - else - LAST_TID=$(printf '%s-%02d' "$TEST_AREA" "$TEST_N") - fi + LAST_TID=$(printf '%s-%03d' "$TEST_AREA" "$TEST_N") } -# Dim test id: "www.exchange-01 " or empty _fmt_tid() { + # prefix "www-001 " or empty if [ -n "${LAST_TID:-}" ]; then - printf '%s%s%s ' "$D" "$LAST_TID" "$N" - fi -} -# Boxed badge cell: ┌ OK ┐ with filled bg (tag left-padded, width 7 for BLOCKER) -# $1=badge style $2=tag text -_fmt_badge() { - local badge="$1" tag="$2" inner - inner=$(printf -- '%-7s' "$tag") - if [ "${BOX:-0}" = "1" ]; then - printf '%s┌ %s┐%s' "$badge" "$inner" "$N" - else - printf '%s[ %s]%s' "$badge" "$inner" "$N" - fi -} -# One concrete line: ┌ OK ┐ tid label · detail -# $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:-}" - 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" - else - printf -- '%s %s%s%s%s\n' \ - "$(_fmt_badge "$badge" "$tag")" "$(_fmt_tid)" "$lcol" "$label" "$N" + printf '%s ' "$LAST_TID" fi } ok() { - # ok "what is good" ["concrete evidence: HTTP 200 · 12ms · …"] + # Forms (same idea as info/warn): + # ok "what passed" + # ok "what passed" "detail / ms / bytes / …" local label="$1" detail="${2:-}" _take_tid - _msg_line "$BG_OK" "OK" "$W" "$label" "$detail" + if [ -n "$detail" ]; then + printf '%s[OK]%s %s%s — %s\n' "$G" "$N" "$(_fmt_tid)" "$label" "$detail" + else + printf '%s[OK]%s %s%s\n' "$G" "$N" "$(_fmt_tid)" "$label" + fi PASS_N=$((PASS_N + 1)) } -# component-scoped error: err bank "what failed" "why / HTTP / path" +# component-scoped error: err bank "libeufin down" "detail" err() { local comp="$1" msg="$2" detail="${3:-}" _take_tid - _msg_line "$BG_ERR" "ERROR" "$W" "${comp}: ${msg}" "$detail" + printf '%s[ERROR]%s %s%s: %s%s\n' "$R" "$N" "$(_fmt_tid)" "$comp" "$msg" "${detail:+ — $detail}" FAIL_N=$((FAIL_N + 1)) - ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ · $detail}") + ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ — $detail}") } -# fail "what failed" ["why / HTTP code / path"] +# legacy fail label ... fail() { local label="$1" detail="${2:-}" _take_tid - _msg_line "$BG_ERR" "ERROR" "$W" "$label" "$detail" + printf '%s[ERROR]%s %s%s%s\n' "$R" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}" FAIL_N=$((FAIL_N + 1)) - ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ · $detail}") + ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ — $detail}") } warn() { - # warn "what is soft-bad" ["why still ok to continue"] - # warn component "what" "why" + # Forms (same idea as err; always try to state the problem): + # warn "problem" + # warn "problem" "why / context" + # warn component "problem" "why / context" local a1="${1:-}" a2="${2:-}" a3="${3:-}" local head detail _take_tid @@ -537,78 +446,56 @@ warn() { head="$a1" detail="" fi - _msg_line "$BG_WARN" "WARN" "$W" "$head" "$detail" + if [ -n "$detail" ]; then + printf '%s[WARN]%s %s%s — %s\n' "$Y" "$N" "$(_fmt_tid)" "$head" "$detail" + else + printf '%s[WARN]%s %s%s\n' "$Y" "$N" "$(_fmt_tid)" "$head" + fi WARN_N=$((WARN_N + 1)) } info() { - # info "topic" ["concrete fact / value / next step"] local label="$1" detail="${2:-}" _take_tid - _msg_line "$BG_INFO" "INFO" "$W" "$label" "$detail" + if [ -n "$detail" ]; then + printf '%s[INFO]%s %s%s — %s\n' "$C" "$N" "$(_fmt_tid)" "$label" "$detail" + else + printf '%s[INFO]%s %s%s\n' "$C" "$N" "$(_fmt_tid)" "$label" + fi INFO_N=$((INFO_N + 1)) } blocker() { - # Hard stop on pay/withdraw path: blocker "step" "why it cannot continue" + # Payment/withdraw path cannot proceed because of this local step="$1" msg="$2" _take_tid - _msg_line "$BG_BLK" "BLOCKER" "$W" "${step}" "$msg" + printf '%s[BLOCKER]%s %s%s: %s\n' "$R$B" "$N" "$(_fmt_tid)" "$step" "$msg" BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg") FAIL_N=$((FAIL_N + 1)) ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg") } -section() { - # Boxed section header (3 lines when colour on) - local title="$*" - local w=${#title} - [ "$w" -lt 24 ] && w=24 - [ "$w" -gt 56 ] && w=56 - local pad line i - pad=$(printf -- "%-${w}s" "$title") - if [ "${BOX:-0}" = "1" ]; then - line="" - i=0 - while [ "$i" -lt $((w + 2)) ]; do - line="${line}═" - i=$((i + 1)) - done - printf -- '\n%s╔%s╗%s\n' "$BG_SEC" "$line" "$N" - printf -- '%s║%s %s%s%s %s║%s\n' "$BG_SEC" "$N" "$B" "$pad" "$N" "$BG_SEC" "$N" - printf -- '%s╚%s╝%s\n' "$BG_SEC" "$line" "$N" - else - printf -- '\n== %s ==\n' "$title" - fi -} +section() { printf '\n%s== %s ==%s\n' "$B" "$*" "$N"; } summary() { echo "" if [ "${#BLOCKERS[@]}" -gt 0 ]; then - if [ "${BOX:-0}" = "1" ]; then - printf -- '%s┌ BLOCKERS · pay/withdraw cannot finish ┐%s\n' "$BG_BLK" "$N" - else - printf -- '--- BLOCKERS (pay/withdraw cannot finish) ---\n' - fi + printf '%s--- BLOCKERS (fix/withdraw path) ---%s\n' "$R$B" "$N" local b for b in "${BLOCKERS[@]}"; do - printf -- '%s •%s %s%s%s\n' "$M" "$N" "$W" "$b" "$N" + printf '%s • %s%s\n' "$R" "$b" "$N" done fi if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then - if [ "${BOX:-0}" = "1" ]; then - printf -- '%s┌ ERRORS · failed checks ┐%s\n' "$BG_ERR" "$N" - else - printf -- '--- ERRORS (failed checks) ---\n' - fi + printf '%s--- ERRORS ---%s\n' "$R" "$N" local e for e in "${ERRORS[@]}"; do case "$e" in BLOCKER*) continue ;; esac - printf -- '%s •%s %s%s%s\n' "$R" "$N" "$W" "$e" "$N" + printf '%s • %s%s\n' "$R" "$e" "$N" done fi - printf -- '%stotals:%s %s%d OK%s' "$D" "$N" "$G" "$PASS_N" "$N" - [ "$FAIL_N" -gt 0 ] && printf -- ', %s%d ERROR%s' "$R" "$FAIL_N" "$N" - [ "$WARN_N" -gt 0 ] && printf -- ', %s%d WARN%s' "$Y" "$WARN_N" "$N" - [ "$INFO_N" -gt 0 ] && printf -- ', %s%d INFO%s' "$C" "$INFO_N" "$N" - [ "${#BLOCKERS[@]}" -gt 0 ] && printf -- ', %s%d BLOCKER%s' "$M" "${#BLOCKERS[@]}" "$N" + printf 'totals: %s%d OK%s' "$G" "$PASS_N" "$N" + [ "$FAIL_N" -gt 0 ] && printf ', %s%d ERROR%s' "$R" "$FAIL_N" "$N" + [ "$WARN_N" -gt 0 ] && printf ', %s%d WARN%s' "$Y" "$WARN_N" "$N" + [ "$INFO_N" -gt 0 ] && printf ', %d INFO' "$INFO_N" + [ "${#BLOCKERS[@]}" -gt 0 ] && printf ', %s%d BLOCKER%s' "$R" "${#BLOCKERS[@]}" "$N" printf '\n' [ "$FAIL_N" -eq 0 ] }