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

@ -14,12 +14,14 @@ TimeoutStartSec=480
# Do not keep "active" after run — timer may fire again cleanly # Do not keep "active" after run — timer may fire again cleanly
RemainAfterExit=no RemainAfterExit=no
Environment=TZ=Europe/Zurich Environment=TZ=Europe/Zurich
# Stack overrides (goa / stage-testpaysan / custom) — optional
EnvironmentFile=-%h/.config/taler-landing/stack.env
# GOA defaults if stack.env absent
Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log
Environment=SECRETS_BANK=%h/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank Environment=SECRETS_BANK=%h/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank
Environment=BANK_URL=http://127.0.0.1:9012 Environment=BANK_URL=http://127.0.0.1:9012
Environment=BANK_PUBLIC_URL=https://bank.hacktivism.ch Environment=BANK_PUBLIC_URL=https://bank.hacktivism.ch
Environment=EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config Environment=EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config
# All accounts except exchange (double-count of withdraw credits on exchange ledger)
Environment=SCAN_SKIP=exchange Environment=SCAN_SKIP=exchange
Environment=TX_PAGE=500 Environment=TX_PAGE=500
Environment=MAX_TX_PAGES=0 Environment=MAX_TX_PAGES=0

View file

@ -1,12 +1,21 @@
# taler-landing — central stats (hernani user systemd) # taler-landing — generic multi-stack landing stats
Public landing numbers on One collector (`collect_bank_stats.py` + `collect-landing-stats.sh`) for any
currency / stack. **Defaults = GOA / hacktivism**; stage TESTPAYSAN uses a
profile file.
Public landing numbers (example GOA):
- https://bank.hacktivism.ch/intro/ → `stats.json` - https://bank.hacktivism.ch/intro/ → `stats.json`
- https://exchange.hacktivism.ch/intro/ → `stats.json` - https://exchange.hacktivism.ch/intro/ → `stats.json`
- https://taler.hacktivism.ch/intro/ → `stats.json` - https://taler.hacktivism.ch/intro/ → `stats.json`
are produced by **one host process** as user **`hernani`**, not by three unrelated in-container crons. Stage TESTPAYSAN (shared bank feed on all three hosts):
- https://stage.bank.lefrancpaysan.ch/intro/stats.json
(same file also under stage.exchange / stage.monnaie landings)
Produced by **one host process** (user systemd timer), not three crons.
## Why host / user systemd ## Why host / user systemd
@ -77,16 +86,37 @@ Preferred order:
Explorer password optional (shared-pool balance). Explorer password optional (shared-pool balance).
## Profiles
| File | Stack |
|------|--------|
| `profiles/goa.env` | hacktivism GOA (container publish) |
| `profiles/stage-testpaysan.env` | FrancPaysan stage; **shared** host `stats.json` |
```bash
# Stage (on stagepaysan):
STACK_PROFILE=stage-testpaysan ./scripts/taler-landing/install-landing-stats-host.sh
# or: francpaysan-admin-log/scripts/stagepaysan/install-landing-stats.sh
```
`~/.config/taler-landing/stack.env` is sourced on each run.
## Env overrides ## Env overrides
| Env | Default | Meaning | | Env | Default | Meaning |
|-----|---------|---------| |-----|---------|---------|
| `BANK_URL` | `http://127.0.0.1:9012` | libeufin loopback | | `BANK_URL` | `http://127.0.0.1:9012` | libeufin loopback |
| `BANK_CURRENCY` | *(auto)* | `GOA` / `TESTPAYSAN` / … |
| `SCAN_SKIP` | `exchange` | usernames excluded from flow | | `SCAN_SKIP` | `exchange` | usernames excluded from flow |
| `COLLECT_BANK` / `_EXCHANGE` / `_MERCHANT` | `1` | phase switches |
| `COLLECT_RESOURCES` | `1` | container RSS/loadavg merge |
| `HOST_STATS_DIRS` | empty | space-separated host dirs for **shared** bank `stats.json` |
| `PUBLISH_PODMAN` | `1` | also `podman cp` into container landings |
| `TX_PAGE` | `500` | transactions page size | | `TX_PAGE` | `500` | transactions page size |
| `MAX_TX_PAGES` | `0` | `0` = unlimited pages / account | | `MAX_TX_PAGES` | `0` | `0` = unlimited pages / account |
| `ACCOUNTS_DELTA` | `-10000` | account list window | | `ACCOUNTS_DELTA` | `-10000` | account list window |
| `ADMIN_LOG` | `%h/src/koopa/koopa-admin-log` | refresh in-container scripts | | `ADMIN_LOG` | `%h/src/koopa/koopa-admin-log` | refresh in-container scripts |
| `STATS_SOURCE_LABEL` | `host collect_bank_stats.py` | `stats.source` field |
## UI alt names ## UI alt names

