612 lines
21 KiB
Bash
Executable file
612 lines
21 KiB
Bash
Executable file
#!/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}"
|
||
# Per-account transaction window (libeufin delta). Was -100 → systematically
|
||
# undercounted credits/withdraws on active accounts (broken public stats).
|
||
TX_DELTA="${TX_DELTA:--50000}"
|
||
# Max accounts to list + scan (was 80 → missed later accounts; bank has 100+).
|
||
MAX_SCAN_ACCOUNTS="${MAX_SCAN_ACCOUNTS:-500}"
|
||
# Account-list page size for GET /accounts?delta=… (must cover all users)
|
||
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--500}"
|
||
# curl timeout per account (deeper history needs more headroom)
|
||
TX_CURL_TIMEOUT="${TX_CURL_TIMEOUT:-25}"
|
||
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" <<EOF
|
||
{
|
||
"ok": ${ok_json},
|
||
"at": $(json_str "$(now_iso)"),
|
||
"at_human": $(json_str "$(now_human)"),
|
||
"error": $( [ -n "$msg" ] && json_str "$msg" || echo null )
|
||
}
|
||
EOF
|
||
}
|
||
|
||
write_err() {
|
||
local msg="$1"
|
||
write_run false "$msg"
|
||
rm -f "$TMP" 2>/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
|
||
# Use a large negative delta so we list *all* accounts (delta=-80 truncated
|
||
# the roster and undercounted bank_accounts + flow).
|
||
curl -sS -m 30 -H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||
"${BANK}/accounts?delta=${ACCOUNTS_DELTA}" >"$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_OK=0
|
||
SCAN_EMPTY=0
|
||
SCAN_FAIL=0
|
||
# 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
|
||
# Safe filename (usernames are mostly [A-Za-z0-9_-])
|
||
safe=$(printf '%s' "$uname" | tr -c 'A-Za-z0-9._-' '_')
|
||
code=$(curl -sS -m "${TX_CURL_TIMEOUT}" -o "$WORKDIR/tx-${safe}.json" -w '%{http_code}' \
|
||
-H "Authorization: Bearer ${AUTH_TOKEN}" \
|
||
"${BANK}/accounts/${uname}/transactions?delta=${TX_DELTA}" 2>/dev/null || echo 000)
|
||
# 204 / empty body: account with no transactions — normal, not an error
|
||
if [ "$code" = "204" ] || [ ! -s "$WORKDIR/tx-${safe}.json" ]; then
|
||
SCAN_EMPTY=$((SCAN_EMPTY + 1))
|
||
continue
|
||
fi
|
||
if [ "$code" != "200" ]; then
|
||
SCAN_FAIL=$((SCAN_FAIL + 1))
|
||
continue
|
||
fi
|
||
SCAN_OK=$((SCAN_OK + 1))
|
||
tr -d '\n' <"$WORKDIR/tx-${safe}.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 10) with CEST times — landing shows all 10
|
||
RECENT_WD_N="${RECENT_WD_N:-10}"
|
||
: >"$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 "$RECENT_WD_N" ]; 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)
|
||
# Milliseconds as integer, rounded (not truncated) so sub-ms loopback does not
|
||
# always show 0 when we fall back to in-container URLs.
|
||
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{
|
||
ms = (t+0)*1000
|
||
if (ms > 0 && ms < 1) ms = 1
|
||
printf "%d", int(ms + 0.5)
|
||
}'
|
||
}
|
||
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
|
||
# Prefer public URL for real client latency (Caddy → bank); fall back to loopback.
|
||
# Integration used to probe only $BANK (127.0.0.1:9012) → ~0–1 ms and not comparable
|
||
# to /config which already used the public host.
|
||
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 [ "$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_PUBLIC_BASE}/taler-integration/config")
|
||
INT_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/taler-integration/config" 2>/dev/null || echo "000")
|
||
if [ "$INT_HTTP" != "200" ]; then
|
||
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")
|
||
fi
|
||
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" <<EOF
|
||
{
|
||
"ok": true,
|
||
"currency": "GOA",
|
||
"timezone": $(json_str "$TZ"),
|
||
"generated_at": $(json_str "$GEN_ISO"),
|
||
"generated_at_human": $(json_str "$GEN_HUMAN"),
|
||
"generated_at_unix": $NOW,
|
||
"source": "in-container landing-stats.sh",
|
||
"bank_url": $(json_str "$BANK"),
|
||
"scan": {
|
||
"tx_delta": $(json_str "$TX_DELTA"),
|
||
"accounts_delta": $(json_str "$ACCOUNTS_DELTA"),
|
||
"max_scan_accounts": ${MAX_SCAN_ACCOUNTS:-0},
|
||
"accounts_listed": ${ACCOUNTS_N:-0},
|
||
"accounts_scanned_ok": ${SCAN_OK:-0},
|
||
"accounts_empty_tx": ${SCAN_EMPTY:-0},
|
||
"accounts_scan_fail": ${SCAN_FAIL:-0},
|
||
"note": "empty_tx includes HTTP 204 (no ledger rows) — normal for new auto-accounts"
|
||
},
|
||
"bank_accounts": {
|
||
"total": ${ACCOUNTS_N:-0},
|
||
"users": ${ACCOUNTS_USERS:-0},
|
||
"with_withdraws": ${ACCOUNTS_WITH_WD:-0}
|
||
},
|
||
"wallets": {
|
||
"unique_reserves": ${WALLETS_N:-0},
|
||
"note": "unique reserve pubs from Taler withdrawals (one per wallet withdraw)"
|
||
},
|
||
"balance_explorer": $(json_str "$BALANCE"),
|
||
"flow": {
|
||
"incoming": {
|
||
"label": "Incoming bank credits",
|
||
"count": ${N_INCOMING:-0},
|
||
"amount": $(json_str "$TOTAL_IN_AMT"),
|
||
"value": ${TOTAL_IN_N:-0}
|
||
},
|
||
"withdraw": {
|
||
"label": "Taler withdrawals to wallets",
|
||
"count": ${N_WD:-0},
|
||
"amount": $(json_str "$TOTAL_WD_FLOW_AMT"),
|
||
"value": ${TOTAL_WD_FLOW_N:-0}
|
||
},
|
||
"other_out": {
|
||
"label": "Other debits (non-withdraw)",
|
||
"amount": $(json_str "$TOTAL_OTHER_OUT_AMT"),
|
||
"value": ${TOTAL_OTHER_OUT_N:-0}
|
||
},
|
||
"total_in": $(json_str "$TOTAL_IN_AMT"),
|
||
"total_in_value": ${TOTAL_IN_N:-0},
|
||
"total_out": $(json_str "$TOTAL_OUT_AMT"),
|
||
"total_out_value": ${TOTAL_OUT_N:-0},
|
||
"note": "incoming=credits; withdraw=Taler withdrawal debits; excl. admin+exchange accounts"
|
||
},
|
||
"withdraws": {
|
||
"count": ${N_WD:-0},
|
||
"total_amount": $(json_str "$TOTAL_AMT"),
|
||
"total_value": ${TOTAL_WD:-0},
|
||
"last_amount": $( [ -n "$LAST_AMT" ] && json_str "$LAST_AMT" || echo null ),
|
||
"last_at": $( [ -n "$LAST_AT_H" ] && json_str "$LAST_AT_H" || echo null ),
|
||
"last_at_iso": $( [ -n "$LAST_AT_ISO" ] && json_str "$LAST_AT_ISO" || echo null ),
|
||
"last_at_unix": ${LAST_TS:-null},
|
||
"last_subject": $( [ -n "$LAST_SUBJ" ] && json_str "$LAST_SUBJ" || echo null ),
|
||
"last_24h": { "count": ${N24:-0}, "amount": $(json_str "$W24_AMT"), "value": ${W24:-0} },
|
||
"last_7d": { "count": ${N7:-0}, "amount": $(json_str "$W7_AMT"), "value": ${W7:-0} }
|
||
},
|
||
"recent_withdraws": $RECENT_JSON,
|
||
"recent_incoming": $RECENT_IN_JSON,
|
||
"demo": {
|
||
"uri": $( [ -n "$DEMO_URI" ] && json_str "$DEMO_URI" || echo null ),
|
||
"amount": $( [ -n "$DEMO_AMT" ] && json_str "$DEMO_AMT" || echo null ),
|
||
"created": $( [ -n "$DEMO_CREATED" ] && json_str "$DEMO_CREATED" || echo null ),
|
||
"withdrawal_id": $( [ -n "$DEMO_WID" ] && json_str "$DEMO_WID" || echo null ),
|
||
"status": $( [ -n "$DEMO_STATUS" ] && json_str "$DEMO_STATUS" || echo null ),
|
||
"ready": $DEMO_READY
|
||
},
|
||
"performance": {
|
||
"config_http": $(json_str "$CONFIG_HTTP"),
|
||
"config_ms": $(num_or_null "$CONFIG_MS"),
|
||
"integration_http": $(json_str "$INT_HTTP"),
|
||
"integration_ms": $(num_or_null "$INT_MS"),
|
||
"webui_http": $(json_str "$WEBUI_HTTP"),
|
||
"webui_ms": $(num_or_null "$WEBUI_MS"),
|
||
"loadavg": $(json_str "${LOADAVG:-}"),
|
||
"memory": {
|
||
${MEM_JSON}
|
||
}
|
||
}
|
||
}
|
||
EOF
|
||
grep -q '"ok": true' "$TMP" || { write_err "tmp json missing ok:true"; exit 1; }
|
||
mv -f "$TMP" "$OUT"
|
||
write_run true
|
||
echo "ok accounts=${ACCOUNTS_N} wallets=${WALLETS_N} withdraws=${N_WD} total=${TOTAL_AMT} tz=${TZ} -> $OUT"
|