landing-stats: make collector multi-stack reusable
This commit is contained in:
parent
e1cc6d7141
commit
b6c3225e97
7 changed files with 291 additions and 58 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue