Compare commits

..

No commits in common. "e46aeb408af803cbf08ee295f6948f8ab647dc16" and "d6f4f649c1d151e4250a75999dc39320d163879f" have entirely different histories.

19 changed files with 465 additions and 1204 deletions

View file

@ -14,14 +14,12 @@ TimeoutStartSec=480
# Do not keep "active" after run — timer may fire again cleanly
RemainAfterExit=no
Environment=TZ=Europe/Zurich
# Stack overrides (goa / stage-testpaysan / custom) — optional
EnvironmentFile=-%h/.config/taler-landing/stack.env
# GOA defaults if stack.env absent
Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log
Environment=SECRETS_BANK=%h/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank
Environment=BANK_URL=http://127.0.0.1:9012
Environment=BANK_PUBLIC_URL=https://bank.hacktivism.ch
Environment=EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config
# All accounts except exchange (double-count of withdraw credits on exchange ledger)
Environment=SCAN_SKIP=exchange
Environment=TX_PAGE=500
Environment=MAX_TX_PAGES=0

View file

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

View file

@ -1,21 +1,12 @@
# taler-landing — generic multi-stack landing stats
# taler-landing — central stats (hernani user systemd)
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):
Public landing numbers on
- https://bank.hacktivism.ch/intro/ → `stats.json`
- https://exchange.hacktivism.ch/intro/ → `stats.json`
- https://taler.hacktivism.ch/intro/ → `stats.json`
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.
are produced by **one host process** as user **`hernani`**, not by three unrelated in-container crons.
## Why host / user systemd
@ -86,37 +77,16 @@ Preferred order:
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 | Default | Meaning |
|-----|---------|---------|
| `BANK_URL` | `http://127.0.0.1:9012` | libeufin loopback |
| `BANK_CURRENCY` | *(auto)* | `GOA` / `TESTPAYSAN` / … |
| `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 |
| `MAX_TX_PAGES` | `0` | `0` = unlimited pages / account |
| `ACCOUNTS_DELTA` | `-10000` | account list window |
| `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

View file

@ -1,14 +1,10 @@
#!/usr/bin/env bash
# Generic landing-stats orchestrator (any Taler stack).
# Central landing-stats collector for hernani@koopa (user systemd timer).
#
# Defaults = GOA / hacktivism on koopa. Override via env or EnvironmentFile
# (see profiles: goa | stage-testpaysan).
#
# - 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)
# - Bank: full account+ledger scan via collect_bank_stats.py → bank container
# - Exchange / merchant: existing in-container scripts, then amount_alt enrich
# - 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)
#
# Install: scripts/taler-landing/install-landing-stats-host.sh
set -euo pipefail
@ -16,20 +12,6 @@ set -euo pipefail
export TZ="${TZ:-Europe/Zurich}"
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)" "$*"; }
ROOT="$(cd "$(dirname "$0")" && pwd)"
@ -40,8 +22,6 @@ elif [ -f "${HOME}/.local/lib/taler-landing/collect_bank_stats.py" ]; then
LIB="${HOME}/.local/lib/taler-landing"
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"
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
LIB="$ROOT"
fi
@ -49,13 +29,6 @@ fi
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}"
# 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}"
EX_CTR="${EX_CTR:-taler-hacktivism-exchange-ansible}"
MER_CTR="${MER_CTR:-taler-hacktivism}"
@ -63,19 +36,10 @@ MER_CTR="${MER_CTR:-taler-hacktivism}"
BANK_URL="${BANK_URL:-http://127.0.0.1:9012}"
BANK_PUBLIC_URL="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}"
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}"
EX_LANDING_IN="${EX_LANDING_IN:-/var/www/exchange-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}"
mkdir -p "$WORKDIR"
@ -100,9 +64,7 @@ MEM_SRC="${MEM_SNAPSHOT_SRC:-$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh}"
merge_container_resources() {
local label="$1" ctr="$2" stats_file="$3"
[ "${COLLECT_RESOURCES}" = "1" ] || return 0
[ -f "$stats_file" ] || return 0
[ -n "$ctr" ] || return 0
if ! ctr_running "$ctr"; then
log "WARN: $label resources: $ctr not running"
return 1
@ -133,36 +95,6 @@ merge_container_resources() {
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() {
local name="$1" f
for f in \
@ -175,26 +107,12 @@ read_pass() {
return 0
fi
done
# container secrets: /root/… (GOA) or /data/secrets/… (FrancPaysan stage)
if [ -n "${BANK_CTR:-}" ] && podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then
for path in "/root/${name}" "/data/secrets/${name}" "/data/secrets/bank-admin-password.txt"; do
case "$path" in
*bank-admin-password.txt)
[ "$name" = "bank-admin-password.txt" ] || continue
;;
esac
if podman exec "$BANK_CTR" test -r "$path" 2>/dev/null; then
podman exec "$BANK_CTR" cat "$path" 2>/dev/null | tr -d '\n\r'
# container (hernani can podman exec root files inside owned containers)
if podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then
if podman exec "$BANK_CTR" test -r "/root/${name}" 2>/dev/null; then
podman exec "$BANK_CTR" cat "/root/${name}" 2>/dev/null | tr -d '\n'
return 0
fi
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
return 1
}
@ -205,8 +123,6 @@ ctr_running() {
publish_json() {
local ctr="$1" dest_dir="$2" src_stats="$3" src_run="$4"
[ "${PUBLISH_PODMAN}" = "1" ] || return 0
[ -n "$ctr" ] || return 0
if ! ctr_running "$ctr"; then
log "WARN: $ctr not running — skip publish $dest_dir"
return 1
@ -250,7 +166,6 @@ collect_bank() {
BANK_URL="$BANK_URL" \
BANK_PUBLIC_URL="$BANK_PUBLIC_URL" \
EXCHANGE_CONFIG_URL="$EXCHANGE_CONFIG_URL" \
BANK_CURRENCY="${BANK_CURRENCY:-}" \
BANK_ADMIN_PASS="$admin_pass" \
BANK_EXPLORER_PASS="$explorer_pass" \
DEMO_DIR="${demo_dir}" \
@ -258,7 +173,6 @@ collect_bank() {
TX_PAGE="${TX_PAGE:-500}" \
MAX_TX_PAGES="${MAX_TX_PAGES:-0}" \
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}" \
STATS_SOURCE_LABEL="$STATS_SOURCE_LABEL" \
"$PY" "$LIB/collect_bank_stats.py" \
--out "$WORKDIR/bank-stats.json" \
--run-out "$WORKDIR/bank-stats-run.json" \
@ -267,13 +181,11 @@ collect_bank() {
set -e
if [ "$ec_bank" -eq 0 ] && [ -f "$WORKDIR/bank-stats.json" ]; then
# Optional: attach in-container RSS/loadavg (not host /proc)
# Always attach in-container RSS/loadavg (not host /proc)
merge_container_resources bank "$BANK_CTR" "$WORKDIR/bank-stats.json" || true
publish_json "$BANK_CTR" "$BANK_LANDING_IN" \
"$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json"
# Shared public feed: same bank stats on all HOST_STATS_DIRS (bank/ex/merchant landings)
publish_host_stats "$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json"
log "bank: OK (podman=${BANK_CTR:-none} host_dirs=${HOST_STATS_DIRS:-none})"
log "bank: OK → ${BANK_CTR}:${BANK_LANDING_IN}/stats.json"
else
log "ERROR: bank collect failed (ec=$ec_bank) — previous stats.json kept"
[ -f "$WORKDIR/bank-stats-run.json" ] || \
@ -360,26 +272,13 @@ collect_merchant() {
# ---------------------------------------------------------------------------
main() {
log "=== taler-landing-stats start (user=$(id -un) lib=$LIB currency=${BANK_CURRENCY:-auto}) ==="
log "phases bank=${COLLECT_BANK} exchange=${COLLECT_EXCHANGE} merchant=${COLLECT_MERCHANT} resources=${COLLECT_RESOURCES}"
if [ "${COLLECT_BANK}" = "1" ]; then
log "=== taler-landing-stats start (user=$(id -un) lib=$LIB) ==="
collect_bank || ec_bank=$?
else
log "skip bank collect"
fi
if [ "${COLLECT_EXCHANGE}" = "1" ]; then
collect_exchange || ec_ex=$?
else
log "skip exchange collect"
fi
if [ "${COLLECT_MERCHANT}" = "1" ]; then
collect_merchant || ec_mer=$?
else
log "skip merchant collect"
fi
log "=== done bank=$ec_bank exchange=$ec_ex merchant=$ec_mer ==="
# non-zero if bank failed (primary public flow numbers); soft on ex/mer
if [ "${COLLECT_BANK}" = "1" ] && [ "$ec_bank" -ne 0 ]; then
if [ "$ec_bank" -ne 0 ]; then
return 1
fi
return 0

View file

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

View file

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

View file

@ -1,18 +0,0 @@
# Stack profile: GOA / hacktivism
# systemd EnvironmentFile: KEY=value only (no export)
BANK_CTR=taler-hacktivism-bank
EX_CTR=taler-hacktivism-exchange-ansible
MER_CTR=taler-hacktivism
BANK_URL=http://127.0.0.1:9012
BANK_PUBLIC_URL=https://bank.hacktivism.ch
EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config
BANK_CURRENCY=GOA
BANK_LANDING_IN=/var/www/bank-landing
EX_LANDING_IN=/var/www/exchange-landing
MER_LANDING_IN=/var/www/merchant-landing
COLLECT_BANK=1
COLLECT_EXCHANGE=1
COLLECT_MERCHANT=1
COLLECT_RESOURCES=1
PUBLISH_PODMAN=1
STATS_SOURCE_LABEL="host collect_bank_stats.py goa hacktivism"

View file

@ -1,22 +0,0 @@
# Stack profile: FrancPaysan stage / TESTPAYSAN (ops user stagepaysan)
# systemd EnvironmentFile: KEY=value only (no export, no shell expansion)
BANK_CTR=stage-lfp-bank
EX_CTR=stage-lfp-exchange-ansible
MER_CTR=stage-lfp-merchant
BANK_URL=http://127.0.0.1:9032
BANK_PUBLIC_URL=https://stage.bank.lefrancpaysan.ch
EXCHANGE_CONFIG_URL=https://stage.exchange.lefrancpaysan.ch/config
BANK_CURRENCY=TESTPAYSAN
SCAN_SKIP=exchange
BANK_LANDING_IN=/data/var/www
EX_LANDING_IN=/data/var/www
MER_LANDING_IN=/data/var/www
# Shared public feed: one bank scan → same stats.json on all three Caddy landings
# Colon-separated for systemd EnvironmentFile
HOST_STATS_DIRS=/var/www/lfp-stage/bank:/var/www/lfp-stage/exchange:/var/www/lfp-stage/merchant
COLLECT_BANK=1
COLLECT_EXCHANGE=0
COLLECT_MERCHANT=0
COLLECT_RESOURCES=0
PUBLISH_PODMAN=1
STATS_SOURCE_LABEL="host collect_bank_stats.py stage TESTPAYSAN shared"

View file

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

View file

@ -1,26 +1,24 @@
# taler-monitoring
Report for the **GOA** stack with boxed severity badges and **grouped test IDs**
`area.group-NN` (e.g. `www.exchange-01`, `e2e.pay-03`):
Report for the **GOA** stack with clear severity tags and **per-area test IDs**:
| Tag | Meaning |
|-----|---------|
| `┌ OK ┐` | check passed |
| `┌ INFO ┐` | status / note |
| `┌ WARN ┐` | degraded but maybe not fatal |
| `┌ ERROR ┐` | component problem |
| `┌ BLOCKER ┐` | **withdraw/pay path cannot complete** because of this |
| `[OK]` | check passed |
| `[INFO]` | inside status (container, ports, log noise) |
| `[WARN]` | degraded but maybe not fatal |
| `[ERROR]` | component problem |
| `[BLOCKER]` | **withdraw/pay path cannot complete** because of this |
IDs reset per **group** inside an area — so “too many www tests” become
`www.exchange-*`, `www.bank-*`, `www.landing-*`, etc.
IDs: **`www-001`**, **`inside-001`**, **`versions-001`**, **`sanity-001`**, **`server-001`**, **`e2e-001`** …
Catalog: **[TESTS.md](./TESTS.md)**.
```text
┌ OK ┐ www.exchange-01 exchange /config · HTTP 200
┌ BLOCKER┐ e2e.prereq-02 merchant HTTP 502
[OK] www-001 exchange /config https://exchange…/config
[BLOCKER] e2e-015 prereq: merchant HTTP 502
```
End of each phase: totals + list of **BLOCKERS** and **ERRORS** (ids included).
End of each phase: totals + list of **BLOCKERS** and **ERRORS**.
## Commands
@ -43,27 +41,8 @@ End of each phase: totals + list of **BLOCKERS** and **ERRORS** (ids included).
./taler-monitoring.sh --bank https://bank.demo.taler.net \
--exchange https://exchange.demo.taler.net \
--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`)
Each monitored stack must say **what is bank, exchange, and merchant-backend**.

View file

@ -1,132 +1,217 @@
# taler-monitoring — grouped test IDs
# taler-monitoring — test IDs by area
Every check line is numbered **`area.group-NN`** (two-digit group counter):
Every check line is numbered **per area** as `AREA-NNN` (zero-padded):
| 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
┌ OK ┐ www.exchange-01 exchange /config · HTTP 200
┌ ERROR ┐ e2e.pay-03 order create failed · HTTP 502
┌ BLOCKER┐ e2e.prereq-02 merchant secret missing
[OK] www-001 exchange /config https://exchange…/config
[ERROR] e2e-012 bank-auth: admin token failed
[BLOCKER] e2e-015 prereq: merchant HTTP 502
```
| 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).
IDs are assigned **in run order** within the area (`set_area` resets the counter). Optional soft checks still consume a number when they WARN.
---
## www — public URLs (`./taler-monitoring.sh urls`)
| Group | Checks (typical) |
|-------|------------------|
| **www.exchange-** | `/config`, currency, **alt_unit_names**; `/keys` (+ alt soft); `/intro/`, `/`; **`/terms`**, **`/privacy`**, `/terms/` |
| **www.perf-** | outside-in RTT: bank/exchange/merchant `/config`, keys, webui, intro — ms; WARN ≥ `PERF_WARN_MS` (8000); ERROR ≥ `PERF_FAIL_MS` (20000) |
| **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.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.merchant-** | `/config` currency + currencies alt_unit_names; listed exchanges alt; webui/intro; **`/terms`**, **`/privacy`** |
| **www.paivana-** | local GOA paywall front (redirect to template) |
| **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 |
| ID | Check |
|----|--------|
| www-… | exchange `/config`, currency, **alt_unit_names** |
| www-… | exchange `/keys` (+ alt_unit_names soft) |
| www-… | exchange `/intro/`, `/` (302→intro) |
| www-… | **exchange `/terms`** body (not empty / not API error) |
| www-… | **exchange `/privacy`** body |
| www-… | exchange `/terms/` (200 or redirect) |
| www-… | bank `/config`, currency, **alt_unit_names** |
| 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`. Local stack may require content needle (terms/privacy/FADP/GOA…).
**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…).
**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).
**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.
**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.
**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.
**alt_unit_names rule:** non-empty map including scale key `"0"`. Multi-currency merchant: follow each `exchanges[]` public `/config`.
**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`.
(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`)
| Group | Checks |
|-------|--------|
| **inside.ssh-** | ssh reachability / remote collect |
| **inside.bank-** | container, libeufin, postgres, local `/config`, nginx, DNS pins |
| **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 |
| ID | Check (typical order) |
|----|------------------------|
| inside-001 | ssh koopa (or **koopa-external** fallback) |
| inside-002+ | per-component emit: container, ports, libeufin/httpd, postgres, local `/config`/`/keys`, wirewatch, DNS pin, caddy |
| inside-… | **load / memory**: host loadavg + RAM; bank/exchange/merchant podman CPU/mem/block + process RSS by role + DB sizes |
Remote lines `E|comp|LEVEL|key|detail` each become one numbered result under that component group.
Remote lines `E|comp|LEVEL|key|detail` each become one numbered result.
SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` when LAN is unreachable.
SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` (default `koopa-external`) when LAN is unreachable.
---
## e2e — withdraw + pay (`./taler-monitoring.sh e2e`)
## sanity — bank · exchange · merchant (`./taler-monitoring.sh sanity`)
| Group | Checks |
|-------|--------|
| **e2e.prereq-** | wallet-cli, currency, budgets, secrets, public reachability |
| **e2e.load-** | host/container load snapshots (before / after ATM / after pay) |
| **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 |
| ID | Section |
|----|---------|
| sanity-001… | bank public + server |
| sanity-… | exchange public + server |
| sanity-… | merchant public + server |
When filing an issue, quote the full id + label, e.g.
`e2e.atm-03 ATM withdraw 5 GOA · bank confirmed, wallet empty`.
Sequential through the whole script (one `set_area sanity`).
---
## versions / sanity / ladder / server
## versions — packages vs deb.taler.net (`./taler-monitoring.sh versions`)
| Area.group | Meaning |
|------------|---------|
| **versions.outside-** | deb.taler.net suite index |
| **versions.inside-** | packages in containers |
| **versions.compare-** | installed vs suite |
| **sanity.bank-** / **.exchange-** / **.merchant-** | public + optional server-side per component |
| **ladder.plan-** / **.load-** / **.withdraw-** / **.pay-** / **.report-** | GOA amount ladder |
| **server-** | SSH host ports / processes (flat unless grouped later) |
### Outside (runner / public network — no SSH)
| ID (order) | Check |
|------------|--------|
| versions-… | DNS `deb.taler.net` |
| versions-… | HTTPS portal + apt base URL |
| versions-… | suite `InRelease` / `Release` |
| versions-… | suite `Packages` + `Packages.gz` |
| 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.
---
## Visual format (colour on)
## server — SSH ports (`./taler-monitoring.sh server`)
| 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
╔══════════════════════════════╗
║ 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
withdraw: [0] + random mids + [max1] + [max]
pay: [0] + (withdraw_mid / scale) + [max1] + [max] # always ≤ matching withdraw mid
```
`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).
TSV `range`: `pin:0` | `random` | `pin:max-1` | `pin:max`.
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,37 +23,19 @@ e2e_over() { [ "$(e2e_left)" -le 0 ]; }
# stdout only for JSON; logs go to stderr file so balance parse stays clean
wcli_bal_snap() {
local out="$1"
if [ -z "${WALLET_CLI:-}" ]; then
if [ -z "${WALLET_CLI:-}" ] || [ ! -f "${WDB:-}" ]; then
echo "(no wallet)" >"$out"
return 1
fi
# one-shot needs DB file; serve mode needs socket
if [ -z "${WSERVE_PID:-}" ] || [ ! -S "${WSOCK:-}" ]; then
if [ ! -f "${WDB:-}" ]; then
echo "(no wallet)" >"$out"
return 1
fi
fi
local err="${out}.err"
if command -v perl >/dev/null 2>&1; then
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
perl -e 'alarm shift; exec @ARGV' 10 \
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \
>"$out" 2>"$err" || true
else
perl -e 'alarm shift; exec @ARGV' 10 \
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
>"$out" 2>"$err" || true
fi
else
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \
>"$out" 2>"$err" || true
else
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
>"$out" 2>"$err" || true
fi
fi
[ -s "$out" ]
}
@ -122,9 +104,9 @@ wait_wallet_balance() {
local rounds="${2:-15}"
local sleep_s="${3:-3}"
local r av
# Poll balance only — never run-until-done (hangs; banned in monitoring).
info "wallet settle wait" "up to ${rounds}×${sleep_s}s for avail>${min_n} ${CUR} (poll balance only; no run-until-done)"
info "wallet settle wait" "up to ${rounds}×${sleep_s}s for avail>${min_n} ${CUR} (wirewatch lag is common)"
for r in $(seq 1 "$rounds"); do
wcli run-until-done >"$SCRATCH/settle-run.out" 2>&1 || true
wcli_bal_snap "$SCRATCH/bal-live.out" || wcli balance >"$SCRATCH/bal-live.out" 2>&1 || true
av=$(wallet_avail_num "$SCRATCH/bal-live.out")
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > float(sys.argv[2]) else 1)" "$av" "$min_n" 2>/dev/null; then
@ -200,7 +182,6 @@ e2e_skip_rest() {
# Area e2e-### — withdraw + pay cycle
set_area e2e
set_group prereq
section "e2e · prerequisites"
info "e2e budget" "${E2E_TIMEOUT}s (set E2E_TIMEOUT= to change)"
@ -239,22 +220,12 @@ except Exception: print("")' 2>/dev/null || true)
if [ "$E2E_REMOTE" = "1" ]; then
# remote: still ATM-shaped but smaller notes; keep cheap
: "${E2E_WITHDRAW_VALUES:=10 20 50}"
if [ "$CUR" = "TESTPAYSAN" ]; then
# match fixed public templates e2e-001 / e2e-005 / e2e-1
: "${E2E_PAY_VALUES:=0.01 0.05 1}"
else
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}"
fi
else
# local GOA: classic ATM notes + 4200 for paivana paywall template
: "${E2E_WITHDRAW_VALUES:=20 50 100 200 4200}"
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 0.5 1 2 5 10}"
fi
# Public template pays (landing/shop style) — default on for TESTPAYSAN stage
if [ -z "${E2E_USE_TEMPLATES:-}" ]; then
if [ "$CUR" = "TESTPAYSAN" ]; then E2E_USE_TEMPLATES=1; else E2E_USE_TEMPLATES=0; fi
fi
: "${E2E_USE_TEMPLATES:=0}"
# Build CUR:amount lists
build_amt_list() {
@ -328,27 +299,12 @@ BANK_HOST=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$BA
SCRATCH=$(mktemp -d)
WDB="$SCRATCH/wallet.sqlite3"
WSOCK="$SCRATCH/wallet.sock"
WSERVE_PID=""
# Client/server wallet (advanced serve): long-lived shepherd without run-until-done.
# Default on — wallet-cli only completes withdraw/pay while a serve process is up.
: "${E2E_WALLET_SERVE:=1}"
METRICS_DIR="${METRICS_DIR:-$SCRATCH/metrics}"
mkdir -p "$METRICS_DIR"
export METRICS_DIR CUR="${CUR:-${EXPECT_CURRENCY:-GOA}}" WDB
export PATH="${HOME}/.local/bin:${HOME}/taler/opt/taler-wallet-cli/usr/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
stop_wallet_serve() {
if [ -n "${WSERVE_PID:-}" ] && kill -0 "$WSERVE_PID" 2>/dev/null; then
kill "$WSERVE_PID" 2>/dev/null || true
wait "$WSERVE_PID" 2>/dev/null || true
fi
WSERVE_PID=""
rm -f "${WSOCK:-}" 2>/dev/null || true
}
# Always dump balances on any exit (timeout, blocker, signal, success)
trap 'ec=$?; e2e_finish "$ec"; stop_wallet_serve; rm -rf "$SCRATCH"; exit "$ec"' EXIT
trap 'ec=$?; e2e_finish "$ec"; rm -rf "$SCRATCH"; exit "$ec"' EXIT
# Abort cleanly on login / KYC / registration barriers (esp. remote domains)
e2e_abort_auth() {
@ -426,12 +382,10 @@ else
fi
# Host + bank/exchange/merchant RAM/CPU/load (SSH koopa or koopa-external)
set_group load
section "e2e · load snapshot (before withdraw/pay)"
metrics_report_load "${METRICS_DIR}/load-before.json" "e2e-start" || true
# Local stack: koopa secrets. Remote: only explicit env (never leak local passwords to foreign banks).
set_group prereq
if [ "$E2E_REMOTE" = "1" ]; then
ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-}"
MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}"
@ -448,29 +402,13 @@ else
exit 1
fi
if [ -n "$MPW" ]; then
# Secrets often store full "secret-token:…"; strip before re-prefixing Bearer.
MPW=$(printf '%s' "$MPW" | tr -d '\n\r')
case "$MPW" in
secret-token:*) MPW_TOKEN="$MPW" ;;
*) MPW_TOKEN="secret-token:${MPW}" ;;
esac
ok "merchant secret (instance ${MERCHANT_INSTANCE})"
elif [ "$E2E_REMOTE" = "1" ]; then
MPW_TOKEN=""
warn "merchant secret" "missing — private orders fail unless E2E_USE_TEMPLATES=1 (set E2E_MERCHANT_TOKEN)"
warn "merchant secret" "missing — order create may fail (set E2E_MERCHANT_TOKEN)"
else
blocker "prereq" "merchant instance password missing"
exit 1
fi
# Authorization header for merchant private API (never double secret-token:)
merchant_auth_header() {
local t="${1:-${MPW_TOKEN:-}}"
[ -n "$t" ] || return 1
case "$t" in
secret-token:*) printf 'Authorization: Bearer %s' "$t" ;;
*) printf 'Authorization: Bearer secret-token:%s' "$t" ;;
esac
}
# Public reachability gates (remote: soft-skip if bank/merchant absent)
for pair in \
@ -514,31 +452,7 @@ info "e2e user" "$USER withdraw=$WITHDRAW_AMT pay=$PAY_AMT"
BANK="$BANK_PUBLIC"
INST="$MERCHANT_INSTANCE"
start_wallet_serve() {
[ "${E2E_WALLET_SERVE}" = "1" ] || return 0
[ -n "${WALLET_CLI:-}" ] && [ -f "${WALLET_CLI}" ] || return 1
stop_wallet_serve
rm -f "$WSOCK"
# serve holds the DB open; clients use --wallet-connection only
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults \
advanced serve --unix-path "$WSOCK" \
>"$SCRATCH/wallet-serve.log" 2>&1 &
WSERVE_PID=$!
local i
for i in $(seq 1 40); do
if [ -S "$WSOCK" ] && kill -0 "$WSERVE_PID" 2>/dev/null; then
ok "wallet serve" "pid=$WSERVE_PID sock=$WSOCK (no run-until-done)"
return 0
fi
sleep 0.15
done
warn "wallet serve" "socket not ready — falling back to one-shot CLI (coins may stay pending)"
stop_wallet_serve
return 1
}
# $1 = max seconds for this call (optional); rest = wallet-cli args
# Prefer live serve socket so the shepherd stays up (alternative to run-until-done).
wcli() {
local maxc=12
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
@ -550,21 +464,12 @@ wcli() {
cap=$(e2e_left)
[ "$cap" -gt "$maxc" ] && cap=$maxc
[ "$cap" -lt 3 ] && return 124
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
if command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$cap" \
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
else
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
fi
else
if command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$cap" \
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
else
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
fi
fi
}
# Pay must not be starved: use reserved seconds even if global budget is tight
@ -581,26 +486,16 @@ wcli_pay() {
cap=12
fi
info "pay wallet cap" "${cap}s"
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
if command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$cap" \
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
else
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
fi
else
if command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$cap" \
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
else
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
fi
fi
}
# When coins never become available: run inside + targeted bank/exchange dig
dig_when_no_coins() {
set_group dig
section "e2e · coins missing — inside dig"
info "reason" "wallet empty after withdraw path (often wirewatch timing) — probing bank + exchange"
if [ -n "${WID:-}" ]; then
@ -685,7 +580,6 @@ BAL_BEFORE=$(fmt_bal "$SCRATCH/bal-before.out")
info "BALANCE before" "$BAL_BEFORE"
# ---------------------------------------------------------------------------
set_group bank
section "e2e · bank (account · credit · withdraw)"
# ---------------------------------------------------------------------------
AT=""
@ -784,11 +678,8 @@ else
fi
# ---------------------------------------------------------------------------
set_group wallet
section "e2e · wallet setup (exchange + ToS once)"
# ---------------------------------------------------------------------------
# Long-lived serve = client/server wallet (shepherd keeps running; no run-until-done).
start_wallet_serve || true
if ! wcli exchanges add "${EXCHANGE_PUBLIC}/" >"$SCRATCH/ex-add.out" 2>&1; then
if ! grep -qiE 'already|ok' "$SCRATCH/ex-add.out"; then
blocker "wallet-exchange" "exchanges add failed — $(tail -c 160 "$SCRATCH/ex-add.out" | tr '\n' ' ')"
@ -883,13 +774,12 @@ e2e_one_withdraw() {
fi
cp "$SCRATCH/accept-$tag.out" "$SCRATCH/accept.out"
# Poll bank withdrawal status only (no run-until-done). force-select if stuck pending.
st=""
for i in 1 2 3 4 5 6 8 10; do
for i in 1 2 3 4; do
e2e_over && break
wcli run-until-done >"$SCRATCH/sel-$tag-$i.out" 2>&1 || true
st=$(wd_status)
case "$st" in selected|confirmed|aborted) break ;; esac
sleep 0.4
done
if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then
wcli transactions >"$SCRATCH/tx-pre.json" 2>&1 || true
@ -934,9 +824,10 @@ else:
fi
fake_incoming_speedup
# Short per-ATM settle: poll balance + bank transfer_done only (never run-until-done)
local ok_bal=0 r av xfer
for r in 1 2 3 4 5 6 8 10; do
# Short per-ATM poll; full settle wait happens after the ladder (avoids false FAIL)
local ok_bal=0 r av
for r in 1 2 3 4 5 6; do
wcli run-until-done >"$SCRATCH/run-$tag-$r.out" 2>&1 || true
wcli_bal_snap "$SCRATCH/bal-$tag.out" || wcli balance >"$SCRATCH/bal-$tag.out" 2>&1 || true
av=$(wallet_avail_num "$SCRATCH/bal-$tag.out")
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av" 2>/dev/null; then
@ -945,13 +836,6 @@ else:
info "balance" "$(fmt_bal "$SCRATCH/bal-$tag.out")"
break
fi
xfer=$(curl -sS -m 8 "$BANK/taler-integration/withdrawal-operation/${WID}" 2>/dev/null \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("transfer_done"), d.get("status"))' 2>/dev/null || echo "?")
if echo "$xfer" | grep -qi True; then
ok "ATM $WITHDRAW_AMT bank transfer_done" "wallet avail=${CUR}:${av} ($xfer) — no run-until-done"
ok_bal=1
break
fi
sleep 2
done
metrics_report_coins "after-ATM-${tag}" || true
@ -969,28 +853,8 @@ else:
return 1
}
# Map CUR:amount → public template id (TESTPAYSAN stage e2e-* templates).
# Override with E2E_TEMPLATE_MAP="0.01=e2e-001 0.05=e2e-005 1=e2e-1"
e2e_template_id_for_amount() {
local amt="$1"
local val map_entry k v
val=$(printf '%s' "$amt" | awk -F: '{print $NF}')
: "${E2E_TEMPLATE_MAP:=0.01=e2e-001 0.05=e2e-005 1=e2e-1 1.0=e2e-1}"
for map_entry in $E2E_TEMPLATE_MAP; do
k="${map_entry%%=*}"
v="${map_entry#*=}"
if [ "$k" = "$val" ]; then
printf '%s' "$v"
return 0
fi
done
return 1
}
# One payment: e2e_one_pay AMOUNT [SUMMARY] [TAG]
# SUMMARY defaults to "monitoring pay $AMOUNT"; TAG defaults from amount.
# When E2E_USE_TEMPLATES=1 and amount maps to a public template, uses
# POST /instances/{inst}/templates/{id} (same as landings/shops).
e2e_one_pay() {
PAY_AMT="$1"
local PAY_SUM="${2:-monitoring pay ${PAY_AMT}}"
@ -1003,28 +867,11 @@ e2e_one_pay() {
section "e2e · pay $PAY_AMT · $(format_amount_alt "$PAY_AMT") · $PAY_SUM"
e2e_over && { warn "pay" "budget exhausted — skip $PAY_AMT ($PAY_SUM)"; return 1; }
metrics_report_coins "before-pay-${tag}" || true
# Prefer public templates when enabled (stage TESTPAYSAN / shop-style orders)
local tpl_id=""
if [ "${E2E_USE_TEMPLATES:-0}" = "1" ]; then
tpl_id=$(e2e_template_id_for_amount "$PAY_AMT" 2>/dev/null || true)
if [ -n "$tpl_id" ]; then
info "pay $PAY_AMT" "via public template $tpl_id (E2E_USE_TEMPLATES=1)"
e2e_one_pay_public_template "$tpl_id" "$PAY_SUM" "$PAY_AMT"
return $?
fi
warn "pay $PAY_AMT" "no template map for amount — fall back to private order"
fi
if [ -z "${MPW:-}" ] && [ -z "${MPW_TOKEN:-}" ]; then
if [ -z "${MPW:-}" ]; then
warn "pay $PAY_AMT" "no merchant token"
return 1
fi
local AUTH
AUTH=$(merchant_auth_header) || {
warn "pay $PAY_AMT" "no merchant token"
return 1
}
AUTH="Authorization: Bearer secret-token:${MPW}"
# JSON-escape summary for curl -d
local SUM_JSON
SUM_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$PAY_SUM" 2>/dev/null || printf '"%s"' "$PAY_SUM")
@ -1034,9 +881,8 @@ e2e_one_pay() {
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders" 2>"$SCRATCH/ord-$tag.err" || true
if ! grep -q order_id "$SCRATCH/ord-$tag.json" 2>/dev/null; then
if [ "$E2E_REMOTE" != "1" ] && koopa_ssh_ok; then
local mpw_raw="${MPW_TOKEN:-secret-token:${MPW}}"
koopa_ssh_run 20 \
"curl -skS -m 12 -X POST -H 'Authorization: Bearer ${mpw_raw}' -H 'Content-Type: application/json' -d '{\"order\":{\"summary\":${SUM_JSON},\"amount\":\"${PAY_AMT}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}' 'https://127.0.0.1:9010/instances/${INST}/private/orders'" \
"curl -skS -m 12 -X POST -H 'Authorization: Bearer secret-token:${MPW}' -H 'Content-Type: application/json' -d '{\"order\":{\"summary\":${SUM_JSON},\"amount\":\"${PAY_AMT}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}' 'https://127.0.0.1:9010/instances/${INST}/private/orders'" \
>"$SCRATCH/ord-$tag.json" 2>"$SCRATCH/ord-$tag.err" || true
fi
fi
@ -1080,37 +926,23 @@ except Exception: print("")
return 1
fi
ok "wallet handle pay $PAY_AMT ($PAY_SUM)"
# Settle: poll merchant order + wallet transactions only (never run-until-done)
local r
for r in 1 2 3 4 5 6; do
for r in 1 2 3; do
wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true
wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true
if [ -n "${AUTH:-}" ]; then
curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
fi
if [ -n "${OTOK:-}" ]; then
curl -skS -m 8 -o "$SCRATCH/ord-pub-$tag.json" \
"${MERCHANT_PUBLIC}/instances/${INST}/orders/$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$OID")?token=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$OTOK")" \
2>/dev/null || true
fi
if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \
|| grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null \
|| python3 -c 'import json,sys
for p in sys.argv[1:]:
try:
d=json.load(open(p))
if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid":
sys.exit(0)
except Exception:
pass
sys.exit(1)
' "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null; then
d=json.load(open(sys.argv[1]))
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1)
' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then
ok "payment settled $PAY_AMT ($PAY_SUM · order $OID)"
metrics_report_coins "after-pay-${tag}" || true
metrics_record_flow spent "$PAY_AMT" || true
return 0
fi
sleep 1
done
metrics_report_coins "after-pay-fail-${tag}" || true
warn "pay $PAY_AMT ($PAY_SUM)" "not settled for order $OID"
@ -1213,34 +1045,33 @@ except Exception: print("")
fi
ok "shop $pname" "wallet accepted pay URI"
# Settle: public order status + transactions only (never run-until-done)
local r AUTH=""
AUTH=$(merchant_auth_header 2>/dev/null) || AUTH=""
for r in 1 2 3 4 5 6; do
if [ -n "${MPW:-}" ]; then
AUTH="Authorization: Bearer secret-token:${MPW}"
fi
for r in 1 2 3; do
wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true
wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true
if [ -n "$AUTH" ]; then
curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
fi
curl -skS -m 8 -o "$SCRATCH/ord-pub-$tag.json" "$statusUrl" 2>/dev/null || true
if python3 -c 'import json,sys
for p in sys.argv[1:]:
try:
d=json.load(open(p))
if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid":
sys.exit(0)
except Exception:
pass
sys.exit(1)
' "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null \
|| grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \
|| grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null; then
d=json.load(open(sys.argv[1]))
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1)
' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then
ok "shop $pname" "payment settled ($pamt · order $OID)"
metrics_report_coins "after-shop-${tag}" || true
metrics_record_flow spent "$pamt" || true
return 0
fi
sleep 1
fi
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
metrics_report_coins "after-shop-fail-${tag}" || true
warn "shop $pname ($pid)" "not settled for order $OID"
@ -1248,7 +1079,6 @@ sys.exit(1)
}
# ---------------------------------------------------------------------------
set_group atm
section "e2e · ATM withdraw ladder"
# ---------------------------------------------------------------------------
WITHDRAW_OK=0
@ -1279,14 +1109,12 @@ for WITHDRAW_AMT in $WITHDRAW_LIST; do
esac
done
info "ATM withdraw summary" "$WITHDRAW_REPORT (ok=$WITHDRAW_OK_N lag=$WITHDRAW_LAG_N fail=$WITHDRAW_FAIL_N)"
set_group load
section "e2e · load snapshot (after ATM withdraws)"
metrics_report_load "${METRICS_DIR}/load-after-withdraw.json" "after-withdraw" || true
cp -f "${METRICS_DIR}/load-after-withdraw.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true
metrics_report_coins "after-ATM-ladder" || true
# Settlement catch-up: bank may have confirmed while wallet was still empty
set_group settle
section "e2e · wallet settlement (timing)"
if ! wait_wallet_balance 0 "${E2E_SETTLE_ROUNDS}" "${E2E_SETTLE_SLEEP}"; then
if [ "$WITHDRAW_LAG_N" -gt 0 ] || [ "$WITHDRAW_OK_N" -gt 0 ]; then
@ -1326,7 +1154,6 @@ else
fi
# ---------------------------------------------------------------------------
set_group pay
section "e2e · variable payments (if balance allows)"
# ---------------------------------------------------------------------------
PAY_OK=0
@ -1334,16 +1161,13 @@ PAY_OK_N=0
PAY_FAIL_N=0
PAY_SKIP_N=0
PAY_REPORT=""
if [ -z "${MPW:-}" ] && [ "${E2E_USE_TEMPLATES:-0}" != "1" ]; then
if [ -z "${MPW:-}" ]; then
if [ "$E2E_REMOTE" = "1" ]; then
e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN (or E2E_USE_TEMPLATES=1)"
e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN"
fi
blocker "merchant-order" "no merchant token"
exit 1
fi
if [ -z "${MPW:-}" ] && [ "${E2E_USE_TEMPLATES:-0}" = "1" ]; then
info "pay" "no merchant token — public templates only (E2E_USE_TEMPLATES=1)"
fi
for PAY_AMT in $PAY_LIST; do
e2e_over && { warn "pay ladder" "time budget low — stopping more payments"; break; }
@ -1369,7 +1193,6 @@ for PAY_AMT in $PAY_LIST; do
fi
done
info "pay summary" "$PAY_REPORT (ok=$PAY_OK_N fail=$PAY_FAIL_N skip=$PAY_SKIP_N)"
set_group load
section "e2e · load snapshot (after payments)"
metrics_report_load "${METRICS_DIR}/load-after-pay.json" "after-pay" || true
cp -f "${METRICS_DIR}/load-after-pay.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true
@ -1384,47 +1207,18 @@ if [ "$PAY_OK" != "1" ]; then
fi
# ---------------------------------------------------------------------------
set_group shop
section "e2e · shop products (public templates)"
section "e2e · GOA shop products (merchant landing catalog)"
# ---------------------------------------------------------------------------
# Public template POST + wallet pay — catalog random pick of N products.
# GOA: goa-shop on hacktivism. TESTPAYSAN: farmer shops (jardin / fermes).
# Public template POST + wallet pay — full catalog list, random pick of N products.
SHOP_OK=0
SHOP_OK_N=0
SHOP_FAIL_N=0
SHOP_SKIP_N=0
SHOP_REPORT=""
# Auto catalog for stage TESTPAYSAN when not overridden
if [ "${CUR:-}" = "TESTPAYSAN" ] && [ -z "${E2E_SHOP_PRODUCTS_SET:-}" ] && [ -z "${E2E_SHOP_FORCE_GOA:-}" ]; then
# id|name|amount|instance — force stage catalog (GOA default is set earlier)
E2E_SHOP_PRODUCTS="oeufs-6|Œufs plein air × 6|TESTPAYSAN:5|jardin-du-creux
pain-seigle|Pain de seigle 800 g|TESTPAYSAN:4.5|jardin-du-creux
jus-pomme|Jus de pomme 1 L|TESTPAYSAN:6|jardin-du-creux
panier-legumes|Panier légumes|TESTPAYSAN:25|fermes-des-collines
fromage-chevre|Fromage de chèvre 200 g|TESTPAYSAN:8.5|fermes-des-collines
miel-printemps|Miel de printemps 250 g|TESTPAYSAN:12|fermes-des-collines"
# Force stage defaults (GOA goa-shop is already set earlier via :=)
if [ -z "${E2E_SHOP_INSTANCE_SET:-}" ]; then
E2E_SHOP_INSTANCE=jardin-du-creux
fi
if [ -z "${E2E_SHOP_PAYTO_SET:-}" ]; then
E2E_SHOP_PAYTO="payto://x-taler-bank/stage.bank.lefrancpaysan.ch/jardin-du-creux"
fi
E2E_SHOP_PICK_N="${E2E_SHOP_PICK_N:-2}"
E2E_SHOP_ENABLE=1
fi
# GOA local only by default; remote non-GOA shops need E2E_SHOP_ENABLE=1 or TESTPAYSAN auto above
if [ -z "${E2E_SHOP_ENABLE:-}" ]; then
if [ "$E2E_REMOTE" = "1" ] || [ "${CUR:-}" != "GOA" ]; then
E2E_SHOP_ENABLE=0
else
E2E_SHOP_ENABLE=1
fi
fi
if [ "${E2E_SHOP_ENABLE}" != "1" ]; then
info "shop" "SKIPPED (set E2E_SHOP_ENABLE=1 or use TESTPAYSAN stage catalog)"
if [ "$E2E_REMOTE" = "1" ] || [ "${CUR:-}" != "GOA" ]; then
info "goa-shop" "SKIPPED (remote or non-GOA currency)"
else
# Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount[|instance] lines)
# Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount lines)
SHOP_CATALOG=()
while IFS= read -r line; do
[ -z "$line" ] && continue
@ -1438,7 +1232,7 @@ EOF
case "$PICK_N" in ''|*[!0-9]*) PICK_N=2 ;; esac
if [ "$PICK_N" -lt 1 ]; then PICK_N=1; fi
if [ "$SHOP_CAT_N" -eq 0 ]; then
warn "shop" "empty E2E_SHOP_PRODUCTS catalog"
warn "goa-shop" "empty E2E_SHOP_PRODUCTS catalog"
else
if [ "$PICK_N" -gt "$SHOP_CAT_N" ]; then PICK_N=$SHOP_CAT_N; fi
# Shuffle catalog, take first PICK_N (portable: python)
@ -1451,26 +1245,24 @@ random.shuffle(lines)
print('\n'.join(lines[:n]))
" "$PICK_N"
)
info "shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · default instance ${E2E_SHOP_INSTANCE}"
info "shop payto" "$E2E_SHOP_PAYTO"
info "shop pick" "$(printf '%s\n' "$SHOP_PICKED" | cut -d'|' -f1,2 | tr '\n' '; ' | sed 's/; $//')"
# e2e_one_pay_public_template uses global INST — pin per product if 4th field set
info "goa-shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · instance ${E2E_SHOP_INSTANCE}"
info "goa-shop payto" "$E2E_SHOP_PAYTO"
info "goa-shop pick" "$(printf '%s\n' "$SHOP_PICKED" | cut -d'|' -f1,2 | tr '\n' '; ' | sed 's/; $//')"
# e2e_one_pay_public_template uses global INST — temporarily pin shop instance
_E2E_INST_SAVE="$INST"
INST="${E2E_SHOP_INSTANCE}"
while IFS= read -r line; do
[ -z "$line" ] && continue
e2e_over && { warn "shop" "time budget low — stopping product pays"; break; }
e2e_over && { warn "goa-shop" "time budget low — stopping product pays"; break; }
pid=$(printf '%s' "$line" | cut -d'|' -f1)
pname=$(printf '%s' "$line" | cut -d'|' -f2)
pamt=$(printf '%s' "$line" | cut -d'|' -f3)
pinst=$(printf '%s' "$line" | cut -d'|' -f4)
[ -z "$pid" ] || [ -z "$pamt" ] && continue
[ -z "$pname" ] && pname="$pid"
INST="${pinst:-${E2E_SHOP_INSTANCE}}"
av_now=$(wallet_avail_num)
pay_n=$(python3 -c 'import sys; print(float(sys.argv[1].split(":",1)[-1]))' "$pamt" 2>/dev/null || echo 0)
if ! python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) + 1e-12 >= float(sys.argv[2]) else 1)" "$av_now" "$pay_n" 2>/dev/null; then
warn "shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})"
warn "goa-shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})"
SHOP_SKIP_N=$((SHOP_SKIP_N + 1))
SHOP_REPORT="${SHOP_REPORT}${SHOP_REPORT:+ }${pname}=SKIP(bal)"
continue
@ -1494,9 +1286,9 @@ $SHOP_PICKED
EOF
INST="$_E2E_INST_SAVE"
unset _E2E_INST_SAVE
info "shop summary" "$SHOP_REPORT (ok=$SHOP_OK_N fail=$SHOP_FAIL_N skip=$SHOP_SKIP_N · pick $PICK_N/$SHOP_CAT_N)"
info "goa-shop summary" "$SHOP_REPORT (ok=$SHOP_OK_N fail=$SHOP_FAIL_N skip=$SHOP_SKIP_N · pick $PICK_N/$SHOP_CAT_N)"
if [ "$SHOP_OK" != "1" ] && [ "$SHOP_FAIL_N" -gt 0 ]; then
warn "shop" "no sample product payment succeeded ($SHOP_REPORT)"
warn "goa-shop" "no sample product payment succeeded ($SHOP_REPORT)"
fi
section "e2e · load snapshot (after shop pays)"
metrics_report_load "${METRICS_DIR}/load-after-shop.json" "after-shop" || true
@ -1505,7 +1297,6 @@ EOF
fi
# ---------------------------------------------------------------------------
set_group paivana
section "e2e · paivana paywall (template GOA:4200)"
# ---------------------------------------------------------------------------
# Public paywall: https://paivana.hacktivism.ch → 302 to merchant template "paivana".
@ -1597,12 +1388,11 @@ else
info "paivana summary" "${PAIVANA_REPORT:-?} · instance ${E2E_PAIVANA_INSTANCE}"
fi
set_group report
section "e2e · report"
info "user" "$USER"
info "ATM withdraws" "$WITHDRAW_REPORT"
info "payments" "$PAY_REPORT"
info "shop" "${SHOP_REPORT:-(n/a)}"
info "goa-shop" "${SHOP_REPORT:-(n/a)}"
info "paivana" "${PAIVANA_REPORT:-(n/a)}"
info "final balance" "$(fmt_bal "$SCRATCH/bal-live.out" 2>/dev/null || echo "(n/a)")"
info "scratch" "$SCRATCH"

View file

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

View file

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

View file

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

View file

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

View file

@ -5,9 +5,7 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib.sh
source "$ROOT/lib.sh"
# Area www.* — public HTTPS (outside-in)
# Groups: www.exchange / www.perf / www.stats / www.bank / www.merchant /
# www.paivana / www.landing
# Area www-### — public HTTPS (outside-in)
set_area www
section "www · public URLs · ${TALER_DOMAIN:-?} (outside-in, no SSH)"
@ -171,8 +169,7 @@ sys.exit(0)
PY
}
# --- exchange (core; always required) --- www.exchange-NN
set_group exchange
# --- exchange (core; always required) --- www-001 …
check_url "exchange /config" 200 "$EXCHANGE_PUBLIC/config"
code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ec.json")
if [ "$code" = "200" ]; then
@ -222,7 +219,6 @@ fi
# Performance — outside-in public HTTPS latency (this runner, not loopback)
# Same spirit as bank landing-stats public probes; measured here from outside.
# ---------------------------------------------------------------------------
set_group perf
section "www · performance · public HTTPS latency (outside-in)"
# Latency thresholds (ms), outside-in from this runner.
@ -294,18 +290,11 @@ fi
info "perf note" "RTT measured from this host (outside-in)"
# ---------------------------------------------------------------------------
# Landing stats.json (public outside-in) — freshness + display fields.
# Timestamps (required for external measurement):
# generated_at_unix — epoch seconds (preferred)
# generated_at — ISO-8601
# generated_at_human — footer / “updated” UI string
# Bank collector always sets all three; exchange/merchant scripts set unix too.
# Env:
# STATS_STALE_SECS warn if age > this (default 900 = 15m; timer often 515m)
# STATS_FAIL_SECS hard fail if age > this (default 3600); 0 = never hard-fail on age
# Landing load stats (stats.json written *inside* containers, usually public).
# Prefer HTTPS /intro/stats.json (no SSH). If missing and SSH works, cat from
# the container. Report loadavg + RSS + in-container probe ms — one line each.
# ---------------------------------------------------------------------------
: "${STATS_STALE_SECS:=900}"
: "${STATS_FAIL_SECS:=3600}"
: "${STATS_STALE_SECS:=900}" # warn if generated_at older than 15m
_fetch_landing_stats_json() {
# $1=name bank|exchange|merchant $2=base URL → writes $tmp/stats-$1.json, exit 0 if ok
@ -333,265 +322,81 @@ _fetch_landing_stats_json() {
return 1
}
# Validate one stats.json for monitoring display + outside age measurement.
# Prints lines: OK|WARN|ERR|STALE|FAIL <detail>
# Sets $tmp/stats-meta-$name.txt with gen_unix|source for shared-feed compare.
check_landing_stats_json() {
report_landing_load_stats() {
local name="$1" base="$2"
local f="$tmp/stats-${name}.json"
local line hard_missing=0
local line age_s
if ! _fetch_landing_stats_json "$name" "$base"; then
if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
fail "stats ${name}" "HTTP/body missing · ${base}/intro/stats.json"
else
warn "stats ${name}" "stats.json not available"
warn "perf ${name} landing-stats" "stats.json not available (public or SSH)"
fi
return 1
return
fi
ok "stats ${name} reachable" "${base}/intro/stats.json"
# Multi-line verdict from python (STATUS detail)
while IFS= read -r line; do
case "$line" in
META\ *)
printf '%s\n' "${line#META }" >"$tmp/stats-meta-${name}.txt"
;;
OK\ *)
ok "stats ${name}" "${line#OK }"
;;
WARN\ *)
warn "stats ${name}" "${line#WARN }"
;;
STALE\ *)
warn "stats ${name} freshness" "stale · ${line#STALE }"
;;
FAIL\ *)
fail "stats ${name}" "${line#FAIL }"
hard_missing=1
;;
ERR\ *)
if [ "${LOCAL_STACK:-0}" = "1" ] || [ "${CHECK_LANDING:-1}" = "1" ]; then
fail "stats ${name}" "${line#ERR }"
else
warn "stats ${name}" "${line#ERR }"
fi
hard_missing=1
;;
*)
[ -n "$line" ] && warn "stats ${name}" "unparsed · $line"
;;
esac
done < <(python3 - "$f" "$name" "${STATS_STALE_SECS}" "${STATS_FAIL_SECS}" <<'PY' 2>/dev/null || echo "ERR python check failed"
import json, sys, time, re
from datetime import datetime, timezone
path, role, stale_s, fail_s = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4])
now = int(time.time())
def out(kind, msg):
print("%s %s" % (kind, msg))
line=$(python3 - "$f" "${STATS_STALE_SECS}" <<'PY' 2>/dev/null || true
import json, sys, time
path, stale = sys.argv[1], int(sys.argv[2])
try:
d = json.load(open(path))
except Exception as e:
out("ERR", "JSON parse: %s" % str(e)[:100])
print("ERR parse:" + str(e)[:80])
sys.exit(0)
if not isinstance(d, dict):
out("ERR", "body is not a JSON object")
if not d.get("ok", True):
print("ERR ok=false " + str(d.get("error") or "")[:80])
sys.exit(0)
if d.get("ok") is False:
out("ERR", "ok=false · %s" % str(d.get("error") or d.get("hint") or "")[:80])
p = d.get("performance") or {}
if not isinstance(p, dict) or not p:
print("ERR no performance block")
sys.exit(0)
# --- timestamps (outside-in age measurement) ---
gu = d.get("generated_at_unix")
giso = d.get("generated_at") or d.get("generated_at_iso")
ghum = d.get("generated_at_human")
age = None
src_u = None
if gu is not None and str(gu).strip() != "":
try:
src_u = int(float(gu))
# tolerate ms accidental timestamps
if src_u > 10_000_000_000:
src_u //= 1000
age = now - src_u
except Exception:
out("ERR", "generated_at_unix not numeric: %r" % gu)
src_u = None
elif giso:
# parse ISO when unix missing (legacy exchange/merchant before unix field)
try:
s = str(giso).strip()
if s.endswith("Z"):
s = s[:-1] + "+00:00"
# allow +0200 without colon
s = re.sub(r"([+-]\d{2})(\d{2})$", r"\1:\2", s)
dt = datetime.fromisoformat(s)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
src_u = int(dt.timestamp())
age = now - src_u
out("WARN", "no generated_at_unix — derived age from generated_at ISO (deploy collector with unix)")
except Exception as e:
out("ERR", "cannot parse generated_at=%r (%s)" % (giso, e))
else:
out("ERR", "missing generated_at_unix and generated_at — cannot measure freshness outside")
if not ghum and not giso:
out("ERR", "missing generated_at_human/generated_at — UI cannot show “updated” time")
elif ghum:
out("OK", "timestamp human=%s" % ghum)
else:
out("OK", "timestamp iso=%s" % giso)
if src_u is not None:
out("META", "%s|%s|%s" % (src_u, d.get("source") or "", d.get("currency") or ""))
if age is not None and age < -120:
out("WARN", "clock skew · generated_at_unix in the future by %ss" % (-age))
if age is not None:
out("OK", "generated_at_unix=%s age=%ss (now=%s)" % (src_u, age, now))
if fail_s > 0 and age > fail_s:
out("FAIL", "age=%ss > STATS_FAIL_SECS=%s · collector not updating" % (age, fail_s))
elif age > stale_s:
out("STALE", "age=%ss > STATS_STALE_SECS=%s · timer lag or stuck publish" % (age, stale_s))
# --- display fields for landings / monitoring ---
src = str(d.get("source") or "")
# Bank-shaped (incl. stage shared feed on all three landings)
bankish = (
role == "bank"
or "collect_bank_stats" in src
or "shared" in src
or isinstance(d.get("withdraws"), dict)
or isinstance(d.get("bank_accounts"), dict)
)
if bankish:
missing = []
if not isinstance(d.get("withdraws"), dict):
missing.append("withdraws")
ba = d.get("bank_accounts")
wl = d.get("wallets")
if not isinstance(ba, dict) and not isinstance(wl, dict):
missing.append("bank_accounts|wallets")
if d.get("currency") in (None, ""):
missing.append("currency")
if missing:
out("ERR", "bank-shaped display missing: %s" % ",".join(missing))
else:
w = d.get("withdraws") or {}
ba = ba if isinstance(ba, dict) else {}
n_acc = ba.get("total") if ba.get("total") is not None else ba.get("users")
out(
"OK",
"display bank-shape currency=%s accounts=%s withdraws.count=%s source=%s"
% (d.get("currency"), n_acc, w.get("count"), (src or "?")[:48]),
)
elif src == "exchange-db" or (role == "exchange" and "reserves" in d):
need = []
for k in ("reserves", "wire_in_count", "known_coins"):
if k not in d and k.replace("known_coins", "coins_live") not in d:
if k == "known_coins" and ("coins_live" in d or "coins_remaining_amount" in d):
continue
need.append(k)
if need:
out("WARN", "exchange display keys missing: %s" % ",".join(need))
else:
out(
"OK",
"display exchange-db reserves=%s wire_in=%s coins=%s"
% (d.get("reserves"), d.get("wire_in_count"), d.get("known_coins") or d.get("coins_live")),
)
elif src == "merchant-db" or (role == "merchant" and "instances" in d):
if "instances" not in d and "orders" not in d:
out("ERR", "merchant display missing instances/orders")
else:
out(
"OK",
"display merchant-db instances=%s orders=%s paid=%s"
% (d.get("instances"), d.get("orders"), d.get("paid")),
)
else:
out("WARN", "unknown stats schema role=%s source=%s keys=%s" % (role, src[:40], ",".join(list(d.keys())[:8])))
# performance block optional (stage shared bank feed may omit heavy probes)
p = d.get("performance")
if isinstance(p, dict) and p:
mem = p.get("memory") if isinstance(p.get("memory"), dict) else {}
rss = mem.get("container_rss_human") or mem.get("proc_sum_rss_human") or "—"
load = p.get("loadavg") or "—"
bits = []
for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"):
mem = p.get("memory") if isinstance(p.get("memory"), dict) else {}
rss = mem.get("container_rss_human") or mem.get("proc_sum_rss_human") or "?"
load = p.get("loadavg") or "?"
# latency fields differ by site
bits = []
for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"):
if k in p and p[k] is not None:
bits.append("%s=%s" % (k.replace("_ms", ""), p[k]))
lat = " ".join(bits) if bits else "latency=—"
out("OK", "performance loadavg=%s RSS=%s %s" % (load, rss, lat))
else:
out("OK", "performance block optional/absent")
bits.append("%s=%sms" % (k.replace("_ms", ""), p[k]))
lat = " ".join(bits) if bits else "latency=?"
gen = d.get("generated_at_human") or d.get("generated_at") or "?"
# staleness
age = ""
try:
gu = d.get("generated_at_unix")
if gu is not None:
age_s = int(time.time()) - int(gu)
age = " age=%ss" % age_s
if age_s > stale:
print("STALE loadavg=%s RSS=%s %s gen=%s%s" % (load, rss, lat, gen, age))
sys.exit(0)
except Exception:
pass
print("OK loadavg=%s RSS=%s %s gen=%s%s" % (load, rss, lat, gen, age))
PY
)
return 0
case "$line" in
OK\ *)
ok "perf ${name} landing-stats" "${line#OK }"
;;
STALE\ *)
warn "perf ${name} landing-stats" "stale · ${line#STALE }"
;;
ERR\ *)
warn "perf ${name} landing-stats" "${line#ERR }"
;;
*)
warn "perf ${name} landing-stats" "unreadable stats.json"
;;
esac
}
if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
set_group stats
section "www · landing stats.json (freshness + display fields · outside-in)"
info "stats policy" "STALE≥${STATS_STALE_SECS}s WARN · FAIL≥${STATS_FAIL_SECS}s ERROR (0=disable fail) · need generated_at_unix"
check_landing_stats_json "bank" "$BANK_PUBLIC" || true
check_landing_stats_json "exchange" "$EXCHANGE_PUBLIC" || true
check_landing_stats_json "merchant" "$MERCHANT_PUBLIC" || true
# Shared feed: stage-style publish copies same bank stats to all three URLs
if [ -f "$tmp/stats-meta-bank.txt" ] && [ -f "$tmp/stats-meta-exchange.txt" ] && [ -f "$tmp/stats-meta-merchant.txt" ]; then
shared_line=$(python3 - "$tmp/stats-meta-bank.txt" "$tmp/stats-meta-exchange.txt" "$tmp/stats-meta-merchant.txt" <<'PY' 2>/dev/null || true
import sys
def parse(p):
try:
t = open(p).read().strip().split("|", 2)
return t[0], (t[1] if len(t) > 1 else ""), (t[2] if len(t) > 2 else "")
except Exception:
return "", "", ""
b, bs, bc = parse(sys.argv[1])
e, es, ec = parse(sys.argv[2])
m, ms, mc = parse(sys.argv[3])
if not b or not e or not m:
print("SKIP incomplete meta")
elif b == e == m:
print("SHARED gen_unix=%s bank_src=%s" % (b, (bs or "?")[:50]))
else:
# Shared feed only when all three claim the same bank-collector source
# (e.g. stage TESTPAYSAN "… shared"). GOA uses independent exchange-db /
# merchant-db collectors — different gen_unix is expected.
def bankish(s):
s = (s or "").lower()
return "shared" in s or "collect_bank_stats" in s
if bankish(bs) and bankish(es) and bankish(ms):
print("DRIFT bank=%s exchange=%s merchant=%s (expected equal for shared feed)" % (b, e, m))
else:
print("INDEPENDENT bank=%s exchange=%s merchant=%s" % (b, e, m))
PY
)
case "$shared_line" in
SHARED\ *)
ok "stats shared feed" "${shared_line#SHARED }"
;;
DRIFT\ *)
warn "stats shared feed" "timestamps differ · ${shared_line#DRIFT }"
;;
INDEPENDENT\ *)
info "stats feeds" "independent collectors · ${shared_line#INDEPENDENT }"
;;
*)
info "stats feeds" "${shared_line:-n/a}"
;;
esac
fi
info "stats note" "public GET /intro/stats.json; measure age via generated_at_unix vs wall clock"
section "www · performance · landing load stats (stats.json · in-container probes)"
report_landing_load_stats "bank" "$BANK_PUBLIC"
report_landing_load_stats "exchange" "$EXCHANGE_PUBLIC"
report_landing_load_stats "merchant" "$MERCHANT_PUBLIC"
info "perf landing-stats note" "from /intro/stats.json (public); SSH container fallback if needed"
fi
# Rollup + optional JSON for metrics_print_overall
if [ -s "$PERF_TSV" ]; then
@ -665,8 +470,7 @@ fi
info "perf note" "measured from this host (outside-in); thresholds PERF_WARN_MS=${PERF_WARN_MS} PERF_FAIL_MS=${PERF_FAIL_MS}"
# Exchange terms + privacy (same www.exchange group — issues map to exchange)
set_group exchange
# Terms + privacy (legal docs)
check_legal_doc "exchange /terms" "$EXCHANGE_PUBLIC/terms" "terms|GOA|exploration|FADP|revDSG|privacy"
check_legal_doc "exchange /privacy" "$EXCHANGE_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|exploration"
# trailing slash: 200 or redirect to bare path
@ -681,7 +485,6 @@ case "$code" in
esac
# --- bank ---
set_group bank
if [ "${LOCAL_STACK:-1}" = "0" ]; then
check_url_soft "bank /config" 200 "$BANK_PUBLIC/config"
else
@ -705,7 +508,7 @@ if [ "$code" = "200" ]; then
check_url_soft "bank /webui/" 200 "$BANK_PUBLIC/webui/"
check_url_soft "bank /intro/" 200 "$BANK_PUBLIC/intro/"
check_url_soft "bank /" 302,301,200 "$BANK_PUBLIC/"
# Auto-account: GOA shared-pool mint (hacktivism). Not used on TESTPAYSAN stage.
# Auto-account: credentials + shared-pool taler://withdraw (like step 2)
aa_code=$(http_body "$BANK_PUBLIC/intro/auto-account.json" "$tmp/aa.json")
case "$aa_code" in
200)
@ -715,22 +518,11 @@ if [ "$code" = "200" ]; then
fail "bank /intro/auto-account.json" "invalid withdraw/login ($(tr '\n' ' ' <"$tmp/aa-val" | sed 's/[[:space:]]*$//'))"
fi
;;
404|405|501)
if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
405|501|404|502|503|000)
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"
else
warn "bank /intro/auto-account.json" "HTTP $aa_code (optional off-GOA)"
fi
;;
esac
fi
@ -753,7 +545,6 @@ else
fi
# --- merchant ---
set_group merchant
if [ "${LOCAL_STACK:-1}" = "0" ]; then
check_url_soft "merchant /config" 200 "$MERCHANT_PUBLIC/config"
else
@ -819,7 +610,6 @@ esac
# Paivana paywall (local GOA stack) — public front only; pay path is e2e
# ---------------------------------------------------------------------------
if [ "${LOCAL_STACK:-1}" = "1" ] && [ "${E2E_PAIVANA:-1}" != "0" ]; then
set_group paivana
section "www · paivana paywall"
: "${PAIVANA_PUBLIC:=https://paivana.hacktivism.ch}"
PAIVANA_PUBLIC="${PAIVANA_PUBLIC%/}"
@ -856,7 +646,6 @@ if [ "${CHECK_LANDING:-1}" != "1" ]; then
exit 0
fi
set_group landing
section "www · landing exposed links · bank / merchant / exchange"
# Probe one URL: print code to stdout (200 after following redirects counts as 200).
@ -1014,24 +803,18 @@ check_one_landing() {
return
fi
# Required static: qrcode.min.js. GOA og image is soft off-GOA (stage has no og-goa-shop.png).
if _landing_probe "${base}/intro/qrcode.min.js"; then
# Required static assets (hard: qrcode + og; soft: qr-logo)
for url in \
"${base}/intro/qrcode.min.js" \
"${base}/intro/og-goa-shop.png"
do
if _landing_probe "$url"; then
a_ok=$((a_ok + 1))
else
a_fail=$((a_fail + 1))
fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} ${base}/intro/qrcode.min.js"
fi
if _landing_probe "${base}/intro/og-goa-shop.png"; then
a_ok=$((a_ok + 1))
else
if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
a_fail=$((a_fail + 1))
fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} ${base}/intro/og-goa-shop.png"
else
a_soft=$((a_soft + 1))
soft_sample="${soft_sample}${soft_sample:+; }HTTP ${_landing_code} og-goa-shop.png (GOA asset)"
fi
fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} $url"
fi
done
if _landing_probe "${base}/intro/qr-logo.png"; then
a_ok=$((a_ok + 1))
else
@ -1159,15 +942,7 @@ if [ "${LOCAL_STACK:-1}" = "1" ] || [ -n "${BANK_PUBLIC:-}" ]; then
fail "bank /intro/demo-withdraw.json" "invalid taler://withdraw ($(tr '\n' ' ' <"$tmp/dw-val" | sed 's/[[:space:]]*$//'))"
fi
;;
404|405|501)
# GOA bank landing mints demo-withdraw.json; stage TESTPAYSAN uses ATM/wallet guide only
if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)"
else
info "landing bank demo-withdraw" "HTTP $dw_code — skip (not a GOA demo-withdraw stack)"
fi
;;
502|503|000)
405|501|404|502|503|000)
fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)"
;;
*)

View file

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

View file

@ -193,13 +193,6 @@ apply_taler_domain() {
PAY_AMT="${PAY_AMT:-${EXPECT_CURRENCY}:0.01}"
CREDIT_AMT="${CREDIT_AMT:-${EXPECT_CURRENCY}:100}"
;;
TESTPAYSAN)
# Stage FrancPaysan: default instance + public templates (e2e-001/005/1)
MERCHANT_INSTANCE="${MERCHANT_INSTANCE:-default}"
WITHDRAW_AMT="${WITHDRAW_AMT:-TESTPAYSAN:20}"
PAY_AMT="${PAY_AMT:-TESTPAYSAN:0.01}"
CREDIT_AMT="${CREDIT_AMT:-TESTPAYSAN:100}"
;;
CHF)
WITHDRAW_AMT="${WITHDRAW_AMT:-CHF:20}"
PAY_AMT="${PAY_AMT:-CHF:0.01}"
@ -359,30 +352,17 @@ koopa_ssh_python() {
with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" python3 -
}
# ANSI colours — boxed badges (fg+bg) + body (label bold, detail dim).
# Off: NO_COLOR=1 or CLICOLOR=0.
# ANSI colours for tags (green OK / yellow WARN / red ERROR·BLOCKER / cyan INFO).
# Default: always on so pasted logs keep visible tags. Disable: NO_COLOR=1 or CLICOLOR=0.
if [ "${NO_COLOR:-0}" = "1" ] || [ "${CLICOLOR:-1}" = "0" ]; then
G= R= Y= C= M= D= W= B= N=
BG_OK= BG_INFO= BG_WARN= BG_ERR= BG_BLK= BG_SEC=
BOX=0
G= R= Y= C= N= B=
else
G=$'\e[1;32m' # green text (totals)
R=$'\e[1;31m' # red text
Y=$'\e[1;33m' # yellow text
C=$'\e[1;36m' # cyan text
M=$'\e[1;35m' # magenta text
D=$'\e[2m' # dim tid / detail
W=$'\e[1;37m' # bold white label
B=$'\e[1m' # bold section
N=$'\e[0m' # reset
# Badge fill: bright text on solid background
BG_OK=$'\e[1;30;42m' # black on green
BG_INFO=$'\e[1;30;46m' # black on cyan
BG_WARN=$'\e[1;30;43m' # black on yellow
BG_ERR=$'\e[1;37;41m' # white on red
BG_BLK=$'\e[1;37;45m' # white on magenta
BG_SEC=$'\e[1;37;44m' # white on blue (section)
BOX=1
G=$'\e[1;32m' # bold green [OK]
R=$'\e[1;31m' # bold red [ERROR] [BLOCKER]
Y=$'\e[1;33m' # bold yellow [WARN]
C=$'\e[1;36m' # bold cyan [INFO]
B=$'\e[1m'
N=$'\e[0m'
fi
PASS_N=0
@ -392,68 +372,17 @@ INFO_N=0
BLOCKERS=() # human-readable payment/withdraw blockers
ERRORS=() # all ERROR lines (component scope)
# Grouped test IDs: area.group-NN (e.g. www.exchange-01, e2e.pay-03)
# set_area www # phase: www | e2e | inside | versions | …
# set_group exchange # resets counter + prints group box → www.exchange-01
# set_group bank # → www.bank-01
# Without set_group: area-01, area-02, … (flat within area)
# Test IDs by area: www-001, e2e-001, inside-001, …
# Usage: set_area www then each ok/fail/warn/info/blocker/err auto-numbers.
TEST_AREA=""
TEST_GROUP=""
TEST_N=0
# Last issued id (www-001); set by _take_tid — not via $(…) so TEST_N persists.
LAST_TID=""
# Per-group counters so re-entering www.exchange after perf continues NN
# (shell vars TEST_CNT_<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() {
# leave previous group counter saved
_tid_save
TEST_AREA="$1"
TEST_GROUP=""
TEST_N=0
LAST_TID=""
}
# Start a logical sub-group. Short stable names:
# exchange bank merchant perf stats landing paivana
# prereq wallet atm settle pay shop dig load report
# ssh caddy outside inside compare withdraw
# Counter continues if the same group is re-entered later in the area.
# Prints a compact group chip so log + issue text map cleanly (www.bank-03).
set_group() {
local g="$1"
# no-op if same group already active (e.g. inside emit loop)
if [ "$g" = "${TEST_GROUP:-}" ] && [ -n "$g" ]; then
return 0
fi
_tid_save
TEST_GROUP="$g"
LAST_TID=""
if [ -z "${TEST_AREA:-}" ] || [ -z "$g" ]; then
TEST_N=0
return 0
fi
_tid_load "$g"
local tag="${TEST_AREA}.${g}"
if [ "${BOX:-0}" = "1" ]; then
# cyan-outline group chip: ┌ www.exchange ┐
printf '%s%s┌ %s ┐%s\n' "$D" "$C" "$tag" "$N"
else
printf -- '-- %s --\n' "$tag"
fi
}
# Assign next id into LAST_TID (must not run in a subshell).
_take_tid() {
LAST_TID=""
@ -461,69 +390,49 @@ _take_tid() {
return
fi
TEST_N=$((TEST_N + 1))
if [ -n "${TEST_GROUP:-}" ]; then
LAST_TID=$(printf '%s.%s-%02d' "$TEST_AREA" "$TEST_GROUP" "$TEST_N")
_tid_save
else
LAST_TID=$(printf '%s-%02d' "$TEST_AREA" "$TEST_N")
fi
LAST_TID=$(printf '%s-%03d' "$TEST_AREA" "$TEST_N")
}
# Dim test id: "www.exchange-01 " or empty
_fmt_tid() {
# prefix "www-001 " or empty
if [ -n "${LAST_TID:-}" ]; then
printf '%s%s%s ' "$D" "$LAST_TID" "$N"
fi
}
# Boxed badge cell: ┌ OK ┐ with filled bg (tag left-padded, width 7 for BLOCKER)
# $1=badge style $2=tag text
_fmt_badge() {
local badge="$1" tag="$2" inner
inner=$(printf -- '%-7s' "$tag")
if [ "${BOX:-0}" = "1" ]; then
printf '%s┌ %s┐%s' "$badge" "$inner" "$N"
else
printf '%s[ %s]%s' "$badge" "$inner" "$N"
fi
}
# One concrete line: ┌ OK ┐ tid label · detail
# $1=badge style $2=tag text $3=label colour $4=label $5=detail
_msg_line() {
local badge="$1" tag="$2" lcol="$3" label="$4" detail="${5:-}"
if [ -n "$detail" ]; then
printf -- '%s %s%s%s%s %s·%s %s%s%s\n' \
"$(_fmt_badge "$badge" "$tag")" "$(_fmt_tid)" "$lcol" "$label" "$N" "$D" "$N" "$D" "$detail" "$N"
else
printf -- '%s %s%s%s%s\n' \
"$(_fmt_badge "$badge" "$tag")" "$(_fmt_tid)" "$lcol" "$label" "$N"
printf '%s ' "$LAST_TID"
fi
}
ok() {
# ok "what is good" ["concrete evidence: HTTP 200 · 12ms · …"]
# Forms (same idea as info/warn):
# ok "what passed"
# ok "what passed" "detail / ms / bytes / …"
local label="$1" detail="${2:-}"
_take_tid
_msg_line "$BG_OK" "OK" "$W" "$label" "$detail"
if [ -n "$detail" ]; then
printf '%s[OK]%s %s%s — %s\n' "$G" "$N" "$(_fmt_tid)" "$label" "$detail"
else
printf '%s[OK]%s %s%s\n' "$G" "$N" "$(_fmt_tid)" "$label"
fi
PASS_N=$((PASS_N + 1))
}
# component-scoped error: err bank "what failed" "why / HTTP / path"
# component-scoped error: err bank "libeufin down" "detail"
err() {
local comp="$1" msg="$2" detail="${3:-}"
_take_tid
_msg_line "$BG_ERR" "ERROR" "$W" "${comp}: ${msg}" "$detail"
printf '%s[ERROR]%s %s%s: %s%s\n' "$R" "$N" "$(_fmt_tid)" "$comp" "$msg" "${detail:+ — $detail}"
FAIL_N=$((FAIL_N + 1))
ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ · $detail}")
ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ — $detail}")
}
# fail "what failed" ["why / HTTP code / path"]
# legacy fail label ...
fail() {
local label="$1" detail="${2:-}"
_take_tid
_msg_line "$BG_ERR" "ERROR" "$W" "$label" "$detail"
printf '%s[ERROR]%s %s%s%s\n' "$R" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}"
FAIL_N=$((FAIL_N + 1))
ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ · $detail}")
ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ $detail}")
}
warn() {
# warn "what is soft-bad" ["why still ok to continue"]
# warn component "what" "why"
# Forms (same idea as err; always try to state the problem):
# warn "problem"
# warn "problem" "why / context"
# warn component "problem" "why / context"
local a1="${1:-}" a2="${2:-}" a3="${3:-}"
local head detail
_take_tid
@ -537,78 +446,56 @@ warn() {
head="$a1"
detail=""
fi
_msg_line "$BG_WARN" "WARN" "$W" "$head" "$detail"
if [ -n "$detail" ]; then
printf '%s[WARN]%s %s%s — %s\n' "$Y" "$N" "$(_fmt_tid)" "$head" "$detail"
else
printf '%s[WARN]%s %s%s\n' "$Y" "$N" "$(_fmt_tid)" "$head"
fi
WARN_N=$((WARN_N + 1))
}
info() {
# info "topic" ["concrete fact / value / next step"]
local label="$1" detail="${2:-}"
_take_tid
_msg_line "$BG_INFO" "INFO" "$W" "$label" "$detail"
if [ -n "$detail" ]; then
printf '%s[INFO]%s %s%s — %s\n' "$C" "$N" "$(_fmt_tid)" "$label" "$detail"
else
printf '%s[INFO]%s %s%s\n' "$C" "$N" "$(_fmt_tid)" "$label"
fi
INFO_N=$((INFO_N + 1))
}
blocker() {
# Hard stop on pay/withdraw path: blocker "step" "why it cannot continue"
# Payment/withdraw path cannot proceed because of this
local step="$1" msg="$2"
_take_tid
_msg_line "$BG_BLK" "BLOCKER" "$W" "${step}" "$msg"
printf '%s[BLOCKER]%s %s%s: %s\n' "$R$B" "$N" "$(_fmt_tid)" "$step" "$msg"
BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg")
FAIL_N=$((FAIL_N + 1))
ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg")
}
section() {
# Boxed section header (3 lines when colour on)
local title="$*"
local w=${#title}
[ "$w" -lt 24 ] && w=24
[ "$w" -gt 56 ] && w=56
local pad line i
pad=$(printf -- "%-${w}s" "$title")
if [ "${BOX:-0}" = "1" ]; then
line=""
i=0
while [ "$i" -lt $((w + 2)) ]; do
line="${line}"
i=$((i + 1))
done
printf -- '\n%s╔%s╗%s\n' "$BG_SEC" "$line" "$N"
printf -- '%s║%s %s%s%s %s║%s\n' "$BG_SEC" "$N" "$B" "$pad" "$N" "$BG_SEC" "$N"
printf -- '%s╚%s╝%s\n' "$BG_SEC" "$line" "$N"
else
printf -- '\n== %s ==\n' "$title"
fi
}
section() { printf '\n%s== %s ==%s\n' "$B" "$*" "$N"; }
summary() {
echo ""
if [ "${#BLOCKERS[@]}" -gt 0 ]; then
if [ "${BOX:-0}" = "1" ]; then
printf -- '%s┌ BLOCKERS · pay/withdraw cannot finish ┐%s\n' "$BG_BLK" "$N"
else
printf -- '--- BLOCKERS (pay/withdraw cannot finish) ---\n'
fi
printf '%s--- BLOCKERS (fix/withdraw path) ---%s\n' "$R$B" "$N"
local b
for b in "${BLOCKERS[@]}"; do
printf -- '%s •%s %s%s%s\n' "$M" "$N" "$W" "$b" "$N"
printf '%s • %s%s\n' "$R" "$b" "$N"
done
fi
if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then
if [ "${BOX:-0}" = "1" ]; then
printf -- '%s┌ ERRORS · failed checks ┐%s\n' "$BG_ERR" "$N"
else
printf -- '--- ERRORS (failed checks) ---\n'
fi
printf '%s--- ERRORS ---%s\n' "$R" "$N"
local e
for e in "${ERRORS[@]}"; do
case "$e" in BLOCKER*) continue ;; esac
printf -- '%s •%s %s%s%s\n' "$R" "$N" "$W" "$e" "$N"
printf '%s • %s%s\n' "$R" "$e" "$N"
done
fi
printf -- '%stotals:%s %s%d OK%s' "$D" "$N" "$G" "$PASS_N" "$N"
[ "$FAIL_N" -gt 0 ] && printf -- ', %s%d ERROR%s' "$R" "$FAIL_N" "$N"
[ "$WARN_N" -gt 0 ] && printf -- ', %s%d WARN%s' "$Y" "$WARN_N" "$N"
[ "$INFO_N" -gt 0 ] && printf -- ', %s%d INFO%s' "$C" "$INFO_N" "$N"
[ "${#BLOCKERS[@]}" -gt 0 ] && printf -- ', %s%d BLOCKER%s' "$M" "${#BLOCKERS[@]}" "$N"
printf 'totals: %s%d OK%s' "$G" "$PASS_N" "$N"
[ "$FAIL_N" -gt 0 ] && printf ', %s%d ERROR%s' "$R" "$FAIL_N" "$N"
[ "$WARN_N" -gt 0 ] && printf ', %s%d WARN%s' "$Y" "$WARN_N" "$N"
[ "$INFO_N" -gt 0 ] && printf ', %d INFO' "$INFO_N"
[ "${#BLOCKERS[@]}" -gt 0 ] && printf ', %s%d BLOCKER%s' "$R" "${#BLOCKERS[@]}" "$N"
printf '\n'
[ "$FAIL_N" -eq 0 ]
}