Wire goa-amount.js into bank/exchange/merchant intros, deploy it with the landings, and deepen the in-container bank stats fallback to scan all accounts (skip only exchange) when the host timer is down.
623 lines
22 KiB
Bash
Executable file
623 lines
22 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).
|
||
# Prefer the host collector (hernani systemd taler-landing-stats) for full
|
||
# pagination; these defaults keep the in-container fallback deep enough.
|
||
TX_DELTA="${TX_DELTA:--200000}"
|
||
# Max accounts to list + scan (was 80 → missed later accounts; bank has 100+).
|
||
# 0 or empty → no head limit (scan every listed account).
|
||
MAX_SCAN_ACCOUNTS="${MAX_SCAN_ACCOUNTS:-0}"
|
||
# Account-list page size for GET /accounts?delta=… (must cover all users)
|
||
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}"
|
||
# curl timeout per account (deeper history needs more headroom)
|
||
TX_CURL_TIMEOUT="${TX_CURL_TIMEOUT:-45}"
|
||
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 ALL accounts except exchange (withdraw debits live on customers + admin
|
||
# top-ups; exchange credits would double-count the same GOA).
|
||
# Prefer host collect_bank_stats.py (hernani timer) for full pagination.
|
||
{
|
||
echo "$BANK_USER"
|
||
# include admin + every auto-account; drop only exchange
|
||
grep -Eve "^(exchange|${BANK_USER})$" "$WORKDIR/usernames.txt" 2>/dev/null || true
|
||
} | awk 'NF && !seen[$0]++' >"$WORKDIR/scan-users.all"
|
||
if [ -n "${MAX_SCAN_ACCOUNTS}" ] && [ "${MAX_SCAN_ACCOUNTS}" -gt 0 ] 2>/dev/null; then
|
||
head -n "$MAX_SCAN_ACCOUNTS" "$WORKDIR/scan-users.all" >"$WORKDIR/scan-users.txt"
|
||
else
|
||
cp "$WORKDIR/scan-users.all" "$WORKDIR/scan-users.txt"
|
||
fi
|
||
|
||
while IFS= read -r uname; do
|
||
[ -n "$uname" ] || continue
|
||
case "$uname" in 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. exchange only (admin+explorer+all users scanned)"
|
||
},
|
||
"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"
|