View file

@ -1,10 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Central landing-stats collector for hernani@koopa (user systemd timer). # Generic landing-stats orchestrator (any Taler stack).
# #
# - Bank: full account+ledger scan via collect_bank_stats.py → bank container # Defaults = GOA / hacktivism on koopa. Override via env or EnvironmentFile
# - Exchange / merchant: existing in-container scripts, then amount_alt enrich # (see profiles: goa | stage-testpaysan).
# - All three: container RSS / loadavg / top procs via mem-snapshot (podman exec) #
# - Never wipes a good stats.json on failure (writes stats-run.json only) # - Bank: collect_bank_stats.py (currency-agnostic) → stats.json
# - Optional: exchange / merchant in-container generators
# - Optional: container RSS/loadavg merge
# - Publish: podman into container landings and/or host dirs (shared bank stats)
# - Never wipes a good stats.json on failure (stats-run.json only)
# #
# Install: scripts/taler-landing/install-landing-stats-host.sh # Install: scripts/taler-landing/install-landing-stats-host.sh
set -euo pipefail set -euo pipefail
@ -12,6 +16,20 @@ set -euo pipefail
export TZ="${TZ:-Europe/Zurich}" export TZ="${TZ:-Europe/Zurich}"
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin${PATH:+:$PATH}" export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin${PATH:+:$PATH}"
# Optional stack profile: ~/.config/taler-landing/stack.env or STACK_ENV_FILE
if [ -n "${STACK_ENV_FILE:-}" ] && [ -f "${STACK_ENV_FILE}" ]; then
# shellcheck disable=SC1090
set -a
# shellcheck source=/dev/null
. "${STACK_ENV_FILE}"
set +a
elif [ -f "${HOME}/.config/taler-landing/stack.env" ]; then
set -a
# shellcheck source=/dev/null
. "${HOME}/.config/taler-landing/stack.env"
set +a
fi
log() { printf '%s %s\n' "$(date -Iseconds)" "$*"; } log() { printf '%s %s\n' "$(date -Iseconds)" "$*"; }
ROOT="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$(dirname "$0")" && pwd)"
@ -22,6 +40,8 @@ elif [ -f "${HOME}/.local/lib/taler-landing/collect_bank_stats.py" ]; then
LIB="${HOME}/.local/lib/taler-landing" LIB="${HOME}/.local/lib/taler-landing"
elif [ -f "${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing/collect_bank_stats.py" ]; then elif [ -f "${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing/collect_bank_stats.py" ]; then
LIB="${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing" LIB="${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing"
elif [ -f "${HOME}/taler/src/koopa-admin-log/scripts/taler-landing/collect_bank_stats.py" ]; then
LIB="${HOME}/taler/src/koopa-admin-log/scripts/taler-landing"
else else
LIB="$ROOT" LIB="$ROOT"
fi fi
@ -29,6 +49,13 @@ fi
ADMIN_LOG="${ADMIN_LOG:-${HOME}/src/koopa/koopa-admin-log}" ADMIN_LOG="${ADMIN_LOG:-${HOME}/src/koopa/koopa-admin-log}"
SECRETS_BANK="${SECRETS_BANK:-${HOME}/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank}" SECRETS_BANK="${SECRETS_BANK:-${HOME}/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank}"
# Which phases to run (1/0)
COLLECT_BANK="${COLLECT_BANK:-1}"
COLLECT_EXCHANGE="${COLLECT_EXCHANGE:-1}"
COLLECT_MERCHANT="${COLLECT_MERCHANT:-1}"
COLLECT_RESOURCES="${COLLECT_RESOURCES:-1}"
# Containers (empty = skip podman publish for that role)
BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}" BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}"
EX_CTR="${EX_CTR:-taler-hacktivism-exchange-ansible}" EX_CTR="${EX_CTR:-taler-hacktivism-exchange-ansible}"
MER_CTR="${MER_CTR:-taler-hacktivism}" MER_CTR="${MER_CTR:-taler-hacktivism}"
@ -36,10 +63,19 @@ MER_CTR="${MER_CTR:-taler-hacktivism}"
BANK_URL="${BANK_URL:-http://127.0.0.1:9012}" BANK_URL="${BANK_URL:-http://127.0.0.1:9012}"
BANK_PUBLIC_URL="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}" BANK_PUBLIC_URL="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}"
EXCHANGE_CONFIG_URL="${EXCHANGE_CONFIG_URL:-https://exchange.hacktivism.ch/config}" EXCHANGE_CONFIG_URL="${EXCHANGE_CONFIG_URL:-https://exchange.hacktivism.ch/config}"
BANK_CURRENCY="${BANK_CURRENCY:-}" # empty → auto from bank/exchange /config
STATS_SOURCE_LABEL="${STATS_SOURCE_LABEL:-host collect_bank_stats.py}"
# In-container landing roots (podman publish)
BANK_LANDING_IN="${BANK_LANDING_IN:-/var/www/bank-landing}" BANK_LANDING_IN="${BANK_LANDING_IN:-/var/www/bank-landing}"
EX_LANDING_IN="${EX_LANDING_IN:-/var/www/exchange-landing}" EX_LANDING_IN="${EX_LANDING_IN:-/var/www/exchange-landing}"
MER_LANDING_IN="${MER_LANDING_IN:-/var/www/merchant-landing}" MER_LANDING_IN="${MER_LANDING_IN:-/var/www/merchant-landing}"
PUBLISH_PODMAN="${PUBLISH_PODMAN:-1}"
# Host dirs that should get the *same* bank stats.json (shared public feed).
# Colon-separated (systemd EnvironmentFile-safe). Example stage:
# HOST_STATS_DIRS=/var/www/lfp-stage/bank:/var/www/lfp-stage/exchange:/var/www/lfp-stage/merchant
HOST_STATS_DIRS="${HOST_STATS_DIRS:-}"
WORKDIR="${LANDING_STATS_WORKDIR:-${XDG_RUNTIME_DIR:-/tmp}/taler-landing-stats}" WORKDIR="${LANDING_STATS_WORKDIR:-${XDG_RUNTIME_DIR:-/tmp}/taler-landing-stats}"
mkdir -p "$WORKDIR" mkdir -p "$WORKDIR"
@ -64,7 +100,9 @@ MEM_SRC="${MEM_SNAPSHOT_SRC:-$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh}"
merge_container_resources() { merge_container_resources() {
local label="$1" ctr="$2" stats_file="$3" local label="$1" ctr="$2" stats_file="$3"
[ "${COLLECT_RESOURCES}" = "1" ] || return 0
[ -f "$stats_file" ] || return 0 [ -f "$stats_file" ] || return 0
[ -n "$ctr" ] || return 0
if ! ctr_running "$ctr"; then if ! ctr_running "$ctr"; then
log "WARN: $label resources: $ctr not running" log "WARN: $label resources: $ctr not running"
return 1 return 1
@ -95,6 +133,36 @@ merge_container_resources() {
return 0 return 0
} }
# Write stats.json into host directories (shared feed: same file everywhere).
publish_host_stats() {
local src_stats="$1" src_run="${2:-}"
[ -n "${HOST_STATS_DIRS:-}" ] || return 0
[ -f "$src_stats" ] || return 0
local d paths
# Accept colon or space separators
paths=$(printf '%s' "$HOST_STATS_DIRS" | tr ': ' '\n' | sed '/^$/d')
while IFS= read -r d; do
[ -n "$d" ] || continue
if [ -d "$d" ] || mkdir -p "$d" 2>/dev/null; then
if cp -f "$src_stats" "${d}/stats.json" 2>/dev/null; then
chmod a+r "${d}/stats.json" 2>/dev/null || true
[ -n "$src_run" ] && [ -f "$src_run" ] && cp -f "$src_run" "${d}/stats-run.json" 2>/dev/null || true
log "host: published ${d}/stats.json"
continue
fi
fi
if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
sudo mkdir -p "$d" 2>/dev/null || true
sudo cp -f "$src_stats" "${d}/stats.json"
sudo chmod a+r "${d}/stats.json" 2>/dev/null || true
[ -n "$src_run" ] && [ -f "$src_run" ] && sudo cp -f "$src_run" "${d}/stats-run.json" 2>/dev/null || true
log "host: published ${d}/stats.json (sudo)"
else
log "WARN: cannot write ${d}/stats.json (need write or passwordless sudo)"
fi
done <<<"$paths"
}
read_pass() { read_pass() {
local name="$1" f local name="$1" f
for f in \ for f in \
@ -107,12 +175,26 @@ read_pass() {
return 0 return 0
fi fi
done done
# container (hernani can podman exec root files inside owned containers) # container secrets: /root/… (GOA) or /data/secrets/… (FrancPaysan stage)
if podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then if [ -n "${BANK_CTR:-}" ] && podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then
if podman exec "$BANK_CTR" test -r "/root/${name}" 2>/dev/null; then for path in "/root/${name}" "/data/secrets/${name}" "/data/secrets/bank-admin-password.txt"; do
podman exec "$BANK_CTR" cat "/root/${name}" 2>/dev/null | tr -d '\n' case "$path" in
*bank-admin-password.txt)
[ "$name" = "bank-admin-password.txt" ] || continue
;;
esac
if podman exec "$BANK_CTR" test -r "$path" 2>/dev/null; then
podman exec "$BANK_CTR" cat "$path" 2>/dev/null | tr -d '\n\r'
return 0 return 0
fi fi
done
# stage explorer secret naming
if [ "$name" = "bank-explorer-password.txt" ]; then
if podman exec "$BANK_CTR" test -r /data/secrets/bank-explorer-password.txt 2>/dev/null; then
podman exec "$BANK_CTR" cat /data/secrets/bank-explorer-password.txt 2>/dev/null | tr -d '\n\r'
return 0
fi
fi
fi fi
return 1 return 1
} }
@ -123,6 +205,8 @@ ctr_running() {
publish_json() { publish_json() {
local ctr="$1" dest_dir="$2" src_stats="$3" src_run="$4" local ctr="$1" dest_dir="$2" src_stats="$3" src_run="$4"
[ "${PUBLISH_PODMAN}" = "1" ] || return 0
[ -n "$ctr" ] || return 0
if ! ctr_running "$ctr"; then if ! ctr_running "$ctr"; then
log "WARN: $ctr not running — skip publish $dest_dir" log "WARN: $ctr not running — skip publish $dest_dir"
return 1 return 1
@ -166,6 +250,7 @@ collect_bank() {
BANK_URL="$BANK_URL" \ BANK_URL="$BANK_URL" \
BANK_PUBLIC_URL="$BANK_PUBLIC_URL" \ BANK_PUBLIC_URL="$BANK_PUBLIC_URL" \
EXCHANGE_CONFIG_URL="$EXCHANGE_CONFIG_URL" \ EXCHANGE_CONFIG_URL="$EXCHANGE_CONFIG_URL" \
BANK_CURRENCY="${BANK_CURRENCY:-}" \
BANK_ADMIN_PASS="$admin_pass" \ BANK_ADMIN_PASS="$admin_pass" \
BANK_EXPLORER_PASS="$explorer_pass" \ BANK_EXPLORER_PASS="$explorer_pass" \
DEMO_DIR="${demo_dir}" \ DEMO_DIR="${demo_dir}" \
@ -173,6 +258,7 @@ collect_bank() {
TX_PAGE="${TX_PAGE:-500}" \ TX_PAGE="${TX_PAGE:-500}" \
MAX_TX_PAGES="${MAX_TX_PAGES:-0}" \ MAX_TX_PAGES="${MAX_TX_PAGES:-0}" \
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}" \ ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}" \
STATS_SOURCE_LABEL="$STATS_SOURCE_LABEL" \
"$PY" "$LIB/collect_bank_stats.py" \ "$PY" "$LIB/collect_bank_stats.py" \
--out "$WORKDIR/bank-stats.json" \ --out "$WORKDIR/bank-stats.json" \
--run-out "$WORKDIR/bank-stats-run.json" \ --run-out "$WORKDIR/bank-stats-run.json" \
@ -181,11 +267,13 @@ collect_bank() {
set -e set -e
if [ "$ec_bank" -eq 0 ] && [ -f "$WORKDIR/bank-stats.json" ]; then if [ "$ec_bank" -eq 0 ] && [ -f "$WORKDIR/bank-stats.json" ]; then
# Always attach in-container RSS/loadavg (not host /proc) # Optional: attach in-container RSS/loadavg (not host /proc)
merge_container_resources bank "$BANK_CTR" "$WORKDIR/bank-stats.json" || true merge_container_resources bank "$BANK_CTR" "$WORKDIR/bank-stats.json" || true
publish_json "$BANK_CTR" "$BANK_LANDING_IN" \ publish_json "$BANK_CTR" "$BANK_LANDING_IN" \
"$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json" "$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json"
log "bank: OK → ${BANK_CTR}:${BANK_LANDING_IN}/stats.json" # Shared public feed: same bank stats on all HOST_STATS_DIRS (bank/ex/merchant landings)
publish_host_stats "$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json"
log "bank: OK (podman=${BANK_CTR:-none} host_dirs=${HOST_STATS_DIRS:-none})"
else else
log "ERROR: bank collect failed (ec=$ec_bank) — previous stats.json kept" log "ERROR: bank collect failed (ec=$ec_bank) — previous stats.json kept"
[ -f "$WORKDIR/bank-stats-run.json" ] || \ [ -f "$WORKDIR/bank-stats-run.json" ] || \
@ -272,13 +360,26 @@ collect_merchant() {
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
main() { main() {
log "=== taler-landing-stats start (user=$(id -un) lib=$LIB) ===" log "=== taler-landing-stats start (user=$(id -un) lib=$LIB currency=${BANK_CURRENCY:-auto}) ==="
log "phases bank=${COLLECT_BANK} exchange=${COLLECT_EXCHANGE} merchant=${COLLECT_MERCHANT} resources=${COLLECT_RESOURCES}"
if [ "${COLLECT_BANK}" = "1" ]; then
collect_bank || ec_bank=$? collect_bank || ec_bank=$?
else
log "skip bank collect"
fi
if [ "${COLLECT_EXCHANGE}" = "1" ]; then
collect_exchange || ec_ex=$? collect_exchange || ec_ex=$?
else
log "skip exchange collect"
fi
if [ "${COLLECT_MERCHANT}" = "1" ]; then
collect_merchant || ec_mer=$? collect_merchant || ec_mer=$?
else
log "skip merchant collect"
fi
log "=== done bank=$ec_bank exchange=$ec_ex merchant=$ec_mer ===" log "=== done bank=$ec_bank exchange=$ec_ex merchant=$ec_mer ==="
# non-zero if bank failed (primary public flow numbers); soft on ex/mer # non-zero if bank failed (primary public flow numbers); soft on ex/mer
if [ "$ec_bank" -ne 0 ]; then if [ "${COLLECT_BANK}" = "1" ] && [ "$ec_bank" -ne 0 ]; then
return 1 return 1
fi fi
return 0 return 0

View file

@ -1,23 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Full-scan bank landing stats (all accounts, all ledger money). """Full-scan bank landing stats (all accounts, all ledger money).
Run on koopa as hernani (or anywhere with admin API access). Writes stats.json Generic for any currency stack (GOA, TESTPAYSAN, KUDOS, ). Run on the ops host
compatible with bank.hacktivism.ch/intro/ plus amount_alt fields for compact UI. (with admin API access). Writes stats.json for public landings + amount_alt.
Env (selected): Env (selected):
BANK_URL default http://127.0.0.1:9012 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_USER default admin
BANK_ADMIN_PASS or BANK_ADMIN_PASS_FILE / pass via --admin-pass-file BANK_ADMIN_PASS or BANK_ADMIN_PASS_FILE / pass via --admin-pass-file
BANK_EXPLORER_USER default explorer BANK_EXPLORER_USER default explorer
BANK_EXPLORER_PASS optional (balance_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 -) OUT output path (default stdout if -)
SCAN_SKIP comma usernames excluded from flow (default: exchange) SCAN_SKIP comma usernames excluded from flow (default: exchange)
TX_PAGE page size for transactions delta (default 500) TX_PAGE / MAX_TX_PAGES / ACCOUNTS_DELTA / RECENT_WD_N / TZ
MAX_TX_PAGES 0 = unlimited pages per account (default 0) STATS_SOURCE_LABEL optional string in stats.source field
ACCOUNTS_DELTA GET /accounts?delta= (default -10000)
RECENT_WD_N recent withdraws (default 10)
TZ default Europe/Zurich
""" """
from __future__ import annotations from __future__ import annotations
@ -343,6 +342,11 @@ def main() -> int:
"--exchange-config", "--exchange-config",
default=env("EXCHANGE_CONFIG_URL", "https://exchange.hacktivism.ch/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("--out", default=env("OUT", "-"))
ap.add_argument("--run-out", default=env("RUN_OUT", "")) ap.add_argument("--run-out", default=env("RUN_OUT", ""))
ap.add_argument( 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("--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("--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() args = ap.parse_args()
tz_name = env("TZ", "Europe/Zurich") or "Europe/Zurich" tz_name = env("TZ", "Europe/Zurich") or "Europe/Zurich"
@ -378,6 +387,7 @@ def main() -> int:
bank = args.bank.rstrip("/") bank = args.bank.rstrip("/")
skip = {s.strip() for s in (args.skip or "").split(",") if s.strip()} 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: def write_run(ok: bool, err: Optional[str] = None) -> None:
if not args.run_out: if not args.run_out:
@ -397,6 +407,24 @@ def main() -> int:
raise RuntimeError("no admin password (BANK_ADMIN_PASS or --admin-pass-file)") raise RuntimeError("no admin password (BANK_ADMIN_PASS or --admin-pass-file)")
token = get_token(bank, args.admin_user, admin_pass) token = get_token(bank, args.admin_user, admin_pass)
alt = load_alt_from_config(args.exchange_config) 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: if not alt:
alt = dict(DEFAULT_ALT) alt = dict(DEFAULT_ALT)
@ -442,13 +470,24 @@ def main() -> int:
except Exception: except Exception:
continue continue
low = (subject or "").lower() 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": if direction == "credit":
total_in += n total_in += n
n_incoming += 1 n_incoming += 1
incomings.append( incomings.append(
{ {
"kind": "incoming", "kind": "incoming",
"amount": amount if ":" in amount else f"GOA:{amount}", "amount": amount,
"at_unix": ts, "at_unix": ts,
"account": uname, "account": uname,
"subject": subject, "subject": subject,
@ -460,7 +499,7 @@ def main() -> int:
withdraws.append( withdraws.append(
{ {
"kind": "withdraw", "kind": "withdraw",
"amount": amount if ":" in amount else f"GOA:{amount}", "amount": amount,
"at_unix": ts, "at_unix": ts,
"account": uname, "account": uname,
"subject": subject, "subject": subject,
@ -503,7 +542,7 @@ def main() -> int:
users_n = sum(1 for u in accounts if u not in ("admin", "exchange")) users_n = sum(1 for u in accounts if u not in ("admin", "exchange"))
def pack_amt(d: Decimal) -> Dict[str, Any]: 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 { return {
"amount": f["amount"], "amount": f["amount"],
"amount_alt": f["amount_alt"], "amount_alt": f["amount_alt"],
@ -567,10 +606,11 @@ def main() -> int:
} }
) )
# explorer balance # explorer / pool balance (shared demo account when present)
balance = "GOA:0" zero = f"{currency}:0"
bal_alt = "GOA:0" balance = zero
bal_full = "GOA:0" bal_alt = zero
bal_full = zero
if explorer_pass: if explorer_pass:
try: try:
etok = get_token(bank, args.explorer_user, explorer_pass) etok = get_token(bank, args.explorer_user, explorer_pass)
@ -583,7 +623,7 @@ def main() -> int:
balance = str( balance = str(
data.get("balance", {}).get("amount") data.get("balance", {}).get("amount")
if isinstance(data.get("balance"), dict) 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 # sometimes balance is object with amount
if isinstance(data.get("balance"), dict) and data["balance"].get( 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 code == 200 and isinstance(data, dict):
if isinstance(data.get("balance"), 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"): elif data.get("amount"):
balance = str(data["amount"]) balance = str(data["amount"])
bf = format_amount_alt(balance, alt) bf = format_amount_alt(balance, alt)
@ -637,12 +677,12 @@ def main() -> int:
stats = { stats = {
"ok": True, "ok": True,
"currency": "GOA", "currency": currency,
"timezone": tz_name, "timezone": tz_name,
"generated_at": gen_iso, "generated_at": gen_iso,
"generated_at_human": gen_human, "generated_at_human": gen_human,
"generated_at_unix": now_u, "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, "bank_url": bank,
"scan": { "scan": {
"tx_page": args.tx_page, "tx_page": args.tx_page,

View file

@ -1,35 +1,41 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Install central landing-stats collector as hernani user systemd timer. # Install generic landing-stats collector as user systemd timer.
# #
# On koopa: # GOA / koopa (default):
# cd ~/src/koopa/koopa-admin-log # cd ~/src/koopa/koopa-admin-log
# ./scripts/taler-landing/install-landing-stats-host.sh # ./scripts/taler-landing/install-landing-stats-host.sh
# systemctl --user start taler-landing-stats.service # one-shot now
# systemctl --user status taler-landing-stats.timer
# #
# Requires: linger enabled for hernani (loginctl enable-linger hernani) # Stage TESTPAYSAN (stagepaysan on francpaysan host):
# so the timer runs without an interactive login. # STACK_PROFILE=stage-testpaysan ./scripts/taler-landing/install-landing-stats-host.sh
# # or from francpaysan-admin-log: ./scripts/stagepaysan/install-landing-stats.sh
#
# Optional: STACK_ENV_FILE=/path/to/profile.env
# Requires linger: sudo loginctl enable-linger $USER
set -euo pipefail set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)" ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
ADMIN_LOG="${ADMIN_LOG:-$ROOT}" ADMIN_LOG="${ADMIN_LOG:-$ROOT}"
SRC="$ADMIN_LOG/scripts/taler-landing" SRC="$ADMIN_LOG/scripts/taler-landing"
UNIT_SRC="$ADMIN_LOG/configs/systemd/user" UNIT_SRC="$ADMIN_LOG/configs/systemd/user"
PROFILE="${STACK_PROFILE:-goa}"
BIN_DST="${HOME}/.local/bin" BIN_DST="${HOME}/.local/bin"
LIB_DST="${HOME}/.local/lib/taler-landing" LIB_DST="${HOME}/.local/lib/taler-landing"
UNIT_DST="${HOME}/.config/systemd/user" UNIT_DST="${HOME}/.config/systemd/user"
STATE_DST="${HOME}/.local/state/taler-landing-stats" STATE_DST="${HOME}/.local/state/taler-landing-stats"
CFG_DST="${HOME}/.config/taler-landing"
mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST" mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST" "$CFG_DST"
install -m 0755 "$SRC/collect-landing-stats.sh" "$BIN_DST/collect-landing-stats.sh" install -m 0755 "$SRC/collect-landing-stats.sh" "$BIN_DST/collect-landing-stats.sh"
install -m 0755 "$SRC/collect_bank_stats.py" "$LIB_DST/collect_bank_stats.py" install -m 0755 "$SRC/collect_bank_stats.py" "$LIB_DST/collect_bank_stats.py"
install -m 0755 "$SRC/enrich_stats_alt.py" "$LIB_DST/enrich_stats_alt.py" install -m 0755 "$SRC/enrich_stats_alt.py" "$LIB_DST/enrich_stats_alt.py"
install -m 0755 "$SRC/merge_resources.py" "$LIB_DST/merge_resources.py" install -m 0755 "$SRC/merge_resources.py" "$LIB_DST/merge_resources.py"
install -m 0755 "$SRC/collect_container_resources.sh" "$LIB_DST/collect_container_resources.sh" install -m 0755 "$SRC/collect_container_resources.sh" "$LIB_DST/collect_container_resources.sh"
if [ -f "$SRC/test-landing-stats.sh" ]; then
install -m 0755 "$SRC/test-landing-stats.sh" "$BIN_DST/test-landing-stats.sh" 2>/dev/null || \ install -m 0755 "$SRC/test-landing-stats.sh" "$BIN_DST/test-landing-stats.sh" 2>/dev/null || \
install -m 0755 "$SRC/test-landing-stats.sh" "$LIB_DST/test-landing-stats.sh" install -m 0755 "$SRC/test-landing-stats.sh" "$LIB_DST/test-landing-stats.sh"
fi
install -m 0644 "$SRC/goa_amounts.py" "$LIB_DST/goa_amounts.py" install -m 0644 "$SRC/goa_amounts.py" "$LIB_DST/goa_amounts.py"
# mem-snapshot helper lives in taler-shared (copied into containers at collect time) # mem-snapshot helper lives in taler-shared (copied into containers at collect time)
if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then
@ -37,18 +43,34 @@ if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then
"$LIB_DST/mem-snapshot.sh" "$LIB_DST/mem-snapshot.sh"
fi fi
# Wrapper always uses installed lib next to itself when LIB discovery works; # Stack profile → ~/.config/taler-landing/stack.env (sourced by collector)
# also point ADMIN_LOG for in-container script refresh. if [ -n "${STACK_ENV_FILE:-}" ] && [ -f "${STACK_ENV_FILE}" ]; then
install -m 0644 "${STACK_ENV_FILE}" "$CFG_DST/stack.env"
elif [ -f "$SRC/profiles/${PROFILE}.env" ]; then
install -m 0644 "$SRC/profiles/${PROFILE}.env" "$CFG_DST/stack.env"
else
echo "WARN: no profile ${PROFILE}.env — collector uses built-in GOA defaults" >&2
fi
install -m 0644 "$UNIT_SRC/taler-landing-stats.service" "$UNIT_DST/" install -m 0644 "$UNIT_SRC/taler-landing-stats.service" "$UNIT_DST/"
install -m 0644 "$UNIT_SRC/taler-landing-stats.timer" "$UNIT_DST/" install -m 0644 "$UNIT_SRC/taler-landing-stats.timer" "$UNIT_DST/"
# Rewrite ExecStart to installed binary if unit uses %h path — units already do. # Point unit EnvironmentFile at stack profile
if grep -q '^EnvironmentFile=' "$UNIT_DST/taler-landing-stats.service" 2>/dev/null; then
:
else
# inject after [Service]
sed -i '/^\[Service\]/a EnvironmentFile=-%h/.config/taler-landing/stack.env' \
"$UNIT_DST/taler-landing-stats.service"
fi
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now taler-landing-stats.timer systemctl --user enable --now taler-landing-stats.timer
echo "Installed (user=$(id -un)):" echo "Installed (user=$(id -un) profile=${PROFILE}):"
echo " $BIN_DST/collect-landing-stats.sh" echo " $BIN_DST/collect-landing-stats.sh"
echo " $LIB_DST/{collect_bank_stats,enrich_stats_alt,goa_amounts}.py" echo " $LIB_DST/{collect_bank_stats,enrich_stats_alt,goa_amounts}.py"
echo " $CFG_DST/stack.env"
echo " $UNIT_DST/taler-landing-stats.{service,timer} (timer enabled)" echo " $UNIT_DST/taler-landing-stats.{service,timer} (timer enabled)"
echo " logs: $STATE_DST/" echo " logs: $STATE_DST/"
echo echo
@ -61,8 +83,6 @@ echo "Run once now:"
echo " systemctl --user start taler-landing-stats.service" echo " systemctl --user start taler-landing-stats.service"
echo " journalctl --user -u taler-landing-stats.service -n 40 --no-pager" echo " journalctl --user -u taler-landing-stats.service -n 40 --no-pager"
echo echo
echo "Optional secrets (if not readable via podman exec bank /root/…):" echo "Secrets (if not via podman exec bank):"
echo " mkdir -p ~/.config/taler-landing" echo " mkdir -p ~/.config/taler-landing && chmod 700 ~/.config/taler-landing"
echo " cp …/bank-admin-password.txt ~/.config/taler-landing/" echo " # bank-admin-password.txt bank-explorer-password.txt"
echo " cp …/bank-explorer-password.txt ~/.config/taler-landing/"
echo " chmod 600 ~/.config/taler-landing/*"

View file

@ -0,0 +1,18 @@
# Stack profile: GOA / hacktivism
# systemd EnvironmentFile: KEY=value only (no export)
BANK_CTR=taler-hacktivism-bank
EX_CTR=taler-hacktivism-exchange-ansible
MER_CTR=taler-hacktivism
BANK_URL=http://127.0.0.1:9012
BANK_PUBLIC_URL=https://bank.hacktivism.ch
EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config
BANK_CURRENCY=GOA
BANK_LANDING_IN=/var/www/bank-landing
EX_LANDING_IN=/var/www/exchange-landing
MER_LANDING_IN=/var/www/merchant-landing
COLLECT_BANK=1
COLLECT_EXCHANGE=1
COLLECT_MERCHANT=1
COLLECT_RESOURCES=1
PUBLISH_PODMAN=1
STATS_SOURCE_LABEL="host collect_bank_stats.py goa hacktivism"

View file

@ -0,0 +1,22 @@
# Stack profile: FrancPaysan stage / TESTPAYSAN (ops user stagepaysan)
# systemd EnvironmentFile: KEY=value only (no export, no shell expansion)
BANK_CTR=stage-lfp-bank
EX_CTR=stage-lfp-exchange-ansible
MER_CTR=stage-lfp-merchant
BANK_URL=http://127.0.0.1:9032
BANK_PUBLIC_URL=https://stage.bank.lefrancpaysan.ch
EXCHANGE_CONFIG_URL=https://stage.exchange.lefrancpaysan.ch/config
BANK_CURRENCY=TESTPAYSAN
SCAN_SKIP=exchange
BANK_LANDING_IN=/data/var/www
EX_LANDING_IN=/data/var/www
MER_LANDING_IN=/data/var/www
# Shared public feed: one bank scan → same stats.json on all three Caddy landings
# Colon-separated for systemd EnvironmentFile
HOST_STATS_DIRS=/var/www/lfp-stage/bank:/var/www/lfp-stage/exchange:/var/www/lfp-stage/merchant
COLLECT_BANK=1
COLLECT_EXCHANGE=0
COLLECT_MERCHANT=0
COLLECT_RESOURCES=0
PUBLISH_PODMAN=1
STATS_SOURCE_LABEL="host collect_bank_stats.py stage TESTPAYSAN shared"