Compare commits

..

8 commits

Author SHA1 Message Date
Hernâni Marques
e46aeb408a
monitoring: soft-skip GOA-only landing assets off-GOA 2026-07-17 17:03:37 +02:00
Hernâni Marques
3d5f412af3
monitoring: stats.json freshness + display fields outside-in 2026-07-17 16:56:09 +02:00
Hernâni Marques
62d0e426f4
monitoring: e2e without run-until-done (wallet serve + poll) 2026-07-17 16:49:17 +02:00
Hernâni Marques
d2113e7362
monitoring: drop remaining run-until-done from GOA ladder 2026-07-17 16:49:17 +02:00
Hernâni Marques
b6c3225e97
landing-stats: make collector multi-stack reusable 2026-07-17 16:26:18 +02:00
Hernâni Marques
e1cc6d7141
monitoring: boxed badges + area.group test IDs 2026-07-17 15:55:50 +02:00
Hernâni Marques
f8affeb651
monitoring: badge backgrounds for ok/info/warn/error 2026-07-17 15:48:56 +02:00
Hernâni Marques
3d4e883cdb
monitoring: richer colour for ok/info/warn/error lines 2026-07-17 15:48:06 +02:00
19 changed files with 1204 additions and 465 deletions

View file

@ -14,12 +14,14 @@ TimeoutStartSec=480
# Do not keep "active" after run — timer may fire again cleanly # Do not keep "active" after run — timer may fire again cleanly
RemainAfterExit=no RemainAfterExit=no
Environment=TZ=Europe/Zurich 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=ADMIN_LOG=%h/src/koopa/koopa-admin-log
Environment=SECRETS_BANK=%h/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank 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_URL=http://127.0.0.1:9012
Environment=BANK_PUBLIC_URL=https://bank.hacktivism.ch Environment=BANK_PUBLIC_URL=https://bank.hacktivism.ch
Environment=EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config 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=SCAN_SKIP=exchange
Environment=TX_PAGE=500 Environment=TX_PAGE=500
Environment=MAX_TX_PAGES=0 Environment=MAX_TX_PAGES=0

View file

@ -238,6 +238,7 @@ fi
GEN=$(now_iso) GEN=$(now_iso)
HUMAN=$(now_human) HUMAN=$(now_human)
GEN_UNIX=$(date +%s)
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; } num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
MEM_JSON='"container_rss_human": "—"' MEM_JSON='"container_rss_human": "—"'
@ -257,6 +258,7 @@ cat >"$TMP" <<EOF
"timezone": $(json_str "$TZ"), "timezone": $(json_str "$TZ"),
"generated_at": $(json_str "$GEN"), "generated_at": $(json_str "$GEN"),
"generated_at_human": $(json_str "$HUMAN"), "generated_at_human": $(json_str "$HUMAN"),
"generated_at_unix": ${GEN_UNIX},
"reserves": ${RESERVES:-0}, "reserves": ${RESERVES:-0},
"wire_in_count": ${RESERVES_IN:-0}, "wire_in_count": ${RESERVES_IN:-0},
"wire_in_amount": $(json_str "$WIRE_IN_AMT"), "wire_in_amount": $(json_str "$WIRE_IN_AMT"),

View file

@ -1,12 +1,21 @@
# taler-landing — central stats (hernani user systemd) # taler-landing — generic multi-stack landing stats
Public landing numbers on One collector (`collect_bank_stats.py` + `collect-landing-stats.sh`) for any
currency / stack. **Defaults = GOA / hacktivism**; stage TESTPAYSAN uses a
profile file.
Public landing numbers (example GOA):
- https://bank.hacktivism.ch/intro/ → `stats.json` - https://bank.hacktivism.ch/intro/ → `stats.json`
- https://exchange.hacktivism.ch/intro/ → `stats.json` - https://exchange.hacktivism.ch/intro/ → `stats.json`
- https://taler.hacktivism.ch/intro/ → `stats.json` - https://taler.hacktivism.ch/intro/ → `stats.json`
are produced by **one host process** as user **`hernani`**, not by three unrelated in-container crons. Stage TESTPAYSAN (shared bank feed on all three hosts):
- https://stage.bank.lefrancpaysan.ch/intro/stats.json
(same file also under stage.exchange / stage.monnaie landings)
Produced by **one host process** (user systemd timer), not three crons.
## Why host / user systemd ## Why host / user systemd
@ -77,16 +86,37 @@ Preferred order:
Explorer password optional (shared-pool balance). Explorer password optional (shared-pool balance).
## Profiles
| File | Stack |
|------|--------|
| `profiles/goa.env` | hacktivism GOA (container publish) |
| `profiles/stage-testpaysan.env` | FrancPaysan stage; **shared** host `stats.json` |
```bash
# Stage (on stagepaysan):
STACK_PROFILE=stage-testpaysan ./scripts/taler-landing/install-landing-stats-host.sh
# or: francpaysan-admin-log/scripts/stagepaysan/install-landing-stats.sh
```
`~/.config/taler-landing/stack.env` is sourced on each run.
## Env overrides ## Env overrides
| Env | Default | Meaning | | Env | Default | Meaning |
|-----|---------|---------| |-----|---------|---------|
| `BANK_URL` | `http://127.0.0.1:9012` | libeufin loopback | | `BANK_URL` | `http://127.0.0.1:9012` | libeufin loopback |
| `BANK_CURRENCY` | *(auto)* | `GOA` / `TESTPAYSAN` / … |
| `SCAN_SKIP` | `exchange` | usernames excluded from flow | | `SCAN_SKIP` | `exchange` | usernames excluded from flow |
| `COLLECT_BANK` / `_EXCHANGE` / `_MERCHANT` | `1` | phase switches |
| `COLLECT_RESOURCES` | `1` | container RSS/loadavg merge |
| `HOST_STATS_DIRS` | empty | space-separated host dirs for **shared** bank `stats.json` |
| `PUBLISH_PODMAN` | `1` | also `podman cp` into container landings |
| `TX_PAGE` | `500` | transactions page size | | `TX_PAGE` | `500` | transactions page size |
| `MAX_TX_PAGES` | `0` | `0` = unlimited pages / account | | `MAX_TX_PAGES` | `0` | `0` = unlimited pages / account |
| `ACCOUNTS_DELTA` | `-10000` | account list window | | `ACCOUNTS_DELTA` | `-10000` | account list window |
| `ADMIN_LOG` | `%h/src/koopa/koopa-admin-log` | refresh in-container scripts | | `ADMIN_LOG` | `%h/src/koopa/koopa-admin-log` | refresh in-container scripts |
| `STATS_SOURCE_LABEL` | `host collect_bank_stats.py` | `stats.source` field |
## UI alt names ## UI alt names

View file

@ -1,10 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Central landing-stats collector for hernani@koopa (user systemd timer). # Generic landing-stats orchestrator (any Taler stack).
# #
# - Bank: full account+ledger scan via collect_bank_stats.py → bank container # Defaults = GOA / hacktivism on koopa. Override via env or EnvironmentFile
# - Exchange / merchant: existing in-container scripts, then amount_alt enrich # (see profiles: goa | stage-testpaysan).
# - All three: container RSS / loadavg / top procs via mem-snapshot (podman exec) #
# - Never wipes a good stats.json on failure (writes stats-run.json only) # - Bank: collect_bank_stats.py (currency-agnostic) → stats.json
# - Optional: exchange / merchant in-container generators
# - Optional: container RSS/loadavg merge
# - Publish: podman into container landings and/or host dirs (shared bank stats)
# - Never wipes a good stats.json on failure (stats-run.json only)
# #
# Install: scripts/taler-landing/install-landing-stats-host.sh # Install: scripts/taler-landing/install-landing-stats-host.sh
set -euo pipefail set -euo pipefail
@ -12,6 +16,20 @@ set -euo pipefail
export TZ="${TZ:-Europe/Zurich}" export TZ="${TZ:-Europe/Zurich}"
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin${PATH:+:$PATH}" export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin${PATH:+:$PATH}"
# Optional stack profile: ~/.config/taler-landing/stack.env or STACK_ENV_FILE
if [ -n "${STACK_ENV_FILE:-}" ] && [ -f "${STACK_ENV_FILE}" ]; then
# shellcheck disable=SC1090
set -a
# shellcheck source=/dev/null
. "${STACK_ENV_FILE}"
set +a
elif [ -f "${HOME}/.config/taler-landing/stack.env" ]; then
set -a
# shellcheck source=/dev/null
. "${HOME}/.config/taler-landing/stack.env"
set +a
fi
log() { printf '%s %s\n' "$(date -Iseconds)" "$*"; } log() { printf '%s %s\n' "$(date -Iseconds)" "$*"; }
ROOT="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$(dirname "$0")" && pwd)"
@ -22,6 +40,8 @@ elif [ -f "${HOME}/.local/lib/taler-landing/collect_bank_stats.py" ]; then
LIB="${HOME}/.local/lib/taler-landing" LIB="${HOME}/.local/lib/taler-landing"
elif [ -f "${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing/collect_bank_stats.py" ]; then elif [ -f "${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing/collect_bank_stats.py" ]; then
LIB="${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing" LIB="${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing"
elif [ -f "${HOME}/taler/src/koopa-admin-log/scripts/taler-landing/collect_bank_stats.py" ]; then
LIB="${HOME}/taler/src/koopa-admin-log/scripts/taler-landing"
else else
LIB="$ROOT" LIB="$ROOT"
fi fi
@ -29,6 +49,13 @@ fi
ADMIN_LOG="${ADMIN_LOG:-${HOME}/src/koopa/koopa-admin-log}" ADMIN_LOG="${ADMIN_LOG:-${HOME}/src/koopa/koopa-admin-log}"
SECRETS_BANK="${SECRETS_BANK:-${HOME}/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank}" SECRETS_BANK="${SECRETS_BANK:-${HOME}/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank}"
# Which phases to run (1/0)
COLLECT_BANK="${COLLECT_BANK:-1}"
COLLECT_EXCHANGE="${COLLECT_EXCHANGE:-1}"
COLLECT_MERCHANT="${COLLECT_MERCHANT:-1}"
COLLECT_RESOURCES="${COLLECT_RESOURCES:-1}"
# Containers (empty = skip podman publish for that role)
BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}" BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}"
EX_CTR="${EX_CTR:-taler-hacktivism-exchange-ansible}" EX_CTR="${EX_CTR:-taler-hacktivism-exchange-ansible}"
MER_CTR="${MER_CTR:-taler-hacktivism}" MER_CTR="${MER_CTR:-taler-hacktivism}"
@ -36,10 +63,19 @@ MER_CTR="${MER_CTR:-taler-hacktivism}"
BANK_URL="${BANK_URL:-http://127.0.0.1:9012}" BANK_URL="${BANK_URL:-http://127.0.0.1:9012}"
BANK_PUBLIC_URL="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}" BANK_PUBLIC_URL="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}"
EXCHANGE_CONFIG_URL="${EXCHANGE_CONFIG_URL:-https://exchange.hacktivism.ch/config}" EXCHANGE_CONFIG_URL="${EXCHANGE_CONFIG_URL:-https://exchange.hacktivism.ch/config}"
BANK_CURRENCY="${BANK_CURRENCY:-}" # empty → auto from bank/exchange /config
STATS_SOURCE_LABEL="${STATS_SOURCE_LABEL:-host collect_bank_stats.py}"
# In-container landing roots (podman publish)
BANK_LANDING_IN="${BANK_LANDING_IN:-/var/www/bank-landing}" BANK_LANDING_IN="${BANK_LANDING_IN:-/var/www/bank-landing}"
EX_LANDING_IN="${EX_LANDING_IN:-/var/www/exchange-landing}" EX_LANDING_IN="${EX_LANDING_IN:-/var/www/exchange-landing}"
MER_LANDING_IN="${MER_LANDING_IN:-/var/www/merchant-landing}" MER_LANDING_IN="${MER_LANDING_IN:-/var/www/merchant-landing}"
PUBLISH_PODMAN="${PUBLISH_PODMAN:-1}"
# Host dirs that should get the *same* bank stats.json (shared public feed).
# Colon-separated (systemd EnvironmentFile-safe). Example stage:
# HOST_STATS_DIRS=/var/www/lfp-stage/bank:/var/www/lfp-stage/exchange:/var/www/lfp-stage/merchant
HOST_STATS_DIRS="${HOST_STATS_DIRS:-}"
WORKDIR="${LANDING_STATS_WORKDIR:-${XDG_RUNTIME_DIR:-/tmp}/taler-landing-stats}" WORKDIR="${LANDING_STATS_WORKDIR:-${XDG_RUNTIME_DIR:-/tmp}/taler-landing-stats}"
mkdir -p "$WORKDIR" mkdir -p "$WORKDIR"
@ -64,7 +100,9 @@ MEM_SRC="${MEM_SNAPSHOT_SRC:-$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh}"
merge_container_resources() { merge_container_resources() {
local label="$1" ctr="$2" stats_file="$3" local label="$1" ctr="$2" stats_file="$3"
[ "${COLLECT_RESOURCES}" = "1" ] || return 0
[ -f "$stats_file" ] || return 0 [ -f "$stats_file" ] || return 0
[ -n "$ctr" ] || return 0
if ! ctr_running "$ctr"; then if ! ctr_running "$ctr"; then
log "WARN: $label resources: $ctr not running" log "WARN: $label resources: $ctr not running"
return 1 return 1
@ -95,6 +133,36 @@ merge_container_resources() {
return 0 return 0
} }
# Write stats.json into host directories (shared feed: same file everywhere).
publish_host_stats() {
local src_stats="$1" src_run="${2:-}"
[ -n "${HOST_STATS_DIRS:-}" ] || return 0
[ -f "$src_stats" ] || return 0
local d paths
# Accept colon or space separators
paths=$(printf '%s' "$HOST_STATS_DIRS" | tr ': ' '\n' | sed '/^$/d')
while IFS= read -r d; do
[ -n "$d" ] || continue
if [ -d "$d" ] || mkdir -p "$d" 2>/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() { read_pass() {
local name="$1" f local name="$1" f
for f in \ for f in \
@ -107,12 +175,26 @@ read_pass() {
return 0 return 0
fi fi
done done
# container (hernani can podman exec root files inside owned containers) # container secrets: /root/… (GOA) or /data/secrets/… (FrancPaysan stage)
if podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then if [ -n "${BANK_CTR:-}" ] && 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 for path in "/root/${name}" "/data/secrets/${name}" "/data/secrets/bank-admin-password.txt"; do
podman exec "$BANK_CTR" cat "/root/${name}" 2>/dev/null | tr -d '\n' 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 return 0
fi 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
fi
fi fi
return 1 return 1
} }
@ -123,6 +205,8 @@ ctr_running() {
publish_json() { publish_json() {
local ctr="$1" dest_dir="$2" src_stats="$3" src_run="$4" 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 if ! ctr_running "$ctr"; then
log "WARN: $ctr not running — skip publish $dest_dir" log "WARN: $ctr not running — skip publish $dest_dir"
return 1 return 1
@ -166,6 +250,7 @@ collect_bank() {
BANK_URL="$BANK_URL" \ BANK_URL="$BANK_URL" \
BANK_PUBLIC_URL="$BANK_PUBLIC_URL" \ BANK_PUBLIC_URL="$BANK_PUBLIC_URL" \
EXCHANGE_CONFIG_URL="$EXCHANGE_CONFIG_URL" \ EXCHANGE_CONFIG_URL="$EXCHANGE_CONFIG_URL" \
BANK_CURRENCY="${BANK_CURRENCY:-}" \
BANK_ADMIN_PASS="$admin_pass" \ BANK_ADMIN_PASS="$admin_pass" \
BANK_EXPLORER_PASS="$explorer_pass" \ BANK_EXPLORER_PASS="$explorer_pass" \
DEMO_DIR="${demo_dir}" \ DEMO_DIR="${demo_dir}" \
@ -173,6 +258,7 @@ collect_bank() {
TX_PAGE="${TX_PAGE:-500}" \ TX_PAGE="${TX_PAGE:-500}" \
MAX_TX_PAGES="${MAX_TX_PAGES:-0}" \ MAX_TX_PAGES="${MAX_TX_PAGES:-0}" \
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}" \ ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}" \
STATS_SOURCE_LABEL="$STATS_SOURCE_LABEL" \
"$PY" "$LIB/collect_bank_stats.py" \ "$PY" "$LIB/collect_bank_stats.py" \
--out "$WORKDIR/bank-stats.json" \ --out "$WORKDIR/bank-stats.json" \
--run-out "$WORKDIR/bank-stats-run.json" \ --run-out "$WORKDIR/bank-stats-run.json" \
@ -181,11 +267,13 @@ collect_bank() {
set -e set -e
if [ "$ec_bank" -eq 0 ] && [ -f "$WORKDIR/bank-stats.json" ]; then if [ "$ec_bank" -eq 0 ] && [ -f "$WORKDIR/bank-stats.json" ]; then
# Always attach in-container RSS/loadavg (not host /proc) # Optional: attach in-container RSS/loadavg (not host /proc)
merge_container_resources bank "$BANK_CTR" "$WORKDIR/bank-stats.json" || true merge_container_resources bank "$BANK_CTR" "$WORKDIR/bank-stats.json" || true
publish_json "$BANK_CTR" "$BANK_LANDING_IN" \ publish_json "$BANK_CTR" "$BANK_LANDING_IN" \
"$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json" "$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json"
log "bank: OK → ${BANK_CTR}:${BANK_LANDING_IN}/stats.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})"
else else
log "ERROR: bank collect failed (ec=$ec_bank) — previous stats.json kept" log "ERROR: bank collect failed (ec=$ec_bank) — previous stats.json kept"
[ -f "$WORKDIR/bank-stats-run.json" ] || \ [ -f "$WORKDIR/bank-stats-run.json" ] || \
@ -272,13 +360,26 @@ collect_merchant() {
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
main() { main() {
log "=== taler-landing-stats start (user=$(id -un) lib=$LIB) ===" 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=$? collect_bank || ec_bank=$?
else
log "skip bank collect"
fi
if [ "${COLLECT_EXCHANGE}" = "1" ]; then
collect_exchange || ec_ex=$? collect_exchange || ec_ex=$?
else
log "skip exchange collect"
fi
if [ "${COLLECT_MERCHANT}" = "1" ]; then
collect_merchant || ec_mer=$? collect_merchant || ec_mer=$?
else
log "skip merchant collect"
fi
log "=== done bank=$ec_bank exchange=$ec_ex 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 # non-zero if bank failed (primary public flow numbers); soft on ex/mer
if [ "$ec_bank" -ne 0 ]; then if [ "${COLLECT_BANK}" = "1" ] && [ "$ec_bank" -ne 0 ]; then
return 1 return 1
fi fi
return 0 return 0

View file

@ -1,23 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Full-scan bank landing stats (all accounts, all ledger money). """Full-scan bank landing stats (all accounts, all ledger money).
Run on koopa as hernani (or anywhere with admin API access). Writes stats.json Generic for any currency stack (GOA, TESTPAYSAN, KUDOS, ). Run on the ops host
compatible with bank.hacktivism.ch/intro/ plus amount_alt fields for compact UI. (with admin API access). Writes stats.json for public landings + amount_alt.
Env (selected): Env (selected):
BANK_URL default http://127.0.0.1:9012 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_USER default admin
BANK_ADMIN_PASS or BANK_ADMIN_PASS_FILE / pass via --admin-pass-file BANK_ADMIN_PASS or BANK_ADMIN_PASS_FILE / pass via --admin-pass-file
BANK_EXPLORER_USER default explorer BANK_EXPLORER_USER default explorer
BANK_EXPLORER_PASS optional (balance_explorer) BANK_EXPLORER_PASS optional (balance_explorer)
EXCHANGE_CONFIG_URL for alt_unit_names (default https://exchange.hacktivism.ch/config) EXCHANGE_CONFIG_URL for alt_unit_names + currency
BANK_PUBLIC_URL public base for latency probes
OUT output path (default stdout if -) OUT output path (default stdout if -)
SCAN_SKIP comma usernames excluded from flow (default: exchange) SCAN_SKIP comma usernames excluded from flow (default: exchange)
TX_PAGE page size for transactions delta (default 500) TX_PAGE / MAX_TX_PAGES / ACCOUNTS_DELTA / RECENT_WD_N / TZ
MAX_TX_PAGES 0 = unlimited pages per account (default 0) STATS_SOURCE_LABEL optional string in stats.source field
ACCOUNTS_DELTA GET /accounts?delta= (default -10000)
RECENT_WD_N recent withdraws (default 10)
TZ default Europe/Zurich
""" """
from __future__ import annotations from __future__ import annotations
@ -343,6 +342,11 @@ def main() -> int:
"--exchange-config", "--exchange-config",
default=env("EXCHANGE_CONFIG_URL", "https://exchange.hacktivism.ch/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("--out", default=env("OUT", "-"))
ap.add_argument("--run-out", default=env("RUN_OUT", "")) ap.add_argument("--run-out", default=env("RUN_OUT", ""))
ap.add_argument( ap.add_argument(
@ -364,6 +368,11 @@ def main() -> int:
) )
ap.add_argument("--recent", type=int, default=int(env("RECENT_WD_N", "10") or "10")) 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("--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() args = ap.parse_args()
tz_name = env("TZ", "Europe/Zurich") or "Europe/Zurich" tz_name = env("TZ", "Europe/Zurich") or "Europe/Zurich"
@ -378,6 +387,7 @@ def main() -> int:
bank = args.bank.rstrip("/") bank = args.bank.rstrip("/")
skip = {s.strip() for s in (args.skip or "").split(",") if s.strip()} 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: def write_run(ok: bool, err: Optional[str] = None) -> None:
if not args.run_out: if not args.run_out:
@ -397,6 +407,24 @@ def main() -> int:
raise RuntimeError("no admin password (BANK_ADMIN_PASS or --admin-pass-file)") raise RuntimeError("no admin password (BANK_ADMIN_PASS or --admin-pass-file)")
token = get_token(bank, args.admin_user, admin_pass) token = get_token(bank, args.admin_user, admin_pass)
alt = load_alt_from_config(args.exchange_config) 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: if not alt:
alt = dict(DEFAULT_ALT) alt = dict(DEFAULT_ALT)
@ -442,13 +470,24 @@ def main() -> int:
except Exception: except Exception:
continue continue
low = (subject or "").lower() 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": if direction == "credit":
total_in += n total_in += n
n_incoming += 1 n_incoming += 1
incomings.append( incomings.append(
{ {
"kind": "incoming", "kind": "incoming",
"amount": amount if ":" in amount else f"GOA:{amount}", "amount": amount,
"at_unix": ts, "at_unix": ts,
"account": uname, "account": uname,
"subject": subject, "subject": subject,
@ -460,7 +499,7 @@ def main() -> int:
withdraws.append( withdraws.append(
{ {
"kind": "withdraw", "kind": "withdraw",
"amount": amount if ":" in amount else f"GOA:{amount}", "amount": amount,
"at_unix": ts, "at_unix": ts,
"account": uname, "account": uname,
"subject": subject, "subject": subject,
@ -503,7 +542,7 @@ def main() -> int:
users_n = sum(1 for u in accounts if u not in ("admin", "exchange")) users_n = sum(1 for u in accounts if u not in ("admin", "exchange"))
def pack_amt(d: Decimal) -> Dict[str, Any]: def pack_amt(d: Decimal) -> Dict[str, Any]:
f = format_amount_alt(f"GOA:{d}", alt) f = format_amount_alt(f"{currency}:{d}", alt)
return { return {
"amount": f["amount"], "amount": f["amount"],
"amount_alt": f["amount_alt"], "amount_alt": f["amount_alt"],
@ -567,10 +606,11 @@ def main() -> int:
} }
) )
# explorer balance # explorer / pool balance (shared demo account when present)
balance = "GOA:0" zero = f"{currency}:0"
bal_alt = "GOA:0" balance = zero
bal_full = "GOA:0" bal_alt = zero
bal_full = zero
if explorer_pass: if explorer_pass:
try: try:
etok = get_token(bank, args.explorer_user, explorer_pass) etok = get_token(bank, args.explorer_user, explorer_pass)
@ -583,7 +623,7 @@ def main() -> int:
balance = str( balance = str(
data.get("balance", {}).get("amount") data.get("balance", {}).get("amount")
if isinstance(data.get("balance"), dict) if isinstance(data.get("balance"), dict)
else data.get("amount") or data.get("balance") or "GOA:0" else data.get("amount") or data.get("balance") or zero
) )
# sometimes balance is object with amount # sometimes balance is object with amount
if isinstance(data.get("balance"), dict) and data["balance"].get( if isinstance(data.get("balance"), dict) and data["balance"].get(
@ -608,7 +648,7 @@ def main() -> int:
) )
if code == 200 and isinstance(data, dict): if code == 200 and isinstance(data, dict):
if isinstance(data.get("balance"), dict): if isinstance(data.get("balance"), dict):
balance = str(data["balance"].get("amount") or "GOA:0") balance = str(data["balance"].get("amount") or zero)
elif data.get("amount"): elif data.get("amount"):
balance = str(data["amount"]) balance = str(data["amount"])
bf = format_amount_alt(balance, alt) bf = format_amount_alt(balance, alt)
@ -637,12 +677,12 @@ def main() -> int:
stats = { stats = {
"ok": True, "ok": True,
"currency": "GOA", "currency": currency,
"timezone": tz_name, "timezone": tz_name,
"generated_at": gen_iso, "generated_at": gen_iso,
"generated_at_human": gen_human, "generated_at_human": gen_human,
"generated_at_unix": now_u, "generated_at_unix": now_u,
"source": "host collect_bank_stats.py (hernani systemd)", "source": str(args.source_label or "host collect_bank_stats.py"),
"bank_url": bank, "bank_url": bank,
"scan": { "scan": {
"tx_page": args.tx_page, "tx_page": args.tx_page,

View file

@ -1,35 +1,41 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Install central landing-stats collector as hernani user systemd timer. # Install generic landing-stats collector as user systemd timer.
# #
# On koopa: # GOA / koopa (default):
# cd ~/src/koopa/koopa-admin-log # cd ~/src/koopa/koopa-admin-log
# ./scripts/taler-landing/install-landing-stats-host.sh # ./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
# #
# Requires: linger enabled for hernani (loginctl enable-linger hernani) # Stage TESTPAYSAN (stagepaysan on francpaysan host):
# so the timer runs without an interactive login. # 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
set -euo pipefail set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)" ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
ADMIN_LOG="${ADMIN_LOG:-$ROOT}" ADMIN_LOG="${ADMIN_LOG:-$ROOT}"
SRC="$ADMIN_LOG/scripts/taler-landing" SRC="$ADMIN_LOG/scripts/taler-landing"
UNIT_SRC="$ADMIN_LOG/configs/systemd/user" UNIT_SRC="$ADMIN_LOG/configs/systemd/user"
PROFILE="${STACK_PROFILE:-goa}"
BIN_DST="${HOME}/.local/bin" BIN_DST="${HOME}/.local/bin"
LIB_DST="${HOME}/.local/lib/taler-landing" LIB_DST="${HOME}/.local/lib/taler-landing"
UNIT_DST="${HOME}/.config/systemd/user" UNIT_DST="${HOME}/.config/systemd/user"
STATE_DST="${HOME}/.local/state/taler-landing-stats" STATE_DST="${HOME}/.local/state/taler-landing-stats"
CFG_DST="${HOME}/.config/taler-landing"
mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST" mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST" "$CFG_DST"
install -m 0755 "$SRC/collect-landing-stats.sh" "$BIN_DST/collect-landing-stats.sh" 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/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/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/merge_resources.py" "$LIB_DST/merge_resources.py"
install -m 0755 "$SRC/collect_container_resources.sh" "$LIB_DST/collect_container_resources.sh" 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" "$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 0755 "$SRC/test-landing-stats.sh" "$LIB_DST/test-landing-stats.sh"
fi
install -m 0644 "$SRC/goa_amounts.py" "$LIB_DST/goa_amounts.py" 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) # mem-snapshot helper lives in taler-shared (copied into containers at collect time)
if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then
@ -37,18 +43,34 @@ if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then
"$LIB_DST/mem-snapshot.sh" "$LIB_DST/mem-snapshot.sh"
fi fi
# Wrapper always uses installed lib next to itself when LIB discovery works; # Stack profile → ~/.config/taler-landing/stack.env (sourced by collector)
# also point ADMIN_LOG for in-container script refresh. 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
install -m 0644 "$UNIT_SRC/taler-landing-stats.service" "$UNIT_DST/" install -m 0644 "$UNIT_SRC/taler-landing-stats.service" "$UNIT_DST/"
install -m 0644 "$UNIT_SRC/taler-landing-stats.timer" "$UNIT_DST/" install -m 0644 "$UNIT_SRC/taler-landing-stats.timer" "$UNIT_DST/"
# Rewrite ExecStart to installed binary if unit uses %h path — units already do. # 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
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now taler-landing-stats.timer systemctl --user enable --now taler-landing-stats.timer
echo "Installed (user=$(id -un)):" echo "Installed (user=$(id -un) profile=${PROFILE}):"
echo " $BIN_DST/collect-landing-stats.sh" echo " $BIN_DST/collect-landing-stats.sh"
echo " $LIB_DST/{collect_bank_stats,enrich_stats_alt,goa_amounts}.py" 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 " $UNIT_DST/taler-landing-stats.{service,timer} (timer enabled)"
echo " logs: $STATE_DST/" echo " logs: $STATE_DST/"
echo echo
@ -61,8 +83,6 @@ echo "Run once now:"
echo " systemctl --user start taler-landing-stats.service" echo " systemctl --user start taler-landing-stats.service"
echo " journalctl --user -u taler-landing-stats.service -n 40 --no-pager" echo " journalctl --user -u taler-landing-stats.service -n 40 --no-pager"
echo echo
echo "Optional secrets (if not readable via podman exec bank /root/…):" echo "Secrets (if not via podman exec bank):"
echo " mkdir -p ~/.config/taler-landing" echo " mkdir -p ~/.config/taler-landing && chmod 700 ~/.config/taler-landing"
echo " cp …/bank-admin-password.txt ~/.config/taler-landing/" echo " # bank-admin-password.txt bank-explorer-password.txt"
echo " cp …/bank-explorer-password.txt ~/.config/taler-landing/"
echo " chmod 600 ~/.config/taler-landing/*"

View file

@ -0,0 +1,18 @@
# 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"

View file

@ -0,0 +1,22 @@
# 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"

View file

@ -363,6 +363,7 @@ CUR_JSON="${CUR_JSON}
GEN=$(now_iso) GEN=$(now_iso)
HUMAN=$(now_human) HUMAN=$(now_human)
GEN_UNIX=$(date +%s)
# Live performance probes (exchange-style) # Live performance probes (exchange-style)
measure_ms() { measure_ms() {
@ -406,6 +407,7 @@ cat >"$TMP" <<EOF
"timezone": $(json_str "$TZ"), "timezone": $(json_str "$TZ"),
"generated_at": $(json_str "$GEN"), "generated_at": $(json_str "$GEN"),
"generated_at_human": $(json_str "$HUMAN"), "generated_at_human": $(json_str "$HUMAN"),
"generated_at_unix": ${GEN_UNIX},
"instances": ${INSTANCES:-0}, "instances": ${INSTANCES:-0},
"orders": ${TOTAL_CONTRACTS:-0}, "orders": ${TOTAL_CONTRACTS:-0},
"paid": ${TOTAL_PAID:-0}, "paid": ${TOTAL_PAID:-0},

View file

@ -1,24 +1,26 @@
# taler-monitoring # taler-monitoring
Report for the **GOA** stack with clear severity tags and **per-area test IDs**: Report for the **GOA** stack with boxed severity badges and **grouped test IDs**
`area.group-NN` (e.g. `www.exchange-01`, `e2e.pay-03`):
| Tag | Meaning | | Tag | Meaning |
|-----|---------| |-----|---------|
| `[OK]` | check passed | | `┌ OK ┐` | check passed |
| `[INFO]` | inside status (container, ports, log noise) | | `┌ INFO ┐` | status / note |
| `[WARN]` | degraded but maybe not fatal | | `┌ WARN ┐` | degraded but maybe not fatal |
| `[ERROR]` | component problem | | `┌ ERROR ┐` | component problem |
| `[BLOCKER]` | **withdraw/pay path cannot complete** because of this | | `┌ BLOCKER ┐` | **withdraw/pay path cannot complete** because of this |
IDs: **`www-001`**, **`inside-001`**, **`versions-001`**, **`sanity-001`**, **`server-001`**, **`e2e-001`** … IDs reset per **group** inside an area — so “too many www tests” become
`www.exchange-*`, `www.bank-*`, `www.landing-*`, etc.
Catalog: **[TESTS.md](./TESTS.md)**. Catalog: **[TESTS.md](./TESTS.md)**.
```text ```text
[OK] www-001 exchange /config https://exchange…/config ┌ OK ┐ www.exchange-01 exchange /config · HTTP 200
[BLOCKER] e2e-015 prereq: merchant HTTP 502 ┌ BLOCKER┐ e2e.prereq-02 merchant HTTP 502
``` ```
End of each phase: totals + list of **BLOCKERS** and **ERRORS**. End of each phase: totals + list of **BLOCKERS** and **ERRORS** (ids included).
## Commands ## Commands
@ -41,8 +43,27 @@ End of each phase: totals + list of **BLOCKERS** and **ERRORS**.
./taler-monitoring.sh --bank https://bank.demo.taler.net \ ./taler-monitoring.sh --bank https://bank.demo.taler.net \
--exchange https://exchange.demo.taler.net \ --exchange https://exchange.demo.taler.net \
--merchant https://backend.demo.taler.net --currency KUDOS urls --merchant https://backend.demo.taler.net --currency KUDOS urls
# Landing stats.json freshness (outside-in; age via generated_at_unix)
STATS_STALE_SECS=900 STATS_FAIL_SECS=3600 ./taler-monitoring.sh -d stage.lefrancpaysan.ch urls
``` ```
### Landing `stats.json` (www.stats)
Public `GET …/intro/stats.json` must expose timestamps so monitoring can tell
whether the collector is still updating:
| Field | Role |
|-------|------|
| `generated_at_unix` | epoch seconds — **preferred** for age = now unix |
| `generated_at` | ISO-8601 (fallback if unix missing) |
| `generated_at_human` | UI “updated” / footer string |
Also checks display payloads (bank-shape `withdraws` / accounts, or exchange-db /
merchant-db counters). Soft WARN if age &gt; `STATS_STALE_SECS` (default 900);
ERROR if age &gt; `STATS_FAIL_SECS` (default 3600; set `0` to disable).
Stage shared feed: same `gen_unix` on bank + exchange + monnaies.
## Domain profiles (`domains.conf`) ## Domain profiles (`domains.conf`)
Each monitored stack must say **what is bank, exchange, and merchant-backend**. Each monitored stack must say **what is bank, exchange, and merchant-backend**.

View file

@ -1,217 +1,132 @@
# taler-monitoring — test IDs by area # taler-monitoring — grouped test IDs
Every check line is numbered **per area** as `AREA-NNN` (zero-padded): Every check line is numbered **`area.group-NN`** (two-digit group counter):
| Area | Phase script | Meaning |
|------|--------------|---------|
| **www** | `check_urls.sh` | public HTTPS (outside-in) |
| **inside** | `check_inside.sh` | containers / processes on koopa |
| **versions** | `check_versions.sh` | deb.taler.net available + packages vs trixie |
| **sanity** | `check_sanity.sh` | public + server per component |
| **server** | `check_server.sh` | SSH host ports / processes |
| **e2e** | `check_e2e.sh` | withdraw + pay cycle |
Format in output:
```text ```text
[OK] www-001 exchange /config https://exchange…/config ┌ OK ┐ www.exchange-01 exchange /config · HTTP 200
[ERROR] e2e-012 bank-auth: admin token failed ┌ ERROR ┐ e2e.pay-03 order create failed · HTTP 502
[BLOCKER] e2e-015 prereq: merchant HTTP 502 ┌ BLOCKER┐ e2e.prereq-02 merchant secret missing
``` ```
IDs are assigned **in run order** within the area (`set_area` resets the counter). Optional soft checks still consume a number when they WARN. | Area | Phase script | Groups (examples) |
|------|--------------|-------------------|
| **www** | `check_urls.sh` | `exchange` `perf` `stats` `bank` `merchant` `paivana` `landing` |
| **inside** | `check_inside.sh` | `ssh` `bank` `exchange` `merchant` `caddy` `load` |
| **versions** | `check_versions.sh` | `outside` `inside` `compare` |
| **sanity** | `check_sanity.sh` | `bank` `exchange` `merchant` |
| **server** | `check_server.sh` | (flat `server-NN` or host groups) |
| **e2e** | `check_e2e.sh` | `prereq` `load` `bank` `wallet` `atm` `settle` `pay` `shop` `paivana` `dig` `report` |
| **ladder** | `check_goa_ladder.sh` | `plan` `load` `withdraw` `pay` `report` |
**Why groups:** flat `www-001``www-080` was hard to talk about.
`www.bank-04` / `e2e.atm-02` pin the failure to a logical block.
Usage in scripts:
```bash
set_area www
set_group exchange # → www.exchange-01, www.exchange-02, …
set_group bank # counter resets → www.bank-01
```
`set_group` also prints a small group chip (`┌ www.exchange ┐`) so logs show section boundaries.
Re-entering the same group later (e.g. exchange legal after perf) **continues** NN — `www.exchange-03`, not a second `-01`.
Without `set_group`, IDs stay flat: `area-01`, `area-02`, …
Optional soft checks still consume a number when they WARN.
Numbering follows **executed** checks (early skip may shift later NN inside the same group).
--- ---
## www — public URLs (`./taler-monitoring.sh urls`) ## www — public URLs (`./taler-monitoring.sh urls`)
| ID | Check | | Group | Checks (typical) |
|----|--------| |-------|------------------|
| www-… | exchange `/config`, currency, **alt_unit_names** | | **www.exchange-** | `/config`, currency, **alt_unit_names**; `/keys` (+ alt soft); `/intro/`, `/`; **`/terms`**, **`/privacy`**, `/terms/` |
| www-… | exchange `/keys` (+ alt_unit_names soft) | | **www.perf-** | outside-in RTT: bank/exchange/merchant `/config`, keys, webui, intro — ms; WARN ≥ `PERF_WARN_MS` (8000); ERROR ≥ `PERF_FAIL_MS` (20000) |
| www-… | exchange `/intro/`, `/` (302→intro) | | **www.stats-** | `/intro/stats.json` **reachable**; **timestamps** (`generated_at_unix` + human/ISO); **freshness** vs wall clock (`STATS_STALE_SECS` WARN, `STATS_FAIL_SECS` ERROR); **display fields** (bank-shape withdraws/accounts, or exchange-db / merchant-db keys); optional performance block; shared-feed equal `gen_unix` when bank collector is published to all three |
| www-… | **exchange `/terms`** body (not empty / not API error) | | **www.bank-** | `/config`, currency, alt_unit_names; integration/webui/intro; **auto-account.json** (required GOA/local; skip/soft off-GOA e.g. TESTPAYSAN); `/terms`, `/privacy` |
| www-… | **exchange `/privacy`** body | | **www.merchant-** | `/config` currency + currencies alt_unit_names; listed exchanges alt; webui/intro; **`/terms`**, **`/privacy`** |
| www-… | exchange `/terms/` (200 or redirect) | | **www.paivana-** | local GOA paywall front (redirect to template) |
| www-… | bank `/config`, currency, **alt_unit_names** | | **www.landing-** | own-stack intro links; static assets (`qrcode.min.js` hard; `og-goa-shop.png` hard only GOA/local); **demo-withdraw.json** GOA-only; shop-pay soft; cross-links local |
| www-… | bank integration / webui / intro / `/` |
| www-… | **bank `/terms`** body |
| www-… | **bank `/privacy`** (or `/intro/privacy.html` fallback) |
| www-… | merchant `/config`, currency, currencies alt_unit_names |
| www-… | each merchant `exchanges[]` `/config` alt_unit_names |
| www-… | merchant `/intro/`, `/webui/`, `/` |
| www-… | **merchant `/terms`** body (dual-currency notice) |
| www-… | **merchant `/privacy`** body (must not be `not configured`) |
| www-… | merchant `/terms/` redirect |
| www-… | **landing (aggregated per site)**: one line for bank/merchant/exchange (`/intro` + assets + own/external link counts); failures listed as samples; soft external/shop assets compact |
| www-… | cross-links: one line (4 intros) on local stack |
| www-… | bank demo-withdraw + shop assets: one line (+ soft shop-pay if missing) |
| www-… | **bank `/intro/demo-withdraw.json`**`taler://withdraw/HOST:PORT/taler-integration/…` + integration op HTTP 200 |
| www-… | **landing exposed links** (bank / merchant / exchange): parse each `/intro/` HTML, probe every own-stack `https://` + root-relative `href`/`src`/`content`, soft-check external stores/docs |
| www-… | landing static: `qrcode.min.js`, `og-goa-shop.png`, `qr-logo.png`, shop-pay.js/css |
| www-… | cross-links between bank ↔ merchant ↔ exchange intros (local stack) |
| www-… | **bank `/intro/demo-withdraw.json`**`taler://withdraw/HOST/taler-integration/…` (no default `:443`/`:80`; non-default port OK) + integration op HTTP 200 |
| www-… | bank `/intro/auto-account.json` (earlier) → same withdraw shape, **no payto_uri**, login at `/webui/` |
| www-… | **performance** (outside-in RTT): bank/exchange/merchant `/config`, keys, webui, intro — ms; WARN ≥ `PERF_WARN_MS` (8000); ERROR ≥ `PERF_FAIL_MS` (20000) |
| www-… | **landing load stats**: `/intro/stats.json` → loadavg, container RSS, in-container probe ms (public; SSH container cat fallback); warn if stale/missing |
**Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. On local stack, optional content needle (terms/privacy/FADP/GOA…). **Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. Local stack may require content needle (terms/privacy/FADP/GOA…).
**Performance rule:** Measured from the **monitoring runner** (public URLs via Caddy), not container loopback. HTTP must match expect (usually 200); **latency (ms) is on every perf OK/WARN/ERROR line**, plus a **perf summary** (n / min / p50 / avg / max). Slow ≥ `PERF_WARN_MS` (default 8000) → WARN; ≥ `PERF_FAIL_MS` (default 20000) → ERROR. **Performance rule:** Measured from the **monitoring runner** (public via Caddy), not container loopback. Latency (ms) on every perf line + **perf summary** (n / min / p50 / avg / max).
**Landing links rule:** Own-stack (bank/exchange/taler.\* + page host) must be HTTP 200 (or redirect→200). External (App Store, Play, F-Droid, wallet.taler.net, docs/git.taler.net, …) soft WARN if down. Auto-account wallet link must be `taler://withdraw/HOST/taler-integration/…` (default ports stripped for mobile wallets), never payto. **Landing links rule:** Own-stack must be HTTP 200 (or redirect→200). External stores/docs soft WARN. Auto-account wallet link must be `taler://withdraw/HOST/taler-integration/…` (default ports stripped), never payto.
**alt_unit_names rule:** wallet codec requires a non-empty map including scale key `"0"`. For multi-currency merchant, also follow every entry in `exchanges[]` and check that exchanges public `/config`. **alt_unit_names rule:** non-empty map including scale key `"0"`. Multi-currency merchant: follow each `exchanges[]` public `/config`.
(IDs after a failed early check may shift if later soft checks are skipped when body missing — numbering follows **executed** checks.)
--- ---
## inside — koopa SSH (`./taler-monitoring.sh inside`) ## inside — koopa SSH (`./taler-monitoring.sh inside`)
| ID | Check (typical order) | | Group | Checks |
|----|------------------------| |-------|--------|
| inside-001 | ssh koopa (or **koopa-external** fallback) | | **inside.ssh-** | ssh reachability / remote collect |
| inside-002+ | per-component emit: container, ports, libeufin/httpd, postgres, local `/config`/`/keys`, wirewatch, DNS pin, caddy | | **inside.bank-** | container, libeufin, postgres, local `/config`, nginx, DNS pins |
| inside-… | **load / memory**: host loadavg + RAM; bank/exchange/merchant podman CPU/mem/block + process RSS by role + DB sizes | | **inside.exchange-** | container, httpd, wirewatch, aggregator, transfer, local keys, DNS |
| **inside.merchant-** | container, httpd, wirewatch, depositcheck, DNS |
| **inside.caddy-** | host reverse-proxy process |
| **inside.load-** | host loadavg + RAM; per-container RSS/CPU |
Remote lines `E|comp|LEVEL|key|detail` each become one numbered result. Remote lines `E|comp|LEVEL|key|detail` each become one numbered result under that component group.
SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` (default `koopa-external`) when LAN is unreachable. SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` when LAN is unreachable.
--- ---
## sanity — bank · exchange · merchant (`./taler-monitoring.sh sanity`) ## e2e — withdraw + pay (`./taler-monitoring.sh e2e`)
| ID | Section | | Group | Checks |
|----|---------| |-------|--------|
| sanity-001… | bank public + server | | **e2e.prereq-** | wallet-cli, currency, budgets, secrets, public reachability |
| sanity-… | exchange public + server | | **e2e.load-** | host/container load snapshots (before / after ATM / after pay) |
| sanity-… | merchant public + server | | **e2e.bank-** | account, credit, withdraw ops |
| **e2e.wallet-** | exchange + ToS, wallet setup |
| **e2e.atm-** | ATM withdraw ladder rungs |
| **e2e.settle-** | wallet settlement wait / spendable balance |
| **e2e.pay-** | variable payments |
| **e2e.shop-** | GOA shop product catalog pays |
| **e2e.paivana-** | template paywall pay |
| **e2e.dig-** | coins-missing dig (inside + withdrawal-operation) |
| **e2e.report-** | final tallies |
Sequential through the whole script (one `set_area sanity`). When filing an issue, quote the full id + label, e.g.
`e2e.atm-03 ATM withdraw 5 GOA · bank confirmed, wallet empty`.
--- ---
## versions — packages vs deb.taler.net (`./taler-monitoring.sh versions`) ## versions / sanity / ladder / server
### Outside (runner / public network — no SSH) | Area.group | Meaning |
|------------|---------|
| ID (order) | Check | | **versions.outside-** | deb.taler.net suite index |
|------------|--------| | **versions.inside-** | packages in containers |
| versions-… | DNS `deb.taler.net` | | **versions.compare-** | installed vs suite |
| versions-… | HTTPS portal + apt base URL | | **sanity.bank-** / **.exchange-** / **.merchant-** | public + optional server-side per component |
| versions-… | suite `InRelease` / `Release` | | **ladder.plan-** / **.load-** / **.withdraw-** / **.pay-** / **.report-** | GOA amount ladder |
| versions-… | suite `Packages` + `Packages.gz` | | **server-** | SSH host ports / processes (flat unless grouped later) |
| versions-… | sample pool `.deb` fetchable (Range 200/206) |
| versions-… | suite offers `taler-exchange`, `taler-merchant`, `libeufin-bank` |
| versions-… | optional `trixie-testing` Packages |
| versions-… | TLS verify (soft) |
### Inside (SSH koopa containers)
| ID | Check |
|----|--------|
| versions-… | ssh koopa |
| versions-… | each container → `InRelease` (pasta can reach apt repo) |
| versions-… | each container lists `deb.taler.net` in apt sources |
| versions-… | each installed `taler*` / `libeufin*` / `libtaler*` / `libdonau*` vs suite version |
| versions-… | core packages installed (`taler-exchange`, `libeufin-bank`, `taler-merchant`) |
Outside always runs. Inside skipped with `SKIP_SSH=1` (still reports outside results).
Compare rules:
- **match** suite → OK
- **ahead** of suite (often testing/dev) → INFO
- **behind** suite → ERROR for core packages, WARN otherwise (`TALER_PKG_BEHIND=error` forces ERROR)
Default suite: **trixie** (`TALER_APT_SUITE`, `TALER_APT_BASE=https://deb.taler.net/apt/debian`).
Without SSH (`SKIP_SSH=1` or remote domain): still runs outside-in repo checks; skips container install compare.
--- ---
## server — SSH ports (`./taler-monitoring.sh server`) ## Visual format (colour on)
| ID | Check |
|----|--------|
| server-001 | ssh |
| server-002+ | containers, local pasta ports, processes, caddy |
---
## e2e — payment path (`./taler-monitoring.sh e2e`)
Local GOA also: **ATM includes GOA:4200**, then **paivana** (HTTP 302 on `PAIVANA_PUBLIC` + public template pay `paivana` / GOA:4200 on `goa-shop`). Disable with `E2E_PAIVANA=0`.
| ID | Step (approx.) |
|----|----------------|
| e2e-001 | budget info |
| e2e-002 | wallet-cli present |
| e2e-003 | mode / currency info |
| e2e-004… | secrets, reachability gates |
| e2e-… | account, credit, withdraw, confirm, coins, order, pay ladder |
| e2e-… | **load snapshots** at e2e-start, after-withdraw, after-pay, after-shop, e2e-end (host loadavg/RAM + container RSS/CPU/DB) |
| e2e-… | **coins** before/after each ATM withdraw, settle, pay, shop: count in circulation / spent, amount + denoms using **`alt_unit_names`** (e.g. `5 Kilo-GOA (GOA:5000)`), Δ vs previous snap |
| e2e-… | **GOA shop products** — full catalog list; **random pick of 2** (override `E2E_SHOP_PICK_N`) |
| e2e-… | balances, dig on failure, load delta + overall metrics |
Shop product pays use instance `goa-shop` (default) and catalog `E2E_SHOP_PRODUCTS`
(`id|Product name|amount` lines). Each e2e run **shuffles** the catalog and pays
only `E2E_SHOP_PICK_N` products (**default 2**). Flow matches the landing popup
(public POST `/templates/{id}`, not private orders). Report labels use product name.
Blockers keep the same ID prefix: `[BLOCKER] e2e-0NN step: message`.
---
## ladder — GOA withdraw ranges (`./taler-monitoring.sh ladder`)
| Step | What |
|------|------|
| ladder-001 | `GET /intro/auto-account.json` (personal account, GOA:0) |
| ladder-… | **load snapshot** before withdraws (host + bank/exchange/merchant mem/CPU) |
| ladder-… | **Phase A withdraw** (23): mint explorer pool — 0 → random → max1 → max into **one cumulative wallet** (mids × `LADDER_WITHDRAW_SCALE`, default 1.5) |
| ladder-… | wallet-cli accept-uri + explorer confirm when `selected`; settle; coins snap |
| ladder-… | **Phase B pay** (23): same shape 0 → random → max1 → max via merchant orders + `handle-uri --yes` |
| ladder-… | **load snapshot** after + delta + overall timing (withdraw + pay) |
| report | withdraw TSV + pay TSV + JSON |
Default amounts (`build_ladder_pair`, strictly increasing):
```text ```text
withdraw: [0] + random mids + [max1] + [max] ╔══════════════════════════════╗
pay: [0] + (withdraw_mid / scale) + [max1] + [max] # always ≤ matching withdraw mid ║ www · public URLs · … ║
╚══════════════════════════════╝
┌ www.exchange ┐
┌ OK ┐ www.exchange-01 exchange /config · HTTP 200
┌ www.bank ┐
┌ OK ┐ www.bank-01 bank /config · HTTP 200
┌ ERROR ┐ www.bank-04 bank /intro/auto-account.json · invalid withdraw
┌ ERRORS · failed checks ┐
• www.bank-04 bank /intro/auto-account.json · invalid withdraw
totals: 40 OK, 1 ERROR, 2 WARN, 5 INFO
``` ```
TSV `range`: `pin:0` | `random` | `pin:max-1` | `pin:max`. `NO_COLOR=1` or `CLICOLOR=0` disables boxes/colours (ASCII `[ OK ]` + `-- group --` headers).
Always use `printf --` friendly plain headers when colour is off (leading `---` is not a printf option).
Soft: absolute **max** mint/pay may `CEILING_REJECT` / skip (WARN). **max-1** is a hard pin.
`LADDER_PAY=0` skips phase B.
| Env | Default | Meaning |
|-----|---------|---------|
| `LADDER_STEPS` | `23` | total rungs (0 + mids + max) |
| `LADDER_MAX_AMOUNT` | `4503599627370496` | absolute last pin |
| `LADDER_TIMEOUT_S` | `3600` | large rungs need time |
| `LADDER_MAX_RUNGS` | `99` | cap list length after build |
```bash
./taler-monitoring.sh ladder
LADDER_STEPS=10 ./taler-monitoring.sh ladder
LADDER_REPORT_DIR=/tmp/my-ladder ./taler-monitoring.sh ladder
```
---
## Run one area
```bash
./taler-monitoring.sh urls # www only
./taler-monitoring.sh inside # inside only
./taler-monitoring.sh versions # deb.taler.net + package drift
./taler-monitoring.sh e2e # e2e only
./taler-monitoring.sh ladder # GOA amount ladder + timings
./taler-monitoring.sh -d taler.net urls
```

View file

@ -23,19 +23,37 @@ e2e_over() { [ "$(e2e_left)" -le 0 ]; }
# stdout only for JSON; logs go to stderr file so balance parse stays clean # stdout only for JSON; logs go to stderr file so balance parse stays clean
wcli_bal_snap() { wcli_bal_snap() {
local out="$1" local out="$1"
if [ -z "${WALLET_CLI:-}" ] || [ ! -f "${WDB:-}" ]; then if [ -z "${WALLET_CLI:-}" ]; then
echo "(no wallet)" >"$out" echo "(no wallet)" >"$out"
return 1 return 1
fi 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" local err="${out}.err"
if command -v perl >/dev/null 2>&1; then 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 \ perl -e 'alarm shift; exec @ARGV' 10 \
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \ node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
>"$out" 2>"$err" || true >"$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 else
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \ node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
>"$out" 2>"$err" || true >"$out" 2>"$err" || true
fi fi
fi
[ -s "$out" ] [ -s "$out" ]
} }
@ -104,9 +122,9 @@ wait_wallet_balance() {
local rounds="${2:-15}" local rounds="${2:-15}"
local sleep_s="${3:-3}" local sleep_s="${3:-3}"
local r av local r av
info "wallet settle wait" "up to ${rounds}×${sleep_s}s for avail>${min_n} ${CUR} (wirewatch lag is common)" # 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)"
for r in $(seq 1 "$rounds"); do 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 wcli_bal_snap "$SCRATCH/bal-live.out" || wcli balance >"$SCRATCH/bal-live.out" 2>&1 || true
av=$(wallet_avail_num "$SCRATCH/bal-live.out") 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 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
@ -182,6 +200,7 @@ e2e_skip_rest() {
# Area e2e-### — withdraw + pay cycle # Area e2e-### — withdraw + pay cycle
set_area e2e set_area e2e
set_group prereq
section "e2e · prerequisites" section "e2e · prerequisites"
info "e2e budget" "${E2E_TIMEOUT}s (set E2E_TIMEOUT= to change)" info "e2e budget" "${E2E_TIMEOUT}s (set E2E_TIMEOUT= to change)"
@ -220,12 +239,22 @@ except Exception: print("")' 2>/dev/null || true)
if [ "$E2E_REMOTE" = "1" ]; then if [ "$E2E_REMOTE" = "1" ]; then
# remote: still ATM-shaped but smaller notes; keep cheap # remote: still ATM-shaped but smaller notes; keep cheap
: "${E2E_WITHDRAW_VALUES:=10 20 50}" : "${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}" : "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}"
fi
else else
# local GOA: classic ATM notes + 4200 for paivana paywall template # local GOA: classic ATM notes + 4200 for paivana paywall template
: "${E2E_WITHDRAW_VALUES:=20 50 100 200 4200}" : "${E2E_WITHDRAW_VALUES:=20 50 100 200 4200}"
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 0.5 1 2 5 10}" : "${E2E_PAY_VALUES:=0.01 0.05 0.1 0.5 1 2 5 10}"
fi 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 CUR:amount lists
build_amt_list() { build_amt_list() {
@ -299,12 +328,27 @@ BANK_HOST=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$BA
SCRATCH=$(mktemp -d) SCRATCH=$(mktemp -d)
WDB="$SCRATCH/wallet.sqlite3" 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}" METRICS_DIR="${METRICS_DIR:-$SCRATCH/metrics}"
mkdir -p "$METRICS_DIR" mkdir -p "$METRICS_DIR"
export METRICS_DIR CUR="${CUR:-${EXPECT_CURRENCY:-GOA}}" WDB 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" 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) # Always dump balances on any exit (timeout, blocker, signal, success)
trap 'ec=$?; e2e_finish "$ec"; rm -rf "$SCRATCH"; exit "$ec"' EXIT trap 'ec=$?; e2e_finish "$ec"; stop_wallet_serve; rm -rf "$SCRATCH"; exit "$ec"' EXIT
# Abort cleanly on login / KYC / registration barriers (esp. remote domains) # Abort cleanly on login / KYC / registration barriers (esp. remote domains)
e2e_abort_auth() { e2e_abort_auth() {
@ -382,10 +426,12 @@ else
fi fi
# Host + bank/exchange/merchant RAM/CPU/load (SSH koopa or koopa-external) # Host + bank/exchange/merchant RAM/CPU/load (SSH koopa or koopa-external)
set_group load
section "e2e · load snapshot (before withdraw/pay)" section "e2e · load snapshot (before withdraw/pay)"
metrics_report_load "${METRICS_DIR}/load-before.json" "e2e-start" || true 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). # Local stack: koopa secrets. Remote: only explicit env (never leak local passwords to foreign banks).
set_group prereq
if [ "$E2E_REMOTE" = "1" ]; then if [ "$E2E_REMOTE" = "1" ]; then
ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-}" ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-}"
MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}" MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}"
@ -402,13 +448,29 @@ else
exit 1 exit 1
fi fi
if [ -n "$MPW" ]; then 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})" ok "merchant secret (instance ${MERCHANT_INSTANCE})"
elif [ "$E2E_REMOTE" = "1" ]; then elif [ "$E2E_REMOTE" = "1" ]; then
warn "merchant secret" "missing — order create may fail (set E2E_MERCHANT_TOKEN)" MPW_TOKEN=""
warn "merchant secret" "missing — private orders fail unless E2E_USE_TEMPLATES=1 (set E2E_MERCHANT_TOKEN)"
else else
blocker "prereq" "merchant instance password missing" blocker "prereq" "merchant instance password missing"
exit 1 exit 1
fi 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) # Public reachability gates (remote: soft-skip if bank/merchant absent)
for pair in \ for pair in \
@ -452,7 +514,31 @@ info "e2e user" "$USER withdraw=$WITHDRAW_AMT pay=$PAY_AMT"
BANK="$BANK_PUBLIC" BANK="$BANK_PUBLIC"
INST="$MERCHANT_INSTANCE" 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 # $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() { wcli() {
local maxc=12 local maxc=12
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
@ -464,12 +550,21 @@ wcli() {
cap=$(e2e_left) cap=$(e2e_left)
[ "$cap" -gt "$maxc" ] && cap=$maxc [ "$cap" -gt "$maxc" ] && cap=$maxc
[ "$cap" -lt 3 ] && return 124 [ "$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 if command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$cap" \ perl -e 'alarm shift; exec @ARGV' "$cap" \
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
else else
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
fi fi
fi
} }
# Pay must not be starved: use reserved seconds even if global budget is tight # Pay must not be starved: use reserved seconds even if global budget is tight
@ -486,16 +581,26 @@ wcli_pay() {
cap=12 cap=12
fi fi
info "pay wallet cap" "${cap}s" 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 if command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$cap" \ perl -e 'alarm shift; exec @ARGV' "$cap" \
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
else else
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
fi fi
fi
} }
# When coins never become available: run inside + targeted bank/exchange dig # When coins never become available: run inside + targeted bank/exchange dig
dig_when_no_coins() { dig_when_no_coins() {
set_group dig
section "e2e · coins missing — inside dig" section "e2e · coins missing — inside dig"
info "reason" "wallet empty after withdraw path (often wirewatch timing) — probing bank + exchange" info "reason" "wallet empty after withdraw path (often wirewatch timing) — probing bank + exchange"
if [ -n "${WID:-}" ]; then if [ -n "${WID:-}" ]; then
@ -580,6 +685,7 @@ BAL_BEFORE=$(fmt_bal "$SCRATCH/bal-before.out")
info "BALANCE before" "$BAL_BEFORE" info "BALANCE before" "$BAL_BEFORE"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group bank
section "e2e · bank (account · credit · withdraw)" section "e2e · bank (account · credit · withdraw)"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
AT="" AT=""
@ -678,8 +784,11 @@ else
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group wallet
section "e2e · wallet setup (exchange + ToS once)" 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 ! wcli exchanges add "${EXCHANGE_PUBLIC}/" >"$SCRATCH/ex-add.out" 2>&1; then
if ! grep -qiE 'already|ok' "$SCRATCH/ex-add.out"; 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' ' ')" blocker "wallet-exchange" "exchanges add failed — $(tail -c 160 "$SCRATCH/ex-add.out" | tr '\n' ' ')"
@ -774,12 +883,13 @@ e2e_one_withdraw() {
fi fi
cp "$SCRATCH/accept-$tag.out" "$SCRATCH/accept.out" cp "$SCRATCH/accept-$tag.out" "$SCRATCH/accept.out"
# Poll bank withdrawal status only (no run-until-done). force-select if stuck pending.
st="" st=""
for i in 1 2 3 4; do for i in 1 2 3 4 5 6 8 10; do
e2e_over && break e2e_over && break
wcli run-until-done >"$SCRATCH/sel-$tag-$i.out" 2>&1 || true
st=$(wd_status) st=$(wd_status)
case "$st" in selected|confirmed|aborted) break ;; esac case "$st" in selected|confirmed|aborted) break ;; esac
sleep 0.4
done done
if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then
wcli transactions >"$SCRATCH/tx-pre.json" 2>&1 || true wcli transactions >"$SCRATCH/tx-pre.json" 2>&1 || true
@ -824,10 +934,9 @@ else:
fi fi
fake_incoming_speedup fake_incoming_speedup
# Short per-ATM poll; full settle wait happens after the ladder (avoids false FAIL) # Short per-ATM settle: poll balance + bank transfer_done only (never run-until-done)
local ok_bal=0 r av local ok_bal=0 r av xfer
for r in 1 2 3 4 5 6; do for r in 1 2 3 4 5 6 8 10; 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 wcli_bal_snap "$SCRATCH/bal-$tag.out" || wcli balance >"$SCRATCH/bal-$tag.out" 2>&1 || true
av=$(wallet_avail_num "$SCRATCH/bal-$tag.out") 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 if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av" 2>/dev/null; then
@ -836,6 +945,13 @@ else:
info "balance" "$(fmt_bal "$SCRATCH/bal-$tag.out")" info "balance" "$(fmt_bal "$SCRATCH/bal-$tag.out")"
break break
fi 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 sleep 2
done done
metrics_report_coins "after-ATM-${tag}" || true metrics_report_coins "after-ATM-${tag}" || true
@ -853,8 +969,28 @@ else:
return 1 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] # One payment: e2e_one_pay AMOUNT [SUMMARY] [TAG]
# SUMMARY defaults to "monitoring pay $AMOUNT"; TAG defaults from amount. # 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() { e2e_one_pay() {
PAY_AMT="$1" PAY_AMT="$1"
local PAY_SUM="${2:-monitoring pay ${PAY_AMT}}" local PAY_SUM="${2:-monitoring pay ${PAY_AMT}}"
@ -867,11 +1003,28 @@ e2e_one_pay() {
section "e2e · pay $PAY_AMT · $(format_amount_alt "$PAY_AMT") · $PAY_SUM" 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; } e2e_over && { warn "pay" "budget exhausted — skip $PAY_AMT ($PAY_SUM)"; return 1; }
metrics_report_coins "before-pay-${tag}" || true metrics_report_coins "before-pay-${tag}" || true
if [ -z "${MPW:-}" ]; then
# 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
warn "pay $PAY_AMT" "no merchant token" warn "pay $PAY_AMT" "no merchant token"
return 1 return 1
fi fi
AUTH="Authorization: Bearer secret-token:${MPW}" local AUTH
AUTH=$(merchant_auth_header) || {
warn "pay $PAY_AMT" "no merchant token"
return 1
}
# JSON-escape summary for curl -d # JSON-escape summary for curl -d
local SUM_JSON 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") SUM_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$PAY_SUM" 2>/dev/null || printf '"%s"' "$PAY_SUM")
@ -881,8 +1034,9 @@ e2e_one_pay() {
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders" 2>"$SCRATCH/ord-$tag.err" || true "${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 ! grep -q order_id "$SCRATCH/ord-$tag.json" 2>/dev/null; then
if [ "$E2E_REMOTE" != "1" ] && koopa_ssh_ok; then if [ "$E2E_REMOTE" != "1" ] && koopa_ssh_ok; then
local mpw_raw="${MPW_TOKEN:-secret-token:${MPW}}"
koopa_ssh_run 20 \ koopa_ssh_run 20 \
"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'" \ "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'" \
>"$SCRATCH/ord-$tag.json" 2>"$SCRATCH/ord-$tag.err" || true >"$SCRATCH/ord-$tag.json" 2>"$SCRATCH/ord-$tag.err" || true
fi fi
fi fi
@ -926,23 +1080,37 @@ except Exception: print("")
return 1 return 1
fi fi
ok "wallet handle pay $PAY_AMT ($PAY_SUM)" ok "wallet handle pay $PAY_AMT ($PAY_SUM)"
# Settle: poll merchant order + wallet transactions only (never run-until-done)
local r local r
for r in 1 2 3; do for r in 1 2 3 4 5 6; do
wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true
wcli_pay transactions >"$SCRATCH/tx-$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" \ curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true "${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
if 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 \ || grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null \
|| python3 -c 'import json,sys || python3 -c 'import json,sys
d=json.load(open(sys.argv[1])) for p in sys.argv[1:]:
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1) try:
' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then 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
ok "payment settled $PAY_AMT ($PAY_SUM · order $OID)" ok "payment settled $PAY_AMT ($PAY_SUM · order $OID)"
metrics_report_coins "after-pay-${tag}" || true metrics_report_coins "after-pay-${tag}" || true
metrics_record_flow spent "$PAY_AMT" || true metrics_record_flow spent "$PAY_AMT" || true
return 0 return 0
fi fi
sleep 1
done done
metrics_report_coins "after-pay-fail-${tag}" || true metrics_report_coins "after-pay-fail-${tag}" || true
warn "pay $PAY_AMT ($PAY_SUM)" "not settled for order $OID" warn "pay $PAY_AMT ($PAY_SUM)" "not settled for order $OID"
@ -1045,33 +1213,34 @@ except Exception: print("")
fi fi
ok "shop $pname" "wallet accepted pay URI" ok "shop $pname" "wallet accepted pay URI"
# Settle: public order status + transactions only (never run-until-done)
local r AUTH="" local r AUTH=""
if [ -n "${MPW:-}" ]; then AUTH=$(merchant_auth_header 2>/dev/null) || AUTH=""
AUTH="Authorization: Bearer secret-token:${MPW}" for r in 1 2 3 4 5 6; do
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 wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true
if [ -n "$AUTH" ]; then if [ -n "$AUTH" ]; then
curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \ curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true "${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
fi
curl -skS -m 8 -o "$SCRATCH/ord-pub-$tag.json" "$statusUrl" 2>/dev/null || true
if python3 -c 'import json,sys if python3 -c 'import json,sys
d=json.load(open(sys.argv[1])) for p in sys.argv[1:]:
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1) try:
' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then 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 \
|| 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 ($pamt · order $OID)"
metrics_report_coins "after-shop-${tag}" || true metrics_report_coins "after-shop-${tag}" || true
metrics_record_flow spent "$pamt" || true metrics_record_flow spent "$pamt" || true
return 0 return 0
fi fi
fi sleep 1
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 via wallet tx ($pamt · order $OID)"
metrics_report_coins "after-shop-${tag}" || true
metrics_record_flow spent "$pamt" || true
return 0
fi
done done
metrics_report_coins "after-shop-fail-${tag}" || true metrics_report_coins "after-shop-fail-${tag}" || true
warn "shop $pname ($pid)" "not settled for order $OID" warn "shop $pname ($pid)" "not settled for order $OID"
@ -1079,6 +1248,7 @@ sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="p
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group atm
section "e2e · ATM withdraw ladder" section "e2e · ATM withdraw ladder"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
WITHDRAW_OK=0 WITHDRAW_OK=0
@ -1109,12 +1279,14 @@ for WITHDRAW_AMT in $WITHDRAW_LIST; do
esac esac
done done
info "ATM withdraw summary" "$WITHDRAW_REPORT (ok=$WITHDRAW_OK_N lag=$WITHDRAW_LAG_N fail=$WITHDRAW_FAIL_N)" 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)" section "e2e · load snapshot (after ATM withdraws)"
metrics_report_load "${METRICS_DIR}/load-after-withdraw.json" "after-withdraw" || true 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 cp -f "${METRICS_DIR}/load-after-withdraw.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true
metrics_report_coins "after-ATM-ladder" || true metrics_report_coins "after-ATM-ladder" || true
# Settlement catch-up: bank may have confirmed while wallet was still empty # Settlement catch-up: bank may have confirmed while wallet was still empty
set_group settle
section "e2e · wallet settlement (timing)" section "e2e · wallet settlement (timing)"
if ! wait_wallet_balance 0 "${E2E_SETTLE_ROUNDS}" "${E2E_SETTLE_SLEEP}"; then if ! wait_wallet_balance 0 "${E2E_SETTLE_ROUNDS}" "${E2E_SETTLE_SLEEP}"; then
if [ "$WITHDRAW_LAG_N" -gt 0 ] || [ "$WITHDRAW_OK_N" -gt 0 ]; then if [ "$WITHDRAW_LAG_N" -gt 0 ] || [ "$WITHDRAW_OK_N" -gt 0 ]; then
@ -1154,6 +1326,7 @@ else
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group pay
section "e2e · variable payments (if balance allows)" section "e2e · variable payments (if balance allows)"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
PAY_OK=0 PAY_OK=0
@ -1161,13 +1334,16 @@ PAY_OK_N=0
PAY_FAIL_N=0 PAY_FAIL_N=0
PAY_SKIP_N=0 PAY_SKIP_N=0
PAY_REPORT="" PAY_REPORT=""
if [ -z "${MPW:-}" ]; then if [ -z "${MPW:-}" ] && [ "${E2E_USE_TEMPLATES:-0}" != "1" ]; then
if [ "$E2E_REMOTE" = "1" ]; then if [ "$E2E_REMOTE" = "1" ]; then
e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN" e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN (or E2E_USE_TEMPLATES=1)"
fi fi
blocker "merchant-order" "no merchant token" blocker "merchant-order" "no merchant token"
exit 1 exit 1
fi 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 for PAY_AMT in $PAY_LIST; do
e2e_over && { warn "pay ladder" "time budget low — stopping more payments"; break; } e2e_over && { warn "pay ladder" "time budget low — stopping more payments"; break; }
@ -1193,6 +1369,7 @@ for PAY_AMT in $PAY_LIST; do
fi fi
done done
info "pay summary" "$PAY_REPORT (ok=$PAY_OK_N fail=$PAY_FAIL_N skip=$PAY_SKIP_N)" 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)" section "e2e · load snapshot (after payments)"
metrics_report_load "${METRICS_DIR}/load-after-pay.json" "after-pay" || true 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 cp -f "${METRICS_DIR}/load-after-pay.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true
@ -1207,18 +1384,47 @@ if [ "$PAY_OK" != "1" ]; then
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
section "e2e · GOA shop products (merchant landing catalog)" set_group shop
section "e2e · shop products (public templates)"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Public template POST + wallet pay — full catalog list, random pick of N products. # Public template POST + wallet pay — catalog random pick of N products.
# GOA: goa-shop on hacktivism. TESTPAYSAN: farmer shops (jardin / fermes).
SHOP_OK=0 SHOP_OK=0
SHOP_OK_N=0 SHOP_OK_N=0
SHOP_FAIL_N=0 SHOP_FAIL_N=0
SHOP_SKIP_N=0 SHOP_SKIP_N=0
SHOP_REPORT="" 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 if [ "$E2E_REMOTE" = "1" ] || [ "${CUR:-}" != "GOA" ]; then
info "goa-shop" "SKIPPED (remote or non-GOA currency)" E2E_SHOP_ENABLE=0
else else
# Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount lines) 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)"
else
# Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount[|instance] lines)
SHOP_CATALOG=() SHOP_CATALOG=()
while IFS= read -r line; do while IFS= read -r line; do
[ -z "$line" ] && continue [ -z "$line" ] && continue
@ -1232,7 +1438,7 @@ EOF
case "$PICK_N" in ''|*[!0-9]*) PICK_N=2 ;; esac case "$PICK_N" in ''|*[!0-9]*) PICK_N=2 ;; esac
if [ "$PICK_N" -lt 1 ]; then PICK_N=1; fi if [ "$PICK_N" -lt 1 ]; then PICK_N=1; fi
if [ "$SHOP_CAT_N" -eq 0 ]; then if [ "$SHOP_CAT_N" -eq 0 ]; then
warn "goa-shop" "empty E2E_SHOP_PRODUCTS catalog" warn "shop" "empty E2E_SHOP_PRODUCTS catalog"
else else
if [ "$PICK_N" -gt "$SHOP_CAT_N" ]; then PICK_N=$SHOP_CAT_N; fi if [ "$PICK_N" -gt "$SHOP_CAT_N" ]; then PICK_N=$SHOP_CAT_N; fi
# Shuffle catalog, take first PICK_N (portable: python) # Shuffle catalog, take first PICK_N (portable: python)
@ -1245,24 +1451,26 @@ random.shuffle(lines)
print('\n'.join(lines[:n])) print('\n'.join(lines[:n]))
" "$PICK_N" " "$PICK_N"
) )
info "goa-shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · instance ${E2E_SHOP_INSTANCE}" info "shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · default instance ${E2E_SHOP_INSTANCE}"
info "goa-shop payto" "$E2E_SHOP_PAYTO" info "shop payto" "$E2E_SHOP_PAYTO"
info "goa-shop pick" "$(printf '%s\n' "$SHOP_PICKED" | cut -d'|' -f1,2 | tr '\n' '; ' | sed 's/; $//')" info "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_one_pay_public_template uses global INST — pin per product if 4th field set
_E2E_INST_SAVE="$INST" _E2E_INST_SAVE="$INST"
INST="${E2E_SHOP_INSTANCE}" INST="${E2E_SHOP_INSTANCE}"
while IFS= read -r line; do while IFS= read -r line; do
[ -z "$line" ] && continue [ -z "$line" ] && continue
e2e_over && { warn "goa-shop" "time budget low — stopping product pays"; break; } e2e_over && { warn "shop" "time budget low — stopping product pays"; break; }
pid=$(printf '%s' "$line" | cut -d'|' -f1) pid=$(printf '%s' "$line" | cut -d'|' -f1)
pname=$(printf '%s' "$line" | cut -d'|' -f2) pname=$(printf '%s' "$line" | cut -d'|' -f2)
pamt=$(printf '%s' "$line" | cut -d'|' -f3) pamt=$(printf '%s' "$line" | cut -d'|' -f3)
pinst=$(printf '%s' "$line" | cut -d'|' -f4)
[ -z "$pid" ] || [ -z "$pamt" ] && continue [ -z "$pid" ] || [ -z "$pamt" ] && continue
[ -z "$pname" ] && pname="$pid" [ -z "$pname" ] && pname="$pid"
INST="${pinst:-${E2E_SHOP_INSTANCE}}"
av_now=$(wallet_avail_num) 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) 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 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 "goa-shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})" warn "shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})"
SHOP_SKIP_N=$((SHOP_SKIP_N + 1)) SHOP_SKIP_N=$((SHOP_SKIP_N + 1))
SHOP_REPORT="${SHOP_REPORT}${SHOP_REPORT:+ }${pname}=SKIP(bal)" SHOP_REPORT="${SHOP_REPORT}${SHOP_REPORT:+ }${pname}=SKIP(bal)"
continue continue
@ -1286,9 +1494,9 @@ $SHOP_PICKED
EOF EOF
INST="$_E2E_INST_SAVE" INST="$_E2E_INST_SAVE"
unset _E2E_INST_SAVE unset _E2E_INST_SAVE
info "goa-shop summary" "$SHOP_REPORT (ok=$SHOP_OK_N fail=$SHOP_FAIL_N skip=$SHOP_SKIP_N · pick $PICK_N/$SHOP_CAT_N)" info "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 if [ "$SHOP_OK" != "1" ] && [ "$SHOP_FAIL_N" -gt 0 ]; then
warn "goa-shop" "no sample product payment succeeded ($SHOP_REPORT)" warn "shop" "no sample product payment succeeded ($SHOP_REPORT)"
fi fi
section "e2e · load snapshot (after shop pays)" section "e2e · load snapshot (after shop pays)"
metrics_report_load "${METRICS_DIR}/load-after-shop.json" "after-shop" || true metrics_report_load "${METRICS_DIR}/load-after-shop.json" "after-shop" || true
@ -1297,6 +1505,7 @@ EOF
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group paivana
section "e2e · paivana paywall (template GOA:4200)" section "e2e · paivana paywall (template GOA:4200)"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Public paywall: https://paivana.hacktivism.ch → 302 to merchant template "paivana". # Public paywall: https://paivana.hacktivism.ch → 302 to merchant template "paivana".
@ -1388,11 +1597,12 @@ else
info "paivana summary" "${PAIVANA_REPORT:-?} · instance ${E2E_PAIVANA_INSTANCE}" info "paivana summary" "${PAIVANA_REPORT:-?} · instance ${E2E_PAIVANA_INSTANCE}"
fi fi
set_group report
section "e2e · report" section "e2e · report"
info "user" "$USER" info "user" "$USER"
info "ATM withdraws" "$WITHDRAW_REPORT" info "ATM withdraws" "$WITHDRAW_REPORT"
info "payments" "$PAY_REPORT" info "payments" "$PAY_REPORT"
info "goa-shop" "${SHOP_REPORT:-(n/a)}" info "shop" "${SHOP_REPORT:-(n/a)}"
info "paivana" "${PAIVANA_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 "final balance" "$(fmt_bal "$SCRATCH/bal-live.out" 2>/dev/null || echo "(n/a)")"
info "scratch" "$SCRATCH" info "scratch" "$SCRATCH"

View file

@ -29,7 +29,10 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib.sh # shellcheck source=lib.sh
source "$ROOT/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_area ladder
set_group plan
SECTION_T0=$(date +%s) SECTION_T0=$(date +%s)
now_ms() { python3 -c 'import time; print(int(time.time()*1000))'; } now_ms() { python3 -c 'import time; print(int(time.time()*1000))'; }
elapsed_ms() { elapsed_ms() {
@ -261,6 +264,7 @@ info "steps" "${LADDER_STEPS} (0 + random≥${LADDER_MIN_AMOUNT} + max-1=${CUR}:
info "withdraw_scale" "${LADDER_WITHDRAW_SCALE}× pay mids (fund pay ladder)" info "withdraw_scale" "${LADDER_WITHDRAW_SCALE}× pay mids (fund pay ladder)"
info "pay_phase" "$([ "${LADDER_PAY}" = "1" ] && echo enabled || echo disabled)" info "pay_phase" "$([ "${LADDER_PAY}" = "1" ] && echo enabled || echo disabled)"
set_group load
section "ladder · load snapshot (before withdraws)" section "ladder · load snapshot (before withdraws)"
metrics_report_load "$LOAD_BEFORE" "ladder-start" || true metrics_report_load "$LOAD_BEFORE" "ladder-start" || true
# Fresh wallet per rung — baseline empty (or last-rung DB if re-used later) # Fresh wallet per rung — baseline empty (or last-rung DB if re-used later)
@ -344,6 +348,7 @@ STOP_REASON=""
STOP_AMOUNT="" STOP_AMOUNT=""
declare -a RUNG_JSON=() declare -a RUNG_JSON=()
set_group withdraw
section "ladder · phase A · withdraw (${LADDER_N} rungs)" section "ladder · phase A · withdraw (${LADDER_N} rungs)"
rung=0 rung=0
for AMT in "$@"; do for AMT in "$@"; do
@ -658,12 +663,7 @@ else:
fi fi
} }
# Short bounded select assist (≤8s) — not a hang path; helps attach fresh reserve_pub # No run-until-done (hangs / banned). Read transactions only for reserve_pub candidates.
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 wcli transactions >"$SCRATCH/tx-$tag.json" 2>&1 || true
t0=$(now_ms) t0=$(now_ms)
@ -787,6 +787,7 @@ done
PAY_OK_N=0 PAY_OK_N=0
PAY_FAIL_N=0 PAY_FAIL_N=0
if [ "${LADDER_PAY}" = "1" ] && [ -n "${PAY_LIST:-}" ] && [ "$FAIL_N_L" -eq 0 ]; then if [ "${LADDER_PAY}" = "1" ] && [ -n "${PAY_LIST:-}" ] && [ "$FAIL_N_L" -eq 0 ]; then
set_group pay
section "ladder · phase B · pay" section "ladder · phase B · pay"
metrics_report_coins "before-pay-ladder" || true metrics_report_coins "before-pay-ladder" || true
# Merchant secret (same as e2e) # Merchant secret (same as e2e)
@ -959,19 +960,12 @@ except Exception: print("")
ms_handle=$(elapsed_ms "$t0") ms_handle=$(elapsed_ms "$t0")
ok "handle-uri $PAMT ${ms_handle}ms" ok "handle-uri $PAMT ${ms_handle}ms"
# Short settle polls (bounded — no infinite hang) # Short settle polls: merchant order + wallet tx only — never run-until-done
t0=$(now_ms) t0=$(now_ms)
settled=0 settled=0
r=0 r=0
while [ "$r" -lt "${LADDER_PAY_SETTLE_ROUNDS}" ]; do while [ "$r" -lt "${LADDER_PAY_SETTLE_ROUNDS}" ]; do
r=$((r + 1)) 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 wcli transactions >"$SCRATCH/tx-$ptag.out" 2>&1 || true
curl -skS -m 8 -o "$SCRATCH/ord-paid-$ptag.json" -H "$AUTH" \ curl -skS -m 8 -o "$SCRATCH/ord-paid-$ptag.json" -H "$AUTH" \
"${MER}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true "${MER}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
@ -983,6 +977,7 @@ sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="p
settled=1 settled=1
break break
fi fi
sleep 0.5
done done
ms_psettle=$(elapsed_ms "$t0") ms_psettle=$(elapsed_ms "$t0")
ms_total=$(elapsed_ms "$t_pay") ms_total=$(elapsed_ms "$t_pay")
@ -1024,6 +1019,7 @@ fi
ms_phase=$(python3 -c 'import sys,time; print(int((time.time()-float(sys.argv[1]))*1000))' "$SECTION_T0") ms_phase=$(python3 -c 'import sys,time; print(int((time.time()-float(sys.argv[1]))*1000))' "$SECTION_T0")
# --- report --- # --- report ---
set_group report
section "ladder · report" section "ladder · report"
info "auto-account" "$ACCT_USER" info "auto-account" "$ACCT_USER"
info "ok_rungs" "$OK_N" info "ok_rungs" "$OK_N"
@ -1128,6 +1124,7 @@ PY
wcli balance 2>&1 | tee "$REPORT_DIR/balance-final.out" | tail -20 || true wcli balance 2>&1 | tee "$REPORT_DIR/balance-final.out" | tail -20 || true
set_group load
section "ladder · load snapshot (after withdraws)" section "ladder · load snapshot (after withdraws)"
metrics_report_load "$LOAD_AFTER" "ladder-end" || true metrics_report_load "$LOAD_AFTER" "ladder-end" || true
if [ -f "$LOAD_BEFORE" ] && [ -f "$LOAD_AFTER" ]; then if [ -f "$LOAD_BEFORE" ] && [ -f "$LOAD_AFTER" ]; then

View file

@ -7,8 +7,10 @@ source "$ROOT/lib.sh"
# shellcheck source=metrics.sh # shellcheck source=metrics.sh
source "$ROOT/metrics.sh" source "$ROOT/metrics.sh"
# Area inside-### — container / process state on koopa (SSH) # Area inside.* — container / process state on koopa (SSH)
# Groups: inside.ssh / inside.bank / inside.exchange / inside.merchant / inside.caddy
set_area inside set_area inside
set_group ssh
section "inside · collect from koopa" section "inside · collect from koopa"
if [ "${SKIP_SSH}" = "1" ]; then if [ "${SKIP_SSH}" = "1" ]; then
@ -128,10 +130,20 @@ if [ -z "$RAW" ] || ! echo "$RAW" | grep -q '^E|'; then
exit 1 exit 1
fi fi
_last_inside_grp=""
while IFS= read -r line; do while IFS= read -r line; do
case "$line" in case "$line" in
E\|*) E\|*)
IFS='|' read -r _ comp level key detail <<<"$line" 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 case "$level" in
OK) ok "[$comp] $key${detail:+ ($detail)}" ;; OK) ok "[$comp] $key${detail:+ ($detail)}" ;;
ERROR) err "$comp" "$key" "$detail" ;; ERROR) err "$comp" "$key" "$detail" ;;
@ -143,6 +155,7 @@ while IFS= read -r line; do
done <<<"$RAW" done <<<"$RAW"
# Host loadavg + RAM + per-container RSS/CPU (same probe as e2e/ladder) # Host loadavg + RAM + per-container RSS/CPU (same probe as e2e/ladder)
set_group load
section "inside · load / memory" section "inside · load / memory"
METRICS_DIR="${METRICS_DIR:-$(mktemp -d)}" METRICS_DIR="${METRICS_DIR:-$(mktemp -d)}"
export METRICS_DIR export METRICS_DIR

View file

@ -23,10 +23,12 @@ json_currency() {
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("currency") or "")' "$1" 2>/dev/null || true 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 # Area sanity.* — public + optional server-side per component
# Groups: sanity.bank / sanity.exchange / sanity.merchant
set_area sanity set_area sanity
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group bank
section "sanity · bank" section "sanity · bank"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
expect_code "bank public /config" 200 "$BANK_PUBLIC/config" expect_code "bank public /config" 200 "$BANK_PUBLIC/config"
@ -98,6 +100,7 @@ else
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group exchange
section "sanity · exchange" section "sanity · exchange"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
expect_code "exchange public /config" 200 "$EXCHANGE_PUBLIC/config" expect_code "exchange public /config" 200 "$EXCHANGE_PUBLIC/config"
@ -193,6 +196,7 @@ else
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group merchant
section "sanity · merchant" section "sanity · merchant"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
expect_code "merchant public /config" 200 "$MERCHANT_PUBLIC/config" expect_code "merchant public /config" 200 "$MERCHANT_PUBLIC/config"

View file

@ -5,8 +5,9 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib.sh # shellcheck source=lib.sh
source "$ROOT/lib.sh" source "$ROOT/lib.sh"
# Area server-### — host/container ports via SSH # Area server.* — host/container ports via SSH
set_area server set_area server
set_group ssh
section "server · ssh ${KOOPA_SSH}" section "server · ssh ${KOOPA_SSH}"
if ! koopa_ssh_ok; then if ! koopa_ssh_ok; then

View file

@ -5,7 +5,9 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib.sh # shellcheck source=lib.sh
source "$ROOT/lib.sh" source "$ROOT/lib.sh"
# Area www-### — public HTTPS (outside-in) # Area www.* — public HTTPS (outside-in)
# Groups: www.exchange / www.perf / www.stats / www.bank / www.merchant /
# www.paivana / www.landing
set_area www set_area www
section "www · public URLs · ${TALER_DOMAIN:-?} (outside-in, no SSH)" section "www · public URLs · ${TALER_DOMAIN:-?} (outside-in, no SSH)"
@ -169,7 +171,8 @@ sys.exit(0)
PY PY
} }
# --- exchange (core; always required) --- www-001 … # --- exchange (core; always required) --- www.exchange-NN
set_group exchange
check_url "exchange /config" 200 "$EXCHANGE_PUBLIC/config" check_url "exchange /config" 200 "$EXCHANGE_PUBLIC/config"
code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ec.json") code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ec.json")
if [ "$code" = "200" ]; then if [ "$code" = "200" ]; then
@ -219,6 +222,7 @@ fi
# Performance — outside-in public HTTPS latency (this runner, not loopback) # Performance — outside-in public HTTPS latency (this runner, not loopback)
# Same spirit as bank landing-stats public probes; measured here from outside. # Same spirit as bank landing-stats public probes; measured here from outside.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group perf
section "www · performance · public HTTPS latency (outside-in)" section "www · performance · public HTTPS latency (outside-in)"
# Latency thresholds (ms), outside-in from this runner. # Latency thresholds (ms), outside-in from this runner.
@ -290,11 +294,18 @@ fi
info "perf note" "RTT measured from this host (outside-in)" info "perf note" "RTT measured from this host (outside-in)"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Landing load stats (stats.json written *inside* containers, usually public). # Landing stats.json (public outside-in) — freshness + display fields.
# Prefer HTTPS /intro/stats.json (no SSH). If missing and SSH works, cat from # Timestamps (required for external measurement):
# the container. Report loadavg + RSS + in-container probe ms — one line each. # 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 515m)
# STATS_FAIL_SECS hard fail if age > this (default 3600); 0 = never hard-fail on age
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
: "${STATS_STALE_SECS:=900}" # warn if generated_at older than 15m : "${STATS_STALE_SECS:=900}"
: "${STATS_FAIL_SECS:=3600}"
_fetch_landing_stats_json() { _fetch_landing_stats_json() {
# $1=name bank|exchange|merchant $2=base URL → writes $tmp/stats-$1.json, exit 0 if ok # $1=name bank|exchange|merchant $2=base URL → writes $tmp/stats-$1.json, exit 0 if ok
@ -322,81 +333,265 @@ _fetch_landing_stats_json() {
return 1 return 1
} }
report_landing_load_stats() { # Validate one stats.json for monitoring display + outside age measurement.
# Prints lines: OK|WARN|ERR|STALE|FAIL <detail>
# Sets $tmp/stats-meta-$name.txt with gen_unix|source for shared-feed compare.
check_landing_stats_json() {
local name="$1" base="$2" local name="$1" base="$2"
local f="$tmp/stats-${name}.json" local f="$tmp/stats-${name}.json"
local line age_s local line hard_missing=0
if ! _fetch_landing_stats_json "$name" "$base"; then if ! _fetch_landing_stats_json "$name" "$base"; then
if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
warn "perf ${name} landing-stats" "stats.json not available (public or SSH)" fail "stats ${name}" "HTTP/body missing · ${base}/intro/stats.json"
else
warn "stats ${name}" "stats.json not available"
fi fi
return return 1
fi 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: try:
d = json.load(open(path)) d = json.load(open(path))
except Exception as e: except Exception as e:
print("ERR parse:" + str(e)[:80]) out("ERR", "JSON parse: %s" % str(e)[:100])
sys.exit(0) sys.exit(0)
if not d.get("ok", True):
print("ERR ok=false " + str(d.get("error") or "")[:80]) if not isinstance(d, dict):
out("ERR", "body is not a JSON object")
sys.exit(0) sys.exit(0)
p = d.get("performance") or {}
if not isinstance(p, dict) or not p: if d.get("ok") is False:
print("ERR no performance block") out("ERR", "ok=false · %s" % str(d.get("error") or d.get("hint") or "")[:80])
sys.exit(0) 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 {} 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 "?" rss = mem.get("container_rss_human") or mem.get("proc_sum_rss_human") or "—"
load = p.get("loadavg") or "?" load = p.get("loadavg") or "—"
# latency fields differ by site
bits = [] bits = []
for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"): for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"):
if k in p and p[k] is not None: if k in p and p[k] is not None:
bits.append("%s=%sms" % (k.replace("_ms", ""), p[k])) bits.append("%s=%s" % (k.replace("_ms", ""), p[k]))
lat = " ".join(bits) if bits else "latency=?" lat = " ".join(bits) if bits else "latency=—"
gen = d.get("generated_at_human") or d.get("generated_at") or "?" out("OK", "performance loadavg=%s RSS=%s %s" % (load, rss, lat))
# staleness else:
age = "" out("OK", "performance block optional/absent")
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 PY
) )
case "$line" in return 0
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 if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
section "www · performance · landing load stats (stats.json · in-container probes)" set_group stats
report_landing_load_stats "bank" "$BANK_PUBLIC" section "www · landing stats.json (freshness + display fields · outside-in)"
report_landing_load_stats "exchange" "$EXCHANGE_PUBLIC" info "stats policy" "STALE≥${STATS_STALE_SECS}s WARN · FAIL≥${STATS_FAIL_SECS}s ERROR (0=disable fail) · need generated_at_unix"
report_landing_load_stats "merchant" "$MERCHANT_PUBLIC" check_landing_stats_json "bank" "$BANK_PUBLIC" || true
info "perf landing-stats note" "from /intro/stats.json (public); SSH container fallback if needed" 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"
fi fi
# Rollup + optional JSON for metrics_print_overall # Rollup + optional JSON for metrics_print_overall
if [ -s "$PERF_TSV" ]; then if [ -s "$PERF_TSV" ]; then
@ -470,7 +665,8 @@ fi
info "perf note" "measured from this host (outside-in); thresholds PERF_WARN_MS=${PERF_WARN_MS} PERF_FAIL_MS=${PERF_FAIL_MS}" info "perf note" "measured from this host (outside-in); thresholds PERF_WARN_MS=${PERF_WARN_MS} PERF_FAIL_MS=${PERF_FAIL_MS}"
# Terms + privacy (legal docs) # Exchange terms + privacy (same www.exchange group — issues map to exchange)
set_group exchange
check_legal_doc "exchange /terms" "$EXCHANGE_PUBLIC/terms" "terms|GOA|exploration|FADP|revDSG|privacy" 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" check_legal_doc "exchange /privacy" "$EXCHANGE_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|exploration"
# trailing slash: 200 or redirect to bare path # trailing slash: 200 or redirect to bare path
@ -485,6 +681,7 @@ case "$code" in
esac esac
# --- bank --- # --- bank ---
set_group bank
if [ "${LOCAL_STACK:-1}" = "0" ]; then if [ "${LOCAL_STACK:-1}" = "0" ]; then
check_url_soft "bank /config" 200 "$BANK_PUBLIC/config" check_url_soft "bank /config" 200 "$BANK_PUBLIC/config"
else else
@ -508,7 +705,7 @@ if [ "$code" = "200" ]; then
check_url_soft "bank /webui/" 200 "$BANK_PUBLIC/webui/" check_url_soft "bank /webui/" 200 "$BANK_PUBLIC/webui/"
check_url_soft "bank /intro/" 200 "$BANK_PUBLIC/intro/" check_url_soft "bank /intro/" 200 "$BANK_PUBLIC/intro/"
check_url_soft "bank /" 302,301,200 "$BANK_PUBLIC/" check_url_soft "bank /" 302,301,200 "$BANK_PUBLIC/"
# Auto-account: credentials + shared-pool taler://withdraw (like step 2) # Auto-account: GOA shared-pool mint (hacktivism). Not used on TESTPAYSAN stage.
aa_code=$(http_body "$BANK_PUBLIC/intro/auto-account.json" "$tmp/aa.json") aa_code=$(http_body "$BANK_PUBLIC/intro/auto-account.json" "$tmp/aa.json")
case "$aa_code" in case "$aa_code" in
200) 200)
@ -518,11 +715,22 @@ if [ "$code" = "200" ]; then
fail "bank /intro/auto-account.json" "invalid withdraw/login ($(tr '\n' ' ' <"$tmp/aa-val" | sed 's/[[:space:]]*$//'))" fail "bank /intro/auto-account.json" "invalid withdraw/login ($(tr '\n' ' ' <"$tmp/aa-val" | sed 's/[[:space:]]*$//'))"
fi fi
;; ;;
405|501|404|502|503|000) 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)" 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"
;; ;;
*) *)
if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
fail "bank /intro/auto-account.json" "HTTP $aa_code want 200" 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
;; ;;
esac esac
fi fi
@ -545,6 +753,7 @@ else
fi fi
# --- merchant --- # --- merchant ---
set_group merchant
if [ "${LOCAL_STACK:-1}" = "0" ]; then if [ "${LOCAL_STACK:-1}" = "0" ]; then
check_url_soft "merchant /config" 200 "$MERCHANT_PUBLIC/config" check_url_soft "merchant /config" 200 "$MERCHANT_PUBLIC/config"
else else
@ -610,6 +819,7 @@ esac
# Paivana paywall (local GOA stack) — public front only; pay path is e2e # Paivana paywall (local GOA stack) — public front only; pay path is e2e
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if [ "${LOCAL_STACK:-1}" = "1" ] && [ "${E2E_PAIVANA:-1}" != "0" ]; then if [ "${LOCAL_STACK:-1}" = "1" ] && [ "${E2E_PAIVANA:-1}" != "0" ]; then
set_group paivana
section "www · paivana paywall" section "www · paivana paywall"
: "${PAIVANA_PUBLIC:=https://paivana.hacktivism.ch}" : "${PAIVANA_PUBLIC:=https://paivana.hacktivism.ch}"
PAIVANA_PUBLIC="${PAIVANA_PUBLIC%/}" PAIVANA_PUBLIC="${PAIVANA_PUBLIC%/}"
@ -646,6 +856,7 @@ if [ "${CHECK_LANDING:-1}" != "1" ]; then
exit 0 exit 0
fi fi
set_group landing
section "www · landing exposed links · bank / merchant / exchange" section "www · landing exposed links · bank / merchant / exchange"
# Probe one URL: print code to stdout (200 after following redirects counts as 200). # Probe one URL: print code to stdout (200 after following redirects counts as 200).
@ -803,18 +1014,24 @@ check_one_landing() {
return return
fi fi
# Required static assets (hard: qrcode + og; soft: qr-logo) # Required static: qrcode.min.js. GOA og image is soft off-GOA (stage has no og-goa-shop.png).
for url in \ if _landing_probe "${base}/intro/qrcode.min.js"; then
"${base}/intro/qrcode.min.js" \
"${base}/intro/og-goa-shop.png"
do
if _landing_probe "$url"; then
a_ok=$((a_ok + 1)) a_ok=$((a_ok + 1))
else else
a_fail=$((a_fail + 1)) a_fail=$((a_fail + 1))
fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} $url" 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"
else
a_soft=$((a_soft + 1))
soft_sample="${soft_sample}${soft_sample:+; }HTTP ${_landing_code} og-goa-shop.png (GOA asset)"
fi
fi fi
done
if _landing_probe "${base}/intro/qr-logo.png"; then if _landing_probe "${base}/intro/qr-logo.png"; then
a_ok=$((a_ok + 1)) a_ok=$((a_ok + 1))
else else
@ -942,7 +1159,15 @@ 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:]]*$//'))" fail "bank /intro/demo-withdraw.json" "invalid taler://withdraw ($(tr '\n' ' ' <"$tmp/dw-val" | sed 's/[[:space:]]*$//'))"
fi fi
;; ;;
405|501|404|502|503|000) 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)
fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)" fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)"
;; ;;
*) *)

View file

@ -19,6 +19,7 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
source "$ROOT/lib.sh" source "$ROOT/lib.sh"
set_area versions set_area versions
set_group outside
SUITE="${TALER_APT_SUITE:-trixie}" SUITE="${TALER_APT_SUITE:-trixie}"
APT_BASE="${TALER_APT_BASE:-https://deb.taler.net/apt/debian}" APT_BASE="${TALER_APT_BASE:-https://deb.taler.net/apt/debian}"
@ -166,6 +167,7 @@ fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 2) INSIDE — containers can reach deb.taler.net (pasta / install path) # 2) INSIDE — containers can reach deb.taler.net (pasta / install path)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group inside
section "versions · inside · containers → deb.taler.net" section "versions · inside · containers → deb.taler.net"
if [ "${SKIP_SSH}" = "1" ]; then if [ "${SKIP_SSH}" = "1" ]; then
@ -266,6 +268,7 @@ ok "collected installed packages" "${n_inst} rows"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 3) compare installed vs trixie (and note testing) # 3) compare installed vs trixie (and note testing)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group compare
section "versions · compare installed vs ${SUITE}" section "versions · compare installed vs ${SUITE}"
export BEHIND_MODE HAVE_TESTING export BEHIND_MODE HAVE_TESTING

View file

@ -193,6 +193,13 @@ apply_taler_domain() {
PAY_AMT="${PAY_AMT:-${EXPECT_CURRENCY}:0.01}" PAY_AMT="${PAY_AMT:-${EXPECT_CURRENCY}:0.01}"
CREDIT_AMT="${CREDIT_AMT:-${EXPECT_CURRENCY}:100}" 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) CHF)
WITHDRAW_AMT="${WITHDRAW_AMT:-CHF:20}" WITHDRAW_AMT="${WITHDRAW_AMT:-CHF:20}"
PAY_AMT="${PAY_AMT:-CHF:0.01}" PAY_AMT="${PAY_AMT:-CHF:0.01}"
@ -352,17 +359,30 @@ koopa_ssh_python() {
with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" python3 - with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" python3 -
} }
# ANSI colours for tags (green OK / yellow WARN / red ERROR·BLOCKER / cyan INFO). # ANSI colours — boxed badges (fg+bg) + body (label bold, detail dim).
# Default: always on so pasted logs keep visible tags. Disable: NO_COLOR=1 or CLICOLOR=0. # Off: NO_COLOR=1 or CLICOLOR=0.
if [ "${NO_COLOR:-0}" = "1" ] || [ "${CLICOLOR:-1}" = "0" ]; then if [ "${NO_COLOR:-0}" = "1" ] || [ "${CLICOLOR:-1}" = "0" ]; then
G= R= Y= C= N= B= G= R= Y= C= M= D= W= B= N=
BG_OK= BG_INFO= BG_WARN= BG_ERR= BG_BLK= BG_SEC=
BOX=0
else else
G=$'\e[1;32m' # bold green [OK] G=$'\e[1;32m' # green text (totals)
R=$'\e[1;31m' # bold red [ERROR] [BLOCKER] R=$'\e[1;31m' # red text
Y=$'\e[1;33m' # bold yellow [WARN] Y=$'\e[1;33m' # yellow text
C=$'\e[1;36m' # bold cyan [INFO] C=$'\e[1;36m' # cyan text
B=$'\e[1m' M=$'\e[1;35m' # magenta text
N=$'\e[0m' 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
fi fi
PASS_N=0 PASS_N=0
@ -372,17 +392,68 @@ INFO_N=0
BLOCKERS=() # human-readable payment/withdraw blockers BLOCKERS=() # human-readable payment/withdraw blockers
ERRORS=() # all ERROR lines (component scope) ERRORS=() # all ERROR lines (component scope)
# Test IDs by area: www-001, e2e-001, inside-001, … # Grouped test IDs: area.group-NN (e.g. www.exchange-01, e2e.pay-03)
# Usage: set_area www then each ok/fail/warn/info/blocker/err auto-numbers. # 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_AREA="" TEST_AREA=""
TEST_GROUP=""
TEST_N=0 TEST_N=0
# Last issued id (www-001); set by _take_tid — not via $(…) so TEST_N persists.
LAST_TID="" LAST_TID=""
# Per-group counters so re-entering www.exchange after perf continues NN
# (shell vars TEST_CNT_<area>_<group>).
_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() { set_area() {
# leave previous group counter saved
_tid_save
TEST_AREA="$1" TEST_AREA="$1"
TEST_GROUP=""
TEST_N=0 TEST_N=0
LAST_TID="" 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). # Assign next id into LAST_TID (must not run in a subshell).
_take_tid() { _take_tid() {
LAST_TID="" LAST_TID=""
@ -390,49 +461,69 @@ _take_tid() {
return return
fi fi
TEST_N=$((TEST_N + 1)) TEST_N=$((TEST_N + 1))
LAST_TID=$(printf '%s-%03d' "$TEST_AREA" "$TEST_N") 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
} }
# Dim test id: "www.exchange-01 " or empty
_fmt_tid() { _fmt_tid() {
# prefix "www-001 " or empty
if [ -n "${LAST_TID:-}" ]; then if [ -n "${LAST_TID:-}" ]; then
printf '%s ' "$LAST_TID" 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"
fi fi
} }
ok() { ok() {
# Forms (same idea as info/warn): # ok "what is good" ["concrete evidence: HTTP 200 · 12ms · …"]
# ok "what passed"
# ok "what passed" "detail / ms / bytes / …"
local label="$1" detail="${2:-}" local label="$1" detail="${2:-}"
_take_tid _take_tid
if [ -n "$detail" ]; then _msg_line "$BG_OK" "OK" "$W" "$label" "$detail"
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)) PASS_N=$((PASS_N + 1))
} }
# component-scoped error: err bank "libeufin down" "detail" # component-scoped error: err bank "what failed" "why / HTTP / path"
err() { err() {
local comp="$1" msg="$2" detail="${3:-}" local comp="$1" msg="$2" detail="${3:-}"
_take_tid _take_tid
printf '%s[ERROR]%s %s%s: %s%s\n' "$R" "$N" "$(_fmt_tid)" "$comp" "$msg" "${detail:+ — $detail}" _msg_line "$BG_ERR" "ERROR" "$W" "${comp}: ${msg}" "$detail"
FAIL_N=$((FAIL_N + 1)) FAIL_N=$((FAIL_N + 1))
ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ — $detail}") ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ · $detail}")
} }
# legacy fail label ... # fail "what failed" ["why / HTTP code / path"]
fail() { fail() {
local label="$1" detail="${2:-}" local label="$1" detail="${2:-}"
_take_tid _take_tid
printf '%s[ERROR]%s %s%s%s\n' "$R" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}" _msg_line "$BG_ERR" "ERROR" "$W" "$label" "$detail"
FAIL_N=$((FAIL_N + 1)) FAIL_N=$((FAIL_N + 1))
ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ $detail}") ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ · $detail}")
} }
warn() { warn() {
# Forms (same idea as err; always try to state the problem): # warn "what is soft-bad" ["why still ok to continue"]
# warn "problem" # warn component "what" "why"
# warn "problem" "why / context"
# warn component "problem" "why / context"
local a1="${1:-}" a2="${2:-}" a3="${3:-}" local a1="${1:-}" a2="${2:-}" a3="${3:-}"
local head detail local head detail
_take_tid _take_tid
@ -446,56 +537,78 @@ warn() {
head="$a1" head="$a1"
detail="" detail=""
fi fi
if [ -n "$detail" ]; then _msg_line "$BG_WARN" "WARN" "$W" "$head" "$detail"
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)) WARN_N=$((WARN_N + 1))
} }
info() { info() {
# info "topic" ["concrete fact / value / next step"]
local label="$1" detail="${2:-}" local label="$1" detail="${2:-}"
_take_tid _take_tid
if [ -n "$detail" ]; then _msg_line "$BG_INFO" "INFO" "$W" "$label" "$detail"
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)) INFO_N=$((INFO_N + 1))
} }
blocker() { blocker() {
# Payment/withdraw path cannot proceed because of this # Hard stop on pay/withdraw path: blocker "step" "why it cannot continue"
local step="$1" msg="$2" local step="$1" msg="$2"
_take_tid _take_tid
printf '%s[BLOCKER]%s %s%s: %s\n' "$R$B" "$N" "$(_fmt_tid)" "$step" "$msg" _msg_line "$BG_BLK" "BLOCKER" "$W" "${step}" "$msg"
BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg") BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg")
FAIL_N=$((FAIL_N + 1)) FAIL_N=$((FAIL_N + 1))
ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg") ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg")
} }
section() { printf '\n%s== %s ==%s\n' "$B" "$*" "$N"; } 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
}
summary() { summary() {
echo "" echo ""
if [ "${#BLOCKERS[@]}" -gt 0 ]; then if [ "${#BLOCKERS[@]}" -gt 0 ]; then
printf '%s--- BLOCKERS (fix/withdraw path) ---%s\n' "$R$B" "$N" 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
local b local b
for b in "${BLOCKERS[@]}"; do for b in "${BLOCKERS[@]}"; do
printf '%s • %s%s\n' "$R" "$b" "$N" printf -- '%s •%s %s%s%s\n' "$M" "$N" "$W" "$b" "$N"
done done
fi fi
if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then
printf '%s--- ERRORS ---%s\n' "$R" "$N" if [ "${BOX:-0}" = "1" ]; then
printf -- '%s┌ ERRORS · failed checks ┐%s\n' "$BG_ERR" "$N"
else
printf -- '--- ERRORS (failed checks) ---\n'
fi
local e local e
for e in "${ERRORS[@]}"; do for e in "${ERRORS[@]}"; do
case "$e" in BLOCKER*) continue ;; esac case "$e" in BLOCKER*) continue ;; esac
printf '%s • %s%s\n' "$R" "$e" "$N" printf -- '%s •%s %s%s%s\n' "$R" "$N" "$W" "$e" "$N"
done done
fi fi
printf 'totals: %s%d OK%s' "$G" "$PASS_N" "$N" 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" [ "$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" [ "$WARN_N" -gt 0 ] && printf -- ', %s%d WARN%s' "$Y" "$WARN_N" "$N"
[ "$INFO_N" -gt 0 ] && printf ', %d INFO' "$INFO_N" [ "$INFO_N" -gt 0 ] && printf -- ', %s%d INFO%s' "$C" "$INFO_N" "$N"
[ "${#BLOCKERS[@]}" -gt 0 ] && printf ', %s%d BLOCKER%s' "$R" "${#BLOCKERS[@]}" "$N" [ "${#BLOCKERS[@]}" -gt 0 ] && printf -- ', %s%d BLOCKER%s' "$M" "${#BLOCKERS[@]}" "$N"
printf '\n' printf '\n'
[ "$FAIL_N" -eq 0 ] [ "$FAIL_N" -eq 0 ]
} }