diff --git a/.gitignore b/.gitignore
index 5516c4f..9f39d9e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ __pycache__/
*.swp
*~
.tmp/
+scripts/taler-monitoring/settings.conf
diff --git a/2026/2026-07-16--container-autostart.md b/2026/2026-07-16--container-autostart.md
new file mode 100644
index 0000000..840a329
--- /dev/null
+++ b/2026/2026-07-16--container-autostart.md
@@ -0,0 +1,50 @@
+# Container autostart on reboot — 2026-07-16
+
+## How it is set (hernani@koopa, rootless podman)
+
+1. **`loginctl` Linger=yes** for `hernani` — user systemd runs without login.
+2. **`podman-restart.service` (user)** enabled — on boot starts all containers with
+ restart policy that should come up (`unless-stopped` / `always`, not manually stopped).
+3. **Per-container (or stack) user units** under `~/.config/systemd/user/container-*.service`
+ also enabled (belt-and-suspenders + DB ordering drop-ins).
+4. **Restart policy** on every production container: **`unless-stopped`**.
+
+## Production set (should-start-on-boot)
+
+| Container |
+|-----------|
+| `taler-hacktivism`, `taler-hacktivism-bank`, `taler-hacktivism-exchange-ansible` |
+| `koopa-forgejo`, `koopa-forgejo-db` |
+| `koopa-prime-jellyfin`, `koopa-prime-qbittorrent` |
+| `koopa-tor-relay`, `koopa-nym` |
+| `koopa-bonfire`, `koopa-bonfire-db` |
+| `koopa-castopod`, `koopa-castopod-mariadb`, `koopa-castopod-redis` |
+| `koopa-paivana`, `koopa-paivana-upstream` |
+| `koopa-tops-ng1` … `ng3` (+ `container-koopa-tops.service` ensure) |
+
+## Changed this day
+
+- Taler stack: `restart=no` → **`unless-stopped`**
+- New units: forgejo(+db), prime jellyfin/qbittorrent, paivana-upstream, three taler containers
+- Enabled **`podman-restart.service`**
+
+## Checks after reboot
+
+```bash
+systemctl --user is-enabled podman-restart.service
+podman ps -a --filter should-start-on-boot=true --format '{{.Names}} {{.Status}}'
+systemctl --user --failed
+systemctl --user is-active taler-merchant-apps.service taler-bank-apps.service
+~/.local/bin/ensure-taler-apps.sh status
+```
+
+## Taler apps inside containers (added same day, later)
+
+Merchant/bank containers only run **`sleep infinity`**. Autostart of the
+*apps* is **not** podman-restart alone — see:
+
+- `scripts/taler-shared/ensure-taler-apps.sh`
+- user units **`taler-merchant-apps.service`**, **`taler-bank-apps.service`**
+- drop-ins under `configs/systemd/user/container-taler-*.service.d/apps.conf`
+- install: `scripts/taler-shared/install-ensure-taler-apps.sh`
+- narrative: `2026-07-16--reboot-recovery.md`
diff --git a/2026/2026-07-16--koopa-nym.md b/2026/2026-07-16--koopa-nym.md
new file mode 100644
index 0000000..3c9a94e
--- /dev/null
+++ b/2026/2026-07-16--koopa-nym.md
@@ -0,0 +1,15 @@
+# koopa-nym (nym.com nym-node) — 2026-07-16
+
+Admin-log mirror for a new podman container **`koopa-nym`** running
+[nym-node](https://nym.com/docs/operators/nodes/nym-node) for the Nym mixnet /
+NymVPN network.
+
+| Item | Value |
+|------|--------|
+| Config | `configs/nym/` |
+| Scripts | `scripts/nym/` |
+| Default mode | `mixnode` |
+| HTTP host port | **9080** (avoids Tor **8080**) |
+| Mixnet | **1789** |
+
+Bonding and wallet mnemonics stay on the host secrets layout — not in this repo.
diff --git a/2026/2026-07-16--reboot-recovery.md b/2026/2026-07-16--reboot-recovery.md
new file mode 100644
index 0000000..e7d60aa
--- /dev/null
+++ b/2026/2026-07-16--reboot-recovery.md
@@ -0,0 +1,51 @@
+# Reboot recovery — 2026-07-16
+
+Host **koopa** rebooted (~20:23 CEST). Containers came up via
+`unless-stopped` + user units, but **Taler app processes inside** merchant/bank
+did **not** (containers run `sleep infinity`).
+
+## What was broken after boot
+
+| Symptom | Cause | Outcome |
+|---------|--------|---------|
+| `taler.hacktivism.ch/intro` **502** | only `sleep infinity` in merchant | fixed manually, then **automated** |
+| `bank.hacktivism.ch/intro` **502** | bank same; landing nginx off | fixed + automated |
+| Bank **auto-confirm** missing | not started after boot | automated (`--loop 2`) |
+| `koopa-tor-relay` crash-loop | host **`tor.service`** binds **8080**/**9051** | still open → `ISSUES.md` |
+| `koopa-paivana` **502** | merchant private API down at start | unit now **After** merchant-apps |
+| Exchange / forgejo / bonfire / castopod / tops / nym / prime | mostly OK | — |
+
+## Automation (removes manual post-boot)
+
+Installed on koopa (user **hernani**):
+
+| Piece | Role |
+|-------|------|
+| `~/.local/bin/ensure-taler-apps.sh` | start base + merchant/bank apps + auto-confirm |
+| `taler-merchant-apps.service` | oneshot after `container-taler-hacktivism` |
+| `taler-bank-apps.service` | oneshot after `container-taler-hacktivism-bank` |
+| drop-ins `container-taler-*.service.d/apps.conf` | `Wants=` the apps units |
+| paivana `order.conf` | `After=taler-merchant-apps.service` |
+
+Repo sources: `scripts/taler-shared/`, `configs/systemd/user/`.
+Install/reinstall: `scripts/taler-shared/install-ensure-taler-apps.sh`.
+
+```bash
+# status / force now
+systemctl --user start taler-merchant-apps.service taler-bank-apps.service
+~/.local/bin/ensure-taler-apps.sh status
+```
+
+Auto-confirm poll: **2 s** (was 4 s).
+
+## Quick health
+
+```bash
+curl -sk -o /dev/null -w '%{http_code}\n' https://taler.hacktivism.ch/config
+curl -sk -o /dev/null -w '%{http_code}\n' https://taler.hacktivism.ch/intro/
+curl -sk -o /dev/null -w '%{http_code}\n' https://bank.hacktivism.ch/config
+curl -sk -o /dev/null -w '%{http_code}\n' https://bank.hacktivism.ch/intro/
+curl -sk -o /dev/null -w '%{http_code}\n' https://exchange.hacktivism.ch/keys
+```
+
+Expected: **200** on config/intro/keys (site root may **302** → `/intro/`).
diff --git a/2026/2026-07-16.md b/2026/2026-07-16.md
new file mode 100644
index 0000000..7893b00
--- /dev/null
+++ b/2026/2026-07-16.md
@@ -0,0 +1,7 @@
+# 2026-07-16
+
+- **koopa-nym** (nym.com) mirror → `2026-07-16--koopa-nym.md`
+- **container autostart** (podman-restart + units) → `2026-07-16--container-autostart.md`
+- **reboot recovery + Taler in-container app autostart** → `2026-07-16--reboot-recovery.md`
+ (`ensure-taler-apps.sh`, `taler-*-apps.service`, auto-confirm **2 s**)
+- **Open issues** → [`ISSUES.md`](../ISSUES.md)
diff --git a/2026/2026-07-17.md b/2026/2026-07-17.md
new file mode 100644
index 0000000..de35634
--- /dev/null
+++ b/2026/2026-07-17.md
@@ -0,0 +1,5 @@
+# 2026-07-17
+
+- Bank: `taler://withdraw` **ohne `:443`** (Wallet-Apps); live deployed
+- Merchant/bank in-container apps after reboot via `ensure-taler-apps` + user units
+- Admin-log: 13 Commits (19:04–01:07) + follow-up; `main` force-pushed → Forgejo
diff --git a/ISSUES.md b/ISSUES.md
new file mode 100644
index 0000000..33f4f2a
--- /dev/null
+++ b/ISSUES.md
@@ -0,0 +1,139 @@
+# Open issues
+
+Stand **2026-07-16** (koopa, live). Keine Secrets.
+
+## Index
+
+| ID | Severity | Status | Kurz |
+|----|----------|--------|------|
+| [I-01](#i-2026-07-16-01) | high | open | firewalld: Nym-Ports unbestätigt |
+| [I-02](#i-2026-07-16-02) | medium | open | Nym nicht bonded / nicht in Explorern |
+| [I-03](#i-2026-07-16-03) | medium | mitigated | Taler-Apps nach Reboot |
+| [I-04](#i-2026-07-16-04) | low | open | Stray `kind_taussig` |
+| [I-05](#i-2026-07-16-05) | low | open | Alte exited Podman-Container |
+| [I-06](#i-2026-07-16-06) | low–medium | open | Nym HTTP API `*:9080` |
+| [I-07](#i-2026-07-16-07) | medium | **fixed** | Tor host vs `koopa-tor-relay` |
+
+---
+
+### I-2026-07-16-01
+
+**firewalld: Nym-Ports auf koopa unbestätigt**
+Severity: high · Host: koopa · Status: open
+
+VeciGate DNAT **1789/tcp+udp**, **1790/tcp**, **51822/udp** → koopa.
+Ohne passende **firewalld**-Allow-Regeln stirbt der Traffic am Host.
+
+Agent konnte `firewall-cmd` nicht ausführen (sudo-Passwort).
+
+```bash
+sudo firewall-cmd --permanent --add-port=1789/tcp
+sudo firewall-cmd --permanent --add-port=1789/udp
+sudo firewall-cmd --permanent --add-port=1790/tcp
+sudo firewall-cmd --permanent --add-port=51822/udp
+sudo firewall-cmd --reload
+sudo firewall-cmd --list-ports
+```
+
+---
+
+### I-2026-07-16-02
+
+**Nym-Node nicht bonded / nicht in öffentlichen Explorern**
+Severity: medium · Host: koopa (`koopa-nym`) · Status: open
+
+Container läuft (mixnode); API auf **9080** antwortet.
+Ohne **Bond** im Nym-Wallet erscheint der Node typischerweise nicht in Harbour Master / Explorern (anders als Tor nach ORPort-Publish).
+
+Siehe `configs/nym/PUBLIC-LISTING.md`.
+
+Logs: WARN `validator.nymtech.net/api/.../refresh-described` → **404** (API-Pfad; Node läuft trotzdem im Mixmode).
+
+**Identity key (public):** `55gPqeyHHj4CwpVZXLEQy9MjSvVTVXM8t2pMmNCH2MsW`
+
+---
+
+### I-2026-07-16-03
+
+**`podman-restart` / Taler-Apps nach Reboot**
+Severity: medium → mitigated · Host: koopa (hernani) · Status: mitigated 2026-07-16 Abend
+
+**Beobachtet:** Container up, aber Merchant/Bank nur `sleep infinity` → öffentlich **502** bis manuelles `start_base` / `start_*.sh`.
+
+**Mitigation:** User-Units `taler-merchant-apps.service` / `taler-bank-apps.service` + `~/.local/bin/ensure-taler-apps.sh`
+→ `2026/2026-07-16--reboot-recovery.md`. Nächster voller Reboot als Test.
+
+```bash
+systemctl --user is-active podman-restart.service \
+ taler-merchant-apps.service taler-bank-apps.service
+~/.local/bin/ensure-taler-apps.sh status
+```
+
+---
+
+### I-2026-07-16-04
+
+**Stray-Container `kind_taussig`**
+Severity: low · Host: koopa · Status: open
+
+Container **Up** ~5 Tage: `kind_taussig` (`debian:bookworm-slim`), einmaliger Wallet/DEB-Test, `restart=no`, kein Prod-Name.
+
+```bash
+podman stop kind_taussig && podman rm kind_taussig
+```
+
+---
+
+### I-2026-07-16-05
+
+**Alte exited Podman-Container (Müll)**
+Severity: low · Host: koopa · Status: open
+
+Mehrere **Exited**-Container (Monate/Jahre), z. B. `pensive_dubinsky`, `hardcore_edison`, `taler-exchange-no-network`, `debian00`, …
+Kein Autostart; nur Unordnung in `podman ps -a`.
+
+Optional: `podman container prune` (nur exited, nach Review).
+
+---
+
+### I-2026-07-16-06
+
+**Nym HTTP-API lauscht auf `*:9080`**
+Severity: low–medium · Host: koopa · Status: open (compose so konfiguriert)
+
+`ss` zeigt **\*:9080** (rootlessport). **Kein** VeciGate-DNAT für 9080 → WAN normalerweise nicht erreichbar; LAN ja.
+
+Härten optional: in compose nur `127.0.0.1:9080:8080` publishen.
+
+Hinweis: `nym-node` startet intern mit `--http-bind-address [::]:8080` (Container-intern). Öffentlich relevant ist das Host-Mapping **9080**, nicht WAN :8080 (das ist Tor OR).
+
+---
+
+### I-2026-07-16-07
+
+**`koopa-tor-relay` vs host `tor.service` (Port-Clash)**
+Severity: medium · Host: koopa · Status: **fixed** 2026-07-16
+
+**War:** Nach Reboot crash-loopte `koopa-tor-relay` — Host-`tor.service` hielt **8080**/**9051**.
+**Soll:** nur Container (`KoopaRelay`, ORPort **8080**, VeciGate WAN→koopa:8080).
+
+**Fix:** Host-Tor gestoppt/disabled; ggf. hängenden Prozess gekillt; User-Unit:
+
+```bash
+sudo systemctl disable --now tor
+sudo systemctl mask tor # optional
+systemctl --user enable --now container-koopa-tor-relay.service
+ss -lntp | grep -E '8080|9051'
+podman ps --filter name=koopa-tor-relay
+```
+
+**Live (nach Fix):** Host-`tor` inactive/disabled; `koopa-tor-relay` Up; 8080 + 127.0.0.1:9051 vom Relay-Prozess.
+
+---
+
+## Kein Issue (Scan)
+
+- Prod-Container mit Boot-Autostart: Up
+- Taler-Ports 9010–9015 antworten (401/302 OK)
+- Linger=yes, container-*-Units enabled
+- Tor-OR: Container-Pfad (I-07 fixed)
diff --git a/README.md b/README.md
index c165035..13bdec7 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,10 @@ Ops log and config mirror for host **koopa** (openSUSE Tumbleweed).
**Secrets:** not in this repo — see **`SECRETS.md`** and sibling **`koopa-admin-secrets`**.
+## Open issues
+
+**Visible issue list:** [`ISSUES.md`](ISSUES.md)
+
## Git origin
| | |
diff --git a/SECRETS.md b/SECRETS.md
index e74c10a..3b6d709 100644
--- a/SECRETS.md
+++ b/SECRETS.md
@@ -22,6 +22,7 @@ Live passwords/keys live in sibling **`koopa-admin-secrets`**.
| Prime (Jellyfin + qBittorrent) | `koopa/home-hernani/koopa-prime/{.env,users.env}` | `~/koopa-prime/` |
| Forgejo (**rootless**, git.hacktivism.ch) | `koopa/home-hernani/koopa-forgejo/{.env,users.env,compose.yml}` | `~/koopa-forgejo/` |
| Paivana (GOA paywall) | `koopa/home-hernani/koopa-paivana/secrets/*` | `~/koopa-paivana/secrets/` |
+| Nym (`koopa-nym`, nym.com) | `koopa/home-hernani/koopa-nym/.env` | `~/koopa-nym/.env` (+ `~/koopa-nym/data/` identity, host-only) |
## Rules
diff --git a/configs/README.md b/configs/README.md
index f1f03fb..1fff7ba 100644
--- a/configs/README.md
+++ b/configs/README.md
@@ -10,10 +10,15 @@ Directories are named to match **live podman container names** where possible.
| `taler-exchange-ansible/` | **`taler-hacktivism-exchange-ansible`** | `taler-hacktivism-exchange-ansible:landing` |
| `bank-landing/` `exchange-landing/` `merchant-landing/` | nginx landing snippets | ports 9013–9015 |
| `koopa-*` apps | `koopa-castopod`, `koopa-bonfire`, … | compose mirrors |
-| `tops/` | `koopa-tops-ng1` … `ng3` | `nginx:1.27-alpine` |
+| `tops/` | `koopa-tops-ng1` … `ng3` | `nginxinc/nginx-unprivileged:1.27-alpine` (non-root, :8080) |
| `caddy/` `firewalld/` `systemd/` | host services | |
-| `tor/` | **`koopa-tor-relay`** (podman host net) | `localhost/koopa-tor-relay:latest` |
-| `paivana/` | **`koopa-paivana`** (+ upstream) | `localhost/koopa-paivana:latest` |
+| `tor/` | **`koopa-tor-relay`** (podman host net) | `localhost/koopa-tor-relay:latest` (**non-root** uid 1000) |
+| `nym/` | **`koopa-nym`** (nym.com nym-node) | `localhost/koopa-nym:latest` (**non-root** uid 1000) |
+| `paivana/` | **`koopa-paivana`** (+ upstream) | `localhost/koopa-paivana:latest` (**non-root**); upstream unprivileged nginx |
+| `forgejo/` | **`koopa-forgejo`** | rootless image + `user: 1000` + `userns keep-id` |
+| `prime/` | jellyfin / qbittorrent | linuxserver **PUID/PGID=1000** |
+
+**Container process privilege policy:** service processes must not run as root inside the container when we control the image/compose. Pattern: uid/gid **1000** + rootless podman **`userns_mode: keep-id`** (see forgejo/nym/tor/paivana). Official DB images already drop to `postgres`/`redis`/`mysql`. Exceptions: **`taler-exchange-ansible`** (lab image with root SSH — not production service), third-party app images without a rootless variant (bonfire/castopod — track upstream).
**Authoritative running inventory:** `host/overview/LIVE.md`.
diff --git a/configs/bank-landing/README.md b/configs/bank-landing/README.md
index d59e83a..68f290f 100644
--- a/configs/bank-landing/README.md
+++ b/configs/bank-landing/README.md
@@ -103,11 +103,31 @@ taler-wallet-cli exchanges accept-tos https://exchange.hacktivism.ch/
---
-## Live stats (in bank container)
+## Live stats (central host collector · hernani systemd)
-Stats are **not** computed on the laptop or host browser. A small script runs
-**inside the bank container**, queries libeufin for the demo funding account
-(`explorer`), and writes a public JSON file next to the landing assets.
+**Primary:** user **hernani** on koopa runs `taler-landing-stats.timer`
+(`scripts/taler-landing/`). That process full-scans **all bank accounts**
+(except `exchange` double-count), paginates every ledger, writes
+`stats.json` with **GOA alt units** (`amount_alt` / Kilo-GOA / Mega-GOA / …),
+and refreshes exchange + merchant stats the same way.
+
+```bash
+# on koopa as hernani
+./scripts/taler-landing/install-landing-stats-host.sh
+sudo loginctl enable-linger hernani
+systemctl --user start taler-landing-stats.service
+```
+
+See **`scripts/taler-landing/README.md`**.
+
+**Fallback:** in-container `landing-stats.sh` (below) if the host timer is down.
+It now also scans admin + all users (skips only `exchange`).
+
+## Live stats (in bank container · fallback)
+
+Stats are **not** computed in the browser. A small script can still run
+**inside the bank container**, query libeufin (all accounts), and write a
+public JSON file next to the landing assets.
| Piece | Path / role |
|-------|-------------|
@@ -131,9 +151,9 @@ Stats are **not** computed on the laptop or host browser. A small script runs
| Env | Default | Meaning |
|-----|---------|---------|
-| `TX_DELTA` | `-50000` | per-account ledger window (`GET …/transactions?delta=`) |
-| `ACCOUNTS_DELTA` | `-500` | account-list window (`GET /accounts?delta=`) |
-| `MAX_SCAN_ACCOUNTS` | `500` | max usernames to scan |
+| `TX_DELTA` | `-200000` | per-account ledger window (`GET …/transactions?delta=`) |
+| `ACCOUNTS_DELTA` | `-10000` | account-list window (`GET /accounts?delta=`) |
+| `MAX_SCAN_ACCOUNTS` | `0` (unlimited) | max usernames to scan; `0` = all |
Older defaults (`TX_DELTA=-100`, `MAX_SCAN_ACCOUNTS=80`) **undercounted** credits/withdraws
and account totals on this stack. Empty accounts often return **HTTP 204** (no body) —
diff --git a/configs/bank-landing/goa-amount.js b/configs/bank-landing/goa-amount.js
new file mode 100644
index 0000000..67c3859
--- /dev/null
+++ b/configs/bank-landing/goa-amount.js
@@ -0,0 +1,169 @@
+/* Compact GOA / CUR:amount display using currency alt_unit_names.
+ * Large values → "1.23 Mega-GOA" (title = full GOA:n) so landing tiles fit.
+ */
+(function (global) {
+ "use strict";
+
+ var DEFAULT_ALT = {
+ "24": "Yotta-GOA",
+ "21": "Zetta-GOA",
+ "18": "Exa-GOA",
+ "15": "Peta-GOA",
+ "12": "Tera-GOA",
+ "9": "Giga-GOA",
+ "6": "Mega-GOA",
+ "3": "Kilo-GOA",
+ "0": "GOA",
+ "-1": "Deci-GOA",
+ "-2": "Centi-GOA",
+ "-3": "Milli-GOA",
+ "-6": "Micro-GOA",
+ "-7": "Deci-Micro-GOA",
+ "-8": "Atomic-GOA"
+ };
+
+ var THRESHOLD = 1000;
+
+ function parseAmount(s) {
+ if (s == null || s === "") return { cur: "GOA", val: 0 };
+ if (typeof s === "number") return { cur: "GOA", val: s };
+ var t = String(s).trim();
+ var i = t.indexOf(":");
+ if (i < 0) return { cur: "GOA", val: parseFloat(t) || 0 };
+ return {
+ cur: t.slice(0, i) || "GOA",
+ val: parseFloat(t.slice(i + 1)) || 0
+ };
+ }
+
+ function fmtCoeff(v) {
+ if (!isFinite(v)) return "?";
+ var a = Math.abs(v);
+ if (Math.abs(a - Math.round(a)) < 1e-9) return String(Math.round(v));
+ var s = v.toFixed(4).replace(/\.?0+$/, "");
+ return s;
+ }
+
+ function baseStr(cur, val) {
+ if (!isFinite(val)) return String(cur) + ":?";
+ if (Math.abs(val - Math.round(val)) < 1e-9) return cur + ":" + Math.round(val);
+ return cur + ":" + String(val).replace(/(\.\d*?[1-9])0+$/, "$1").replace(/\.0+$/, "");
+ }
+
+ /**
+ * @param {string|number} amount
+ * @param {object} [altMap]
+ * @returns {{ display: string, title: string, raw: string, usedAlt: boolean }}
+ */
+ function format(amount, altMap) {
+ if (amount == null || amount === "") {
+ return { display: "—", title: "", raw: "", usedAlt: false };
+ }
+ // Prefer server-provided alt if caller passes object {amount, amount_alt}
+ if (typeof amount === "object" && amount) {
+ if (amount.amount_alt) {
+ var rawO = amount.amount || amount.amount_full || String(amount.amount_alt);
+ return {
+ display: String(amount.amount_alt),
+ title: amount.amount_full || rawO,
+ raw: String(rawO),
+ usedAlt: true
+ };
+ }
+ amount = amount.amount || amount.amount_full || "";
+ }
+
+ var alt = altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT;
+ var p = parseAmount(amount);
+ var raw = baseStr(p.cur, p.val);
+ // Foreign currencies must not pick up Kilo-GOA / Mega-GOA labels
+ var baseName = (alt && alt["0"]) || p.cur;
+ var curU = String(p.cur || "").toUpperCase();
+ var baseU = String(baseName || "").toUpperCase();
+ if (curU && curU !== "GOA" && (baseU === "GOA" || baseU.indexOf("GOA") !== -1)) {
+ return { display: raw, title: raw, raw: raw, usedAlt: false };
+ }
+ if (p.val === 0) {
+ return { display: raw, title: raw, raw: raw, usedAlt: false };
+ }
+ if (Math.abs(p.val) < THRESHOLD) {
+ return { display: raw, title: raw, raw: raw, usedAlt: false };
+ }
+
+ var scales = [];
+ Object.keys(alt || {}).forEach(function (k) {
+ var n = parseInt(k, 10);
+ if (!isNaN(n)) scales.push({ sc: n, name: String(alt[k]) });
+ });
+ scales.sort(function (a, b) { return b.sc - a.sc; });
+
+ var abs = Math.abs(p.val);
+ var chosen = null;
+ for (var i = 0; i < scales.length; i++) {
+ var unit = Math.pow(10, scales[i].sc);
+ if (!(unit > 0)) continue;
+ var coeff = abs / unit;
+ if (coeff >= 1) {
+ chosen = {
+ sc: scales[i].sc,
+ name: scales[i].name,
+ coeff: p.val >= 0 ? coeff : -coeff
+ };
+ break;
+ }
+ }
+ if (!chosen || chosen.sc === 0) {
+ return { display: raw, title: raw, raw: raw, usedAlt: false };
+ }
+ var short = fmtCoeff(chosen.coeff) + " " + chosen.name;
+ return {
+ display: short,
+ title: short + " (" + raw + ")",
+ raw: raw,
+ usedAlt: true
+ };
+ }
+
+ function setText(el, amount, altMap) {
+ if (!el) return;
+ var f = format(amount, altMap);
+ el.textContent = f.display;
+ if (f.title && f.title !== f.display) el.setAttribute("title", f.title);
+ else el.removeAttribute("title");
+ }
+
+ function setById(id, amount, altMap) {
+ setText(document.getElementById(id), amount, altMap);
+ }
+
+ /** Prefer amount_alt from stats row, else format amount. */
+ function pick(rowOrAmount, altMap) {
+ if (rowOrAmount && typeof rowOrAmount === "object") {
+ if (rowOrAmount.amount_alt) {
+ return format({
+ amount: rowOrAmount.amount,
+ amount_alt: rowOrAmount.amount_alt,
+ amount_full: rowOrAmount.amount_full
+ }, altMap);
+ }
+ return format(rowOrAmount.amount || rowOrAmount.total_amount, altMap);
+ }
+ return format(rowOrAmount, altMap);
+ }
+
+ function rememberAlt(stats) {
+ if (stats && stats.alt_unit_names && typeof stats.alt_unit_names === "object") {
+ global.__GOA_ALT_UNITS = stats.alt_unit_names;
+ }
+ }
+
+ global.GoaAmount = {
+ format: format,
+ setText: setText,
+ setById: setById,
+ pick: pick,
+ rememberAlt: rememberAlt,
+ DEFAULT_ALT: DEFAULT_ALT,
+ THRESHOLD: THRESHOLD
+ };
+})(typeof window !== "undefined" ? window : globalThis);
diff --git a/configs/bank-landing/index.html b/configs/bank-landing/index.html
index 5dfebb3..bd47143 100644
--- a/configs/bank-landing/index.html
+++ b/configs/bank-landing/index.html
@@ -1491,6 +1491,34 @@ tw run-until-done && tw balance
+
+
+
+