diff --git a/scripts/taler-bank/landing-stats-install.sh b/scripts/taler-bank/landing-stats-install.sh new file mode 100644 index 0000000..8086558 --- /dev/null +++ b/scripts/taler-bank/landing-stats-install.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Install + run landing-stats.sh *inside* the bank container, and optionally +# install a cron entry there. Documented in configs/bank-landing/README.md. +# +# Run on koopa host (root or user in podman group): +# ./landing-stats-install.sh # copy + one-shot run +# ./landing-stats-install.sh --cron # also install *minutely* cron inside container +# ./landing-stats-install.sh --run-only # only exec existing script +set -euo pipefail + +DO_CRON=0 +RUN_ONLY=0 +# cron schedule (default: every minute) +: "${LANDING_STATS_CRON:=* * * * *}" +for a in "$@"; do + case "$a" in + --cron) DO_CRON=1 ;; + --run-only) RUN_ONLY=1 ;; + -h|--help) + sed -n '2,12p' "$0" | sed 's/^# \?//' + exit 0 + ;; + esac +done + +ROOT=$(cd "$(dirname "$0")" && pwd) +SRC="$ROOT/landing-stats.sh" +[ -f "$SRC" ] || { echo "missing $SRC" >&2; exit 1; } + +# Prefer the known names from ops docs +C="" +for name in taler-hacktivism-bank taler-bank-hacktivism; do + if podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$name"; then + C=$name + break + fi +done +if [ -z "$C" ]; then + C=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -i bank | head -1 || true) +fi +[ -n "$C" ] || { echo "no bank container running" >&2; exit 1; } +echo "container=$C" + +# Host password → container /root (if not already there) +if [ -f /root/bank-explorer-password.txt ]; then + podman cp /root/bank-explorer-password.txt "$C:/root/bank-explorer-password.txt" 2>/dev/null || true +fi + +if [ "$RUN_ONLY" != "1" ]; then + podman exec "$C" mkdir -p /usr/local/bin /var/www/bank-landing + podman cp "$SRC" "$C:/usr/local/bin/landing-stats.sh" + podman exec "$C" chmod 755 /usr/local/bin/landing-stats.sh + echo "installed /usr/local/bin/landing-stats.sh" +fi + +# Ensure landing dir exists for nginx intro +podman exec "$C" mkdir -p /var/www/bank-landing + +echo "running landing-stats.sh inside $C …" +podman exec \ + -e LANDING_DIR=/var/www/bank-landing \ + -e BANK_USER=explorer \ + "$C" /usr/local/bin/landing-stats.sh + +if [ "$DO_CRON" = "1" ]; then + # default: every minute (* * * * *); override with LANDING_STATS_CRON='*/5 * * * *' + podman exec -e LANDING_STATS_CRON="$LANDING_STATS_CRON" "$C" bash -c ' + sched="${LANDING_STATS_CRON:-* * * * *}" + line="$sched TZ=Europe/Zurich LANDING_DIR=/var/www/bank-landing /usr/local/bin/landing-stats.sh >>/var/log/landing-stats.log 2>&1" + (crontab -l 2>/dev/null | grep -v landing-stats.sh; echo "$line") | crontab - + echo "cron installed:"; crontab -l | grep landing-stats + ' +fi + +echo "public check: curl -sS https://bank.hacktivism.ch/intro/stats.json | head" +# If host maps 9013 → container landing, show local file +podman exec "$C" head -c 400 /var/www/bank-landing/stats.json 2>/dev/null || true +echo diff --git a/scripts/taler-bank/landing-stats.sh b/scripts/taler-bank/landing-stats.sh new file mode 100755 index 0000000..9d3a089 --- /dev/null +++ b/scripts/taler-bank/landing-stats.sh @@ -0,0 +1,567 @@ +#!/bin/bash +# Generate public landing stats JSON for bank.hacktivism.ch intro page. +# +# *** Run INSIDE the bank container *** (taler-hacktivism-bank). +# Writes: $LANDING_DIR/stats.json → https://bank.hacktivism.ch/intro/stats.json +# +# Pure bash + curl + awk (no python). Times in Europe/Zurich (CET/CEST). +# +# Documented: configs/bank-landing/README.md +set -euo pipefail + +LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}" +# Demo funding account (for recent withdraw list / flow) +BANK_USER="${BANK_USER:-explorer}" +# Admin lists all accounts + can read other accounts' txs +ADMIN_USER="${ADMIN_USER:-admin}" +TX_DELTA="${TX_DELTA:--100}" +# Cap how many accounts we scan for wallet/reserve stats +MAX_SCAN_ACCOUNTS="${MAX_SCAN_ACCOUNTS:-80}" +export TZ="${TZ:-Europe/Zurich}" + +PASS="${BANK_PASS:-}" +ADMIN_PASS="${BANK_ADMIN_PASS:-}" + +detect_bank() { + if [ -n "${BANK_URL:-}" ]; then + echo "${BANK_URL%/}" + return + fi + local port="" conf u code + for conf in /etc/libeufin/bank-overrides.conf /etc/libeufin/libeufin-bank.conf; do + if [ -f "$conf" ]; then + port=$(grep -E '^\s*PORT\s*=' "$conf" 2>/dev/null | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}') + [ -n "$port" ] && break + fi + done + port="${port:-9012}" + for u in "http://127.0.0.1:${port}" "http://127.0.0.1:9012" "http://127.0.0.1:8080"; do + code=$(curl -sS -m 2 -o /dev/null -w '%{http_code}' "$u/config" 2>/dev/null || echo 000) + if [ "$code" = "200" ]; then + echo "$u" + return + fi + done + echo "http://127.0.0.1:${port}" +} + +BANK="$(detect_bank)" +BANK="${BANK%/}" + +read_pass_file() { + local f + for f in "$@"; do + if [ -f "$f" ] && [ -r "$f" ]; then + tr -d '\n' <"$f" + return 0 + fi + done + return 1 +} + +if [ -z "$PASS" ]; then + PASS=$(read_pass_file \ + "/root/bank-${BANK_USER}-password.txt" \ + /root/bank-explorer-password.txt \ + "/etc/libeufin/secrets/bank-${BANK_USER}-password.txt" || true) +fi +if [ -z "$ADMIN_PASS" ]; then + ADMIN_PASS=$(read_pass_file \ + /root/bank-admin-password.txt \ + /etc/libeufin/secrets/bank-admin-password.txt || true) +fi + +WORKDIR=$(mktemp -d) +trap 'rm -rf "$WORKDIR"' EXIT +OUT="$LANDING_DIR/stats.json" +RUN="$LANDING_DIR/stats-run.json" +TMP="${OUT}.tmp.$$" +mkdir -p "$LANDING_DIR" + +now_iso() { date +%Y-%m-%dT%H:%M%z | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/'; } +now_unix() { date +%s; } +now_human() { date +"%Y-%m-%d %H:%M %Z"; } +iso_from_unix() { + local u="$1" + date -d "@${u}" +%Y-%m-%dT%H:%M%z 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' \ + || date -r "${u}" +%Y-%m-%dT%H:%M%z 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' \ + || echo "" +} +# Human CEST/CET label for display: "2026-07-09 20:49 CEST" +human_from_unix() { + local u="$1" + date -d "@${u}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null \ + || date -r "${u}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null \ + || echo "" +} + +json_str() { + printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr '\n' ' ')" +} + +# Public run status for intro page. Never wipe stats.json on failure. +write_run() { + local ok_json="$1" msg="${2:-}" + cat >"$RUN" </dev/null || true + echo "error: $msg (stats.json left unchanged; stats-run.json updated)" >&2 +} + +token_for() { + local user="$1" pass="$2" out="$3" + echo '{"scope":"readonly"}' >"$WORKDIR/tok-req.json" + curl -sS -m 12 -u "${user}:${pass}" \ + -H 'Content-Type: application/json' \ + -d @"$WORKDIR/tok-req.json" \ + "${BANK}/accounts/${user}/token" >"$out" || true + awk -F'"' '/access_token/ { + for (i=1;i<=NF;i++) if ($i=="access_token") { print $(i+2); exit } + }' "$out" 2>/dev/null || true +} + +extract_field() { + # extract "key":"value" or "key": number from a JSON blob (first hit) + local key="$1" file="$2" + awk -v key="$key" ' + function between(s, k, p, rest, q2, r) { + p = index(s, "\"" k "\"") + if (p == 0) return "" + rest = substr(s, p + length(k) + 2) + while (rest ~ /^[[:space:]:]/) rest = substr(rest, 2) + if (substr(rest, 1, 1) == "\"") { + rest = substr(rest, 2) + q2 = index(rest, "\"") + if (q2 == 0) return "" + return substr(rest, 1, q2 - 1) + } + r = "" + while (rest ~ /^[0-9]/) { + r = r substr(rest, 1, 1) + rest = substr(rest, 2) + } + return r + } + { print between($0, key); exit } + ' "$file" 2>/dev/null +} + +# --- tokens --- +if [ -z "$PASS" ] && [ -z "$ADMIN_PASS" ]; then + write_err "no bank password (explorer or admin) for stats" + exit 1 +fi + +ADMIN_TOKEN="" +if [ -n "$ADMIN_PASS" ]; then + ADMIN_TOKEN=$(token_for "$ADMIN_USER" "$ADMIN_PASS" "$WORKDIR/admin-tok.json") +fi +USER_TOKEN="" +if [ -n "$PASS" ]; then + USER_TOKEN=$(token_for "$BANK_USER" "$PASS" "$WORKDIR/user-tok.json") +fi +# Prefer admin for everything when available +AUTH_TOKEN="${ADMIN_TOKEN:-$USER_TOKEN}" +if [ -z "$AUTH_TOKEN" ]; then + write_err "token failed (admin/explorer)" + exit 1 +fi + +# --- account list --- +ACCOUNTS_N=0 +ACCOUNTS_USERS=0 +: >"$WORKDIR/usernames.txt" +if [ -n "$ADMIN_TOKEN" ]; then + curl -sS -m 20 -H "Authorization: Bearer ${ADMIN_TOKEN}" \ + "${BANK}/accounts?delta=-${MAX_SCAN_ACCOUNTS}" >"$WORKDIR/accounts.json" || true + # usernames from "username":"..." + tr -d '\n' <"$WORKDIR/accounts.json" \ + | sed 's/},{/}\n{/g' \ + | while IFS= read -r line; do + u=$(printf '%s' "$line" | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + [ -n "$u" ] && echo "$u" + done >"$WORKDIR/usernames.txt" || true + ACCOUNTS_N=$(grep -cve '^\s*$' "$WORKDIR/usernames.txt" 2>/dev/null || echo 0) + ACCOUNTS_USERS=$(grep -Eve '^(admin|exchange)$' "$WORKDIR/usernames.txt" 2>/dev/null | grep -cve '^\s*$' || echo 0) +else + echo "$BANK_USER" >"$WORKDIR/usernames.txt" + ACCOUNTS_N=1 + ACCOUNTS_USERS=1 +fi + +# --- scan txs: distinguish incoming (credit) vs withdraw (Taler debit) --- +# all-wd.tsv: amount \t t_s \t subject \t username \t reserve +# all-in.tsv: amount \t t_s \t subject \t username +# flow.tsv: kind \t amount_num kind = incoming | withdraw | other_out +: >"$WORKDIR/all-wd.tsv" +: >"$WORKDIR/all-in.tsv" +: >"$WORKDIR/flow.tsv" +# Scan customer accounts only (not exchange/admin — exchange credits would double-count) +{ + echo "$BANK_USER" + grep -Eve "^(admin|exchange|${BANK_USER})$" "$WORKDIR/usernames.txt" 2>/dev/null || true +} | awk 'NF && !seen[$0]++' | head -n "$MAX_SCAN_ACCOUNTS" >"$WORKDIR/scan-users.txt" + +while IFS= read -r uname; do + [ -n "$uname" ] || continue + case "$uname" in admin|exchange) continue ;; esac + curl -sS -m 12 -H "Authorization: Bearer ${AUTH_TOKEN}" \ + "${BANK}/accounts/${uname}/transactions?delta=${TX_DELTA}" \ + >"$WORKDIR/tx-${uname}.json" 2>/dev/null || continue + tr -d '\n' <"$WORKDIR/tx-${uname}.json" \ + | sed 's/},{/}\n{/g' \ + >"$WORKDIR/tx-lines.txt" 2>/dev/null || continue + awk -v uname="$uname" -v flowf="$WORKDIR/flow.tsv" -v inf="$WORKDIR/all-in.tsv" ' + function between(s, key, p, rest, q2, r) { + p = index(s, "\"" key "\"") + if (p == 0) return "" + rest = substr(s, p + length(key) + 2) + while (rest ~ /^[[:space:]:]/) rest = substr(rest, 2) + if (substr(rest, 1, 1) == "\"") { + rest = substr(rest, 2) + q2 = index(rest, "\"") + if (q2 == 0) return "" + return substr(rest, 1, q2 - 1) + } + r = "" + while (rest ~ /^[0-9]/) { + r = r substr(rest, 1, 1) + rest = substr(rest, 2) + } + return r + } + function amt_n(a, p) { + p = index(a, ":") + if (p == 0) return 0 + return substr(a, p + 1) + 0 + } + { + line = $0 + dir = between(line, "direction") + amt = between(line, "amount") + ts = between(line, "t_s") + subj = between(line, "subject") + low = tolower(subj) + if (amt == "") next + n = amt_n(amt) + if (dir == "credit") { + # Incoming: bank credits into customer accounts (admin top-up, transfers in) + print "incoming\t" n >> flowf + printf "%s\t%s\t%s\t%s\n", amt, ts, subj, uname >> inf + } else if (dir == "debit" && index(low, "withdraw") > 0) { + # Withdraw: Taler withdrawal debit → exchange → wallet coins + print "withdraw\t" n >> flowf + res = subj + sub(/^.*[Ww]ithdrawal[ ]+/, "", res) + gsub(/[^A-Za-z0-9]/, "", res) + printf "%s\t%s\t%s\t%s\t%s\n", amt, ts, subj, uname, res + } else if (dir == "debit") { + # Other debits (non-withdraw transfers) + print "other_out\t" n >> flowf + } + } + ' "$WORKDIR/tx-lines.txt" >>"$WORKDIR/all-wd.tsv" 2>/dev/null || true +done <"$WORKDIR/scan-users.txt" + +# Sum by kind +TOTAL_IN_N=$(awk -F'\t' '$1=="incoming"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0) +TOTAL_WD_FLOW_N=$(awk -F'\t' '$1=="withdraw"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0) +TOTAL_OTHER_OUT_N=$(awk -F'\t' '$1=="other_out"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0) +N_INCOMING=$(awk -F'\t' '$1=="incoming"{c++} END{print c+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0) +# total out = withdraw + other debits +TOTAL_OUT_N=$(awk -v a="${TOTAL_WD_FLOW_N:-0}" -v b="${TOTAL_OTHER_OUT_N:-0}" 'BEGIN{printf "%.8f", a+b}') + +# Sort by t_s descending +sort -t$'\t' -k2,2nr "$WORKDIR/all-wd.tsv" -o "$WORKDIR/all-wd-sorted.tsv" 2>/dev/null \ + || cp "$WORKDIR/all-wd.tsv" "$WORKDIR/all-wd-sorted.tsv" +sort -t$'\t' -k2,2nr "$WORKDIR/all-in.tsv" -o "$WORKDIR/all-in-sorted.tsv" 2>/dev/null \ + || cp "$WORKDIR/all-in.tsv" "$WORKDIR/all-in-sorted.tsv" + +# Unique reserves = individual wallet withdraws (each wallet reserve_pub) +WALLETS_N=$(awk -F'\t' '$5!=""{print $5}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0) +# Accounts that funded at least one withdraw +ACCOUNTS_WITH_WD=$(awk -F'\t' '$4!=""{print $4}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0) + +# Aggregates +NOW=$(now_unix) +GEN_ISO=$(now_iso) +GEN_HUMAN=$(date +"%Y-%m-%d %H:%M %Z") + +# Build recent withdraws JSON array (up to 8) with CEST times +: >"$WORKDIR/recent.jsonl" +N_WD=0 +TOTAL_WD=0 +W24=0; N24=0; W7=0; N7=0 +DAY=$((NOW - 86400)) +WEEK=$((NOW - 7 * 86400)) +LAST_AMT=""; LAST_TS=""; LAST_SUBJ="" + +while IFS=$'\t' read -r amt ts subj uname res; do + [ -n "$amt" ] || continue + n=$(printf '%s' "$amt" | awk -F: '{print $2+0}') + N_WD=$((N_WD + 1)) + TOTAL_WD=$(awk -v a="$TOTAL_WD" -v b="$n" 'BEGIN{printf "%.8f", a+b}') + ts_n=${ts:-0} + if [ "$ts_n" -ge "$DAY" ] 2>/dev/null; then + W24=$(awk -v a="$W24" -v b="$n" 'BEGIN{printf "%.8f", a+b}') + N24=$((N24 + 1)) + fi + if [ "$ts_n" -ge "$WEEK" ] 2>/dev/null; then + W7=$(awk -v a="$W7" -v b="$n" 'BEGIN{printf "%.8f", a+b}') + N7=$((N7 + 1)) + fi + if [ -z "$LAST_AMT" ]; then + LAST_AMT=$amt + LAST_TS=$ts_n + LAST_SUBJ=$subj + fi + if [ "$N_WD" -le 8 ]; then + at_h=""; at_iso="" + if [ -n "$ts_n" ] && [ "$ts_n" != "0" ]; then + at_h=$(human_from_unix "$ts_n") + at_iso=$(iso_from_unix "$ts_n") + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$amt" "$ts_n" "$at_h" "$at_iso" "$uname" "$res" >>"$WORKDIR/recent.jsonl" + fi +done <"$WORKDIR/all-wd-sorted.tsv" + +fmt_goa() { + awk -v n="$1" 'BEGIN{ + if (n+0 == int(n+0)) printf "GOA:%d", int(n+0); + else printf "GOA:%.8g", n+0; + }' +} + +TOTAL_AMT=$(fmt_goa "$TOTAL_WD") +TOTAL_IN_AMT=$(fmt_goa "${TOTAL_IN_N:-0}") +TOTAL_OUT_AMT=$(fmt_goa "${TOTAL_OUT_N:-0}") +TOTAL_WD_FLOW_AMT=$(fmt_goa "${TOTAL_WD_FLOW_N:-0}") +TOTAL_OTHER_OUT_AMT=$(fmt_goa "${TOTAL_OTHER_OUT_N:-0}") +W24_AMT=$(fmt_goa "$W24") +W7_AMT=$(fmt_goa "$W7") + +LAST_AT_H=""; LAST_AT_ISO="" +if [ -n "${LAST_TS:-}" ] && [ "$LAST_TS" != "0" ]; then + LAST_AT_H=$(human_from_unix "$LAST_TS") + LAST_AT_ISO=$(iso_from_unix "$LAST_TS") +fi + +# recent_withdraws JSON +RECENT_JSON="[" +first=1 +while IFS=$'\t' read -r amt ts_n at_h at_iso uname res; do + [ -n "$amt" ] || continue + if [ "$first" = 1 ]; then first=0; else RECENT_JSON="${RECENT_JSON},"; fi + RECENT_JSON="${RECENT_JSON} + { + \"kind\": \"withdraw\", + \"amount\": $(json_str "$amt"), + \"at\": $(json_str "$at_h"), + \"at_iso\": $(json_str "$at_iso"), + \"at_unix\": ${ts_n:-null}, + \"account\": $(json_str "$uname"), + \"reserve\": $(json_str "$res") + }" +done <"$WORKDIR/recent.jsonl" +RECENT_JSON="${RECENT_JSON} + ]" + +# recent incoming (up to 5) +: >"$WORKDIR/recent-in.jsonl" +N_IN_LIST=0 +while IFS=$'\t' read -r amt ts subj uname; do + [ -n "$amt" ] || continue + N_IN_LIST=$((N_IN_LIST + 1)) + [ "$N_IN_LIST" -le 5 ] || break + ts_n=${ts:-0} + at_h=""; at_iso="" + if [ -n "$ts_n" ] && [ "$ts_n" != "0" ]; then + at_h=$(human_from_unix "$ts_n") + at_iso=$(iso_from_unix "$ts_n") + fi + printf '%s\t%s\t%s\t%s\t%s\n' "$amt" "$ts_n" "$at_h" "$at_iso" "$uname" >>"$WORKDIR/recent-in.jsonl" +done <"$WORKDIR/all-in-sorted.tsv" + +RECENT_IN_JSON="[" +first=1 +while IFS=$'\t' read -r amt ts_n at_h at_iso uname; do + [ -n "$amt" ] || continue + if [ "$first" = 1 ]; then first=0; else RECENT_IN_JSON="${RECENT_IN_JSON},"; fi + RECENT_IN_JSON="${RECENT_IN_JSON} + { + \"kind\": \"incoming\", + \"amount\": $(json_str "$amt"), + \"at\": $(json_str "$at_h"), + \"at_iso\": $(json_str "$at_iso"), + \"at_unix\": ${ts_n:-null}, + \"account\": $(json_str "$uname") + }" +done <"$WORKDIR/recent-in.jsonl" +RECENT_IN_JSON="${RECENT_IN_JSON} + ]" + +# Balance of explorer (optional display not required in footer) +BALANCE="GOA:0" +if [ -n "${USER_TOKEN:-$AUTH_TOKEN}" ]; then + curl -sS -m 10 -H "Authorization: Bearer ${USER_TOKEN:-$AUTH_TOKEN}" \ + "${BANK}/accounts/${BANK_USER}" >"$WORKDIR/acct.json" || true + BALANCE=$(awk -F'"' '/"amount"/ { + for (i=1;i<=NF;i++) if ($i=="amount") { print $(i+2); exit } + }' "$WORKDIR/acct.json" 2>/dev/null || echo "GOA:0") +fi + +# Demo block kept for page QR logic only (not shown in footer) +DEMO_URI=""; DEMO_AMT=""; DEMO_CREATED=""; DEMO_WID=""; DEMO_STATUS="" +[ -f "$LANDING_DIR/withdraw.uri" ] && DEMO_URI=$(tr -d '\n' <"$LANDING_DIR/withdraw.uri") +[ -f "$LANDING_DIR/withdraw.amount" ] && DEMO_AMT=$(tr -d '\n' <"$LANDING_DIR/withdraw.amount") +[ -f "$LANDING_DIR/withdraw.created" ] && DEMO_CREATED=$(tr -d '\n' <"$LANDING_DIR/withdraw.created") +if [ -n "$DEMO_URI" ]; then + DEMO_WID=$(basename "$DEMO_URI") + curl -sS -m 8 \ + "${BANK}/taler-integration/withdrawal-operation/${DEMO_WID}" \ + >"$WORKDIR/demo-wd.json" 2>/dev/null || true + DEMO_STATUS=$(awk -F'"' '/"status"/ { + for (i=1;i<=NF;i++) if ($i=="status") { print $(i+2); exit } + }' "$WORKDIR/demo-wd.json" 2>/dev/null || true) +fi +DEMO_READY=false +case "$DEMO_STATUS" in + pending|selected) DEMO_READY=true ;; + "") [ -n "$DEMO_URI" ] && DEMO_READY=true ;; +esac +[ "$DEMO_STATUS" = "confirmed" ] && DEMO_READY=false +[ "$DEMO_STATUS" = "aborted" ] && DEMO_READY=false + +# Sanity: never publish an empty-looking success if admin scan should have accounts +if [ -n "$ADMIN_TOKEN" ] && [ "${ACCOUNTS_N:-0}" = "0" ]; then + write_err "accounts list empty after admin scan" + exit 1 +fi + +# Live performance probes (same idea as exchange landing-stats) +measure_ms() { + local url="$1" t + t=$(curl -sS -o /dev/null -m 8 -w '%{time_total}' "$url" 2>/dev/null || echo "") + [ -z "$t" ] && { echo "null"; return; } + awk -v t="$t" 'BEGIN{printf "%d", (t+0)*1000}' +} +num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; } +# Prefer public URL for real client latency; fall back to local bank URL +BANK_PUBLIC_BASE="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}" +CONFIG_MS=$(measure_ms "${BANK_PUBLIC_BASE}/config") +CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/config" 2>/dev/null || echo "000") +# If public failed, try in-container +if [ "$CONFIG_HTTP" != "200" ]; then + CONFIG_MS=$(measure_ms "${BANK}/config") + CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK}/config" 2>/dev/null || echo "000") +fi +INT_MS=$(measure_ms "${BANK}/taler-integration/config") +INT_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK}/taler-integration/config" 2>/dev/null || echo "000") +WEBUI_MS=$(measure_ms "${BANK_PUBLIC_BASE}/webui/") +WEBUI_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/webui/" 2>/dev/null || echo "000") +LOADAVG="" +[ -r /proc/loadavg ] && LOADAVG=$(awk '{print $1","$2","$3}' /proc/loadavg) + +MEM_JSON='"container_rss_human": "—"' +MEM_HELPER="${MEM_HELPER:-/usr/local/lib/landing-mem-snapshot.sh}" +if [ -f "$MEM_HELPER" ]; then + # shellcheck disable=SC1090 + . "$MEM_HELPER" + mem_snapshot_json || true +fi + +cat >"$TMP" < $OUT"