landing-stats: make collector multi-stack reusable

This commit is contained in:
Hernâni Marques 2026-07-17 16:26:18 +02:00
parent e1cc6d7141
commit b6c3225e97
No known key found for this signature in database
GPG key ID: CB5738652768F7E9
7 changed files with 291 additions and 58 deletions

View file

@ -1,23 +1,22 @@
#!/usr/bin/env python3
"""Full-scan bank landing stats (all accounts, all ledger money).
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.
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.
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 (default https://exchange.hacktivism.ch/config)
EXCHANGE_CONFIG_URL for alt_unit_names + currency
BANK_PUBLIC_URL public base for latency probes
OUT output path (default stdout if -)
SCAN_SKIP comma usernames excluded from flow (default: exchange)
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
TX_PAGE / MAX_TX_PAGES / ACCOUNTS_DELTA / RECENT_WD_N / TZ
STATS_SOURCE_LABEL optional string in stats.source field
"""
from __future__ import annotations
@ -343,6 +342,11 @@ 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(
@ -364,6 +368,11 @@ def main() -> int:
)
ap.add_argument("--recent", type=int, default=int(env("RECENT_WD_N", "10") or "10"))
ap.add_argument("--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"
@ -378,6 +387,7 @@ 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:
@ -397,6 +407,24 @@ 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)
@ -442,13 +470,24 @@ 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 if ":" in amount else f"GOA:{amount}",
"amount": amount,
"at_unix": ts,
"account": uname,
"subject": subject,
@ -460,7 +499,7 @@ def main() -> int:
withdraws.append(
{
"kind": "withdraw",
"amount": amount if ":" in amount else f"GOA:{amount}",
"amount": amount,
"at_unix": ts,
"account": uname,
"subject": subject,
@ -503,7 +542,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"GOA:{d}", alt)
f = format_amount_alt(f"{currency}:{d}", alt)
return {
"amount": f["amount"],
"amount_alt": f["amount_alt"],
@ -567,10 +606,11 @@ def main() -> int:
}
)
# explorer balance
balance = "GOA:0"
bal_alt = "GOA:0"
bal_full = "GOA:0"
# explorer / pool balance (shared demo account when present)
zero = f"{currency}:0"
balance = zero
bal_alt = zero
bal_full = zero
if explorer_pass:
try:
etok = get_token(bank, args.explorer_user, explorer_pass)
@ -583,7 +623,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 "GOA:0"
else data.get("amount") or data.get("balance") or zero
)
# sometimes balance is object with amount
if isinstance(data.get("balance"), dict) and data["balance"].get(
@ -608,7 +648,7 @@ def main() -> int:
)
if code == 200 and isinstance(data, dict):
if isinstance(data.get("balance"), dict):
balance = str(data["balance"].get("amount") or "GOA:0")
balance = str(data["balance"].get("amount") or zero)
elif data.get("amount"):
balance = str(data["amount"])
bf = format_amount_alt(balance, alt)
@ -637,12 +677,12 @@ def main() -> int:
stats = {
"ok": True,
"currency": "GOA",
"currency": currency,
"timezone": tz_name,
"generated_at": gen_iso,
"generated_at_human": gen_human,
"generated_at_unix": now_u,
"source": "host collect_bank_stats.py (hernani systemd)",
"source": str(args.source_label or "host collect_bank_stats.py"),
"bank_url": bank,
"scan": {
"tx_page": args.tx_page,