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 + + + +