Compare commits

...

No commits in common. "main" and "v1.13.8" have entirely different histories.

46 changed files with 2043 additions and 5206 deletions

View file

@ -9,7 +9,7 @@ Companions:
- Android GUI → [`android-test/GUI-AUTOMATION-NOTES.md`](./android-test/GUI-AUTOMATION-NOTES.md) - Android GUI → [`android-test/GUI-AUTOMATION-NOTES.md`](./android-test/GUI-AUTOMATION-NOTES.md)
- Git / suite tree → [`VERSIONS.md`](./VERSIONS.md) - Git / suite tree → [`VERSIONS.md`](./VERSIONS.md)
Scripts: `check_e2e.sh`, `check_amount_ladder.sh`, `taler-monitoring.sh`. Scripts: `check_e2e.sh`, `check_goa_ladder.sh`, `taler-monitoring.sh`.
Legend: Legend:
@ -47,82 +47,15 @@ Legend:
--- ---
## 3. `reserve_pub` / force-select is fragile (5114 + empty scrape) ## 3. `reserve_pub` / force-select is fragile (5114)
| | | | | |
|--|--| |--|--|
| **Seen** | ladder force-select, e2e confirm path, Android spinner issues; **repro 2026-07-19 magikoopa** GOA ladder: `force-select skipped · no reserve_pub for this withdraw (WID=…); wallet may not have selected yet` | | **Seen** | ladder force-select, e2e confirm path, Android spinner issues |
| **Area** | **cli** / **core** / **bank** / **ops** | | **Area** | **cli** / **core** / **bank** |
| **Problem** | (a) Cumulative wallets re-emit old reserves; binding the wrong `reserve_pub` yields **HTTP 409 / code 5114** (“already bound”). (b) Automation scrapes accept/tx dumps for candidates — when the dump has **no** pub at all, ladder WARNs and skips force-select (soft), then often **SKIP_CONFIRM**. (c) Empty scrape is **not** the same as “bank out of money”; do not treat as balance failure. (d) **Mon bug (fixed v1.20.2):** extract used `json.loads(file_from_first_brace)` on wallet dumps that append log lines after JSON → **zero** objects parsed; regex only scanned accept (which has no `reservePub`) while the pub lives under `transactions[].withdrawalDetails.reservePub`. | | **Problem** | Cumulative wallets re-emit old reserves; binding the wrong `reserve_pub` yields **HTTP 409 / code 5114** (“already bound”). Automation scrapes accept/tx dumps for candidates. |
| **Workaround** | **Mon ≥1.20.2:** `JSONDecoder.raw_decode` + regex on accept **and** transactions; score by WID in `bankConfirmationUrl` / amount. Still: score pubs by WID/amount; try several; treat 5114 as “stale reserve” not “out of money”. Refresh `transactions` before each force-select try. Soft-skip confirm if still pending after polls. Fix wallet DB / helper first if accept/tx only print FATAL (see §1516). | | **Workaround** | Score pubs by WID/amount; try several; treat 5114 as “stale reserve” not “out of money”. |
| **Wanted** | CLI returns **the** `reserve_pub` for the just-accepted withdraw in structured JSON; `force-select` / bank-side select API documented with idempotency. Optional: bank integration already exposes `selected_reserve_pub` when wallet selected — prefer that over scraping wallet logs. | | **Wanted** | CLI returns **the** `reserve_pub` for the just-accepted withdraw in structured JSON; `force-select` / bank-side select API documented with idempotency. |
### 3a. Ladder pin amounts hide mid-range behaviour
| | |
|--|--|
| **Seen** | `LADDER_STEPS=3` plan collapses to pins only: `GOA:0`, `max-1`, `max` (no random mid rungs) |
| **Area** | **ops** / **bank** / **cli** |
| **Problem** | **Zero** withdraw often never produces a usable `reserve_pub` / coins (wallet 7006 or no denoms). **Absolute max** and sometimes **max-1** hit **mint HTTP 500 / code 5110** SQL `P0001` (ceiling probe). Reproducing “real” force-select needs a **mid** amount (e.g. `GOA:1` … exchange min denom scale), not only pins. |
| **Workaround** | Use `LADDER_STEPS≥5` (or higher) so random mids exist; treat zero / absolute-max / max-1-5110 as probes (soft `CEILING_REJECT` in mon ≥1.20.2). |
| **Wanted** | Ladder plan docs: pin vs mid semantics; CLI structured error for amount-too-large / zero. |
### 3b. We do **not** manage to withdraw **in time** (GOA) — bank path unreliable, not load
| | |
|--|--|
| **Bottom line** | On live **hacktivism GOA**, automation **does not get spendable coins in time** for the pay phase (default wait **90 s**). Max-search may report a “best” amount and bank may even show `transfer_done`; pay still sees **`available=GOA:0`** with large **`pendingIncoming`**. Coins might arrive **later** (wirewatch / wallet) — **at least not within mons budget**. Recurring (several runs, 2026-07-19). |
| **Area** | **bank** + wire/wallet settle — **not** mon overload, **not** “laptop too slow” |
| **Not the cause: ecosystem load** | Outside-in check **2026-07-19 ~14:56 CEST**: bank/exchange/merchant all **HTTP 200**, config ~**50150 ms**, exchange `/keys` ~**0.5 s**. Container **loadavg ~1.51.7**, RSS bank **~737 MiB** / exchange **~670 MiB**, bank probes **47 ms**, **`wirewatch_running: true`**. Stack is **healthy under moderate load** — failures are **application/SQL/protocol**, not “host on fire.” |
| **Call site (mint)** | `POST ${BANK}/accounts/explorer/withdrawals` → often HTTP **500** `{ code: 5110, hint: "Unexpected sql error with state P0001" }` (**`BANK_UNMANAGED_EXCEPTION`**; Postgres **`P0001`** = `RAISE EXCEPTION`). Should often be clean **409** (insufficient funds); instead opaque **500**. |
| **Config** | `/config` still allows ~**2⁵²** wire max; live mint fails **far below** that. Explorer **balance_explorer** on landing still **~4.5 Peta-GOA**. |
#### Analysis — where withdraw dies
| Stage | What we see | Manage to withdraw? |
|-------|-------------|---------------------|
| **Mint (high amounts)** | 5110/P0001 while hi falls Tera→Giga→Mega→Kilo with **lo=0** | **No** — withdrawal never created |
| **Mint (some mid amounts)** | occasional **HTTP 200** + URI | Mint only — not done |
| **Confirm** | force-select 200 then stuck **`selected`**, confirm **HTTP 500** → mon **`SKIP_CONFIRM`** (bounds unchanged) | **No** — bank never wires; **not** out-of-money (§3 / 5114 class) |
| **Bank settle** | rare full path: confirm 204 + `transfer_done` | Bank side “OK_BANK” |
| **Wallet coins** | almost always **`available=0`** within wait, large **`pendingIncoming`**, board **`done=0`** | **Not in time** for pay → **SKIP_BALANCE** / skip max-pay |
**Pay-wait board (same cumulative wallet), example:**
```text
summary: tsv_ok=2 · wallet_wd=14 (pendingIncoming=14 done=0)
timer 0s left — available still GOA:0
pendingIncoming=GOA:335776
```
Reading: only **2** TSV rows are full OK/OK_BANK, but the wallet holds **14** withdrawal txs still **pendingIncoming** and **none done** after the wait. Max-search also leaves many **mint-OK / confirm-failed** withdraws that **inflate pendingIncoming** and may never complete. After **90 s** + short `run-pending`: **still no spendable balance****we do not manage to withdraw in time** for pay (whether coins would appear later is unknown to the job).
#### What *did* work (sparse) — for contrast
| When / run | What worked | Amounts |
|------------|-------------|---------|
| Landing **2026-07-19 14:1914:29 CEST** | Explorer singles **recorded** on bank intro stats | **~1.52 Giga-GOA**, **~145202 Mega-GOA**, **~1.83.4 Mega-GOA** |
| Max-search run A (same day) | Full bank path (mint→confirm→`transfer_done`) for **two** amounts only; mon best | **`GOA:424114`**, **`GOA:822256`** (~822 Kilo best) |
| Max-search run B (later) | Even fewer full successes; many mint OK + **SKIP_CONFIRM**; mon best collapsed | e.g. full success around **`GOA:12276`** / **`GOA:17257`** (~**17 Kilo** best) |
| Pay after either | Spendable **available** within **90 s** | **`GOA:0`** (not in time) |
So: **landing/history prove large withdraws were possible earlier the same day**; **automation does not manage a usable withdraw in time** (best amount drops run-to-run; coins not available inside mons wait).
#### Mon behaviour (workaround only)
- 5110 → soft **`CEILING_REJECT`** / max-search **`hi:=`**
- Full OK only raises **`lo`**; **SKIP_CONFIRM** does not
- Pay waits up to **`LADDER_PAY_WAIT_AVAILABLE_S`** (default 90) with withdraw board; then **SKIP_BALANCE** if still 0
- **Does not fix** bank SQL mapping, stuck confirm, or wirewatch→coins lag
#### Wanted (bank / exchange ops)
1. Map mint **P0001** to structured errors (not **5110**).
2. Confirm after force-select must not stick **selected** + HTTP 500.
3. After `transfer_done`, coins must become wallet **available** (or document delay + API to wait).
4. File bugs with: max-search “what worked” table + landing `recent_withdraws`/`balance_explorer` + load note (healthy) + pay board `tsv_ok` vs `wallet_wd`/`pendingIncoming`/`done`.
Sources: `https://bank.hacktivism.ch/intro/stats.json`, exchange/merchant intro stats, mon max-ladder logs 2026-07-19.
--- ---
@ -133,7 +66,7 @@ Sources: `https://bank.hacktivism.ch/intro/stats.json`, exchange/merchant intro
| **Seen** | Android endless spinner; GOA `bank.hacktivism.ch` withdraw; fixed in app branch `fix/bank-withdraw-auto-exchange` | | **Seen** | Android endless spinner; GOA `bank.hacktivism.ch` withdraw; fixed in app branch `fix/bank-withdraw-auto-exchange` |
| **Area** | **core** / **cli** | | **Area** | **core** / **cli** |
| **Problem** | Withdraw URI names an exchange the wallet has never added → stuck loading / empty exchange list. CLI requires explicit `exchanges add` + `update` + `accept-tos` every fresh DB. | | **Problem** | Withdraw URI names an exchange the wallet has never added → stuck loading / empty exchange list. CLI requires explicit `exchanges add` + `update` + `accept-tos` every fresh DB. |
| **Workaround** | e2e always adds exchange before bank withdraw ladder; Android needs ensureExchange-style fix. | | **Workaround** | e2e always adds exchange before ATM ladder; Android needs ensureExchange-style fix. |
| **Wanted** | CLI/core: on `accept-uri` withdraw, **auto-add** exchange from URI/bank details (`allowCompletion`), then surface ToS if needed in one command. | | **Wanted** | CLI/core: on `accept-uri` withdraw, **auto-add** exchange from URI/bank details (`allowCompletion`), then surface ToS if needed in one command. |
--- ---
@ -145,8 +78,8 @@ Sources: `https://bank.hacktivism.ch/intro/stats.json`, exchange/merchant intro
| **Seen** | ladder/e2e strip ports; QR checks; mint URIs with `:443` | | **Seen** | ladder/e2e strip ports; QR checks; mint URIs with `:443` |
| **Area** | **cli** / **bank** / **doc** | | **Area** | **cli** / **bank** / **doc** |
| **Problem** | Some bank/wallet outputs include `taler://…host:443/…`. QR validators and some clients reject or double-normalize inconsistently. | | **Problem** | Some bank/wallet outputs include `taler://…host:443/…`. QR validators and some clients reject or double-normalize inconsistently. |
| **Workaround** | mon ≥1.23.4: `normalize_taler_uri` (lib.sh) on ladder/e2e/pay; bank landing `demo-withdraw-api.py` normalizes mint JSON; **urls** phase fails GOA/local if public landings still emit `:443`/`:80` in `taler://` (`www.taler-uri-ports-*`, unit `tests/test_normalize_taler_uri.sh`). | | **Workaround** | `sed 's/:443\//\//g'` everywhere in automation. |
| **Wanted** | libeufin never emits default ports (source fix). | | **Wanted** | Canonical form without default ports in **all** wallet-cli and bank integration outputs; document as invariant. |
--- ---
@ -154,7 +87,7 @@ Sources: `https://bank.hacktivism.ch/intro/stats.json`, exchange/merchant intro
| | | | | |
|--|--| |--|--|
| **Seen** | e2e uses bank admin + bank withdraw create; ladder/gui-chain reimplement explorer mint in Python | | **Seen** | e2e uses bank admin + ATM create; ladder/gui-chain reimplement explorer mint in Python |
| **Area** | **cli** / **ops** | | **Area** | **cli** / **ops** |
| **Problem** | Automation always hand-rolls Basic auth → token → `POST …/withdrawals``taler_withdraw_uri`. Easy to get wrong (auth header, amount currency). | | **Problem** | Automation always hand-rolls Basic auth → token → `POST …/withdrawals``taler_withdraw_uri`. Easy to get wrong (auth header, amount currency). |
| **Workaround** | Shared Python in `goa-chain` / ladder / e2e; secrets files. | | **Workaround** | Shared Python in `goa-chain` / ladder / e2e; secrets files. |
@ -258,59 +191,6 @@ Sources: `https://bank.hacktivism.ch/intro/stats.json`, exchange/merchant intro
--- ---
## 15. `--wallet-db=*.json` is rejected (“memory backend not supported”)
| | |
|--|--|
| **Seen** | **2026-07-19 magikoopa** wallet-cli **1.6.8** / **1.6.11** bundle: any command with `--wallet-db=/tmp/foo.json` fails immediately with `Error: memory backend not supported` |
| **Area** | **cli** / **ops** |
| **Problem** | Current node host treats storage paths ending in **`.json` as unsupported memory backend** and throws before opening sqlite. Old notes / local tests that used `WALLET_DB=….json` break. Accept/transactions then never emit `reserve_pub` → ladder §3 empty-scrape WARNs. |
| **Workaround** | Use a **non-`.json` path**, e.g. `--wallet-db=$SCRATCH/wallet.sqlite3` (ladder already prefers `wallet.sqlite3`). Never pass `….json` as wallet-db. |
| **Wanted** | Clearer error: `wallet-db path must not end in .json; use a directory or .sqlite3 path`. Document legal path forms in `--help`. |
---
## 16. `taler-helper-sqlite3` requires Python ≥ 3.11 on PATH
| | |
|--|--|
| **Seen** | magikoopa: helper is a **Python** script (`#!/usr/bin/env python3`); with Apple `/usr/bin/python3` **3.9.6**`FATAL: python version >=3.11 required but running on 3.9.6` in accept/tx logs; wallet may still exit 0 on some paths while stdout is FATAL-only |
| **Area** | **ops** / **cli** |
| **Problem** | Wallet spawns `taler-helper-sqlite3` from **PATH**. Wrong `python3` ⇒ sqlite backend fails ⇒ no real wallet state ⇒ no `reserve_pub` for force-select. |
| **Workaround** | Put **Python ≥3.11** (or 3.12+) first on PATH for mon jobs; ensure `command -v taler-helper-sqlite3` works; smoke: `taler-helper-sqlite3` starts without FATAL. Prefer Homebrew / user `python3.11+` over macOS system 3.9 for agent PATH. |
| **Wanted** | Helper shebang or wrapper that finds a suitable python; wallet preflight error if helper HELLO fails (not silent empty logs). |
---
## 17. Portable wall-clock for scripts (no GNU `timeout` assumed)
| | |
|--|--|
| **Seen** | macOS agent shells: `timeout: command not found`; e2e already tries `timeout` then `gtimeout` then `perl -e alarm` |
| **Area** | **ops** / **doc** |
| **Problem** | GNU coreutils `timeout` is not on stock macOS. Hard-wiring it breaks portable repro and CI on Darwin. |
| **Workaround** | Prefer **python3** `subprocess.Popen(…).wait(timeout=…)` for outer budgets; for wallet-cli inner calls keep e2e order: `timeout``gtimeout` → perl alarm. Document both. |
| **Wanted** | One suite helper `run_with_budget SECS cmd…` in `lib.sh` used by ladder/e2e/smoke. |
---
## 18. How to withdraw (operator checklist, GOA laptop)
Portable path that works when §§1516 are fixed:
1. **PATH:** `taler-helper-sqlite3` + **python3 ≥ 3.11** ahead of system 3.9.
2. **Wallet DB:** `WDB=$TMP/wallet.sqlite3`**not** `*.json`.
3. **Exchange:** `exchanges add``update -f``accept-tos` for `https://exchange.hacktivism.ch/`.
4. **Bank mint (explorer):** token with explorer password from `SECRETS_ROOT` / `EXP_PW`; `POST …/accounts/explorer/withdrawals` with e.g. `{"amount":"GOA:1"}` (avoid pure **0** and absolute **max** pins for happy-path tests).
5. **Strip** `:443` from `taler_withdraw_uri`.
6. **`withdraw accept-uri --exchange URL URI`**.
7. **Bank:** poll `…/taler-integration/withdrawal-operation/{WID}` → when `selected`, confirm; if stuck `pending`, force-select with scraped `reserve_pub` (§3).
8. **Settle:** poll wallet balance / bank `transfer_done`**no** `run-until-done` (§1).
If step 6s stdout is only `FATAL: python version…` or `memory backend not supported`, **stop** and fix §1516 before debugging force-select.
---
## Priority shortlist for `taler-wallet-cli` / core ## Priority shortlist for `taler-wallet-cli` / core
If only a few changes land, these unlock the most automation: If only a few changes land, these unlock the most automation:
@ -320,7 +200,6 @@ If only a few changes land, these unlock the most automation:
3. **Auto-add exchange + ToS on accept-uri** (§4, §8) 3. **Auto-add exchange + ToS on accept-uri** (§4, §8)
4. **Canonical URIs without :443** (§5) 4. **Canonical URIs without :443** (§5)
5. **Emit reserve_pub for last withdraw** (§3) 5. **Emit reserve_pub for last withdraw** (§3)
6. **Clear wallet-db path rules + helper preflight** (§15, §16)
--- ---
@ -333,12 +212,7 @@ If only a few changes land, these unlock the most automation:
| exchange | Explicit add/update/accept-tos | | exchange | Explicit add/update/accept-tos |
| ladder explorer | `read_secret` / EXP_PW_FILE | | ladder explorer | `read_secret` / EXP_PW_FILE |
| stage maxima | bank `max_wire` + keys min denom | | stage maxima | bank `max_wire` + keys min denom |
| force-select | multi-rpub try; soft-skip confirm; empty scrape → WARN not balance fail | | force-select | multi-rpub try; soft-skip confirm |
| higher amounts / withdraw fail | **§3b: we do not manage to withdraw in time** on GOA (healthy load); mint 5110/P0001 + SKIP_CONFIRM + pendingIncoming; pay still available=0 after 90s; landing had Giga/Mega earlier same day |
| pay vs balance | never order more than wallet **available** (soft `SKIP_BALANCE`); pendingIncoming does not count as spendable |
| pay wait UX | board: OK/OK_BANK start order + live pendingIncoming/`done`; often `tsv_ok≪wallet_wd` and **done=0** after 90s → skip pay |
| wallet-db path | Prefer `*.sqlite3` (never `*.json` as db path) |
| helper / python | mon PATH must include helper + python≥3.11 |
--- ---

View file

@ -41,13 +41,9 @@ command -v qrencode zbarimg
| `jq` | nice-to-have; suite uses python3 for JSON | | `jq` | nice-to-have; suite uses python3 for JSON |
| `dig` / `bind-utils` | debug DNS; not required for default phases | | `dig` / `bind-utils` | debug DNS; not required for default phases |
| `ssh` | only for access mode **ssh** (laptop → `KOOPA_SSH` / stage `INSIDE_SSH`); not needed for **host-podman** | | `ssh` | only for access mode **ssh** (laptop → `KOOPA_SSH` / stage `INSIDE_SSH`); not needed for **host-podman** |
| `node` + `taler-wallet-cli` (.mjs) | **e2e** / ladder only (+ secrets) | | `node` / `pnpm` + wallet-cli | **e2e** / ladder only (+ secrets) |
| **`taler-helper-sqlite3` on PATH** | **required for e2e** — wallet-core spawns it for withdraw accept-uri / pay; missing → ENOENT mid-ladder. Put next to wallet bindir (`~/.local/bin` on stagepaysan) |
| `timeout` (coreutils) | preferred hard wall for `wcli` (suite falls back to perl only if missing) |
| `secrets.env` / admin passwords | e2e only — never system packages | | `secrets.env` / admin passwords | e2e only — never system packages |
**Preflight (agents / ops):** before trusting e2e on a host, smoke on the **same PATH** mon uses (`run-host-report` / systemd user): `command -v node taler-helper-sqlite3`, `node "$WALLET_CLI" --version`, short temp-DB `balance`. Do not wait for a full mon run to discover ENOENT.
### systemd user (no extra packages) ### systemd user (no extra packages)
- `systemd` user session + **`loginctl enable-linger hernani`** (already **Linger=yes** on koopa) - `systemd` user session + **`loginctl enable-linger hernani`** (already **Linger=yes** on koopa)
@ -70,17 +66,6 @@ command -v qrencode zbarimg
--- ---
## Franc Paysan stage host (stagepaysan) ## Franc Paysan hosts (francpaysan / stagepaysan)
FP stage (`stagepaysan` via SSH alias `francpaysan-stage-user`): curl, python3, podman if inside, qrencode/zbar for full urls; suite at `~/src/taler-monitoring`. FP hosts (`francpaysan` / `stagepaysan`): same idea — curl, python3, podman if inside, qrencode/zbar for full urls; suite at `~/src/taler-monitoring`.
**e2e on stagepaysan** also needs user-local:
| Path | Role |
|------|------|
| `~/.local/bin/node` | Node for wallet-cli |
| `~/.local/bin/taler-wallet-cli.mjs` (or `WALLET_CLI=`) | wallet CLI |
| `~/.local/bin/taler-helper-sqlite3` | **required** — accept-uri / coins |
| `~/francpaysan-secrets/stage/*` | bank admin + merchant token (layout like laptop `francpaysan-secrets`) |
Host-agent PATH already puts `$HOME/.local/bin` first; still smoke-test under that PATH.

View file

@ -45,7 +45,7 @@ Related:
Host packages (koopa / runners): **[DEPENDENCIES.md](./DEPENDENCIES.md)**. Host packages (koopa / runners): **[DEPENDENCIES.md](./DEPENDENCIES.md)**.
Host-agents (GOA + FP; shared report pipeline): **[host-agent/README.md](./host-agent/README.md)**. Host-agents (GOA + FP; shared report pipeline): **[host-agent/README.md](./host-agent/README.md)**.
Public HTML site-gen (same timeout/log/HTML defaults): **[site-gen/README.md](./site-gen/README.md)**. Public HTML site-gen (same timeout/log/HTML defaults): **[site-gen/README.md](./site-gen/README.md)**.
FP stage host-agent: `host-agent/run-fp-stage-monitoring.sh` + `host-agent/env/stagepaysan.env.example` (see `host-agent/README.md`). FP host-agent: `host-agent/run-fp-prod-monitoring.sh` / `run-fp-stage-monitoring.sh` + `host-agent/env/*.env.example` (see `host-agent/README.md`).
Report for the **GOA** stack with boxed severity badges and **grouped test IDs** Report for the **GOA** stack with boxed severity badges and **grouped test IDs**
`area.group-NN` (e.g. `www.exchange-01`, `e2e.pay-03`): `area.group-NN` (e.g. `www.exchange-01`, `e2e.pay-03`):
@ -203,9 +203,8 @@ Each monitored stack must say **what is bank, exchange, and merchant-backend**.
Edit **[domains.conf](./domains.conf)** (or set `TALER_DOMAINS_CONF=`): Edit **[domains.conf](./domains.conf)** (or set `TALER_DOMAINS_CONF=`):
```text ```text
# name bank exchange merchant currency local landing lang locale [canonical] # name bank exchange merchant-backend currency local [canonical]
taler-ops.ch bank.… exchange.… my.… CHF 0 0 en de-CH taler-ops.ch bank.taler-ops.ch exchange.taler-ops.ch my.taler-ops.ch CHF 0
stage.lefrancpaysan.ch stage.bank.… stage.exchange.… stage.monnaie.… TESTPAYSAN 0 1 fr fr-CH
``` ```
| Field | Meaning | | Field | Meaning |
@ -217,12 +216,8 @@ stage.lefrancpaysan.ch stage.bank.… stage.exchange.… stage.monnaie.… T
| **currency** | `GOA` / `KUDOS` / `CHF` / … | | **currency** | `GOA` / `KUDOS` / `CHF` / … |
| **local** | `1` = koopa SSH stack; `0` = public only | | **local** | `1` = koopa SSH stack; `0` = public only |
| **landing** | `1` = check `/intro` landings + assets; `0` = skip (TOPS / mytops) | | **landing** | `1` = check `/intro` landings + assets; `0` = skip (TOPS / mytops) |
| **lang** | UI language **`en`** / **`fr`** / **`de`** when `TALER_MON_LANG_SET≠1`. Stock profiles use **en** or **fr** only — **`de` is never a default** (opt-in via `--lang de` or an explicit profile edit). |
| **locale** | Regional l10n (BCP 47): **`fr-CH`** for FrancPaysan (Suisse romande); **`de-CH`** for other Swiss stacks. Aliases `ch-FR` / `ch-DE` accepted. Not the same as UI `lang`. |
| **canonical** | optional label after alias (e.g. `my.taler-ops.ch``taler-ops.ch`) | | **canonical** | optional label after alias (e.g. `my.taler-ops.ch``taler-ops.ch`) |
Workstation `TALER_MON_LANG=en` does **not** override a profile with `lang=fr`. Force English on FP only with `--lang en` / `TALER_MON_LANG_SET=1`.
Then: Then:
```bash ```bash
@ -267,21 +262,19 @@ Other domains: never SSH. Optional **e2e** aborts cleanly on login/KYC.
| | Local (koopa) | Remote | | | Local (koopa) | Remote |
|--|---------------|--------| |--|---------------|--------|
| **Bank withdraw** | 20 · 50 · 100 · 200 · **4200** (paivana) | 10 · 20 · 50 | | **ATM withdraw** | 20 · 50 · 100 · 200 · **4200** (paivana) | 10 · 20 · 50 |
| **Pay ladder** | 0.01 … 10 | 0.01 … 1 | | **Pay ladder** | 0.01 … 10 | 0.01 … 1 |
| **Paivana** | HTTP paywall + template pay **GOA:4200** (`goa-shop` / `paivana`) | skipped | | **Paivana** | HTTP paywall + template pay **GOA:4200** (`goa-shop` / `paivana`) | skipped |
```bash ```bash
./taler-monitoring.sh e2e ./taler-monitoring.sh e2e
./taler-monitoring.sh -d taler.net urls e2e ./taler-monitoring.sh -d taler.net urls e2e
# Amount ladder (any currency): classic 0 → … → max, then optional pay # Amount ladder (withdraw 0 → … → max, then pay): same script for GOA + stage
./taler-monitoring.sh ladder # classic (current domain) ./taler-monitoring.sh ladder # GOA / current domain
./taler-monitoring.sh -d stage.lefrancpaysan.ch ladder # TESTPAYSAN maxima ./taler-monitoring.sh -d stage.lefrancpaysan.ch ladder # TESTPAYSAN maxima
# Max-search: highest withdrawable + highest payable (≤ available; GOA free template goa-free)
./taler-monitoring.sh max-ladder
# Stage auto-reads bank max_wire_transfer_amount (e.g. 2000) and min denom (0.01). # Stage auto-reads bank max_wire_transfer_amount (e.g. 2000) and min denom (0.01).
# Explorer password: francpaysan-stage-user …/bank-explorer-password.txt (or EXP_PW=). # Explorer password: francpaysan-stage-user …/bank-explorer-password.txt (or EXP_PW=).
# Override: LADDER_MODE=max LADDER_MAX_PROBES=16 LADDER_MAX_AMOUNT=100 LADDER_STEPS=7 LADDER_PAY=0 … # Override: LADDER_MAX_AMOUNT=100 LADDER_STEPS=7 LADDER_PAY=0 …
# customize e2e: # customize e2e:
E2E_WITHDRAW_VALUES="20 50 100" E2E_PAY_VALUES="0.05 1 5" ./taler-monitoring.sh e2e E2E_WITHDRAW_VALUES="20 50 100" E2E_PAY_VALUES="0.05 1 5" ./taler-monitoring.sh e2e
E2E_VARIABLE=0 WITHDRAW_AMT=GOA:50 PAY_AMT=GOA:1 ./taler-monitoring.sh e2e # single fixed E2E_VARIABLE=0 WITHDRAW_AMT=GOA:50 PAY_AMT=GOA:1 ./taler-monitoring.sh e2e # single fixed
@ -306,7 +299,7 @@ E2E_VARIABLE=0 WITHDRAW_AMT=GOA:50 PAY_AMT=GOA:1 ./taler-monitoring.sh e2e # s
| **versions** | `deb.taler.net` reachable (InRelease/Packages/pool `.deb`); containers can reach it + have apt source; installed Taler packages vs **trixie** | | **versions** | `deb.taler.net` reachable (InRelease/Packages/pool `.deb`); containers can reach it + have apt source; installed Taler packages vs **trixie** |
| **sanity** | bank · exchange · merchant sections (public + server) | | **sanity** | bank · exchange · merchant sections (public + server) |
| **e2e** | account → credit → withdraw → wallet → confirm → order → pay | | **e2e** | account → credit → withdraw → wallet → confirm → order → pay |
| **ladder** | amount ladder modules: classic 23-rung wd+pay, or max-search wd+pay (`max-ladder`) | | **ladder** | GOA withdraw: fixed **0** + low bands + **random high ranges** + fixed **max** |
```bash ```bash
# package suite (default trixie on deb.taler.net) # package suite (default trixie on deb.taler.net)
@ -345,8 +338,8 @@ Amounts use exchange **`alt_unit_names`** (Kilo-GOA, Mega-GOA, …) with the bas
**GOA value in parentheses**: **GOA value in parentheses**:
```text ```text
[INFO] coins after-bank-wd — amount_circ=2 Kilo-GOA (GOA:2000) [INFO] coins after-ATM — amount_circ=2 Kilo-GOA (GOA:2000)
[INFO] coins after-bank-wd — denoms_in_circ 1 Kilo-GOA (GOA:1000)×2 (=2 Kilo-GOA (GOA:2000)) [INFO] coins after-ATM — denoms_in_circ 1 Kilo-GOA (GOA:1000)×2 (=2 Kilo-GOA (GOA:2000))
== ladder · rung 12 GOA:1000000 · 1 Mega-GOA (GOA:1000000) == == ladder · rung 12 GOA:1000000 · 1 Mega-GOA (GOA:1000000) ==
``` ```
@ -377,7 +370,7 @@ snapshotted via SSH whenever the stack is under load:
| Phase | When | | Phase | When |
|-------|------| |-------|------|
| **inside** | end of container status | | **inside** | end of container status |
| **e2e** | before work · after bank withdraws · after payments · after shop · e2e-end (+ delta) | | **e2e** | before work · after ATM withdraws · after payments · after shop · e2e-end (+ delta) |
| **ladder** | before first rung · after last rung (+ delta + timing overall) | | **ladder** | before first rung · after last rung (+ delta + timing overall) |
```bash ```bash
@ -396,14 +389,8 @@ LADDER_LOAD=0 ./taler-monitoring.sh ladder
| `METRICS_LOAD` | `1` | `0` = skip all host/container load probes | | `METRICS_LOAD` | `1` | `0` = skip all host/container load probes |
| `LADDER_LOAD` | `1` | `0` = skip load in ladder only | | `LADDER_LOAD` | `1` | `0` = skip load in ladder only |
| `LADDER_PAY` | `1` | `0` = skip payment ladder after withdraws | | `LADDER_PAY` | `1` | `0` = skip payment ladder after withdraws |
| `LADDER_MODE` | `classic` | default **classic** 23-rung wd+pay; `max` = max-search wd+pay (`max-ladder`) | | `LADDER_WITHDRAW_SCALE` | `1.5` | withdraw mids / pay mids ratio (funding headroom) |
| `LADDER_MAX_PROBES` | `32` | max-search probe budget; `0` = until converge / timeout | | `LADDER_STEPS` | `23` | rungs for both withdraw and pay (0 + mids + max1 + max) |
| `LADDER_HIGH_RUNGS` | `6` | max-search: first N probes random-high, then bisect |
| `LADDER_MAX_TOL_REL` | `0.001` | max-search relative lo/hi gap to stop |
| `LADDER_FREE_TEMPLATE` | `goa-free` | public free-amount pay template (hacktivism) |
| `LADDER_USE_FREE_TEMPLATE` | `1` | create pays via template POST `{"amount":…}` |
| `LADDER_WITHDRAW_SCALE` | `1.5` | classic: withdraw mids / pay mids ratio (funding headroom) |
| `LADDER_STEPS` | `23` | classic: rungs for withdraw and pay (0 + mids + max1 + max) |
| `METRICS_LOAD_SSH_TIMEOUT` | `90` | seconds for remote load python | | `METRICS_LOAD_SSH_TIMEOUT` | `90` | seconds for remote load python |
## Needs ## Needs
@ -446,10 +433,7 @@ Taler Operations **rusty** host (`taler-devtesting` forced command):
``` ```
Needs SSH to `devtesting@rusty.taler-ops.ch` (Host `taler-rusty-devtesting`). Needs SSH to `devtesting@rusty.taler-ops.ch` (Host `taler-rusty-devtesting`).
Probes: CLI help, `geniban` (CH IBAN / synthetic debit funding), baseline `fake-incoming`, Probes: CLI help, `geniban` (CH IBAN), `fake-incoming` CHF to exchange credit
optional **amount ladder** (`DEVTESTING_LADDER=1`, default **23** rungs up to **CHF:50000**).
Each rung reuses geniban debit payto so volume covers the max rung without a real bank withdraw.
`fake-incoming` CHF to exchange credit
IBAN. A nexus bounce *missing reserve public key* is **OK** for the plumbing IBAN. A nexus bounce *missing reserve public key* is **OK** for the plumbing
probe (no real wallet reserve required). probe (no real wallet reserve required).

View file

@ -24,11 +24,11 @@ Every check line has a **global** run number and a **grouped** id:
| **surface** | `check_surface.sh` | ecosystem inventory + **nmap** OS fingerprint; mail/mattermost content reported via surface page | | **surface** | `check_surface.sh` | ecosystem inventory + **nmap** OS fingerprint; mail/mattermost content reported via surface page |
| **sanity** | `check_sanity.sh` | `bank` `exchange` `merchant` | | **sanity** | `check_sanity.sh` | `bank` `exchange` `merchant` |
| **server** | `check_server.sh` | (flat `server-NN` or host groups) | | **server** | `check_server.sh` | (flat `server-NN` or host groups) |
| **e2e** | `check_e2e.sh` | `prereq` `load` `bank` `wallet` `bankwd` `settle` `pay` `shop` `paivana` `dig` `report` | | **e2e** | `check_e2e.sh` | `prereq` `load` `bank` `wallet` `atm` `settle` `pay` `shop` `paivana` `dig` `report` |
| **ladder** | `check_amount_ladder.sh` + `ladder/lib_pay.sh` | `plan` `load` `withdraw` `pay` `report` — classic / max-search; phases `ladder` · `max-ladder` · `goa-ladder` (alias) | | **ladder** | `check_goa_ladder.sh` | `plan` `load` `withdraw` `pay` `report` |
**Why groups:** flat `www-001``www-080` was hard to talk about. **Why groups:** flat `www-001``www-080` was hard to talk about.
`www.bank-04` / `e2e.bankwd-02` pin the failure to a logical block. `www.bank-04` / `e2e.atm-02` pin the failure to a logical block.
Usage in scripts: Usage in scripts:
@ -59,13 +59,10 @@ Numbering follows **executed** checks (early skip may shift later NN inside the
| **www.webui-** | SPA fingerprints: **`/webui/version.txt`**, **`/webui/version-overlay.txt`**, `index.html` / `index.js`; optional pin via `EXPECT_WEBUI_VERSION` / `EXPECT_WEBUI_OVERLAY` / `WEBUI_OVERLAY_DENY` | | **www.webui-** | SPA fingerprints: **`/webui/version.txt`**, **`/webui/version-overlay.txt`**, `index.html` / `index.js`; optional pin via `EXPECT_WEBUI_VERSION` / `EXPECT_WEBUI_OVERLAY` / `WEBUI_OVERLAY_DENY` |
| **www.paivana-** | local GOA paywall front (redirect to template) | | **www.paivana-** | local GOA paywall front (redirect to template) |
| **www.landing-** | own-stack intro links; static assets (`qrcode.min.js` hard; `og-goa-shop.png` hard only GOA/local); **demo-withdraw.json** GOA-only; shop-pay soft; stage merchant shop: `shop-ui.js` + `shops.css` + `qrcode.min.js`; cross-links local | | **www.landing-** | own-stack intro links; static assets (`qrcode.min.js` hard; `og-goa-shop.png` hard only GOA/local); **demo-withdraw.json** GOA-only; shop-pay soft; stage merchant shop: `shop-ui.js` + `shops.css` + `qrcode.min.js`; cross-links local |
| **www.taler-uri-ports-** | **no default `:443`/`:80`** in public `taler://` URIs — bank/exchange/merchant `/intro/` HTML, `demo-withdraw.json`, `auto-account.json`, bank `stats.json` (ERROR on GOA/local). Unit: `tests/test_normalize_taler_uri.sh` |
| **www.qr-** | QR payloads: harvest `taler://` / `payto://` / app `data-qr-url` from landings + mint JSON; **form** check; **qrencode → zbarimg** exact roundtrip; optional static QR images. Needs `qrencode` + `zbar-tools`. Skip: `QR_CHECK=0` | | **www.qr-** | QR payloads: harvest `taler://` / `payto://` / app `data-qr-url` from landings + mint JSON; **form** check; **qrencode → zbarimg** exact roundtrip; optional static QR images. Needs `qrencode` + `zbar-tools`. Skip: `QR_CHECK=0` |
**QR rule:** `taler://withdraw/HOST/taler-integration/UUID` (no default `:443`/`:80`); `taler://pay/` / `pay-template/`; `payto://…` shape OK; decoded PNG must equal payload. Form errors on withdraw/pay/payto are **ERROR** on local/GOA. **QR rule:** `taler://withdraw/HOST/taler-integration/UUID` (no default `:443`/`:80`); `taler://pay/` / `pay-template/`; `payto://…` shape OK; decoded PNG must equal payload. Form errors on withdraw/pay/payto are **ERROR** on local/GOA.
**Default port rule:** public and automation URIs must never keep libeufin-style `host:443` / `host:80`. Consumers use `normalize_taler_uri` (lib.sh); bank landing API normalizes in `demo-withdraw-api.py`.
**Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. Local stack may require content needle (terms/privacy/FADP/GOA…). **Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. Local stack may require content needle (terms/privacy/FADP/GOA…).
**Performance rule:** Measured from the **monitoring runner** (public via Caddy), not container loopback. Latency (ms) on every perf line + **perf summary** (n / min / p50 / avg / max). **Performance rule:** Measured from the **monitoring runner** (public via Caddy), not container loopback. Latency (ms) on every perf line + **perf summary** (n / min / p50 / avg / max).
@ -100,10 +97,10 @@ SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` when LAN is unrea
| Group | Checks | | Group | Checks |
|-------|--------| |-------|--------|
| **e2e.prereq-** | wallet-cli, currency, budgets, secrets, public reachability | | **e2e.prereq-** | wallet-cli, currency, budgets, secrets, public reachability |
| **e2e.load-** | host/container load snapshots (before / after bank withdraw / after pay) | | **e2e.load-** | host/container load snapshots (before / after ATM / after pay) |
| **e2e.bank-** | account, credit, withdraw ops | | **e2e.bank-** | account, credit, withdraw ops |
| **e2e.wallet-** | exchange + ToS, wallet setup | | **e2e.wallet-** | exchange + ToS, wallet setup |
| **e2e.bankwd-** | Bank withdraw ladder rungs | | **e2e.atm-** | ATM withdraw ladder rungs |
| **e2e.settle-** | wallet settlement wait / spendable balance | | **e2e.settle-** | wallet settlement wait / spendable balance |
| **e2e.pay-** | variable payments | | **e2e.pay-** | variable payments |
| **e2e.shop-** | GOA shop product catalog pays | | **e2e.shop-** | GOA shop product catalog pays |
@ -112,7 +109,7 @@ SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` when LAN is unrea
| **e2e.report-** | final tallies | | **e2e.report-** | final tallies |
When filing an issue, quote the full id + label, e.g. When filing an issue, quote the full id + label, e.g.
`e2e.bankwd-03 Bank withdraw 5 GOA · bank confirmed, wallet empty`. `e2e.atm-03 ATM withdraw 5 GOA · bank confirmed, wallet empty`.
--- ---
@ -144,7 +141,7 @@ Standalone: `host-agent/run-aptdeploy.sh` (on koopa local; else ssh `$DEPLOY_SSH
- OSV CVE query when package+version known → **ERROR** on hits - OSV CVE query when package+version known → **ERROR** on hits
- Never SSH / never podman on the *targets* - Never SSH / never podman on the *targets*
| **sanity.bank-** / **.exchange-** / **.merchant-** | public + optional server-side per component | | **sanity.bank-** / **.exchange-** / **.merchant-** | public + optional server-side per component |
| **ladder.plan-** / **.load-** / **.withdraw-** / **.pay-** / **.report-** | amount ladder classic or max-search (any currency; stage uses `max_wire`) | | **ladder.plan-** / **.load-** / **.withdraw-** / **.pay-** / **.report-** | amount ladder (GOA ceiling or stage TESTPAYSAN `max_wire`) |
| **server-** | SSH host ports / processes (flat unless grouped later) | | **server-** | SSH host ports / processes (flat unless grouped later) |
| **auth401.*** | merchant 401 / case matrix (WebUI APIs): **signup** `POST /instances` MIX id, **login** Basic user low vs MIX/UPPER, full **case** matrix, **password** bad/empty, **pwchange** `POST private/auth` + re-login, **admin-create** `POST /management/instances` (if admin token), **bearer**, **durable mon401**, **webui** SPA `toLowerCase` probe (`check_auth401.sh`) | | **auth401.*** | merchant 401 / case matrix (WebUI APIs): **signup** `POST /instances` MIX id, **login** Basic user low vs MIX/UPPER, full **case** matrix, **password** bad/empty, **pwchange** `POST private/auth` + re-login, **admin-create** `POST /management/instances` (if admin token), **bearer**, **durable mon401**, **webui** SPA `toLowerCase` probe (`check_auth401.sh`) |
@ -205,5 +202,4 @@ Always use `printf --` friendly plain headers when colour is off (leading `---`
| **devtesting.cli-** | SSH to rusty · `taler-devtesting` CLI | | **devtesting.cli-** | SSH to rusty · `taler-devtesting` CLI |
| **devtesting.geniban-** | synthetic CH IBAN | | **devtesting.geniban-** | synthetic CH IBAN |
| **devtesting.credit-payto-** | exchange credit IBAN for nexus | | **devtesting.credit-payto-** | exchange credit IBAN for nexus |
| **devtesting.fake-incoming-** / **baseline-** | CHF IN via nexus; bounce missing reserve pub = plumbing OK | | **devtesting.fake-incoming-** | CHF IN via nexus; bounce missing reserve pub = plumbing OK |
| **devtesting.ladder-** | CHF fake-incoming amount ladder (default 23 rungs → CHF:50000; synthetic geniban debit each rung) |

View file

@ -1 +1 @@
1.23.4 1.13.8

View file

@ -17,45 +17,6 @@ Git tags: `vMAJOR.FEATURE.FIX` (e.g. `v1.8.0`). File `VERSION` omits the `v` pre
| Tag | Date (UTC) | Notes | | Tag | Date (UTC) | Notes |
|-----|------------|--------| |-----|------------|--------|
| **v1.23.4** | 2026-07-19 | **Bugfix / mon:** shared **normalize_taler_uri** (strip taler:// host :443/:80); ladder/e2e/pay use it. **urls** scans bank+exchange+merchant intros + demo-withdraw/auto-account/stats for default ports (**ERROR** on GOA). Unit test tests/test_normalize_taler_uri.sh. |
| **v1.23.3** | 2026-07-19 | **Bugfix:** payments stale because max-ladder never got coins — root cause on koopa was **taler-wallet-cli missing** (all mint-OK → SKIP_ACCEPT). Hard-fail ladder prereq if CLI/helper missing; settle keeps polling after transfer_done + short run-pending; pay wait default 180s; max-ladder wrapper sets PATH/WALLET_CLI. |
| **v1.23.2** | 2026-07-19 | **Bugfix:** max-ladder host-agent forces **RUN_TIMEOUT=10800** (koopa env had 600 → run aborted mid-ladder); install starts oneshots with **--no-block**. |
| **v1.23.1** | 2026-07-19 | **Fix / scope:** only **`/monitoring-max-ladder/`** for GOA (drop classic `/monitoring-ladder`). Daily timer + install first-run. monpages catalog/Caddy bare redirs for max-ladder only. |
| **v1.23.0** | 2026-07-19 | **Feature:** GOA daily mon page **`/monitoring-max-ladder/`** (max-search) on bank/exchange/taler.hacktivism.ch *(1.23.0 briefly also had classic ladder; removed in 1.23.1)*. |
| **v1.22.1** | 2026-07-19 | **Bugfix (UX):** pay wait after withdraw no longer looks hung — shows **withdraw board in start order** (OK/OK_BANK from TSV) with live wallet status (`pendingIncoming``done`) + timer + aggregate available/pendingIncoming on the **same** wallet DB. Short `run-pending` nudge only (no run-until-done). Default wait 90s (`LADDER_PAY_WAIT_AVAILABLE_S`). |
| **v1.22.0** | 2026-07-19 | **Feature:** modular amount ladder — `ladder/lib_pay.sh` (pay) + withdraw in `check_amount_ladder.sh`; `ladder/extract_rpubs.py`. **max-ladder** hunts highest **withdrawable** and highest **payable** (≤ wallet available). GOA pays use public free-amount template **`goa-free`** (`LADDER_FREE_TEMPLATE`). Classic still default 23-rung wd+pay. Phase alias `goa-ladder` → amount ladder (shim removed). |
| **v1.21.0** | 2026-07-19 | **Feature:** generic **amount ladder** (`check_amount_ladder.sh`; compat shim `check_goa_ladder.sh`). Default **classic** 23-rung withdraw (+ pay when enabled); **max-search** (`max-ladder` / `LADDER_MODE=max`) hunts highest mintable amount (probes default 32, `0`=until timeout). **Pay:** never overspend — soft `SKIP_BALANCE` if amount > wallet available. **Alt units:** report/logs show human names (e.g. `8.15 Tera-GOA (GOA:…)`) + raw. **Fixes bundled:** external extract (no stdin SyntaxError); force-select multi-rpub; soft `CEILING_REJECT` on mint 5110/P0001; crash-proof report. CLI-AUTOMATION-NOTES §3/3a/3b/§1518. |
| **v1.20.1** | 2026-07-19 | **Docs:** CLI-AUTOMATION-NOTES — force-select empty scrape + ladder pins (§3/3a); wallet-db must not be `*.json` (§15); `taler-helper-sqlite3` needs Python ≥3.11 (§16); portable wall-clock (§17); GOA withdraw checklist (§18). No code change. *(Superseded notes folded into **v1.21.0** feature release.)* |
| **v1.20.0** | 2026-07-19 | **Feature:** domains.conf **`locale`** (l10n) — FP **`fr-CH`**, others **`de-CH`** (aliases ch-FR/ch-DE); UI **`lang=de`** supported (sticky + console tags) but **never** a stock profile default; env `TALER_MON_LOCALE` / `TALER_DOMAIN_LOCALE`. |
| **v1.19.1** | 2026-07-19 | **Bugfix / docs:** DEPENDENCIES — e2e requires `taler-helper-sqlite3` on mon PATH + preflight; README domains.conf documents **`lang`** column. |
| **v1.19.0** | 2026-07-19 | **Feature:** domains.conf **`lang`** column (en\|fr) is the global UI language default per stack; i18n + HTML read `TALER_DOMAIN_LANG` / conf (FP profiles `lang=fr`). |
| **v1.18.9** | 2026-07-19 | **Bugfix:** UI lang — *lefrancpaysan*/*francpaysan* always default **fr** unless `TALER_MON_LANG_SET=1` / `--lang`; workstation `TALER_MON_LANG=en` no longer locks English (host-agent + console_to_html). |
| **v1.18.8** | 2026-07-19 | **Bugfix:** e2e wallet — require `taler-helper-sqlite3` on PATH; use absolute `node` + `timeout` instead of fragile `perl -e alarm` for wcli; clearer accept-uri errors (no raw alarm-shift noise). |
| **v1.18.7** | 2026-07-19 | **Bugfix:** mail/surface catalogs — TSA/Anastasis MX is **mail.anastasis.lu** (→ pixel), not non-resolving `anastasis.taler-systems.com`; add `anastasis.lu` + pixel host probes (fixes surface_err mail.*). **Also:** FP stage host-agent defaults include **e2e** + 1800s timeout; stagepaysan.env.example wires francpaysan-secrets + wallet-cli. |
| **v1.18.6** | 2026-07-19 | **Bugfix:** TESTS.md table cell for `e2e.bankwd-` markdown typo from rename. |
| **v1.18.5** | 2026-07-19 | **Bugfix:** TESTS.md e2e group list `atm``bankwd` (missed in 1.18.4). |
| **v1.18.4** | 2026-07-19 | **Bugfix:** e2e group id **atm → bankwd** (`e2e.bankwd-*`); finish ATM wording cleanup so mon logs match **Bank withdraw** (no leftover “atm” phase label). Env `E2E_ATM_MAX` still accepted as alias. |
| **v1.18.3** | 2026-07-19 | **Bugfix / wording:** e2e user-facing **ATM withdraw****Bank withdraw** (amounts still CHF-style cash-note ladder 20/50/100/200/… — “ATM” was only a metaphor for those note sizes, not a physical ATM). Env: `E2E_BANK_WITHDRAW_MAX` (legacy alias `E2E_ATM_MAX`). Internal group id `e2e.atm-*` kept for check-id stability. |
| **v1.18.2** | 2026-07-19 | **Bugfix:** never publish / accept **stale mon HTML assets** — refuse pre-1.15.8 box-drawing sticky JS in site-gen + generated pages; monpages ERROR on public pages still carrying that JS or teed `SyntaxWarning: invalid escape sequence`; purge `site-gen/__pycache__` after suite reset; convert with `PYTHONWARNINGS=error::SyntaxWarning`. |
| **v1.18.1** | 2026-07-19 | **Bugfix:** (1) mon HTML sticky/err counts honor French badges **ERREUR** / **AVERT** (were plain → 0 errors on FP pages); (2) `stage-lfp` + `INSIDE_PODMAN=1` uses **host-podman** (no empty `INSIDE_SSH` fail); (3) reaffirm filter JS has no box-glyph/`\\-` regex (charCodeAt only — kills SyntaxWarning “geheimsprache” noise). |
| **v1.18.0** | 2026-07-19 | **Feature:** mattermost — client compatibility matrix: **mobile**, **desktop**, **vendor support** floors (default ≥10.11.0, ERROR) + **web** WARN on EOL; env `MATTERMOST_DESKTOP/SUPPORT_MIN_SERVER`, `MATTERMOST_CLIENT_CHECK`. |
| **v1.17.0** | 2026-07-19 | **Feature:** mattermost phase — check **server version** (`X-Version-Id`) against **mobile store min** (default **≥10.11.0**); ERROR when too old (newest Android/iOS apps break); env `MATTERMOST_MOBILE_MIN_SERVER` / `MATTERMOST_MOBILE_CHECK=0`. |
| **v1.16.0** | 2026-07-19 | **Feature:** mon sticky — blue **info** + green **ok** filter badges; suite **version tag under status pill** (not blue badge); filters `#filter-info` / `#filter-ok`. |
| **v1.15.8** | 2026-07-19 | **Bugfix:** mon HTML filter JS — drop literal box-drawing / `\-` regex that caused Python SyntaxWarning (and weird glyphs in suite logs); use Unicode code-point loop instead. |
| **v1.15.7** | 2026-07-19 | **Bugfix:** mon HTML — do not paint «perf/ladder summary» as SUMMARY header (restore 2 INFO lines); drop converter SyntaxWarning/noise from public log; «wrote» on stderr; host-agent PYTHONWARNINGS for convert. |
| **v1.15.6** | 2026-07-19 | **Bugfix:** aptdeploy ERROR jump list is self-contained — systemd httpd failures include before/after state, Result/SubState, failed units, and journal snippets (no more vague «see systemd lines»); ldd/--version errors embed concrete missing libs / exit output. |
| **v1.15.5** | 2026-07-19 | **Bugfix:** devtesting CHF ladder — `ssh` was draining ladder stdin (only rung 1/23 ran → progress 45/98 then snap 52/52); use `ssh -n` + `mapfile`; incomplete ladder is WARN not false complete. |
| **v1.15.4** | 2026-07-19 | **Bugfix:** French sticky titles — page label *surveillance*, stack summary *public + interne*, SUMMARY verdict/totals via i18n (`tout est clair`, `totaux :`). |
| **v1.15.3** | 2026-07-19 | **Bugfix:** console language auto-**fr** for `*lefrancpaysan*` domains again — env `TALER_MON_LANG=en` no longer locks English; only `--lang` / `TALER_MON_LANG_SET=1` is explicit. |
| **v1.15.2** | 2026-07-19 | **Bugfix:** warn/error filter — drop blank log lines from HTML; compact console (no black voids above/mid/below); context skips SUMMARY chrome & pure box frames; tighter filter-gap. |
| **v1.15.1** | 2026-07-19 | **Bugfix:** mon HTML — phase `section()` boxes no longer painted as SUMMARY (were blue/green `sum-header`); only real SUMMARY rows get solid severity backgrounds (OK/WARN/ERROR/INFO). |
| **v1.15.0** | 2026-07-19 | **Feature:** CHF **fake-incoming amount ladder** in `devtesting` (`DEVTESTING_LADDER=1`, default **23** log-spaced rungs **CHF:0.01 → CHF:50000**); synthetic geniban debit funds each rung; baseline plumbing probe kept. |
| **v1.14.0** | 2026-07-19 | **Feature:** SUMMARY box rows are **colour-distinct** (per-severity badge + tinted background + bold counts; verdict-coloured header); HTML mon pages style `sum-ok` / `sum-warn` / `sum-err` / … panels the same way. |
| **v1.13.13** | 2026-07-19 | **Bugfix:** progress estimate for **devtesting** / fake-franken is 8 (happy-path numbered checks), not 10 — avoids mid-run 42/48 then end snap 46/46. |
| **v1.13.12** | 2026-07-19 | **Bugfix:** SUMMARY chrome (`[ WARN ] 2`, `warnings only`, OK/ERROR counts without `#NNN`) is **meta**, not warn/error — sticky counts + warn filter no longer double SUMMARY rows; filter context skips blank lines. |
| **v1.13.11** | 2026-07-19 | **Bugfix:** sticky error/warn filter keeps **±3 dimmed context lines** around each match (gray/grayscale) so phase/group context stays readable; dashed gap between distant clusters. |
| **v1.13.10** | 2026-07-19 | **Bugfix:** multi-phase runs print **one global SUMMARY** at parent end (aggregated OK/ERROR/WARN/INFO/BLOCK via `TALER_MON_STATE`); no phase SUMMARY at all (not even last phase); mid-phase still lists ERRORS/BLOCKERS; solo `check_*.sh` unchanged. |
| **v1.13.9** | 2026-07-19 | **Bugfix:** no mid-run SUMMARY / progress standings between phases (no more 34/52 + SUMMARY after urls while monpages still runs); full stand only on last phase or solo check script; `progress_finish` remains. |
| **v1.13.8** | 2026-07-19 | **Bugfix:** monpages **TOP** meta/marker failures (missing generated/version-link/taler-mon-page, empty race body) are **WARN only** — no longer self-red the run; optional `MONPAGES_TOP_STRICT=1` restores ERROR. | | **v1.13.8** | 2026-07-19 | **Bugfix:** monpages **TOP** meta/marker failures (missing generated/version-link/taler-mon-page, empty race body) are **WARN only** — no longer self-red the run; optional `MONPAGES_TOP_STRICT=1` restores ERROR. |
| **v1.13.7** | 2026-07-19 | **Bugfix:** move optional/meta scripts under `meta/` (deprecated mail/mm units, firecuda outside-runner, legacy multi-host site-gen); production path stays host-agent + `site-gen/console_to_html.py`. | | **v1.13.7** | 2026-07-19 | **Bugfix:** move optional/meta scripts under `meta/` (deprecated mail/mm units, firecuda outside-runner, legacy multi-host site-gen); production path stays host-agent + `site-gen/console_to_html.py`. |
| **v1.13.6** | 2026-07-19 | **Bugfix:** machine paths and SSH hosts only via env (`~/.config/taler-monitoring/env` / `taler-monitoring-env`); suite loads env before defaults; no `/home/hernani` or bogus host hardcodes. | | **v1.13.6** | 2026-07-19 | **Bugfix:** machine paths and SSH hosts only via env (`~/.config/taler-monitoring/env` / `taler-monitoring-env`); suite loads env before defaults; no `/home/hernani` or bogus host hardcodes. |

File diff suppressed because it is too large Load diff

View file

@ -127,7 +127,7 @@ _merchant_ver() {
check_ldd_deep() { check_ldd_deep() {
local name=$1 local name=$1
local out missing local out
out=$("$PODMAN_BIN" exec "$name" bash -lc ' out=$("$PODMAN_BIN" exec "$name" bash -lc '
set +e set +e
bin=$(command -v taler-merchant-httpd) bin=$(command -v taler-merchant-httpd)
@ -147,48 +147,15 @@ check_ldd_deep() {
info "ldd" "$line" info "ldd" "$line"
done <<<"$out" done <<<"$out"
if echo "$out" | grep -q 'not found'; then if echo "$out" | grep -q 'not found'; then
missing=$(echo "$out" | grep 'not found' | tr '\n' ' ' | head -c 280) err "ldd" "shared library missing in $name" "see ldd lines above"
err "ldd" "shared library missing in $name" "${missing:-not found (see ldd INFO above)}"
return 1 return 1
fi fi
return 0 return 0
} }
# Build a single-line diagnosis for err() so jump lists are self-contained
# (operators should not need to hunt nearby INFO lines).
_apt_systemd_diagnose() {
local out="$1"
local before after failed journal status_line result
before=$(echo "$out" | sed -n 's/^httpd_active: //p' | tail -1)
after=$(echo "$out" | sed -n 's/^after_start_httpd: //p' | tail -1)
failed=$(echo "$out" | sed -n 's/^failed_taler: //p' | grep -v '^none$' | tr '\n' ';' | sed 's/;$//')
[ -z "$failed" ] && failed=$(echo "$out" | grep -E '^\s*●\s+taler-' | head -3 | tr '\n' ';' | sed 's/;$//')
result=$(echo "$out" | sed -n 's/^httpd_Result: //p' | tail -1)
[ -z "$result" ] && result=$(echo "$out" | sed -n 's/^ *Result: //p' | head -1)
status_line=$(echo "$out" | sed -n 's/^httpd_status_line: //p' | tail -1)
journal=$(echo "$out" | sed -n 's/^httpd_journal: //p' | tr '\n' ' ' | head -c 220)
# fallback: first non-meta journal-ish line
if [ -z "$journal" ]; then
journal=$(echo "$out" | grep -E 'taler-merchant-httpd\[[0-9]+\]|Failed|error|Error|NRestarts|Main PID' | head -3 | tr '\n' ' ' | head -c 220)
fi
local parts=()
[ -n "$before" ] && parts+=("before=${before}")
[ -n "$after" ] && parts+=("after_start=${after}")
[ -n "$result" ] && parts+=("Result=${result}")
[ -n "$status_line" ] && parts+=("status=${status_line}")
[ -n "$failed" ] && [ "$failed" != "none" ] && parts+=("failed_units=${failed}")
[ -n "$journal" ] && parts+=("journal=${journal}")
if [ "${#parts[@]}" -eq 0 ]; then
printf '%s' "no status/journal captured (podman exec empty?)"
return
fi
local IFS=' · '
printf '%s' "${parts[*]}"
}
check_systemd() { check_systemd() {
local name=$1 local name=$1
local out active_httpd before_httpd detail local out active_httpd
out=$("$PODMAN_BIN" exec "$name" bash -lc ' out=$("$PODMAN_BIN" exec "$name" bash -lc '
set +e set +e
echo "system: $(systemctl is-system-running 2>&1)" echo "system: $(systemctl is-system-running 2>&1)"
@ -196,50 +163,17 @@ check_systemd() {
echo "target_active: $(systemctl is-active taler-merchant.target 2>&1)" echo "target_active: $(systemctl is-active taler-merchant.target 2>&1)"
echo "httpd_active: $(systemctl is-active taler-merchant-httpd.service 2>&1)" echo "httpd_active: $(systemctl is-active taler-merchant-httpd.service 2>&1)"
echo "httpd_enabled: $(systemctl is-enabled taler-merchant-httpd.service 2>&1)" echo "httpd_enabled: $(systemctl is-enabled taler-merchant-httpd.service 2>&1)"
# unit properties help when is-active is only "activating"/"failed" systemctl start taler-merchant.target 2>&1 | tail -5
echo "httpd_Result: $(systemctl show -p Result --value taler-merchant-httpd.service 2>&1)" sleep 2
echo "httpd_SubState: $(systemctl show -p SubState --value taler-merchant-httpd.service 2>&1)"
echo "httpd_NRestarts: $(systemctl show -p NRestarts --value taler-merchant-httpd.service 2>&1)"
echo "httpd_ExecMainStatus: $(systemctl show -p ExecMainStatus --value taler-merchant-httpd.service 2>&1)"
echo "httpd_ExecMainCode: $(systemctl show -p ExecMainCode --value taler-merchant-httpd.service 2>&1)"
# one-line status (Active: failed / activating (start) …)
systemctl status taler-merchant-httpd.service --no-pager -l 2>&1 | head -12 | while IFS= read -r sl; do
echo "httpd_status: $sl"
done
# compact single-line for err detail
st1=$(systemctl status taler-merchant-httpd.service --no-pager -l 2>&1 | sed -n "s/^ *Active: //p" | head -1)
echo "httpd_status_line: ${st1:-?}"
systemctl start taler-merchant.target 2>&1 | tail -8 | while IFS= read -r sl; do
echo "start_target: $sl"
done
# wait longer: "activating" often clears only after dbinit
sleep 5
echo "after_start_target: $(systemctl is-active taler-merchant.target 2>&1)" echo "after_start_target: $(systemctl is-active taler-merchant.target 2>&1)"
echo "after_start_httpd: $(systemctl is-active taler-merchant-httpd.service 2>&1)" echo "after_start_httpd: $(systemctl is-active taler-merchant-httpd.service 2>&1)"
echo "after_httpd_Result: $(systemctl show -p Result --value taler-merchant-httpd.service 2>&1)" systemctl --failed --no-legend 2>/dev/null | grep -i taler || echo "failed_taler: none"
echo "after_httpd_SubState: $(systemctl show -p SubState --value taler-merchant-httpd.service 2>&1)"
# journal: last failure reasons (self-contained for ERROR jump list)
journalctl -u taler-merchant-httpd.service -n 12 --no-pager -o cat 2>/dev/null \
| sed "/^$/d" | tail -8 | while IFS= read -r jl; do
echo "httpd_journal: $jl"
done
journalctl -u taler-merchant-dbinit.service -n 6 --no-pager -o cat 2>/dev/null \
| sed "/^$/d" | tail -4 | while IFS= read -r jl; do
echo "dbinit_journal: $jl"
done
ft=$(systemctl --failed --no-legend 2>/dev/null | grep -i taler || true)
if [ -n "$ft" ]; then
echo "$ft" | while IFS= read -r fl; do echo "failed_taler: $fl"; done
else
echo "failed_taler: none"
fi
' 2>/dev/null || echo "systemd probe failed") ' 2>/dev/null || echo "systemd probe failed")
while IFS= read -r line; do while IFS= read -r line; do
[ -n "$line" ] || continue [ -n "$line" ] || continue
info "systemd" "$line" info "systemd" "$line"
done <<<"$out" done <<<"$out"
before_httpd=$(echo "$out" | sed -n 's/^httpd_active: //p' | tail -1)
active_httpd=$(echo "$out" | sed -n 's/^after_start_httpd: //p' | tail -1) active_httpd=$(echo "$out" | sed -n 's/^after_start_httpd: //p' | tail -1)
# export for caller board # export for caller board
APT_LAST_HTTPD="${active_httpd:-?}" APT_LAST_HTTPD="${active_httpd:-?}"
@ -247,17 +181,7 @@ check_systemd() {
ok "systemd httpd" "taler-merchant-httpd active after start taler-merchant.target" ok "systemd httpd" "taler-merchant-httpd active after start taler-merchant.target"
return 0 return 0
fi fi
detail=$(_apt_systemd_diagnose "$out") err "systemd httpd" "taler-merchant-httpd not active" "after_start_httpd=${active_httpd:-?} · see systemd lines"
# Prefer after_* Result if present
local after_result after_sub
after_result=$(echo "$out" | sed -n 's/^after_httpd_Result: //p' | tail -1)
after_sub=$(echo "$out" | sed -n 's/^after_httpd_SubState: //p' | tail -1)
[ -n "$after_result" ] && [ "$after_result" != "success" ] && detail="${detail} · after_Result=${after_result}"
[ -n "$after_sub" ] && detail="${detail} · after_SubState=${after_sub}"
# Human one-liner for jump list (no "see …" without content)
err "systemd httpd" \
"taler-merchant-httpd not active (container=${name})" \
"${detail}"
return 1 return 1
} }
@ -297,21 +221,18 @@ check_merchant_basics() {
done <<<"$out" done <<<"$out"
if echo "$out" | grep -qi 'error while loading shared libraries'; then if echo "$out" | grep -qi 'error while loading shared libraries'; then
local lib_err err "httpd" "shared library error on --version" "see basics/ldd"
lib_err=$(echo "$out" | grep -i 'error while loading shared libraries' | head -1 | tr '\n' ' ' | head -c 240)
err "httpd" "shared library error on --version" "${lib_err:-see basics INFO lines}"
return 1 return 1
fi fi
set +e set +e
local ver_out ec "$PODMAN_BIN" exec "$name" taler-merchant-httpd --version >/dev/null 2>&1
ver_out=$("$PODMAN_BIN" exec "$name" taler-merchant-httpd --version 2>&1) local ec=$?
ec=$?
set -e set -e
if [ "$ec" -eq 0 ]; then if [ "$ec" -eq 0 ]; then
ok "httpd --version" "exit 0" ok "httpd --version" "exit 0"
else else
err "httpd --version" "exit $ec" "$(printf '%s' "$ver_out" | tr '\n' ' ' | head -c 240)" err "httpd --version" "exit $ec"
return 1 return 1
fi fi
@ -387,10 +308,9 @@ check_one() {
if ! check_systemd "$name"; then if ! check_systemd "$name"; then
c_fail=1 c_fail=1
httpd="${APT_LAST_HTTPD:-FAIL}" httpd="${APT_LAST_HTTPD:-FAIL}"
note="${note:+$note; }httpd=${httpd}"
# deeper ldd if httpd failed # deeper ldd if httpd failed
if [ "$ldd" != "FAIL" ]; then if [ "$ldd" != "FAIL" ]; then
check_ldd_deep "$name" || { ldd="FAIL"; c_fail=1; note="${note:+$note; }ldd"; } check_ldd_deep "$name" || { ldd="FAIL"; c_fail=1; }
fi fi
else else
httpd="active" httpd="active"

View file

@ -6,21 +6,18 @@
# #
# Probes: # Probes:
# 1) CLI reachable (fake-incoming --help) # 1) CLI reachable (fake-incoming --help)
# 2) geniban → Swiss IBAN (debit side — “enough money” is synthetic per rung) # 2) geniban → Swiss IBAN
# 3) baseline fake-incoming CHF:1.00 (plumbing) # 3) fake-incoming CHF amount to exchange credit IBAN (nexus IN)
# 4) optional amount ladder: DEVTESTING_LADDER=1 (default) — 23 rungs up to 50k CHF # Bounce "missing reserve public key" = plumbing OK (no real wallet reserve)
# Full credit needs a real reserve pub as --subject (optional later)
# #
# Env: # Env:
# DEVTESTING_SSH default taler-rusty-devtesting (or user@host) # DEVTESTING_SSH default taler-rusty-devtesting (or user@host)
# DEVTESTING_AMOUNT baseline amount (default CHF:1.00) # DEVTESTING_AMOUNT default CHF:1.00
# DEVTESTING_CREDIT_PAYTO exchange IBAN payto # DEVTESTING_CREDIT_PAYTO exchange IBAN payto (auto from EXCHANGE_PUBLIC/keys if empty)
# DEVTESTING_EXCHANGE_IBAN fallback CH6808573105529100001 (TOPS CHF exchange) # DEVTESTING_EXCHANGE_IBAN fallback CH6808573105529100001 (TOPS CHF exchange)
# DEVTESTING_LADDER 1 (default) run amount ladder; 0 = baseline only # DEVTESTING_SKIP=1 skip phase
# DEVTESTING_LADDER_STEPS default 23 # SKIP_SSH=1 skip (same as other SSH phases)
# DEVTESTING_LADDER_MAX default 50000 (CHF major units)
# DEVTESTING_LADDER_MIN default 0.01
# DEVTESTING_LADDER_STOP_ON_FAIL 1 (default) stop ladder on hard fail; 0 = continue
# DEVTESTING_SKIP=1 skip phase
# #
set -euo pipefail set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd) ROOT=$(cd "$(dirname "$0")" && pwd)
@ -40,29 +37,23 @@ fi
: "${DEVTESTING_SSH:=}" : "${DEVTESTING_SSH:=}"
: "${DEVTESTING_AMOUNT:=CHF:1.00}" : "${DEVTESTING_AMOUNT:=CHF:1.00}"
: "${DEVTESTING_EXCHANGE_IBAN:=CH6808573105529100001}" : "${DEVTESTING_EXCHANGE_IBAN:=CH6808573105529100001}"
: "${DEVTESTING_LADDER:=1}"
: "${DEVTESTING_LADDER_STEPS:=23}"
: "${DEVTESTING_LADDER_MAX:=50000}"
: "${DEVTESTING_LADDER_MIN:=0.01}"
: "${DEVTESTING_LADDER_STOP_ON_FAIL:=1}"
: "${SSH_CONNECT_TIMEOUT:=8}" : "${SSH_CONNECT_TIMEOUT:=8}"
: "${SSH_CMD_TIMEOUT:=60}" : "${SSH_CMD_TIMEOUT:=45}"
ssh_dt() { ssh_dt() {
# Prefer Host alias; fall back to explicit user@host if config missing. # Prefer Host alias; fall back to explicit user@host if config missing
# Always -n: do not read local stdin (ladder while-read must not be drained by ssh).
local target="$DEVTESTING_SSH" local target="$DEVTESTING_SSH"
if ! ssh -G "$target" >/dev/null 2>&1; then if ! ssh -G "$target" >/dev/null 2>&1; then
target="devtesting@rusty.taler-ops.ch" target="devtesting@rusty.taler-ops.ch"
fi fi
if command -v timeout >/dev/null 2>&1; then if command -v timeout >/dev/null 2>&1; then
timeout "${SSH_CMD_TIMEOUT}" ssh -n \ timeout "${SSH_CMD_TIMEOUT}" ssh \
-o BatchMode=yes \ -o BatchMode=yes \
-o ConnectTimeout="${SSH_CONNECT_TIMEOUT}" \ -o ConnectTimeout="${SSH_CONNECT_TIMEOUT}" \
-o StrictHostKeyChecking=accept-new \ -o StrictHostKeyChecking=accept-new \
"$target" "$@" "$target" "$@"
else else
ssh -n \ ssh \
-o BatchMode=yes \ -o BatchMode=yes \
-o ConnectTimeout="${SSH_CONNECT_TIMEOUT}" \ -o ConnectTimeout="${SSH_CONNECT_TIMEOUT}" \
-o StrictHostKeyChecking=accept-new \ -o StrictHostKeyChecking=accept-new \
@ -70,57 +61,8 @@ ssh_dt() {
fi fi
} }
# Run one fake-incoming. Sets globals: FI_RC FI_FLAT FI_STATUS
# FI_STATUS: ok_bounce | ok_in | ok_cli | fail_ssh | fail_creditor | fail_nexus | fail_other
run_fake_incoming() {
local amount="$1" subject="$2" credit="$3" debit="$4"
local fi_out
FI_RC=0
FI_FLAT=""
FI_STATUS="fail_other"
fi_out=$(ssh_dt fake-incoming \
--amount "${amount}" \
--subject "${subject}" \
--credit-payto "${credit}" \
--debit-payto "${debit}" 2>&1) || FI_RC=$?
FI_FLAT=$(printf '%s' "$fi_out" | tr '\n' ' ')
if printf '%s' "$FI_FLAT" | grep -qiE 'Permission denied|Connection refused|Could not resolve|No such command'; then
FI_STATUS=fail_ssh
return 1
fi
if printf '%s' "$FI_FLAT" | grep -qiE 'Creditor must be the exchange'; then
FI_STATUS=fail_creditor
return 1
fi
if printf '%s' "$FI_FLAT" | grep -qiE 'Missing amount|unable to read secrets|CalledProcessError'; then
if ! printf '%s' "$FI_FLAT" | grep -qE 'libeufin-nexus - IN .* CHF'; then
FI_STATUS=fail_nexus
return 1
fi
fi
if printf '%s' "$FI_FLAT" | grep -qE 'libeufin-nexus - IN .* CHF:[0-9]'; then
if printf '%s' "$FI_FLAT" | grep -qiE 'bounced.*missing reserve public key'; then
FI_STATUS=ok_bounce
return 0
fi
if printf '%s' "$FI_FLAT" | grep -qiE 'bounced'; then
FI_STATUS=ok_bounce
return 0
fi
FI_STATUS=ok_in
return 0
fi
if [ "$FI_RC" -eq 0 ] && printf '%s' "$FI_FLAT" | grep -qi 'Faking incoming CHF'; then
FI_STATUS=ok_cli
return 0
fi
FI_STATUS=fail_other
return 1
}
set_group cli set_group cli
info "ssh" "DEVTESTING_SSH=${DEVTESTING_SSH} baseline=${DEVTESTING_AMOUNT} ladder=${DEVTESTING_LADDER} steps=${DEVTESTING_LADDER_STEPS} max=CHF:${DEVTESTING_LADDER_MAX}" info "ssh" "DEVTESTING_SSH=${DEVTESTING_SSH} amount=${DEVTESTING_AMOUNT}"
help_out=$(ssh_dt fake-incoming --help 2>&1) || true help_out=$(ssh_dt fake-incoming --help 2>&1) || true
if printf '%s' "$help_out" | grep -q 'fake-incoming'; then if printf '%s' "$help_out" | grep -q 'fake-incoming'; then
@ -135,7 +77,7 @@ set_group geniban
iban_out=$(ssh_dt geniban 2>&1) || true iban_out=$(ssh_dt geniban 2>&1) || true
iban=$(printf '%s\n' "$iban_out" | grep -E '^CH[0-9A-Z]+$' | tail -1 || true) iban=$(printf '%s\n' "$iban_out" | grep -E '^CH[0-9A-Z]+$' | tail -1 || true)
if [ -n "$iban" ]; then if [ -n "$iban" ]; then
ok "geniban" "synthetic IBAN $iban (debit / “withdraw” side for each rung)" ok "geniban" "synthetic IBAN $iban"
else else
fail "geniban" "expected CH… IBAN" "$(printf '%s' "$iban_out" | tr '\n' ' ' | head -c 160)" fail "geniban" "expected CH… IBAN" "$(printf '%s' "$iban_out" | tr '\n' ' ' | head -c 160)"
exit 1 exit 1
@ -150,187 +92,55 @@ if [ -z "$credit_payto" ]; then
fi fi
ok "credit-payto" "$credit_payto" ok "credit-payto" "$credit_payto"
# Subject: not a real reserve — nexus should bounce missing reserve public key
# (proves CHF fake-incoming path). Real reserve pub would credit wirewatch.
subject="mon-fake-franken-$(date -u +%Y%m%d%H%M%S)-$$"
debit_payto="payto://iban/${iban}?receiver-name=MonDevTest" debit_payto="payto://iban/${iban}?receiver-name=MonDevTest"
ts_base=$(date -u +%Y%m%d%H%M%S)
# --- baseline plumbing probe --- set_group fake-incoming
set_group baseline
subject="mon-fake-franken-${ts_base}-base-$$"
info "fake-incoming" "amount=${DEVTESTING_AMOUNT} subject=${subject}" info "fake-incoming" "amount=${DEVTESTING_AMOUNT} subject=${subject}"
if run_fake_incoming "$DEVTESTING_AMOUNT" "$subject" "$credit_payto" "$debit_payto"; then fi_rc=0
case "$FI_STATUS" in fi_out=$(ssh_dt fake-incoming \
ok_bounce) --amount "${DEVTESTING_AMOUNT}" \
ok "fake-incoming" "CHF IN accepted by nexus · bounce/missing reserve pub — plumbing OK" --subject "${subject}" \
info "note" "use real reserve pub as --subject for wirewatch credit (not required for mon probe)" --credit-payto "${credit_payto}" \
;; --debit-payto "${debit_payto}" 2>&1) || fi_rc=$?
ok_in) ok "fake-incoming" "CHF IN recorded by nexus (no bounce line)" ;;
ok_cli) ok "fake-incoming" "CLI completed CHF fake-incoming" ;; # Normalize multi-line for matching
*) ok "fake-incoming" "status=${FI_STATUS}" ;; fi_flat=$(printf '%s' "$fi_out" | tr '\n' ' ')
esac
else if printf '%s' "$fi_flat" | grep -qiE 'Permission denied|Connection refused|Could not resolve|No such command'; then
case "$FI_STATUS" in fail "fake-incoming" "SSH/CLI failure" "$(printf '%s' "$fi_flat" | head -c 200)"
fail_ssh) exit 1
fail "fake-incoming" "SSH/CLI failure" "$(printf '%s' "$FI_FLAT" | head -c 200)"
exit 1
;;
fail_creditor)
fail "fake-incoming" "wrong credit IBAN (not exchange)" "$(printf '%s' "$FI_FLAT" | head -c 220)"
exit 1
;;
fail_nexus)
fail "fake-incoming" "nexus rejected request" "$(printf '%s' "$FI_FLAT" | head -c 220)"
exit 1
;;
*)
fail "fake-incoming" "no CHF IN line from nexus" "rc=${FI_RC} $(printf '%s' "$FI_FLAT" | head -c 200)"
exit 1
;;
esac
fi fi
# --- CHF amount ladder (fake-incoming rungs) --- if printf '%s' "$fi_flat" | grep -qiE 'Creditor must be the exchange'; then
if [ "${DEVTESTING_LADDER}" = "1" ]; then fail "fake-incoming" "wrong credit IBAN (not exchange)" "$(printf '%s' "$fi_flat" | head -c 220)"
set_group ladder exit 1
section "devtesting · CHF fake-incoming ladder (→ ${DEVTESTING_LADDER_MAX})" fi
LADDER_FILE=$(mktemp "${TMPDIR:-/tmp}/devtesting-ladder.XXXXXX") if printf '%s' "$fi_flat" | grep -qiE 'Missing amount|unable to read secrets|CalledProcessError'; then
python3 - <<'PY' "${DEVTESTING_LADDER_MAX}" "${DEVTESTING_LADDER_STEPS}" "${DEVTESTING_LADDER_MIN}" "$LADDER_FILE" # secrets warn alone is soft if we still see IN line
import math, sys if ! printf '%s' "$fi_flat" | grep -qE 'libeufin-nexus - IN .* CHF'; then
from decimal import Decimal, ROUND_HALF_UP fail "fake-incoming" "nexus rejected request" "$(printf '%s' "$fi_flat" | head -c 220)"
from pathlib import Path exit 1
max_amt = Decimal(sys.argv[1])
steps = max(1, int(sys.argv[2]))
min_amt = Decimal(sys.argv[3])
out = Path(sys.argv[4])
if min_amt <= 0:
min_amt = Decimal("0.01")
if min_amt >= max_amt:
min_amt = max_amt / Decimal(1000)
# CHF: 2 decimal places
q2 = Decimal("0.01")
def quant(v: Decimal) -> Decimal:
if v < min_amt:
v = min_amt
return v.quantize(q2, rounding=ROUND_HALF_UP)
def fmt(v: Decimal) -> str:
q = quant(v)
if q == q.to_integral():
return str(int(q))
return format(q, "f")
if steps == 1:
amts = [quant(max_amt)]
elif steps == 2:
amts = [quant(min_amt), quant(max_amt)]
else:
# Exactly `steps` log-spaced rungs from min → max (includes both ends).
# Guarantees max rung = DEVTESTING_LADDER_MAX (“enough” volume at the top).
amts = []
lo, hi = float(min_amt), float(max_amt)
if hi <= lo:
hi = lo * 10
for i in range(steps):
if steps == 1:
v = max_amt
else:
t = i / (steps - 1)
v = Decimal(str(math.exp(math.log(lo) + t * (math.log(hi) - math.log(lo)))))
amts.append(quant(v))
amts[0] = quant(min_amt)
amts[-1] = quant(max_amt)
# enforce strictly increasing
for i in range(1, len(amts)):
if amts[i] <= amts[i - 1]:
amts[i] = quant(amts[i - 1] + min_amt)
amts[-1] = quant(max_amt)
if amts[-1] <= amts[-2]:
# last-but-one squeezed; leave max exact and drop collision by nudging previous
amts[-2] = quant(max(amts[-3] + min_amt if len(amts) > 2 else min_amt, max_amt - Decimal(1)))
if amts[-2] >= amts[-1]:
amts[-2] = quant(amts[-1] - min_amt)
lines = ["CHF:%s" % fmt(a) for a in amts]
out.write_text("\n".join(lines) + "\n")
PY
n_rungs_plan=$(grep -cE '^CHF:' "$LADDER_FILE" || echo 0)
ladder_sum=$(python3 -c '
from decimal import Decimal
s=Decimal(0)
for ln in open("'"$LADDER_FILE"'"):
ln=ln.strip()
if not ln: continue
s += Decimal(ln.split(":",1)[-1])
print(s)
' 2>/dev/null || echo "?")
info "ladder plan" "steps=${n_rungs_plan} max=CHF:${DEVTESTING_LADDER_MAX} min=CHF:${DEVTESTING_LADDER_MIN} sum≈CHF:${ladder_sum} (synthetic debit geniban each rung)"
info "ladder funding" "each rung uses geniban debit payto — no real bank withdraw; volume covers max rung CHF:${DEVTESTING_LADDER_MAX}"
LADDER_OK=0
LADDER_FAIL=0
LADDER_WARN=0
rung=0
# Load amounts into an array first — never `while read … done <file` around ssh:
# without ssh -n, OpenSSH drains the ladder file as remote stdin (only rung 1 ran).
mapfile -t LADDER_AMTS < <(grep -E '^CHF:' "$LADDER_FILE" || true)
rm -f "$LADDER_FILE"
n_rungs=${#LADDER_AMTS[@]}
if [ "$n_rungs" -eq 0 ]; then
fail "ladder" "no CHF: amounts generated"
else
for amt in "${LADDER_AMTS[@]}"; do
[ -n "$amt" ] || continue
rung=$((rung + 1))
subject="mon-fake-ladder-${ts_base}-r${rung}-$$"
info "rung ${rung}/${n_rungs}" "amount=${amt} subject=${subject}"
if run_fake_incoming "$amt" "$subject" "$credit_payto" "$debit_payto"; then
case "$FI_STATUS" in
ok_bounce)
ok "rung ${rung}" "${amt} · nexus IN + bounce (plumbing OK)"
LADDER_OK=$((LADDER_OK + 1))
;;
ok_in)
ok "rung ${rung}" "${amt} · nexus IN recorded"
LADDER_OK=$((LADDER_OK + 1))
;;
ok_cli)
ok "rung ${rung}" "${amt} · CLI ok"
LADDER_OK=$((LADDER_OK + 1))
;;
*)
warn "rung ${rung}" "${amt} · status=${FI_STATUS}"
LADDER_WARN=$((LADDER_WARN + 1))
;;
esac
else
fail "rung ${rung}" "${amt} · ${FI_STATUS}" "$(printf '%s' "$FI_FLAT" | head -c 180)"
LADDER_FAIL=$((LADDER_FAIL + 1))
if [ "${DEVTESTING_LADDER_STOP_ON_FAIL}" = "1" ]; then
warn "ladder stop" "DEVTESTING_LADDER_STOP_ON_FAIL=1 after rung ${rung}/${n_rungs}"
break
fi
fi
done
fi fi
fi
info "ladder summary" "ok=${LADDER_OK} warn=${LADDER_WARN} fail=${LADDER_FAIL} / planned=${n_rungs} · max=CHF:${DEVTESTING_LADDER_MAX}" if printf '%s' "$fi_flat" | grep -qE 'libeufin-nexus - IN .* CHF:[0-9]'; then
if [ "$n_rungs" -eq 0 ]; then if printf '%s' "$fi_flat" | grep -qiE 'bounced.*missing reserve public key'; then
: ok "fake-incoming" "CHF IN accepted by nexus · bounced (no reserve pub) — fake-franken plumbing OK"
elif [ "$LADDER_FAIL" -gt 0 ] && [ "$LADDER_OK" -eq 0 ]; then info "note" "use real reserve pub as --subject for wirewatch credit (not required for mon probe)"
fail "ladder" "all rungs failed" elif printf '%s' "$fi_flat" | grep -qiE 'bounced'; then
elif [ "$LADDER_FAIL" -gt 0 ]; then warn "fake-incoming" "CHF IN bounced (other reason)" "$(printf '%s' "$fi_flat" | grep -oiE 'bounced[^ ]* [^ ]*' | head -c 160)"
warn "ladder" "partial failures fail=${LADDER_FAIL} ok=${LADDER_OK}/${n_rungs}"
elif [ "$LADDER_OK" -lt "$n_rungs" ]; then
# e.g. stdin-eaten loop used to stop after 1 rung with ok=1 fail=0
warn "ladder incomplete" "only ${LADDER_OK}/${n_rungs} rungs ran (ok) — not through max CHF:${DEVTESTING_LADDER_MAX}"
else else
ok "ladder complete" "ok=${LADDER_OK}/${n_rungs} through max CHF:${DEVTESTING_LADDER_MAX}" ok "fake-incoming" "CHF IN recorded by nexus (no bounce line)"
fi fi
elif [ "$fi_rc" -eq 0 ] && printf '%s' "$fi_flat" | grep -qi 'Faking incoming CHF'; then
ok "fake-incoming" "CLI completed CHF fake-incoming"
else else
info "ladder" "skipped (DEVTESTING_LADDER=0)" fail "fake-incoming" "no CHF IN line from nexus" "rc=${fi_rc} $(printf '%s' "$fi_flat" | head -c 200)"
exit 1
fi fi
info "done" "devtesting fake-franken probe finished" info "done" "devtesting fake-franken probe finished"

View file

@ -8,7 +8,7 @@ source "$ROOT/lib.sh"
# shellcheck source=metrics.sh # shellcheck source=metrics.sh
source "$ROOT/metrics.sh" source "$ROOT/metrics.sh"
: "${E2E_TIMEOUT:=180}" # Bank withdraw ladder + wirewatch lag needs room (was 55 — too tight) : "${E2E_TIMEOUT:=180}" # ATM ladder + wirewatch lag needs room (was 55 — too tight)
: "${E2E_PAY_SECS:=22}" # hard reserve for handle-uri + settle (do not starve pay) : "${E2E_PAY_SECS:=22}" # hard reserve for handle-uri + settle (do not starve pay)
: "${E2E_SETTLE_ROUNDS:=18}" : "${E2E_SETTLE_ROUNDS:=18}"
: "${E2E_SETTLE_SLEEP:=3}" : "${E2E_SETTLE_SLEEP:=3}"
@ -35,22 +35,22 @@ wcli_bal_snap() {
fi fi
fi fi
local err="${out}.err" local err="${out}.err"
local _node="${NODE_BIN:-$(command -v node 2>/dev/null || true)}" if command -v perl >/dev/null 2>&1; then
[ -n "$_node" ] || { echo "(no node)" >"$out"; return 1; } if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then perl -e 'alarm shift; exec @ARGV' 10 \
if command -v timeout >/dev/null 2>&1; then node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \
timeout -k 2 10 "$_node" "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \
>"$out" 2>"$err" || true >"$out" 2>"$err" || true
else else
"$_node" "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \ perl -e 'alarm shift; exec @ARGV' 10 \
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
>"$out" 2>"$err" || true >"$out" 2>"$err" || true
fi fi
else else
if command -v timeout >/dev/null 2>&1; then if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
timeout -k 2 10 "$_node" "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \ node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \
>"$out" 2>"$err" || true >"$out" 2>"$err" || true
else else
"$_node" "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \ node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
>"$out" 2>"$err" || true >"$out" 2>"$err" || true
fi fi
fi fi
@ -226,20 +226,18 @@ except Exception: print("")' 2>/dev/null || true)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Variable amounts # Variable amounts
# Withdraw = typical bank cash-note amounts (CHF-style: was called ATM): 20 / 50 / 100 / 200 … # Withdraw = typical ATM notes only (Bankomat): 20 / 50 / 100 / 200 …
# Pay = several smaller purchase amounts # Pay = several smaller purchase amounts
# Env: # Env:
# E2E_WITHDRAW_VALUES="20 50 100" # bare numbers → prefixed with CUR # E2E_WITHDRAW_VALUES="20 50 100" # bare numbers → prefixed with CUR
# E2E_PAY_VALUES="0.01 0.05 0.5 1 2 5" # E2E_PAY_VALUES="0.01 0.05 0.5 1 2 5"
# E2E_VARIABLE=0 # single fixed WITHDRAW_AMT / PAY_AMT # E2E_VARIABLE=0 # single fixed WITHDRAW_AMT / PAY_AMT
# E2E_BANK_WITHDRAW_MAX=N # try at most N bank withdraws (budget) # E2E_ATM_MAX=N # try at most N ATM withdraws (budget)
# E2E_ATM_MAX=N # legacy alias of E2E_BANK_WITHDRAW_MAX
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
: "${E2E_VARIABLE:=1}" : "${E2E_VARIABLE:=1}"
: "${E2E_BANK_WITHDRAW_MAX:=${E2E_ATM_MAX:-5}}" : "${E2E_ATM_MAX:=5}"
E2E_ATM_MAX="${E2E_ATM_MAX:-$E2E_BANK_WITHDRAW_MAX}" # legacy alias
if [ "$E2E_REMOTE" = "1" ]; then if [ "$E2E_REMOTE" = "1" ]; then
# remote: still bank cash-note shaped; TESTPAYSAN needs larger notes for shop pays # remote: still ATM-shaped; TESTPAYSAN needs larger notes for shop pays
if [ "$CUR" = "TESTPAYSAN" ]; then if [ "$CUR" = "TESTPAYSAN" ]; then
: "${E2E_WITHDRAW_VALUES:=20 50}" : "${E2E_WITHDRAW_VALUES:=20 50}"
# farmer shop template face values (oeufs-6=5, fromage=8.5, jus=6) # farmer shop template face values (oeufs-6=5, fromage=8.5, jus=6)
@ -249,7 +247,7 @@ if [ "$E2E_REMOTE" = "1" ]; then
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}" : "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}"
fi fi
else else
# local GOA: classic bank cash-note amounts + 4200 for paivana paywall template # local GOA: classic ATM notes + 4200 for paivana paywall template
: "${E2E_WITHDRAW_VALUES:=20 50 100 200 4200}" : "${E2E_WITHDRAW_VALUES:=20 50 100 200 4200}"
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 0.5 1 2 5 10}" : "${E2E_PAY_VALUES:=0.01 0.05 0.1 0.5 1 2 5 10}"
fi fi
@ -286,7 +284,7 @@ if [ "$E2E_VARIABLE" != "1" ]; then
fi fi
fi fi
# Credit = sum of planned Bank withdraws + small buffer for fees # Credit = sum of planned ATM withdraws + small buffer for fees
CREDIT_AMT=$(python3 -c ' CREDIT_AMT=$(python3 -c '
import sys import sys
cur=sys.argv[1] cur=sys.argv[1]
@ -303,7 +301,7 @@ print("%s:%g" % (cur, s + buf))
WITHDRAW_AMT=$(printf '%s' "$WITHDRAW_LIST" | awk '{print $1}') WITHDRAW_AMT=$(printf '%s' "$WITHDRAW_LIST" | awk '{print $1}')
PAY_AMT=$(printf '%s' "$PAY_LIST" | awk '{print $1}') PAY_AMT=$(printf '%s' "$PAY_LIST" | awk '{print $1}')
# More wall time when running Bank withdraw ladder # More wall time when running ATM ladder
if [ "$E2E_VARIABLE" = "1" ]; then if [ "$E2E_VARIABLE" = "1" ]; then
n_w=$(printf '%s' "$WITHDRAW_LIST" | wc -w | tr -d ' ') n_w=$(printf '%s' "$WITHDRAW_LIST" | wc -w | tr -d ' ')
n_p=$(printf '%s' "$PAY_LIST" | wc -w | tr -d ' ') n_p=$(printf '%s' "$PAY_LIST" | wc -w | tr -d ' ')
@ -311,9 +309,9 @@ if [ "$E2E_VARIABLE" = "1" ]; then
if [ "${E2E_TIMEOUT}" -lt "$need" ]; then if [ "${E2E_TIMEOUT}" -lt "$need" ]; then
E2E_TIMEOUT=$need E2E_TIMEOUT=$need
fi fi
# cap bank withdraw attempts but always keep CUR:4200 if listed (paivana) # cap ATM attempts but always keep CUR:4200 if listed (paivana)
WITHDRAW_LIST=$( WITHDRAW_LIST=$(
printf '%s' "$WITHDRAW_LIST" | tr ' ' '\n' | awk -v max="$E2E_BANK_WITHDRAW_MAX" -v big="${CUR}:4200" ' printf '%s' "$WITHDRAW_LIST" | tr ' ' '\n' | awk -v max="$E2E_ATM_MAX" -v big="${CUR}:4200" '
NF==0 { next } NF==0 { next }
$0==big { has=1; next } $0==big { has=1; next }
{ small[++n]=$0 } { small[++n]=$0 }
@ -378,35 +376,13 @@ WALLET_CLI=$(find_wallet_cli) || {
blocker "prereq" "taler-wallet-cli not found (set WALLET_CLI=)" blocker "prereq" "taler-wallet-cli not found (set WALLET_CLI=)"
exit 1 exit 1
} }
# wallet-core spawns `taler-helper-sqlite3` via PATH (not an absolute path).
# Ensure operator wallet bindir + ~/.local/bin are first (systemd mon jobs).
export PATH="${WALLET_CLI_BINDIR:+$WALLET_CLI_BINDIR:}${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
NODE_BIN=$(command -v node 2>/dev/null || true)
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
blocker "prereq" "node not found on PATH (install Node or set PATH; need node for wallet-cli)"
exit 1
fi
if ! command -v taler-helper-sqlite3 >/dev/null 2>&1; then
for _h in \
"${WALLET_CLI_BINDIR:+$WALLET_CLI_BINDIR/taler-helper-sqlite3}" \
"${HOME}/.local/bin/taler-helper-sqlite3" \
/usr/bin/taler-helper-sqlite3
do
[ -n "$_h" ] && [ -x "$_h" ] && export PATH="$(dirname "$_h"):$PATH" && break
done
fi
if ! command -v taler-helper-sqlite3 >/dev/null 2>&1; then
blocker "prereq" "taler-helper-sqlite3 not on PATH (wallet accept-uri/pay will fail with spawn ENOENT) — install next to wallet-cli or set WALLET_CLI_BINDIR"
exit 1
fi
ok "wallet-helper" "taler-helper-sqlite3=$(command -v taler-helper-sqlite3)"
# Implementation version (--version) + wallet-core API ranges (version command) # Implementation version (--version) + wallet-core API ranges (version command)
WALLET_IMPL_VER=$( WALLET_IMPL_VER=$(
"$NODE_BIN" "$WALLET_CLI" --version 2>/dev/null | head -1 | tr -d '\r' || true node "$WALLET_CLI" --version 2>/dev/null | head -1 | tr -d '\r' || true
) )
WALLET_CORE_VER=$( WALLET_CORE_VER=$(
_wver_db=$(mktemp "${TMPDIR:-/tmp}/wver.XXXXXX.sqlite3" 2>/dev/null || echo "${TMPDIR:-/tmp}/wver-$$.sqlite3") _wver_db=$(mktemp "${TMPDIR:-/tmp}/wver.XXXXXX.sqlite3" 2>/dev/null || echo "${TMPDIR:-/tmp}/wver-$$.sqlite3")
"$NODE_BIN" "$WALLET_CLI" --wallet-db="$_wver_db" --no-throttle --skip-defaults version 2>/dev/null \ node "$WALLET_CLI" --wallet-db="$_wver_db" --no-throttle --skip-defaults version 2>/dev/null \
| python3 -c ' | python3 -c '
import json,re,sys import json,re,sys
raw=sys.stdin.read() raw=sys.stdin.read()
@ -437,7 +413,7 @@ else
fi fi
info "e2e mode" "$([ "$E2E_REMOTE" = "1" ] && echo "remote/public domain (no SSH)" || echo "local koopa stack")" info "e2e mode" "$([ "$E2E_REMOTE" = "1" ] && echo "remote/public domain (no SSH)" || echo "local koopa stack")"
info "currency" "$CUR" info "currency" "$CUR"
info "Bank withdraw ladder" "$WITHDRAW_LIST (credit $CREDIT_AMT)" info "ATM withdraw ladder" "$WITHDRAW_LIST (credit $CREDIT_AMT)"
info "pay ladder" "$PAY_LIST" info "pay ladder" "$PAY_LIST"
info "e2e budget" "${E2E_TIMEOUT}s" info "e2e budget" "${E2E_TIMEOUT}s"
@ -455,7 +431,7 @@ set_group load
section "e2e · load snapshot (before withdraw/pay)" section "e2e · load snapshot (before withdraw/pay)"
metrics_report_load "${METRICS_DIR}/load-before.json" "e2e-start" || true metrics_report_load "${METRICS_DIR}/load-before.json" "e2e-start" || true
# Local stack: koopa secrets. Remote: env first; TESTPAYSAN stage may load stagepaysan secrets via FRANCPAYSAN_SSH/INSIDE_SSH. # Local stack: koopa secrets. Remote: env first; TESTPAYSAN stage may load francpaysan host secrets.
set_group prereq set_group prereq
if [ "$E2E_REMOTE" = "1" ]; then if [ "$E2E_REMOTE" = "1" ]; then
ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-${BANK_ADMIN_PASS:-}}" ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-${BANK_ADMIN_PASS:-}}"
@ -599,7 +575,7 @@ start_wallet_serve() {
stop_wallet_serve stop_wallet_serve
rm -f "$WSOCK" rm -f "$WSOCK"
# serve holds the DB open; clients use --wallet-connection only # serve holds the DB open; clients use --wallet-connection only
"$NODE_BIN" "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults \ node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults \
advanced serve --unix-path "$WSOCK" \ advanced serve --unix-path "$WSOCK" \
>"$SCRATCH/wallet-serve.log" 2>&1 & >"$SCRATCH/wallet-serve.log" 2>&1 &
WSERVE_PID=$! WSERVE_PID=$!
@ -616,25 +592,6 @@ start_wallet_serve() {
return 1 return 1
} }
# Run node wallet-cli with a hard wall clock. Prefer GNU/coreutils `timeout`
# (clear errors). Fall back to perl alarm only if timeout is missing.
# Always use absolute NODE_BIN + WALLET_CLI so systemd PATH quirks cannot
# turn into opaque "alarm shift; exec @ARGV" noise in accept-uri output.
_wcli_exec() {
local cap="$1"
shift
local -a cmd=( "$NODE_BIN" "$WALLET_CLI" "$@" )
if command -v timeout >/dev/null 2>&1; then
timeout -k 2 "$cap" "${cmd[@]}"
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout --kill-after=2 "$cap" "${cmd[@]}"
elif command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$cap" "${cmd[@]}"
else
"${cmd[@]}"
fi
}
# $1 = max seconds for this call (optional); rest = wallet-cli args # $1 = max seconds for this call (optional); rest = wallet-cli args
# Prefer live serve socket so the shepherd stays up (alternative to run-until-done). # Prefer live serve socket so the shepherd stays up (alternative to run-until-done).
wcli() { wcli() {
@ -646,15 +603,22 @@ wcli() {
e2e_over && return 124 e2e_over && return 124
local cap local cap
cap=$(e2e_left) cap=$(e2e_left)
# e2e_left may print warnings on stdout — keep only first integer token [ "$cap" -gt "$maxc" ] && cap=$maxc
cap=$(printf '%s' "$cap" | awk '/^[0-9]+$/{print; exit} {for(i=1;i<=NF;i++) if($i+0==$i){print $i; exit}}') [ "$cap" -lt 3 ] && return 124
[ -z "$cap" ] && cap=$maxc
[ "$cap" -gt "$maxc" ] 2>/dev/null && cap=$maxc
[ "$cap" -lt 3 ] 2>/dev/null && return 124
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
_wcli_exec "$cap" --wallet-connection="$WSOCK" --no-throttle "$@" if command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$cap" \
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
else
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
fi
else else
_wcli_exec "$cap" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" if command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$cap" \
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
else
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
fi
fi fi
} }
@ -663,10 +627,9 @@ wcli_pay() {
local cap=$E2E_PAY_SECS local cap=$E2E_PAY_SECS
local left local left
left=$(e2e_left) left=$(e2e_left)
left=$(printf '%s' "$left" | awk '/^[0-9]+$/{print; exit} {for(i=1;i<=NF;i++) if($i+0==$i){print $i; exit}}') if [ "$left" -gt "$cap" ]; then
if [ -n "$left" ] && [ "$left" -gt "$cap" ] 2>/dev/null; then
cap=$E2E_PAY_SECS cap=$E2E_PAY_SECS
elif [ -n "$left" ] && [ "$left" -ge 8 ] 2>/dev/null; then elif [ "$left" -ge 8 ]; then
cap=$left cap=$left
else else
# still try once with floor 12s so Alarm clock is not the blocker # still try once with floor 12s so Alarm clock is not the blocker
@ -674,9 +637,19 @@ wcli_pay() {
fi fi
info "pay wallet cap" "${cap}s" info "pay wallet cap" "${cap}s"
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
_wcli_exec "$cap" --wallet-connection="$WSOCK" --no-throttle "$@" if command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$cap" \
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
else
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
fi
else else
_wcli_exec "$cap" --wallet-db="$WDB" --no-throttle --skip-defaults "$@" if command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV' "$cap" \
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
else
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
fi
fi fi
} }
@ -890,7 +863,7 @@ else
warn "wallet accept ToS" "$(tail -c 80 "$SCRATCH/ex-tos.out" | tr '\n' ' ')" warn "wallet accept ToS" "$(tail -c 80 "$SCRATCH/ex-tos.out" | tr '\n' ' ')"
fi fi
fi fi
# Empty wallet baseline before bank withdraw / pay load # Empty wallet baseline before ATM / pay load
metrics_report_coins "wallet-ready" || true metrics_report_coins "wallet-ready" || true
wd_status() { wd_status() {
@ -928,15 +901,15 @@ fake_incoming_speedup() {
fi fi
} }
# One bank withdraw: create → accept → select → confirm → settle # One ATM-style withdraw: create → accept → select → confirm → settle
# returns 0 on wallet balance increase, 1 on soft fail (ladder continues) # returns 0 on wallet balance increase, 1 on soft fail (ladder continues)
e2e_one_withdraw() { e2e_one_withdraw() {
WITHDRAW_AMT="$1" WITHDRAW_AMT="$1"
local tag local tag
tag=$(printf '%s' "$WITHDRAW_AMT" | tr '.:' '__') tag=$(printf '%s' "$WITHDRAW_AMT" | tr '.:' '__')
section "e2e · Bank withdraw $WITHDRAW_AMT · $(format_amount_alt "$WITHDRAW_AMT")" section "e2e · ATM withdraw $WITHDRAW_AMT · $(format_amount_alt "$WITHDRAW_AMT")"
e2e_over && { warn "Bank withdraw" "budget exhausted — skip $WITHDRAW_AMT"; return 1; } e2e_over && { warn "ATM withdraw" "budget exhausted — skip $WITHDRAW_AMT"; return 1; }
metrics_report_coins "before-bank-wd-${tag}" || true metrics_report_coins "before-ATM-${tag}" || true
curl -sS -m 15 -o "$SCRATCH/wd-$tag.json" -H "Authorization: Bearer $UT" \ curl -sS -m 15 -o "$SCRATCH/wd-$tag.json" -H "Authorization: Bearer $UT" \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
@ -944,29 +917,23 @@ e2e_one_withdraw() {
"$BANK/accounts/${USER}/withdrawals" "$BANK/accounts/${USER}/withdrawals"
WID=$(python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json"));print(d.get("withdrawal_id") or d.get("id") or "")' 2>/dev/null || true) WID=$(python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json"));print(d.get("withdrawal_id") or d.get("id") or "")' 2>/dev/null || true)
URI=$(python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json"));print(d.get("taler_withdraw_uri") or "")' 2>/dev/null || true) URI=$(python3 -c 'import json;d=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json"));print(d.get("taler_withdraw_uri") or "")' 2>/dev/null || true)
URI_CLEAN=$(normalize_taler_uri "$URI") URI_CLEAN=$(python3 -c 'import json;u=json.load(open("'"$SCRATCH"'/wd-'"$tag"'.json")).get("taler_withdraw_uri") or "";print(u.replace(":443/","/"))' 2>/dev/null || true)
if [ -z "$WID" ] || [ -z "$URI_CLEAN" ]; then if [ -z "$WID" ] || [ -z "$URI_CLEAN" ]; then
warn "Bank withdraw $WITHDRAW_AMT" "create failed — $(head -c 80 "$SCRATCH/wd-$tag.json" | tr '\n' ' ')" warn "ATM withdraw $WITHDRAW_AMT" "create failed — $(head -c 80 "$SCRATCH/wd-$tag.json" | tr '\n' ' ')"
return 1 return 1
fi fi
ok "Bank withdrawal created $WITHDRAW_AMT ($WID)" ok "ATM withdrawal created $WITHDRAW_AMT ($WID)"
cp "$SCRATCH/wd-$tag.json" "$SCRATCH/wd.json" cp "$SCRATCH/wd-$tag.json" "$SCRATCH/wd.json"
# Always prefer stripped URI (libeufin often emits host:443) USE_URI="$URI"
USE_URI="${URI_CLEAN:-$URI}" [ -z "$USE_URI" ] && USE_URI="$URI_CLEAN"
if wcli withdraw accept-uri --exchange "${EXCHANGE_PUBLIC}/" "$USE_URI" >"$SCRATCH/accept-$tag.out" 2>&1; then if wcli withdraw accept-uri --exchange "${EXCHANGE_PUBLIC}/" "$USE_URI" >"$SCRATCH/accept-$tag.out" 2>&1; then
ok "wallet accept $WITHDRAW_AMT" ok "wallet accept $WITHDRAW_AMT"
elif [ -n "$URI" ] && [ "$USE_URI" != "$URI" ] \ elif [ "$USE_URI" != "$URI_CLEAN" ] && [ -n "$URI_CLEAN" ] \
&& wcli withdraw accept-uri --exchange "${EXCHANGE_PUBLIC}/" "$URI" >"$SCRATCH/accept-$tag.out" 2>&1; then && wcli withdraw accept-uri --exchange "${EXCHANGE_PUBLIC}/" "$URI_CLEAN" >"$SCRATCH/accept-$tag.out" 2>&1; then
ok "wallet accept $WITHDRAW_AMT (raw URI fallback)" ok "wallet accept $WITHDRAW_AMT (no :443)"
else else
# Prefer the real Error/ENOENT line over noise (perl -e source, stack frames) warn "ATM withdraw $WITHDRAW_AMT" "accept-uri failed — $(tail -c 100 "$SCRATCH/accept-$tag.out" | tr '\n' ' ')"
_acc_err=$(
grep -E 'Error:|ENOENT|spawn taler-helper|code:|An error occurred' \
"$SCRATCH/accept-$tag.out" 2>/dev/null | head -3 | tr '\n' ' ' | head -c 200
)
[ -z "$_acc_err" ] && _acc_err=$(tail -c 160 "$SCRATCH/accept-$tag.out" 2>/dev/null | tr '\n' ' ')
warn "Bank withdraw $WITHDRAW_AMT" "accept-uri failed — ${_acc_err:-unknown}"
return 1 return 1
fi fi
cp "$SCRATCH/accept-$tag.out" "$SCRATCH/accept.out" cp "$SCRATCH/accept-$tag.out" "$SCRATCH/accept.out"
@ -1008,7 +975,7 @@ else:
fi fi
fi fi
if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then
warn "Bank withdraw $WITHDRAW_AMT" "not selected (status=${st:-?})" warn "ATM withdraw $WITHDRAW_AMT" "not selected (status=${st:-?})"
return 1 return 1
fi fi
if [ "$st" != "confirmed" ]; then if [ "$st" != "confirmed" ]; then
@ -1017,32 +984,32 @@ else:
"$BANK/accounts/${USER}/withdrawals/${WID}/confirm") "$BANK/accounts/${USER}/withdrawals/${WID}/confirm")
case "$code" in case "$code" in
200|204) ok "bank confirm $WITHDRAW_AMT" ;; 200|204) ok "bank confirm $WITHDRAW_AMT" ;;
*) warn "Bank withdraw $WITHDRAW_AMT" "confirm HTTP $code"; return 1 ;; *) warn "ATM withdraw $WITHDRAW_AMT" "confirm HTTP $code"; return 1 ;;
esac esac
fi fi
fake_incoming_speedup fake_incoming_speedup
# Short per-withdraw settle: poll balance + bank transfer_done only (never run-until-done) # Short per-ATM settle: poll balance + bank transfer_done only (never run-until-done)
local ok_bal=0 r av xfer local ok_bal=0 r av xfer
for r in 1 2 3 4 5 6 8 10; do for r in 1 2 3 4 5 6 8 10; do
wcli_bal_snap "$SCRATCH/bal-$tag.out" || wcli balance >"$SCRATCH/bal-$tag.out" 2>&1 || true wcli_bal_snap "$SCRATCH/bal-$tag.out" || wcli balance >"$SCRATCH/bal-$tag.out" 2>&1 || true
av=$(wallet_avail_num "$SCRATCH/bal-$tag.out") av=$(wallet_avail_num "$SCRATCH/bal-$tag.out")
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av" 2>/dev/null; then if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av" 2>/dev/null; then
ok_bal=1 ok_bal=1
ok "wallet funded after bank withdraw $WITHDRAW_AMT" "avail=${CUR}:${av}" ok "wallet funded after ATM $WITHDRAW_AMT" "avail=${CUR}:${av}"
info "balance" "$(fmt_bal "$SCRATCH/bal-$tag.out")" info "balance" "$(fmt_bal "$SCRATCH/bal-$tag.out")"
break break
fi fi
xfer=$(curl -sS -m 8 "$BANK/taler-integration/withdrawal-operation/${WID}" 2>/dev/null \ xfer=$(curl -sS -m 8 "$BANK/taler-integration/withdrawal-operation/${WID}" 2>/dev/null \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("transfer_done"), d.get("status"))' 2>/dev/null || echo "?") | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("transfer_done"), d.get("status"))' 2>/dev/null || echo "?")
if echo "$xfer" | grep -qi True; then if echo "$xfer" | grep -qi True; then
ok "Bank withdraw $WITHDRAW_AMT bank transfer_done" "wallet avail=${CUR}:${av} ($xfer) — no run-until-done" ok "ATM $WITHDRAW_AMT bank transfer_done" "wallet avail=${CUR}:${av} ($xfer) — no run-until-done"
ok_bal=1 ok_bal=1
break break
fi fi
sleep 2 sleep 2
done done
metrics_report_coins "after-bank-wd-${tag}" || true metrics_report_coins "after-ATM-${tag}" || true
if [ "$ok_bal" = "1" ]; then if [ "$ok_bal" = "1" ]; then
metrics_record_flow withdrawn "$WITHDRAW_AMT" || true metrics_record_flow withdrawn "$WITHDRAW_AMT" || true
return 0 return 0
@ -1050,10 +1017,10 @@ else:
# Bank side often already confirmed — treat as timing lag, not hard fail # Bank side often already confirmed — treat as timing lag, not hard fail
st=$(wd_status) st=$(wd_status)
if [ "$st" = "confirmed" ]; then if [ "$st" = "confirmed" ]; then
warn "Bank withdraw $WITHDRAW_AMT" "bank confirmed; wallet not funded yet (settlement timing — will recheck later)" warn "ATM withdraw $WITHDRAW_AMT" "bank confirmed; wallet not funded yet (settlement timing — will recheck later)"
return 2 return 2
fi fi
warn "Bank withdraw $WITHDRAW_AMT" "no wallet balance yet (status=${st:-?})" warn "ATM withdraw $WITHDRAW_AMT" "no wallet balance yet (status=${st:-?})"
return 1 return 1
} }
@ -1368,8 +1335,8 @@ sys.exit(1)
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set_group bankwd set_group atm
section "e2e · Bank withdraw ladder" section "e2e · ATM withdraw ladder"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
WITHDRAW_OK=0 WITHDRAW_OK=0
WITHDRAW_OK_N=0 WITHDRAW_OK_N=0
@ -1377,7 +1344,7 @@ WITHDRAW_LAG_N=0
WITHDRAW_FAIL_N=0 WITHDRAW_FAIL_N=0
WITHDRAW_REPORT="" WITHDRAW_REPORT=""
for WITHDRAW_AMT in $WITHDRAW_LIST; do for WITHDRAW_AMT in $WITHDRAW_LIST; do
e2e_over && { warn "Bank withdraw ladder" "time budget low — stopping more Bank withdraws (not a protocol error)"; break; } e2e_over && { warn "ATM ladder" "time budget low — stopping more ATM withdraws (not a protocol error)"; break; }
set +e set +e
e2e_one_withdraw "$WITHDRAW_AMT" e2e_one_withdraw "$WITHDRAW_AMT"
wc=$? wc=$?
@ -1398,26 +1365,26 @@ for WITHDRAW_AMT in $WITHDRAW_LIST; do
;; ;;
esac esac
done done
info "Bank withdraw summary" "$WITHDRAW_REPORT (ok=$WITHDRAW_OK_N lag=$WITHDRAW_LAG_N fail=$WITHDRAW_FAIL_N)" info "ATM withdraw summary" "$WITHDRAW_REPORT (ok=$WITHDRAW_OK_N lag=$WITHDRAW_LAG_N fail=$WITHDRAW_FAIL_N)"
set_group load set_group load
section "e2e · load snapshot (after Bank withdraws)" section "e2e · load snapshot (after ATM withdraws)"
metrics_report_load "${METRICS_DIR}/load-after-withdraw.json" "after-withdraw" || true metrics_report_load "${METRICS_DIR}/load-after-withdraw.json" "after-withdraw" || true
cp -f "${METRICS_DIR}/load-after-withdraw.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true cp -f "${METRICS_DIR}/load-after-withdraw.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true
metrics_report_coins "after-bank-wd-ladder" || true metrics_report_coins "after-ATM-ladder" || true
# Settlement catch-up: bank may have confirmed while wallet was still empty # Settlement catch-up: bank may have confirmed while wallet was still empty
set_group settle set_group settle
section "e2e · wallet settlement (timing)" section "e2e · wallet settlement (timing)"
if ! wait_wallet_balance 0 "${E2E_SETTLE_ROUNDS}" "${E2E_SETTLE_SLEEP}"; then if ! wait_wallet_balance 0 "${E2E_SETTLE_ROUNDS}" "${E2E_SETTLE_SLEEP}"; then
if [ "$WITHDRAW_LAG_N" -gt 0 ] || [ "$WITHDRAW_OK_N" -gt 0 ]; then if [ "$WITHDRAW_LAG_N" -gt 0 ] || [ "$WITHDRAW_OK_N" -gt 0 ]; then
warn "settlement timing" "bank withdraw path reached bank confirm (lag=$WITHDRAW_LAG_N ok=$WITHDRAW_OK_N) but wallet empty after wait — TIME lag, not protocol error" warn "settlement timing" "ATM path reached bank confirm (lag=$WITHDRAW_LAG_N ok=$WITHDRAW_OK_N) but wallet empty after wait — TIME lag, not protocol error"
fi fi
fi fi
av_now=$(wallet_avail_num) av_now=$(wallet_avail_num)
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av_now" 2>/dev/null; then if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av_now" 2>/dev/null; then
WITHDRAW_OK=1 WITHDRAW_OK=1
if [ "$WITHDRAW_OK_N" = "0" ]; then if [ "$WITHDRAW_OK_N" = "0" ]; then
warn "settlement timing" "coins arrived after Bank withdraw ladder (${CUR}:${av_now}) — earlier 'FAIL' was timing lag" warn "settlement timing" "coins arrived after ATM ladder (${CUR}:${av_now}) — earlier 'FAIL' was timing lag"
WITHDRAW_REPORT="${WITHDRAW_REPORT} → late-OK avail=${CUR}:${av_now}" WITHDRAW_REPORT="${WITHDRAW_REPORT} → late-OK avail=${CUR}:${av_now}"
fi fi
ok "spendable balance for payments" "${CUR}:${av_now}" ok "spendable balance for payments" "${CUR}:${av_now}"
@ -1436,7 +1403,7 @@ else
WITHDRAW_OK=1 WITHDRAW_OK=1
warn "settlement timing" "coins present after dig wait (${CUR}:${av_now}) — timing, not blocker" warn "settlement timing" "coins present after dig wait (${CUR}:${av_now}) — timing, not blocker"
else else
blocker "withdraw-settle" "no spendable ${CUR} after Bank withdraw ladder + settle wait ($WITHDRAW_LIST)" blocker "withdraw-settle" "no spendable ${CUR} after ATM ladder + settle wait ($WITHDRAW_LIST)"
section "e2e · report" section "e2e · report"
info "WITHDRAW" "$WITHDRAW_REPORT — no coins after extended wait" info "WITHDRAW" "$WITHDRAW_REPORT — no coins after extended wait"
info "PAY" "SKIPPED (no spendable balance after settle wait)" info "PAY" "SKIPPED (no spendable balance after settle wait)"
@ -1629,7 +1596,7 @@ set_group paivana
section "e2e · paivana paywall (template GOA:4200)" section "e2e · paivana paywall (template GOA:4200)"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Public paywall: https://paivana.hacktivism.ch → 302 to merchant template "paivana". # Public paywall: https://paivana.hacktivism.ch → 302 to merchant template "paivana".
# Requires spendable ≥ 4200 GOA (Bank withdraw ladder includes GOA:4200 by default). # Requires spendable ≥ 4200 GOA (ATM ladder includes GOA:4200 by default).
: "${PAIVANA_PUBLIC:=https://paivana.hacktivism.ch}" : "${PAIVANA_PUBLIC:=https://paivana.hacktivism.ch}"
: "${E2E_PAIVANA:=1}" : "${E2E_PAIVANA:=1}"
: "${E2E_PAIVANA_TEMPLATE:=paivana}" : "${E2E_PAIVANA_TEMPLATE:=paivana}"
@ -1663,11 +1630,11 @@ else
;; ;;
esac esac
# Ensure wallet can cover 4200 (extra bank withdraw if ladder did not fund enough) # Ensure wallet can cover 4200 (extra ATM if ladder did not fund enough)
pay_need=$(python3 -c 'import sys; print(float(sys.argv[1].split(":",1)[-1]))' "$E2E_PAIVANA_AMOUNT" 2>/dev/null || echo 4200) pay_need=$(python3 -c 'import sys; print(float(sys.argv[1].split(":",1)[-1]))' "$E2E_PAIVANA_AMOUNT" 2>/dev/null || echo 4200)
av_now=$(wallet_avail_num) av_now=$(wallet_avail_num)
if ! python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) + 1e-9 >= float(sys.argv[2]) else 1)" "$av_now" "$pay_need" 2>/dev/null; then if ! python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) + 1e-9 >= float(sys.argv[2]) else 1)" "$av_now" "$pay_need" 2>/dev/null; then
info "paivana" "avail ${CUR}:${av_now} < ${pay_need}Bank withdraw ${E2E_PAIVANA_AMOUNT}" info "paivana" "avail ${CUR}:${av_now} < ${pay_need}ATM withdraw ${E2E_PAIVANA_AMOUNT}"
e2e_over && warn "paivana" "budget low before 4200 withdraw" e2e_over && warn "paivana" "budget low before 4200 withdraw"
set +e set +e
e2e_one_withdraw "$E2E_PAIVANA_AMOUNT" e2e_one_withdraw "$E2E_PAIVANA_AMOUNT"
@ -1720,7 +1687,7 @@ fi
set_group report set_group report
section "e2e · report" section "e2e · report"
info "user" "$USER" info "user" "$USER"
info "Bank withdraws" "$WITHDRAW_REPORT" info "ATM withdraws" "$WITHDRAW_REPORT"
info "payments" "$PAY_REPORT" info "payments" "$PAY_REPORT"
info "shop" "${SHOP_REPORT:-(n/a)}" info "shop" "${SHOP_REPORT:-(n/a)}"
info "paivana" "${PAIVANA_REPORT:-(n/a)}" info "paivana" "${PAIVANA_REPORT:-(n/a)}"

1356
check_goa_ladder.sh Executable file

File diff suppressed because it is too large Load diff

View file

@ -29,14 +29,12 @@ if [ -z "$PROFILE" ]; then
fi fi
fi fi
# Resolve host-podman vs ssh before first check ID. # Resolve host-podman vs ssh before first check ID
# stage-lfp on the FP host itself uses local podman (INSIDE_PODMAN=1 /
# INSIDE_MODE=local-podman); laptop → stage still uses INSIDE_SSH.
_use_local_podman=0 _use_local_podman=0
if [ "${INSIDE_PODMAN:-0}" = "1" ] || [ "${INSIDE_MODE:-}" = "local-podman" ]; then if [ "$PROFILE" != "stage-lfp" ]; then
_use_local_podman=1 if [ "${INSIDE_PODMAN:-0}" = "1" ] || [ "${INSIDE_MODE:-}" = "local-podman" ]; then
elif [ "$PROFILE" != "stage-lfp" ]; then _use_local_podman=1
if command -v podman >/dev/null 2>&1 \ elif command -v podman >/dev/null 2>&1 \
&& podman ps --format '{{.Names}}' 2>/dev/null | grep -qE 'taler-hacktivism'; then && podman ps --format '{{.Names}}' 2>/dev/null | grep -qE 'taler-hacktivism'; then
_use_local_podman=1 _use_local_podman=1
fi fi
@ -54,7 +52,7 @@ section "inside · collect (${PROFILE} · access=${INSIDE_ACCESS})"
info "flags" "INSIDE_ACCESS=${INSIDE_ACCESS} INSIDE_PODMAN=${INSIDE_PODMAN:-0} INSIDE_MODE=${INSIDE_MODE:-} LOCAL_STACK=${LOCAL_STACK:-0} SKIP_SSH=${SKIP_SSH:-0} KOOPA_SSH=${KOOPA_SSH:-} INSIDE_SSH=${INSIDE_SSH:-}" info "flags" "INSIDE_ACCESS=${INSIDE_ACCESS} INSIDE_PODMAN=${INSIDE_PODMAN:-0} INSIDE_MODE=${INSIDE_MODE:-} LOCAL_STACK=${LOCAL_STACK:-0} SKIP_SSH=${SKIP_SSH:-0} KOOPA_SSH=${KOOPA_SSH:-} INSIDE_SSH=${INSIDE_SSH:-}"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# stage-lfp: stagepaysan podman (host-local or SSH INSIDE_SSH from laptop) # stage-lfp: low-priv stagepaysan on the FP stage host (INSIDE_SSH)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if [ "$PROFILE" = "stage-lfp" ]; then if [ "$PROFILE" = "stage-lfp" ]; then
SSH_HOST="${INSIDE_SSH:-}" SSH_HOST="${INSIDE_SSH:-}"
@ -71,23 +69,15 @@ if [ "$PROFILE" = "stage-lfp" ]; then
STAGE_SSH_T="${INSIDE_SSH_TIMEOUT:-${SSH_CMD_TIMEOUT:-24}}" STAGE_SSH_T="${INSIDE_SSH_TIMEOUT:-${SSH_CMD_TIMEOUT:-24}}"
if [ "${STAGE_SSH_T}" -lt 24 ] 2>/dev/null; then STAGE_SSH_T=24; fi if [ "${STAGE_SSH_T}" -lt 24 ] 2>/dev/null; then STAGE_SSH_T=24; fi
if [ "$_use_local_podman" = "1" ]; then if ! mon_ssh_ok "$SSH_HOST"; then
if ! command -v podman >/dev/null 2>&1; then err "ssh" "cannot reach ${SSH_HOST} (stagepaysan low-priv) — set INSIDE_SSH= or SKIP"
err "host" "INSIDE_PODMAN/host-podman but podman missing"
summary
exit 1
fi
ok "host→container" "podman exec on this host (INSIDE_ACCESS=host-podman · no SSH)"
elif ! mon_ssh_ok "$SSH_HOST"; then
err "ssh" "cannot reach ${SSH_HOST:-?} (stagepaysan low-priv) — set INSIDE_SSH= or INSIDE_PODMAN=1"
summary summary
exit 1 exit 1
else
ok "ssh ${SSH_HOST}" "stagepaysan (podman, no sudo)"
fi fi
ok "ssh ${SSH_HOST}" "stagepaysan (podman, no sudo)"
# Inject names/ports (ssh bash -s does not inherit local env; local bash does). # Inject names/ports into remote (ssh bash -s does not inherit local env).
_stage_lfp_script() { RAW=$(
{ {
printf 'BANK_CTR=%q; EX_CTR=%q; MER_CTR=%q\n' "$BANK_CTR" "$EX_CTR" "$MER_CTR" printf 'BANK_CTR=%q; EX_CTR=%q; MER_CTR=%q\n' "$BANK_CTR" "$EX_CTR" "$MER_CTR"
printf 'BANK_PORT=%q; EX_PORT=%q; MER_PORT=%q\n' "$BANK_PORT" "$EX_PORT" "$MER_PORT" printf 'BANK_PORT=%q; EX_PORT=%q; MER_PORT=%q\n' "$BANK_PORT" "$EX_PORT" "$MER_PORT"
@ -183,20 +173,11 @@ fi
echo DONE echo DONE
REMOTE REMOTE
} } | mon_ssh_bash "$SSH_HOST" "${STAGE_SSH_T}" || true
} )
if [ "$_use_local_podman" = "1" ]; then
RAW=$(_stage_lfp_script | bash || true)
else
RAW=$(_stage_lfp_script | mon_ssh_bash "$SSH_HOST" "${STAGE_SSH_T}" || true)
fi
if [ -z "$RAW" ] || ! echo "$RAW" | grep -q '^E|'; then if [ -z "$RAW" ] || ! echo "$RAW" | grep -q '^E|'; then
if [ "$_use_local_podman" = "1" ]; then err "ssh" "stage remote timed out or empty (cap ${STAGE_SSH_T}s · host=${SSH_HOST})"
err "host" "stage collect timed out or empty (local podman)"
else
err "ssh" "stage remote timed out or empty (cap ${STAGE_SSH_T}s · host=${SSH_HOST})"
fi
summary summary
exit 1 exit 1
fi fi
@ -208,7 +189,7 @@ REMOTE
IFS='|' read -r _ comp level key detail <<<"$line" IFS='|' read -r _ comp level key detail <<<"$line"
case "$comp" in case "$comp" in
bank|exchange|merchant|caddy) _g="$comp" ;; bank|exchange|merchant|caddy) _g="$comp" ;;
*) _g="$INSIDE_ACCESS" ;; *) _g="ssh" ;;
esac esac
if [ "$_g" != "$_last_inside_grp" ]; then if [ "$_g" != "$_last_inside_grp" ]; then
set_group "$_g" set_group "$_g"
@ -261,29 +242,7 @@ REMOTE
# Host load as stagepaysan (no container RSS from koopa metrics) # Host load as stagepaysan (no container RSS from koopa metrics)
set_group load set_group load
section "inside · load (stagepaysan host)" section "inside · load (stagepaysan host)"
_load_py() { LOAD_LINE=$(mon_ssh_bash "$SSH_HOST" 8 <<'EOF' || true
python3 - <<'PY'
import os
la=os.getloadavg()
print("loadavg=%.2f,%.2f,%.2f" % la)
try:
with open("/proc/meminfo") as f:
d={}
for line in f:
k,v=line.split(":")[0], line.split(":")[1].strip().split()[0]
d[k]=int(v)
total=d.get("MemTotal",0)/1024/1024
avail=d.get("MemAvailable",0)/1024/1024
used=total-avail
print("mem_used=%.2fGiB avail=%.2fGiB total=%.2fGiB" % (used, avail, total))
except Exception:
print("mem=?")
PY
}
if [ "$_use_local_podman" = "1" ]; then
LOAD_LINE=$(_load_py || true)
else
LOAD_LINE=$(mon_ssh_bash "$SSH_HOST" 8 <<'EOF' || true
python3 - <<'PY' python3 - <<'PY'
import os import os
la=os.getloadavg() la=os.getloadavg()
@ -303,7 +262,6 @@ except Exception:
PY PY
EOF EOF
) )
fi
if [ -n "$LOAD_LINE" ]; then if [ -n "$LOAD_LINE" ]; then
info "stage host" "$(echo "$LOAD_LINE" | tr '\n' ' ')" info "stage host" "$(echo "$LOAD_LINE" | tr '\n' ' ')"
else else
@ -312,8 +270,7 @@ EOF
set_group disk set_group disk
section "inside · disk free space (stage host + containers)" section "inside · disk free space (stage host + containers)"
_disk_script() { _disk_raw=$(mon_ssh_bash "$SSH_HOST" "${STAGE_SSH_T:-24}" <<'DISK' || true
cat <<'DISK'
set +e set +e
echo "###HOST###" echo "###HOST###"
df -Pk / /var /home /tmp /mnt/data 2>/dev/null || df -Pk df -Pk / /var /home /tmp /mnt/data 2>/dev/null || df -Pk
@ -323,16 +280,9 @@ for c in $(podman ps --format '{{.Names}}' 2>/dev/null); do
podman exec "$c" df -Pk / /var /tmp 2>/dev/null || podman exec "$c" df -Pk 2>/dev/null podman exec "$c" df -Pk / /var /tmp 2>/dev/null || podman exec "$c" df -Pk 2>/dev/null
done done
DISK DISK
} )
if [ "$_use_local_podman" = "1" ]; then
_disk_raw=$(_disk_script | bash || true)
_disk_label="host"
else
_disk_raw=$(_disk_script | mon_ssh_bash "$SSH_HOST" "${STAGE_SSH_T:-24}" || true)
_disk_label="ssh:${SSH_HOST}"
fi
_host_df=$(printf '%s\n' "$_disk_raw" | sed -n '/^###HOST###$/,/^###CTRS###$/p' | sed '1d;$d') _host_df=$(printf '%s\n' "$_disk_raw" | sed -n '/^###HOST###$/,/^###CTRS###$/p' | sed '1d;$d')
mon_disk_check_remote_text "$_disk_label" "$_host_df" || true mon_disk_check_remote_text "ssh:${SSH_HOST}" "$_host_df" || true
_ctr=""; _buf="" _ctr=""; _buf=""
while IFS= read -r _line || [ -n "$_line" ]; do while IFS= read -r _line || [ -n "$_line" ]; do
case "$_line" in case "$_line" in

View file

@ -2,8 +2,7 @@
# check_mail.sh — outside-in mail (MX / SMTP / IMAP / SPF / DMARC) # check_mail.sh — outside-in mail (MX / SMTP / IMAP / SPF / DMARC)
# #
# Catalog: mail-catalog.conf (MAIL_CATALOG=… to override) # Catalog: mail-catalog.conf (MAIL_CATALOG=… to override)
# Covers firefly (taler.net, gnunet.org) and Anastasis/TSA # Covers firefly (taler.net, gnunet.org) and pixel (taler-systems.com, …).
# (mail.anastasis.lu → pixel.taler-systems.com; anastasis.lu).
# #
# Outside-only. Phase: mail # Outside-only. Phase: mail
# #

View file

@ -5,20 +5,6 @@
# Override: MATTERMOST_PUBLIC=https://mattermost.example.org # Override: MATTERMOST_PUBLIC=https://mattermost.example.org
# MATTERMOST_HOST=mattermost.example.org # MATTERMOST_HOST=mattermost.example.org
# #
# Client compatibility (feature) — server min for current *official* clients:
# Android / iOS store apps → default ≥ 10.11.0 (Play/App Store + mobile docs 2026)
# Desktop (Win/Mac/Linux) → default ≥ 10.11.0 (current Desktop / ESR pairing)
# Vendor support floor → default ≥ 10.11.0 (ESR; older = unsupported)
# Web SPA → served by this server (always loads) but WARN if
# server is below support floor (security/EOL).
#
# Env:
# MATTERMOST_CLIENT_CHECK=1|0 master switch (default 1)
# MATTERMOST_MOBILE_CHECK=1|0 alias / legacy (if set 0 and CLIENT unset → off)
# MATTERMOST_MOBILE_MIN_SERVER default 10.11.0
# MATTERMOST_DESKTOP_MIN_SERVER default 10.11.0
# MATTERMOST_SUPPORT_MIN_SERVER default 10.11.0 (vendor-supported floor)
#
# Outside-only (no SSH). Phase name: mattermost # Outside-only (no SSH). Phase name: mattermost
# #
set -euo pipefail set -euo pipefail
@ -29,20 +15,6 @@ source "$ROOT/lib.sh"
set_area mattermost set_area mattermost
section "mattermost · public chat (outside-in)" section "mattermost · public chat (outside-in)"
# Defaults: current official clients + vendor ESR floor (2026-07 docs).
: "${MATTERMOST_MOBILE_MIN_SERVER:=10.11.0}"
: "${MATTERMOST_DESKTOP_MIN_SERVER:=10.11.0}"
: "${MATTERMOST_SUPPORT_MIN_SERVER:=10.11.0}"
# Master switch: CLIENT_CHECK, else legacy MOBILE_CHECK, else on
if [ -n "${MATTERMOST_CLIENT_CHECK:-}" ]; then
:
elif [ -n "${MATTERMOST_MOBILE_CHECK:-}" ]; then
MATTERMOST_CLIENT_CHECK="${MATTERMOST_MOBILE_CHECK}"
else
MATTERMOST_CLIENT_CHECK=1
fi
: "${MATTERMOST_MOBILE_CHECK:=${MATTERMOST_CLIENT_CHECK}}"
if [ -n "${MATTERMOST_PUBLIC:-}" ]; then if [ -n "${MATTERMOST_PUBLIC:-}" ]; then
BASE="${MATTERMOST_PUBLIC%/}" BASE="${MATTERMOST_PUBLIC%/}"
elif [ -n "${MATTERMOST_HOST:-}" ]; then elif [ -n "${MATTERMOST_HOST:-}" ]; then
@ -55,7 +27,6 @@ HOST=${HOST#http://}
HOST=${HOST%%/*} HOST=${HOST%%/*}
info "target" "$BASE" info "target" "$BASE"
info "client policy" "mobile≥${MATTERMOST_MOBILE_MIN_SERVER} · desktop≥${MATTERMOST_DESKTOP_MIN_SERVER} · support≥${MATTERMOST_SUPPORT_MIN_SERVER} (CLIENT_CHECK=${MATTERMOST_CLIENT_CHECK})"
tmp=$(mktemp -d) tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT trap 'rm -rf "$tmp"' EXIT
@ -75,139 +46,30 @@ else
fail "landing" "$BASE/ → HTTP $code (want 200 HTML Mattermost SPA)" fail "landing" "$BASE/ → HTTP $code (want 200 HTML Mattermost SPA)"
fi fi
# --- Official Mattermost health API + server version (X-Version-Id) --- # --- Official Mattermost health API ---
set_group api set_group api
# Capture body + headers (version is in X-Version-Id, not always in JSON) code=$(mm_get "$BASE/api/v4/system/ping" "$tmp/ping.json")
hdr_file="$tmp/ping.hdr"
code=$(curl -skS -L --max-redirs 5 -m "${TIMEOUT:-12}" \
-D "$hdr_file" -o "$tmp/ping.json" -w '%{http_code}' \
"$BASE/api/v4/system/ping" 2>/dev/null || echo 000)
MM_SERVER_VER=""
if [ -f "$hdr_file" ]; then
# X-Version-Id: 9.5.3.8427860509.c8cabb… or 10.11.0.…
MM_SERVER_VER=$(grep -i '^x-version-id:' "$hdr_file" | head -1 \
| sed 's/^[Xx]-[Vv]ersion-[Ii]d:[[:space:]]*//' | tr -d '\r' \
| sed 's/^\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/')
fi
if [ "$code" != "200" ]; then if [ "$code" != "200" ]; then
fail "system/ping" "$BASE/api/v4/system/ping → HTTP $code (want 200 JSON)" fail "system/ping" "$BASE/api/v4/system/ping → HTTP $code (want 200 JSON)"
else else
# Ping returns JSON object (fields vary by MM version)
if python3 - "$tmp/ping.json" <<'PY' 2>/dev/null if python3 - "$tmp/ping.json" <<'PY' 2>/dev/null
import json, sys import json, sys
with open(sys.argv[1]) as f: p = sys.argv[1]
with open(p) as f:
d = json.load(f) d = json.load(f)
sys.exit(0 if isinstance(d, dict) else 2) if not isinstance(d, dict):
sys.exit(2)
# older builds may include status; newer still return object with backends
sys.exit(0)
PY PY
then then
ok "system/ping" "$BASE/api/v4/system/ping → HTTP 200 JSON${MM_SERVER_VER:+ · server ${MM_SERVER_VER}}" ok "system/ping" "$BASE/api/v4/system/ping → HTTP 200 JSON"
else else
fail "system/ping" "$BASE/api/v4/system/ping → HTTP 200 but not valid JSON object" fail "system/ping" "$BASE/api/v4/system/ping → HTTP 200 but not valid JSON object"
fi fi
fi fi
# --- Server version vs official client families (mobile / desktop / support / web) ---
set_group clients
mm_ver_ge() {
# $1=server $2=need → 0 if server >= need
python3 - "$1" "$2" <<'PY'
import sys
def parse(v: str):
parts = []
for p in v.strip().split(".")[:3]:
try:
parts.append(int(p))
except ValueError:
parts.append(0)
while len(parts) < 3:
parts.append(0)
return tuple(parts)
sys.exit(0 if parse(sys.argv[1]) >= parse(sys.argv[2]) else 1)
PY
}
# One policy line: id|min_env_default|severity(error|warn)|human label
# severity: error → fail; warn → warn
_mm_client_policies() {
printf '%s\n' \
"mobile|${MATTERMOST_MOBILE_MIN_SERVER}|error|Android/iOS store apps (Mattermost Mobile)" \
"desktop|${MATTERMOST_DESKTOP_MIN_SERVER}|error|Desktop app (Windows/macOS/Linux official)" \
"support|${MATTERMOST_SUPPORT_MIN_SERVER}|error|vendor-supported server floor (ESR/current)" \
"web|${MATTERMOST_SUPPORT_MIN_SERVER}|warn|browser webapp (loads with server, but EOL server is insecure/unsupported)"
}
if [ "${MATTERMOST_CLIENT_CHECK}" = "0" ]; then
info "client compat" "skipped (MATTERMOST_CLIENT_CHECK=0 / MATTERMOST_MOBILE_CHECK=0)"
elif [ -z "$MM_SERVER_VER" ]; then
warn "server version" "could not parse X-Version-Id from /api/v4/system/ping — cannot judge client compatibility"
else
info "server version" "X-Version-Id → ${MM_SERVER_VER}"
while IFS='|' read -r cid cmin csever clabel; do
[ -n "$cid" ] || continue
if mm_ver_ge "$MM_SERVER_VER" "$cmin"; then
ok "${cid} compat" "server ${MM_SERVER_VER}${cmin} · ${clabel} OK"
else
detail="server ${MM_SERVER_VER} < ${cmin} · ${clabel}"
hint="upgrade Mattermost server · set MATTERMOST_${cid^^}_MIN_SERVER= or MATTERMOST_CLIENT_CHECK=0"
# Fix hint for support/web keys
case "$cid" in
mobile) hint="upgrade server · MATTERMOST_MOBILE_MIN_SERVER= / MATTERMOST_CLIENT_CHECK=0" ;;
desktop) hint="upgrade server · MATTERMOST_DESKTOP_MIN_SERVER= / MATTERMOST_CLIENT_CHECK=0" ;;
support) hint="server below vendor support floor · MATTERMOST_SUPPORT_MIN_SERVER= / MATTERMOST_CLIENT_CHECK=0" ;;
web) hint="web SPA is served by this old server (works) but platform is unsupported · upgrade recommended" ;;
esac
if [ "$csever" = "error" ]; then
fail "${cid} compat" "$detail" "$hint"
else
warn "${cid} compat" "$detail · $hint"
fi
fi
done < <(_mm_client_policies)
fi
# Client config (optional): BuildDate / advertised Android/iOS min if server sets it
set_group client-config
code=$(mm_get "$BASE/api/v4/config/client?format=old" "$tmp/client.json")
if [ "$code" = "200" ] && [ -s "$tmp/client.json" ]; then
build=$(python3 - "$tmp/client.json" <<'PY' 2>/dev/null
import json, sys
with open(sys.argv[1]) as f:
d = json.load(f)
print(d.get("BuildDate") or d.get("Version") or "")
PY
)
and_min=$(python3 - "$tmp/client.json" <<'PY' 2>/dev/null
import json, sys
with open(sys.argv[1]) as f:
d = json.load(f)
print((d.get("AndroidMinVersion") or "").strip())
PY
)
ios_min=$(python3 - "$tmp/client.json" <<'PY' 2>/dev/null
import json, sys
with open(sys.argv[1]) as f:
d = json.load(f)
print((d.get("IosMinVersion") or d.get("IOSMinVersion") or "").strip())
PY
)
if [ -n "$build" ]; then
info "client config" "BuildDate/Version=${build}${and_min:+ · AndroidMinVersion=${and_min}}${ios_min:+ · IosMinVersion=${ios_min}}"
else
info "client config" "HTTP 200 (no BuildDate field)"
fi
if [ -n "$and_min" ] && [ "$and_min" != "0.0.0" ]; then
info "android min (server)" "server advertises AndroidMinVersion=${and_min}"
fi
if [ -n "$ios_min" ] && [ "$ios_min" != "0.0.0" ]; then
info "ios min (server)" "server advertises IosMinVersion=${ios_min}"
fi
else
info "client config" "HTTP ${code:-?} — optional"
fi
# --- Login SPA (must be served for users) --- # --- Login SPA (must be served for users) ---
set_group login set_group login
code=$(mm_get "$BASE/login" "$tmp/login.html") code=$(mm_get "$BASE/login" "$tmp/login.html")
@ -247,9 +109,5 @@ else
info "tls cert" "openssl not available — skip expiry check" info "tls cert" "openssl not available — skip expiry check"
fi fi
info "hint" "surface catalog also lists $HOST; client floors: mobile/desktop/support ≥${MATTERMOST_MOBILE_MIN_SERVER}/${MATTERMOST_DESKTOP_MIN_SERVER}/${MATTERMOST_SUPPORT_MIN_SERVER}" info "hint" "surface catalog also lists $HOST; this phase is Mattermost-specific (SPA + /api/v4/system/ping)"
# Non-zero when any fail/err so parent phase + mon pages mark ERROR
if [ "${FAIL_N:-0}" -gt 0 ]; then
exit 1
fi
exit 0 exit 0

View file

@ -100,20 +100,17 @@ _host_allowed() {
} }
# Public mon URL families (suite name "taler-monitoring" ≠ a public path): # Public mon URL families (suite name "taler-monitoring" ≠ a public path):
# 1) /monitoring(+_err) — landing hosts (bare + slash) # 1) /monitoring(+_err) — landing hosts (bare + slash)
# 2) /monitoring-max-ladder(+_err) — GOA max-search amount ladder (daily, hacktivism) # 2) /taler-monitoring-surface(+_err) — taler.hacktivism.ch only
# 3) /taler-monitoring-surface(+_err) — taler.hacktivism.ch only # 3) /taler-monitoring-aptdeploy(+_err) — taler.hacktivism.ch only
# 4) /taler-monitoring-aptdeploy(+_err) — taler.hacktivism.ch only # No /taler-monitoring page. No /taler-monitoring-mail|mattermost pages.
# No classic /monitoring-ladder. No /taler-monitoring page.
_path_allowed() { _path_allowed() {
case "$1" in case "$1" in
monitoring|monitoring_err|\ monitoring|monitoring_err|\
monitoring-max-ladder|monitoring-max-ladder_err|\
taler-monitoring-surface|taler-monitoring-surface_err|\ taler-monitoring-surface|taler-monitoring-surface_err|\
taler-monitoring-aptdeploy|taler-monitoring-aptdeploy_err) return 0 ;; taler-monitoring-aptdeploy|taler-monitoring-aptdeploy_err) return 0 ;;
# explicit deny (never invent a mon page here) # explicit deny (never invent a mon page here)
monitoring-ladder|monitoring-ladder_err|\
taler-monitoring|taler-monitoring-mail|taler-monitoring-mattermost) return 1 ;; taler-monitoring|taler-monitoring-mail|taler-monitoring-mattermost) return 1 ;;
*) return 1 ;; *) return 1 ;;
esac esac
@ -137,11 +134,12 @@ _discover_tree() {
done < <(find "${root%/}" -mindepth 3 -maxdepth 3 -type f -name index.html -print0 2>/dev/null) done < <(find "${root%/}" -mindepth 3 -maxdepth 3 -type f -name index.html -print0 2>/dev/null)
} }
# Landing stacks (GOA 3 + FP stage 3) with /monitoring/ # Nine landing stacks (GOA 3 + FP stage 3 + FP prod 3) with /monitoring/
_landing_allowed() { _landing_nine() {
printf '%s\n' \ printf '%s\n' \
bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch \ bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch \
stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch \
bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch
} }
# Suite catalog of OK pages that must exist for this family. # Suite catalog of OK pages that must exist for this family.
@ -151,7 +149,6 @@ _catalog_urls() {
goa) goa)
for h in bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch; do for h in bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch; do
printf 'https://%s/monitoring/\n' "$h" printf 'https://%s/monitoring/\n' "$h"
printf 'https://%s/monitoring-max-ladder/\n' "$h"
done done
# ecosystem surface (versions / software) + apt-src deploy mon page # ecosystem surface (versions / software) + apt-src deploy mon page
printf 'https://taler.hacktivism.ch/taler-monitoring-surface/\n' printf 'https://taler.hacktivism.ch/taler-monitoring-surface/\n'
@ -165,16 +162,11 @@ _catalog_urls() {
done done
;; ;;
all|nine) all|nine)
# GOA + FP stage landings + surface + aptdeploy (nine kept as alias for inventory) # optional: all nine landings + surface + aptdeploy
while IFS= read -r h; do while IFS= read -r h; do
[ -n "$h" ] || continue [ -n "$h" ] || continue
printf 'https://%s/monitoring/\n' "$h" printf 'https://%s/monitoring/\n' "$h"
case "$h" in done < <(_landing_nine)
*.hacktivism.ch)
printf 'https://%s/monitoring-max-ladder/\n' "$h"
;;
esac
done < <(_landing_allowed)
printf 'https://taler.hacktivism.ch/taler-monitoring-surface/\n' printf 'https://taler.hacktivism.ch/taler-monitoring-surface/\n'
printf 'https://taler.hacktivism.ch/taler-monitoring-aptdeploy/\n' printf 'https://taler.hacktivism.ch/taler-monitoring-aptdeploy/\n'
;; ;;
@ -236,25 +228,6 @@ is_bootstrap_html() {
&& ! grep -qE 'taler-mon:top|id="mon-console"|class="console"' "$f" 2>/dev/null && ! grep -qE 'taler-mon:top|id="mon-console"|class="console"' "$f" 2>/dev/null
} }
# Stale mon HTML from pre-1.15.8 generators: box-drawing char-class regex in
# sticky filter JS (Python SyntaxWarning: invalid escape sequence '\-').
# Never accept these as "current" public assets (v1.18.2+).
is_stale_generator_html() {
local f="$1"
# Old isLowValue() JS (literal box glyphs + \- class)
if grep -qE 'if \(/\^\[[╔╗╚╝║═]' "$f" 2>/dev/null; then
return 0
fi
# Converter noise teed into old logs and re-rendered
if grep -qiE 'SyntaxWarning: invalid escape sequence' "$f" 2>/dev/null; then
return 0
fi
if grep -qiE 'console_to_html\.py:[0-9]+: SyntaxWarning' "$f" 2>/dev/null; then
return 0
fi
return 1
}
# True if body looks like our monitoring HTML (not merchant API / WP 404 / bootstrap). # True if body looks like our monitoring HTML (not merchant API / WP 404 / bootstrap).
is_monitoring_html() { is_monitoring_html() {
local f="$1" local f="$1"
@ -503,19 +476,6 @@ check_one() {
return 0 return 0
fi fi
# Stale generator JS / SyntaxWarning in public body (pre-1.15.8 assets)
if is_stale_generator_html "$body"; then
if [ "$attempt" -lt "$max_try" ]; then
attempt=$((attempt + 1))
sleep "$sleep_s" 2>/dev/null || sleep 1
continue
fi
_mon_fail_or_soft "public mon page stale generator" \
"$url -> old mon HTML (box-drawing JS regex and/or SyntaxWarning in body) — re-run host-agent on suite ≥v1.15.8 / v1.18.2; do not leave pre-update assets live" || { rm -f "$body"; return 1; }
rm -f "$body"
return 0
fi
if ! is_monitoring_html "$body"; then if ! is_monitoring_html "$body"; then
if [ "$attempt" -lt "$max_try" ]; then if [ "$attempt" -lt "$max_try" ]; then
attempt=$((attempt + 1)) attempt=$((attempt + 1))

View file

@ -1316,7 +1316,7 @@ if [ "${LOCAL_STACK:-1}" = "1" ] || [ -n "${BANK_PUBLIC:-}" ]; then
fi fi
;; ;;
404|405|501) 404|405|501)
# GOA bank landing mints demo-withdraw.json; stage TESTPAYSAN uses bank withdraw/wallet guide only # GOA bank landing mints demo-withdraw.json; stage TESTPAYSAN uses ATM/wallet guide only
if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)" fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)"
else else
@ -1339,90 +1339,6 @@ if [ "${LOCAL_STACK:-1}" = "1" ] || [ -n "${BANK_PUBLIC:-}" ]; then
fi fi
fi fi
# ---------------------------------------------------------------------------
# taler:// URIs must not carry default ports :443 / :80 (wallet / QR rule)
# Scans public landings on this stack (bank + exchange + merchant intros,
# bank mint JSON). Fail hard on GOA/local; soft warn elsewhere.
# ---------------------------------------------------------------------------
set_group landings
section "www · taler:// default ports (:443/:80 must be absent)"
_port_scan_fail=0
_port_scan_ok=0
_port_scan_one() {
local label="$1" url="$2"
local f code n detail
f="$tmp/portscan-$$.body"
code=$(http_body "$url" "$f")
if [ "$code" != "200" ]; then
if [ "${LOCAL_STACK:-1}" = "1" ] || [ "${EXPECT_CURRENCY:-}" = "GOA" ]; then
# mint JSON is hard; HTML intros soft if missing
case "$url" in
*demo-withdraw.json|*auto-account.json)
fail "taler-uri ports · $label" "HTTP $code · $url"
_port_scan_fail=$((_port_scan_fail + 1))
;;
*)
warn "taler-uri ports · $label" "HTTP $code · skip · $url"
;;
esac
else
info "taler-uri ports · $label" "HTTP $code · skip"
fi
rm -f "$f"
return 0
fi
detail=$(python3 - "$f" <<'PY' 2>/dev/null || true
import re, sys, json
from pathlib import Path
raw = Path(sys.argv[1]).read_text(errors="replace")
# collect taler:// from JSON fields and raw HTML/JS
uris = set(re.findall(r"taler://[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]+", raw))
# JSON-escaped sequences
uris.update(re.findall(r"taler:\\/\\/[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%\\-]+", raw))
uris = {u.replace("\\/", "/") for u in uris}
bad = []
pat = re.compile(r"taler://\S+:(443|80)(/|$|\?|#)", re.I)
for u in sorted(uris):
if pat.search(u):
bad.append(u[:120])
if bad:
print("BAD\t%d\t%s" % (len(bad), bad[0]))
else:
print("OK\t%d\t" % len(uris))
PY
)
n_kind=${detail%%$'\t'*}
rest=${detail#*$'\t'}
n_count=${rest%%$'\t'*}
sample=${rest#*$'\t'}
if [ "$n_kind" = "BAD" ]; then
if [ "${LOCAL_STACK:-1}" = "1" ] || [ "${EXPECT_CURRENCY:-}" = "GOA" ]; then
fail "taler-uri ports · $label" "default :443/:80 still present · n=${n_count} e.g. ${sample}"
_port_scan_fail=$((_port_scan_fail + 1))
else
warn "taler-uri ports · $label" "default port in URI · ${sample}"
fi
else
ok "taler-uri ports · $label" "no default ports · taler_uris=${n_count:-0} · $url"
_port_scan_ok=$((_port_scan_ok + 1))
fi
rm -f "$f"
}
if [ "${CHECK_LANDING:-1}" = "1" ]; then
_port_scan_one "bank intro HTML" "${BANK_PUBLIC}/intro/"
_port_scan_one "bank demo-withdraw.json" "${BANK_PUBLIC}/intro/demo-withdraw.json"
_port_scan_one "bank auto-account.json" "${BANK_PUBLIC}/intro/auto-account.json"
_port_scan_one "exchange intro HTML" "${EXCHANGE_PUBLIC}/intro/"
_port_scan_one "merchant intro HTML" "${MERCHANT_PUBLIC}/intro/"
# stats may embed demo withdraw URI
_port_scan_one "bank stats.json" "${BANK_PUBLIC}/intro/stats.json"
if [ "$_port_scan_fail" -eq 0 ]; then
ok "taler-uri ports summary" "clean · ${_port_scan_ok} resource(s) checked (bank+exchange+merchant)"
fi
else
info "taler-uri ports" "skipped (CHECK_LANDING=0)"
fi
# Merchant shop assets — GOA: shop-pay.*; stage TESTPAYSAN: /assets/shop-ui.js + # Merchant shop assets — GOA: shop-pay.*; stage TESTPAYSAN: /assets/shop-ui.js +
# shops.css + QR encoder (shop-ui modal needs /intro/qrcode.min.js or /qrcode.min.js). # shops.css + QR encoder (shop-ui modal needs /intro/qrcode.min.js or /qrcode.min.js).
_ma=0 _ma=0

View file

@ -1,53 +1,48 @@
# taler-monitoring domain profiles # taler-monitoring domain profiles
# #
# When you add a stack, declare the three public endpoints here: # When you add a stack, declare the three public endpoints here:
# name bank exchange merchant-backend currency local landing lang locale [canonical] # name bank exchange merchant-backend currency local landing [canonical]
# #
# - name: what -d / TALER_DOMAIN matches (aliases allowed as extra lines) # - name: what -d / TALER_DOMAIN matches (aliases allowed as extra lines)
# - bank / exchange / merchant: hostname or full https:// URL # - bank / exchange / merchant: hostname or full https:// URL
# - currency: GOA | KUDOS | CHF | … (empty or "-" = report only) # - currency: GOA | KUDOS | CHF | … (empty = report only)
# - local: 1 = koopa SSH/inside/e2e stack; 0 = public-only # - local: 1 = koopa SSH/inside/e2e stack; 0 = public-only
# - landing: 1 = check /intro landings + assets; 0 = skip (no GOA-style landings) # - landing: 1 = check /intro landings + assets; 0 = skip (no GOA-style landings)
# - lang: UI language for mon console/HTML (en|fr|de). Source of truth when
# TALER_MON_LANG_SET≠1. Workstation TALER_MON_LANG=en does not override.
# **de is supported but must not be used as a profile default** (no row
# sets lang=de unless an operator deliberately chooses it).
# - locale: BCP 47 regional locale for l10n defaults (not the same as UI lang).
# Use **fr-CH** (Suisse romande) for FrancPaysan; **de-CH** (Deutschschweiz)
# for other Swiss / default stacks. Accepts also ch-FR / ch-DE as aliases.
# - canonical: optional TALER_DOMAIN label after alias match # - canonical: optional TALER_DOMAIN label after alias match
# #
# Legacy lines without locale still parse (locale → de-CH except FP heuristic). # CLI overrides after load: --bank --exchange --merchant --currency
# CLI: --lang en|fr|de · TALER_MON_LANG_SET=1 locks UI lang
# Alternate file: TALER_DOMAINS_CONF=/path/to/domains.conf # Alternate file: TALER_DOMAINS_CONF=/path/to/domains.conf
# Local GOA (koopa) — landings matter · UI en · locale de-CH # Local GOA (koopa) — landings matter
koopa bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 en de-CH hacktivism.ch koopa bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 hacktivism.ch
hacktivism bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 en de-CH hacktivism.ch hacktivism bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 hacktivism.ch
hacktivism.ch bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 en de-CH hacktivism.ch bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1
# Public demo (KUDOS) # Public demo (KUDOS) — has intro-style pages; keep soft landing checks
taler.net bank.demo.taler.net exchange.demo.taler.net backend.demo.taler.net KUDOS 0 1 en de-CH demo.taler.net taler.net bank.demo.taler.net exchange.demo.taler.net backend.demo.taler.net KUDOS 0 1 demo.taler.net
demo.taler.net bank.demo.taler.net exchange.demo.taler.net backend.demo.taler.net KUDOS 0 1 en de-CH demo.taler.net bank.demo.taler.net exchange.demo.taler.net backend.demo.taler.net KUDOS 0 1
# Public test stack (TESTKUDOS) # Public test stack (TESTKUDOS) — no GOA-style landings
test.taler.net bank.test.taler.net exchange.test.taler.net backend.test.taler.net TESTKUDOS 0 0 en de-CH test.taler.net bank.test.taler.net exchange.test.taler.net backend.test.taler.net TESTKUDOS 0 0
# TOPS CHF # TOPS CHF — multi-tenant merchant my.taler-ops.ch; no public GOA landings
taler-ops.ch bank.taler-ops.ch exchange.taler-ops.ch my.taler-ops.ch CHF 0 0 en de-CH taler-ops.ch bank.taler-ops.ch exchange.taler-ops.ch my.taler-ops.ch CHF 0 0
my.taler-ops.ch bank.taler-ops.ch exchange.taler-ops.ch my.taler-ops.ch CHF 0 0 en de-CH taler-ops.ch my.taler-ops.ch bank.taler-ops.ch exchange.taler-ops.ch my.taler-ops.ch CHF 0 0 taler-ops.ch
stage.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 en de-CH # Stage merchant FQDN is stage.my.taler-ops.ch (on betel); mon HTML /monitoring* stage-only
stage.my.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 en de-CH stage.taler-ops.ch stage.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0
my.stage.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 en de-CH stage.taler-ops.ch stage.my.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 stage.taler-ops.ch
my.stage.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 stage.taler-ops.ch
# Franc Paysan — UI fr · regional locale fr-CH (Suisse romande) # Franc Paysan (Infomaniak) — merchant host is monnaie.* (not taler./backend.)
lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr fr-CH # Prod: no monetary launch yet; stage uses TESTPAYSAN (public HTTPS landings + shops)
monnaie.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr fr-CH lefrancpaysan.ch lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0
bank.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr fr-CH lefrancpaysan.ch monnaie.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 lefrancpaysan.ch
exchange.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr fr-CH lefrancpaysan.ch bank.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 lefrancpaysan.ch
exchange.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 lefrancpaysan.ch
# Stage TESTPAYSAN — UI fr · locale fr-CH # Stage TESTPAYSAN — landings on / + farmer shops under stage.monnaie…/shops/
stage.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr fr-CH # name bank exchange merchant currency local landing [canonical]
stage.bank.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr fr-CH stage.lefrancpaysan.ch stage.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1
stage.exchange.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr fr-CH stage.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 stage.lefrancpaysan.ch
stage.monnaie.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr fr-CH stage.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 stage.lefrancpaysan.ch
stage.monnaie.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 stage.lefrancpaysan.ch

View file

@ -19,27 +19,13 @@ Thin wrappers only set stack defaults:
|---------|-------|----------------| |---------|-------|----------------|
| `run-hacktivism-monitoring.sh` | GOA `hacktivism.ch` | `hernani` @ koopa | | `run-hacktivism-monitoring.sh` | GOA `hacktivism.ch` | `hernani` @ koopa |
| `run-fp-stage-monitoring.sh` | FP stage | `stagepaysan` | | `run-fp-stage-monitoring.sh` | FP stage | `stagepaysan` |
| `run-fp-prod-monitoring.sh` | FP prod | `francpaysan` |
| `run-host-report.sh` + `taler-monitoring-mytops-stage.{service,timer}` | mytops **stage / betel** only | `taler-monitoring` @ betel (4h) | | `run-host-report.sh` + `taler-monitoring-mytops-stage.{service,timer}` | mytops **stage / betel** only | `taler-monitoring` @ betel (4h) |
| `run-aptdeploy.sh` | CLI: ensure + **aptdeploy** (no HTML) | local if hostname=koopa, else **ssh $DEPLOY_SSH** | | `run-aptdeploy.sh` | CLI: ensure + **aptdeploy** (no HTML) | local if hostname=koopa, else **ssh $DEPLOY_SSH** |
| `run-aptdeploy-monitoring.sh` | apt-src tests → `/taler-monitoring-aptdeploy*` | `hernani` @ koopa (4h) | | `run-aptdeploy-monitoring.sh` | apt-src tests → `/taler-monitoring-aptdeploy*` | `hernani` @ koopa (4h) |
| `run-surface-monitoring.sh` | **surface** → only `/taler-monitoring-surface*` (ecosystem + mail + mattermost + versions) | `hernani` @ koopa (hourly) | | `run-surface-monitoring.sh` | **surface** → only `/taler-monitoring-surface*` (ecosystem + mail + mattermost + versions) | `hernani` @ koopa (hourly) |
| `run-max-ladder-monitoring.sh` | GOA **max-search** ladder → `/monitoring-max-ladder/` on bank/exchange/taler | `hernani` @ koopa (**daily**) |
Deprecated mail/mattermost wrappers: **`../meta/deprecated/`** (not installed). No classic **`/monitoring-ladder`** page. Deprecated mail/mattermost wrappers: **`../meta/deprecated/`** (not installed).
### GOA max-search ladder page (daily)
| URL | Phase | Timer |
|-----|-------|-------|
| `https://{bank,exchange,taler}.hacktivism.ch/monitoring-max-ladder/` | `max-ladder` | `taler-monitoring-max-ladder.timer` (~06:30 + jitter) |
```bash
systemctl --user enable --now taler-monitoring-max-ladder.timer
# first round now:
systemctl --user start taler-monitoring-max-ladder.service
```
Needs explorer secrets (`SECRETS_ROOT` / bank-explorer password). Wall ~3h (`RUN_TIMEOUT=10800`).
### apt-src deploy tests (hacktivism `/monitoring`) ### apt-src deploy tests (hacktivism `/monitoring`)
@ -68,7 +54,7 @@ Checks: package table, `httpd --version`, libtalerutil/`ldd` on failure, `system
| URL | Role | | URL | Role |
|-----|------| |-----|------|
| `https://{bank,exchange,merchant}/monitoring/` (+ `_err`) | **Landing-stack** report — GOA 3 + FP **stage** 3 (+ mytops stage as configured) | | `https://{bank,exchange,merchant}/monitoring/` (+ `_err`) | **Landing-stack** report — each of the **9** landing fronts (GOA 3 + FP stage 3 + FP prod 3) |
| `https://taler.hacktivism.ch/taler-monitoring-surface/` (+ `_err`) | **Only** ecosystem page: software/versions in the Taler universe (nmap OS, mail firefly+anastasis, mattermost, …) | | `https://taler.hacktivism.ch/taler-monitoring-surface/` (+ `_err`) | **Only** ecosystem page: software/versions in the Taler universe (nmap OS, mail firefly+anastasis, mattermost, …) |
No separate public pages for mail/mattermost. Staging under `~/monitoring-sites-staging/`. No separate public pages for mail/mattermost. Staging under `~/monitoring-sites-staging/`.
@ -78,7 +64,7 @@ systemctl --user enable --now taler-monitoring-surface.timer
systemctl --user start taler-monitoring-surface.service systemctl --user start taler-monitoring-surface.service
``` ```
Env examples: **`env/*.env.example`**. FP stage: `run-fp-stage-monitoring.sh` (same `run-host-report.sh` pipeline). Env examples: **`env/*.env.example`**. FP: `run-fp-prod-monitoring.sh` / `run-fp-stage-monitoring.sh` (same `run-host-report.sh` pipeline).
## GOA (koopa) — host-podman ## GOA (koopa) — host-podman

View file

@ -48,12 +48,9 @@ There is **no** public page `/taler-monitoring`. Each family needs **bare and sl
| Family | Paths | Host | Source timer | | Family | Paths | Host | Source timer |
|--------|-------|------|----------------| |--------|-------|------|----------------|
| **monitoring** | `/monitoring` · `/monitoring/` · `/monitoring_err` · `/monitoring_err/` | bank + exchange + taler (and other landings) | `taler-monitoring-hacktivism.timer` (4h) | | **monitoring** | `/monitoring` · `/monitoring/` · `/monitoring_err` · `/monitoring_err/` | bank + exchange + taler (and other landings) | `taler-monitoring-hacktivism.timer` (4h) |
| **monitoring-max-ladder** | `/monitoring-max-ladder` · `/` · `_err` | bank + exchange + taler (**GOA** max-search only) | `taler-monitoring-max-ladder.timer` (**daily**) |
| **taler-monitoring-surface** | bare + slash + `_err` | **taler.hacktivism.ch only** | `taler-monitoring-surface.timer` (1h) | | **taler-monitoring-surface** | bare + slash + `_err` | **taler.hacktivism.ch only** | `taler-monitoring-surface.timer` (1h) |
| **taler-monitoring-aptdeploy** | bare + slash + `_err` | **taler.hacktivism.ch only** | `taler-monitoring-aptdeploy.timer` (4h) | | **taler-monitoring-aptdeploy** | bare + slash + `_err` | **taler.hacktivism.ch only** | `taler-monitoring-aptdeploy.timer` (4h) |
No classic **`/monitoring-ladder`**. Max-ladder served via Caddy `handle /monitoring*` under `/var/www/monitoring-sites/{host}/`.
HTML is generated as **hernani** into HTML is generated as **hernani** into
`$DEPLOY_STAGING (or $HOME/monitoring-sites-staging)/`. `$DEPLOY_STAGING (or $HOME/monitoring-sites-staging)/`.

View file

@ -57,9 +57,6 @@ urls=(
https://taler.hacktivism.ch/monitoring/ https://taler.hacktivism.ch/monitoring/
https://bank.hacktivism.ch/monitoring/ https://bank.hacktivism.ch/monitoring/
https://exchange.hacktivism.ch/monitoring/ https://exchange.hacktivism.ch/monitoring/
https://taler.hacktivism.ch/monitoring-max-ladder/
https://bank.hacktivism.ch/monitoring-max-ladder/
https://exchange.hacktivism.ch/monitoring-max-ladder/
https://taler.hacktivism.ch/taler-monitoring-surface https://taler.hacktivism.ch/taler-monitoring-surface
https://taler.hacktivism.ch/taler-monitoring-surface/ https://taler.hacktivism.ch/taler-monitoring-surface/
https://taler.hacktivism.ch/taler-monitoring-surface_err/ https://taler.hacktivism.ch/taler-monitoring-surface_err/

View file

@ -4,6 +4,7 @@
# Full stack examples (same reporting generation for all): # Full stack examples (same reporting generation for all):
# env/hacktivism.env.example # env/hacktivism.env.example
# env/stagepaysan.env.example # env/stagepaysan.env.example
# env/francpaysan.env.example
# #
# SUITE_GIT_URL=https://git.hacktivism.ch/hernani/taler-monitoring.git # SUITE_GIT_URL=https://git.hacktivism.ch/hernani/taler-monitoring.git
# SUITE_GIT_REF=main # SUITE_GIT_REF=main

27
host-agent/env/francpaysan.env.example vendored Normal file
View file

@ -0,0 +1,27 @@
# Copy to: ~/.config/taler-monitoring/env (user francpaysan)
# Never inside the git tree — update-suite never overwrites this file.
#
# Shared reporting generation (run-host-report.sh / run-fp-prod-monitoring.sh):
# RUN_TIMEOUT, line-buffered log, always HTML, commit link on git.hacktivism.ch
TALER_DOMAIN=lefrancpaysan.ch
INSIDE_PODMAN=1
INSIDE_MODE=local-podman
LOCAL_STACK=0
CONTINUE_ON_ERROR=1
PHASES="urls inside versions"
RUN_TIMEOUT=600
HTML_OUT=$HOME/monitoring-sites-staging
MON_HOSTS="bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch"
SUITE_DIR=$HOME/src/taler-monitoring
SUITE_GIT_URL=https://git.hacktivism.ch/hernani/taler-monitoring.git
SUITE_GIT_REF=main
SUITE_UPDATE_MODE=reset
SOURCE_REPO_WEB=https://git.hacktivism.ch/hernani/taler-monitoring
# DEPLOY_WWW_ROOT=/var/www/monitoring-sites
# UI language for console + HTML sticky bar (en|fr)
TALER_MON_LANG=fr

View file

@ -18,8 +18,7 @@ SKIP_SSH=1
LOCAL_STACK=0 LOCAL_STACK=0
INSIDE_PODMAN=0 INSIDE_PODMAN=0
CONTINUE_ON_ERROR=1 CONTINUE_ON_ERROR=1
# ladder needs wall time (23× fake-incoming SSH) RUN_TIMEOUT=600
RUN_TIMEOUT=1800
STRICT_EXIT=1 STRICT_EXIT=1
MONPAGES_REQUIRE_PUBLIC=1 MONPAGES_REQUIRE_PUBLIC=1
MONPAGES_INVENTORY=job MONPAGES_INVENTORY=job
@ -33,9 +32,4 @@ CHECK_LANDING=0
# rusty devtesting (SSH Host taler-rusty-devtesting → devtesting@rusty.taler-ops.ch) # rusty devtesting (SSH Host taler-rusty-devtesting → devtesting@rusty.taler-ops.ch)
DEVTESTING_SSH=taler-rusty-devtesting DEVTESTING_SSH=taler-rusty-devtesting
DEVTESTING_AMOUNT=CHF:1.00 DEVTESTING_AMOUNT=CHF:1.00
# CHF fake-incoming amount ladder (23 rungs → 50k)
DEVTESTING_LADDER=1
DEVTESTING_LADDER_STEPS=23
DEVTESTING_LADDER_MAX=50000
DEVTESTING_LADDER_MIN=0.01
# DEVTESTING_SKIP=1 # DEVTESTING_SKIP=1

View file

@ -1,10 +1,8 @@
# Copy to: ~/.config/taler-monitoring/env (user stagepaysan) # Copy to: ~/.config/taler-monitoring/env (user stagepaysan)
# Never inside the git tree — update-suite never overwrites this file. # Never inside the git tree — update-suite never overwrites this file.
# #
# Secrets: use francpaysan-secrets layout (operator tree or host mirror): # Shared reporting generation (run-host-report.sh / run-fp-stage-monitoring.sh):
# $HOME/francpaysan-secrets/stage/bank-admin-password.txt # RUN_TIMEOUT, line-buffered log, always HTML, commit link on git.hacktivism.ch
# $HOME/francpaysan-secrets/stage/default-instance-token.txt
# On stage host, symlink those to /mnt/data/stagepaysan/*/secrets/…
TALER_DOMAIN=stage.lefrancpaysan.ch TALER_DOMAIN=stage.lefrancpaysan.ch
INSIDE_PODMAN=1 INSIDE_PODMAN=1
@ -12,32 +10,21 @@ INSIDE_MODE=local-podman
INSIDE_PROFILE=stage-lfp INSIDE_PROFILE=stage-lfp
LOCAL_STACK=0 LOCAL_STACK=0
CONTINUE_ON_ERROR=1 CONTINUE_ON_ERROR=1
PHASES="urls inside versions monpages e2e" PHASES="urls inside versions"
RUN_TIMEOUT=1800 RUN_TIMEOUT=600
E2E_TIMEOUT=900
E2E_USE_TEMPLATES=1
HTML_OUT=$HOME/monitoring-sites-staging HTML_OUT=$HOME/monitoring-sites-staging
MON_HOSTS="stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch" MON_HOSTS="stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch"
DEPLOY_WWW_ROOT=/var/www/monitoring-sites
# Git clone of taler-monitoring (repo root, not …/taler-monitoring (repo root))
SUITE_DIR=$HOME/src/taler-monitoring SUITE_DIR=$HOME/src/taler-monitoring
SUITE_GIT_URL=https://git.hacktivism.ch/hernani/taler-monitoring.git SUITE_GIT_URL=https://git.hacktivism.ch/hernani/taler-monitoring.git
SUITE_GIT_REF=main SUITE_GIT_REF=main
SUITE_UPDATE_MODE=reset SUITE_UPDATE_MODE=reset
SOURCE_REPO_WEB=https://git.hacktivism.ch/hernani/taler-monitoring SOURCE_REPO_WEB=https://git.hacktivism.ch/hernani/taler-monitoring
FRANCPAYSAN_SECRETS=$HOME/francpaysan-secrets # Optional public docroot if this user can write it
WALLET_CLI=$HOME/.local/lib/taler-wallet-cli/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs # DEPLOY_WWW_ROOT=/var/www/monitoring-sites
WALLET_CLI_CANDIDATES="$HOME/.local/lib/taler-wallet-cli/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs $HOME/.local/bin/taler-wallet-cli.mjs"
INSIDE_BANK_CTR=stage-lfp-bank
INSIDE_EXCHANGE_CTR=stage-lfp-exchange-ansible
INSIDE_MERCHANT_CTR=stage-lfp-merchant
# UI language for console + HTML sticky bar (en|fr)
TALER_MON_LANG=fr TALER_MON_LANG=fr
MONPAGES_REQUIRE_PUBLIC=1
MONPAGES_INVENTORY=job
STRICT_EXIT=1
AGENT_LABEL=fp-stage-host-agent
STATE_NAME=taler-monitoring-stage-lfp

View file

@ -63,21 +63,11 @@ install_local() {
# optional aptdeploy (not a public mon page layout — v1.8.0 simplified overview) # optional aptdeploy (not a public mon page layout — v1.8.0 simplified overview)
install -m 644 "$ROOT/taler-monitoring-aptdeploy.service" "$home_unit/" install -m 644 "$ROOT/taler-monitoring-aptdeploy.service" "$home_unit/"
install -m 644 "$ROOT/taler-monitoring-aptdeploy.timer" "$home_unit/" install -m 644 "$ROOT/taler-monitoring-aptdeploy.timer" "$home_unit/"
# GOA max-search amount ladder page (daily only — no classic /monitoring-ladder)
install -m 644 "$ROOT/taler-monitoring-max-ladder.service" "$home_unit/"
install -m 644 "$ROOT/taler-monitoring-max-ladder.timer" "$home_unit/"
chmod +x "$ROOT/run-max-ladder-monitoring.sh" 2>/dev/null || true
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now taler-monitoring-hacktivism.path systemctl --user enable --now taler-monitoring-hacktivism.path
systemctl --user enable --now taler-monitoring-hacktivism.timer systemctl --user enable --now taler-monitoring-hacktivism.timer
systemctl --user enable --now taler-monitoring-surface.timer systemctl --user enable --now taler-monitoring-surface.timer
systemctl --user enable --now taler-monitoring-aptdeploy.timer systemctl --user enable --now taler-monitoring-aptdeploy.timer
systemctl --user enable --now taler-monitoring-max-ladder.timer
# drop classic ladder mon page if previously installed
systemctl --user disable --now taler-monitoring-ladder.timer 2>/dev/null || true
systemctl --user stop taler-monitoring-ladder.service 2>/dev/null || true
rm -f "$home_unit/taler-monitoring-ladder.service" \
"$home_unit/taler-monitoring-ladder.timer" 2>/dev/null || true
# Remove leftover mail/mattermost units if present (moved to meta/deprecated/) # Remove leftover mail/mattermost units if present (moved to meta/deprecated/)
systemctl --user disable --now taler-monitoring-mattermost.timer 2>/dev/null || true systemctl --user disable --now taler-monitoring-mattermost.timer 2>/dev/null || true
systemctl --user disable --now taler-monitoring-mail.timer 2>/dev/null || true systemctl --user disable --now taler-monitoring-mail.timer 2>/dev/null || true
@ -88,29 +78,23 @@ install_local() {
systemctl --user daemon-reload 2>/dev/null || true systemctl --user daemon-reload 2>/dev/null || true
# initial stamp so path exists # initial stamp so path exists
"$ROOT/touch-software-stamp.sh" "$ROOT/touch-software-stamp.sh"
# oneshot jobs: --no-block so install does not wait hours systemctl --user start taler-monitoring-hacktivism.service || true
systemctl --user start --no-block taler-monitoring-hacktivism.service || true systemctl --user start taler-monitoring-surface.service || true
systemctl --user start --no-block taler-monitoring-surface.service || true systemctl --user start taler-monitoring-aptdeploy.service || true
systemctl --user start --no-block taler-monitoring-aptdeploy.service || true
# first max-ladder round immediately (daily thereafter)
systemctl --user start --no-block taler-monitoring-max-ladder.service || true
echo "--- status ---" echo "--- status ---"
systemctl --user status taler-monitoring-hacktivism.path --no-pager -l | head -15 systemctl --user status taler-monitoring-hacktivism.path --no-pager -l | head -15
systemctl --user status taler-monitoring-hacktivism.timer --no-pager -l | head -12 systemctl --user status taler-monitoring-hacktivism.timer --no-pager -l | head -12
systemctl --user status taler-monitoring-surface.timer --no-pager -l | head -12 systemctl --user status taler-monitoring-surface.timer --no-pager -l | head -12
systemctl --user status taler-monitoring-aptdeploy.timer --no-pager -l | head -12 systemctl --user status taler-monitoring-aptdeploy.timer --no-pager -l | head -12
systemctl --user status taler-monitoring-max-ladder.timer --no-pager -l | head -12
systemctl --user list-timers --all | grep taler-monitoring || true systemctl --user list-timers --all | grep taler-monitoring || true
echo "OK host-agent:" echo "OK host-agent (v1.8.0 simplified):"
echo " · hacktivism 4h → /monitoring on bank/exchange/taler (landing stacks)" echo " · hacktivism 4h → /monitoring on bank/exchange/taler (landing stacks)"
echo " · max-ladder daily → /monitoring-max-ladder/ (GOA max-search, bank/exchange/taler)"
echo " · surface 1h → /taler-monitoring-surface* (ecosystem versions/software)" echo " · surface 1h → /taler-monitoring-surface* (ecosystem versions/software)"
echo " · aptdeploy 4h (optional tests)" echo " · aptdeploy 4h (optional tests; not a public mon-page type)"
echo " · no classic /monitoring-ladder page"
echo " · mail/mattermost: not installed (meta/deprecated/; content in surface)" echo " · mail/mattermost: not installed (meta/deprecated/; content in surface)"
echo "suite: $mon" echo "suite: $mon"
echo "manual max-ladder: systemctl --user start taler-monitoring-max-ladder.service" echo "manual surface: systemctl --user start taler-monitoring-surface.service"
echo "after upgrades: $ROOT/touch-software-stamp.sh" echo "after upgrades: $ROOT/touch-software-stamp.sh"
} }

View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
# FrancPaysan PROD host-agent (user francpaysan on ).
# Shared reporting: run-host-report.sh (timeout, line-buffered log, first-run HTML).
set -uo pipefail
# Language: en|fr (selectable). Default for this wrapper: fr
export TALER_MON_LANG="${TALER_MON_LANG:-fr}"
export TALER_MON_LANG_SET=1
AGENT_DIR=$(cd "$(dirname "$0")" && pwd)
export AGENT_LABEL="${AGENT_LABEL:-fp-prod-host-agent}"
export STATE_NAME="${STATE_NAME:-taler-monitoring-lfp-prod}"
export TALER_DOMAIN="${TALER_DOMAIN:-lefrancpaysan.ch}"
export INSIDE_PODMAN="${INSIDE_PODMAN:-1}"
export INSIDE_MODE="${INSIDE_MODE:-local-podman}"
export LOCAL_STACK="${LOCAL_STACK:-0}"
export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}"
# monpages obligatory (ERROR): only FP mon hosts below — never GOA inventory
export MONPAGES_REQUIRE_PUBLIC="${MONPAGES_REQUIRE_PUBLIC:-1}"
export RUN_TIMEOUT="${RUN_TIMEOUT:-600}"
export PHASES="${PHASES:-urls inside versions monpages}"
export MON_HOSTS="${MON_HOSTS:-bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch}"
export HTML_OUT="${HTML_OUT:-$HOME/monitoring-sites-staging}"
export DEPLOY_WWW_ROOT="${DEPLOY_WWW_ROOT:-/var/www/monitoring-sites}"
exec bash "$AGENT_DIR/run-host-report.sh" "$@"

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# FrancPaysan STAGE host-agent (user stagepaysan via francpaysan-stage-user). # FrancPaysan STAGE host-agent (user stagepaysan on ).
# Shared reporting: run-host-report.sh (timeout, line-buffered log, first-run HTML). # Shared reporting: run-host-report.sh (timeout, line-buffered log, first-run HTML).
set -uo pipefail set -uo pipefail
@ -18,8 +18,8 @@ export LOCAL_STACK="${LOCAL_STACK:-0}"
export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}" export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}"
# monpages obligatory (ERROR): only FP stage mon hosts below — never GOA inventory # monpages obligatory (ERROR): only FP stage mon hosts below — never GOA inventory
export MONPAGES_REQUIRE_PUBLIC="${MONPAGES_REQUIRE_PUBLIC:-1}" export MONPAGES_REQUIRE_PUBLIC="${MONPAGES_REQUIRE_PUBLIC:-1}"
export RUN_TIMEOUT="${RUN_TIMEOUT:-1800}" export RUN_TIMEOUT="${RUN_TIMEOUT:-600}"
export PHASES="${PHASES:-urls inside versions monpages e2e}" export PHASES="${PHASES:-urls inside versions monpages}"
export MON_HOSTS="${MON_HOSTS:-stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch}" export MON_HOSTS="${MON_HOSTS:-stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch}"
export HTML_OUT="${HTML_OUT:-$HOME/monitoring-sites-staging}" export HTML_OUT="${HTML_OUT:-$HOME/monitoring-sites-staging}"
export DEPLOY_WWW_ROOT="${DEPLOY_WWW_ROOT:-/var/www/monitoring-sites}" export DEPLOY_WWW_ROOT="${DEPLOY_WWW_ROOT:-/var/www/monitoring-sites}"

View file

@ -16,12 +16,10 @@ export INSIDE_MODE="${INSIDE_MODE:-local-podman}"
export LOCAL_STACK="${LOCAL_STACK:-1}" export LOCAL_STACK="${LOCAL_STACK:-1}"
export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}" export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}"
export RUN_TIMEOUT="${RUN_TIMEOUT:-600}" export RUN_TIMEOUT="${RUN_TIMEOUT:-600}"
# stack checks + full monpages inventory (landings + max-ladder + surface + aptdeploy). # stack checks + full monpages inventory (landings + surface + aptdeploy bare/slash).
# Specialized pages also have their own timers; full outside-in inventory lives here. # Specialized pages also have their own timers; full outside-in inventory lives here.
# run-max-ladder-monitoring.sh → /monitoring-max-ladder* (daily max-search, GOA) # run-aptdeploy-monitoring.sh → monpages job-only for /taler-monitoring-aptdeploy*
# run-aptdeploy-monitoring.sh → monpages job-only for /taler-monitoring-aptdeploy* # run-surface-monitoring.sh → monpages job-only for /taler-monitoring-surface*
# run-surface-monitoring.sh → monpages job-only for /taler-monitoring-surface*
# No classic /monitoring-ladder page.
export PHASES="${PHASES:-urls inside versions monpages}" export PHASES="${PHASES:-urls inside versions monpages}"
export MON_HOSTS="${MON_HOSTS:-bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch}" export MON_HOSTS="${MON_HOSTS:-bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch}"
export HTML_OUT="${HTML_OUT:-$HOME/monitoring-sites-staging}" export HTML_OUT="${HTML_OUT:-$HOME/monitoring-sites-staging}"

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# run-host-report.sh — global reporting generation for all stacks # run-host-report.sh — global reporting generation for all stacks
# (GOA/hacktivism, FP stage, mytops stage, …). # (GOA/hacktivism, FP stage, FP prod, …).
# #
# Shared behaviour (always): # Shared behaviour (always):
# 1) update-suite → latest git.hacktivism.ch (local ~/.config never overwritten) # 1) update-suite → latest git.hacktivism.ch (local ~/.config never overwritten)
@ -18,6 +18,7 @@
# Thin wrappers only set stack defaults then exec this script: # Thin wrappers only set stack defaults then exec this script:
# run-hacktivism-monitoring.sh # run-hacktivism-monitoring.sh
# run-fp-stage-monitoring.sh # run-fp-stage-monitoring.sh
# run-fp-prod-monitoring.sh
# #
set -uo pipefail set -uo pipefail
@ -40,7 +41,6 @@ _wrap_keys=(
APT_DEPLOY_ENSURE SURFACE_SCOPE TALER_DOMAIN_FROM_CLI STRICT_EXIT SOURCE_SUITE_PATH APT_DEPLOY_ENSURE SURFACE_SCOPE TALER_DOMAIN_FROM_CLI STRICT_EXIT SOURCE_SUITE_PATH
MONPAGES_INVENTORY MONPAGES_CHECK_BARE MONPAGES_BARE_STRICT MONPAGES_REQUIRE_PUBLIC MONPAGES_INVENTORY MONPAGES_CHECK_BARE MONPAGES_BARE_STRICT MONPAGES_REQUIRE_PUBLIC
CHECK_BANK MERCHANT_REQUIRED CHECK_LANDING CHECK_BANK MERCHANT_REQUIRED CHECK_LANDING
WALLET_CLI CLI_JS PATH LADDER_MODE LADDER_PAY LADDER_PAY_WAIT_AVAILABLE_S
) )
for _k in "${_wrap_keys[@]}"; do for _k in "${_wrap_keys[@]}"; do
if [ -n "${!_k+x}" ]; then if [ -n "${!_k+x}" ]; then
@ -76,15 +76,19 @@ export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}"
export AUTH401_CONTINUE="${AUTH401_CONTINUE:-${CONTINUE_ON_ERROR}}" export AUTH401_CONTINUE="${AUTH401_CONTINUE:-${CONTINUE_ON_ERROR}}"
export RUN_TIMEOUT="${RUN_TIMEOUT:-600}" export RUN_TIMEOUT="${RUN_TIMEOUT:-600}"
export TALER_DOMAIN="${TALER_DOMAIN:-hacktivism.ch}" export TALER_DOMAIN="${TALER_DOMAIN:-hacktivism.ch}"
# UI language: --lang / TALER_MON_LANG_SET=1 locks. # UI language: selectable via TALER_MON_LANG / --lang (en|fr).
# Otherwise domains.conf column `lang` → TALER_DOMAIN_LANG → TALER_MON_LANG # If unset, auto: FrancPaysan domains → fr, else en.
# (applied in suite apply_taler_domain / i18n_init). Leave SET=0 so -d can re-apply. if [ "${TALER_MON_LANG_SET:-0}" != "1" ] && [ -z "${TALER_MON_LANG:-}" ]; then
# Bare workstation TALER_MON_LANG=en must not override profile lang=fr. case "$TALER_DOMAIN" in
if [ "${TALER_MON_LANG_SET:-0}" = "1" ]; then *lefrancpaysan*|*francpaysan*) export TALER_MON_LANG=fr ;;
case "${TALER_MON_LANG:-en}" in *) export TALER_MON_LANG=en ;;
esac
elif [ -n "${TALER_MON_LANG:-}" ]; then
case "$TALER_MON_LANG" in
fr|FR|fra|french) export TALER_MON_LANG=fr ;; fr|FR|fra|french) export TALER_MON_LANG=fr ;;
*) export TALER_MON_LANG=en ;; *) export TALER_MON_LANG=en ;;
esac esac
export TALER_MON_LANG_SET=1
fi fi
export TALER_MON_LANG export TALER_MON_LANG
@ -350,29 +354,7 @@ htmlify_host() {
mkdir -p "$HTML_BASE/$host/${HTML_OK_DIR}" "$HTML_BASE/$host/${HTML_ERR_DIR}" mkdir -p "$HTML_BASE/$host/${HTML_OK_DIR}" "$HTML_BASE/$host/${HTML_ERR_DIR}"
if [ -n "$SITE_GEN" ] && [ -f "$SITE_GEN/console_to_html.py" ]; then if [ -n "$SITE_GEN" ] && [ -f "$SITE_GEN/console_to_html.py" ]; then
# Never convert with a pre-1.15.8 generator (old assets → SyntaxWarning + bad sticky JS). python3 "$SITE_GEN/console_to_html.py" \
if grep -qE 'if \(/\^\[[╔╗╚╝║═]' "$SITE_GEN/console_to_html.py" 2>/dev/null; then
echo "ERROR: refuse stale site-gen $SITE_GEN/console_to_html.py (box-drawing JS regex)" >&2
return 1 2>/dev/null || exit 1
fi
# SyntaxWarning = generator regression: fail convert (do not publish old-style pages).
# Other stderr still goes to agent log file only, not the mon console stream.
_mon_html_py() {
local _out="" _i
for _i in "$@"; do
if [ "$_i" = "--out" ]; then _out=1; continue; fi
if [ "$_out" = "1" ]; then _out="$_i"; break; fi
done
PYTHONWARNINGS=error::SyntaxWarning \
python3 "$SITE_GEN/console_to_html.py" "$@" 2>>"${LOG_DIR:-/tmp}/console_to_html.err" || return 1
# Never leave pre-1.15.8 sticky JS in a just-written page (stale log text is filtered in converter).
if [ -n "$_out" ] && [ -f "$_out" ] && grep -qE 'if \(/\^\[[╔╗╚╝║═]' "$_out" 2>/dev/null; then
echo "ERROR: generated mon HTML still has stale box-drawing JS: $_out" >&2
return 1
fi
return 0
}
if ! _mon_html_py \
--lang "${TALER_MON_LANG:-en}" \ --lang "${TALER_MON_LANG:-en}" \
--log "$LOG" \ --log "$LOG" \
--out "$mon_err" \ --out "$mon_err" \
@ -386,49 +368,37 @@ htmlify_host() {
--page-label "$PAGE_LABEL" \ --page-label "$PAGE_LABEL" \
--path-err "$HTML_URL_ERR" \ --path-err "$HTML_URL_ERR" \
--link-other "$HTML_URL_OK" --link-other "$HTML_URL_OK"
then
echo "ERROR: mon HTML convert failed (err page) for $host — refuse publish of stale/broken assets" >&2
return 1 2>/dev/null || exit 1
fi
if [ "$ec" -eq 0 ]; then if [ "$ec" -eq 0 ]; then
if ! _mon_html_py \ python3 "$SITE_GEN/console_to_html.py" \
--lang "${TALER_MON_LANG:-en}" \ --lang "${TALER_MON_LANG:-en}" \
--log "$LOG" \ --log "$LOG" \
--out "$mon" \ --out "$mon" \
--hostname "$host" \ --hostname "$host" \
--mode ok \ --mode ok \
--commit "${COMMIT:-}" \ --commit "${COMMIT:-}" \
--commit-url "${COMMIT_URL:-}" \ --commit-url "${COMMIT_URL:-}" \
--suite-version "${SUITE_VERSION:-unknown}" \ --suite-version "${SUITE_VERSION:-unknown}" \
--suite-version-url "${SUITE_VERSION_URL:-}" \ --suite-version-url "${SUITE_VERSION_URL:-}" \
--suite-path "$SUITE_PATH" \ --suite-path "$SUITE_PATH" \
--page-label "$PAGE_LABEL" \ --page-label "$PAGE_LABEL" \
--path-err "$HTML_URL_ERR" \ --path-err "$HTML_URL_ERR" \
--link-other "" --link-other ""
then
echo "ERROR: mon HTML convert failed (ok page) for $host" >&2
return 1 2>/dev/null || exit 1
fi
rm -rf "$HTML_BASE/$host/${HTML_ERR_DIR}" rm -rf "$HTML_BASE/$host/${HTML_ERR_DIR}"
echo "html $host${HTML_URL_OK} only (clean · ${COMMIT_SHORT:-?})" echo "html $host${HTML_URL_OK} only (clean · ${COMMIT_SHORT:-?})"
else else
if ! _mon_html_py \ python3 "$SITE_GEN/console_to_html.py" \
--lang "${TALER_MON_LANG:-en}" \ --lang "${TALER_MON_LANG:-en}" \
--log "$LOG" \ --log "$LOG" \
--out "$mon" \ --out "$mon" \
--hostname "$host" \ --hostname "$host" \
--mode redirect \ --mode redirect \
--commit "${COMMIT:-}" \ --commit "${COMMIT:-}" \
--commit-url "${COMMIT_URL:-}" \ --commit-url "${COMMIT_URL:-}" \
--suite-version "${SUITE_VERSION:-unknown}" \ --suite-version "${SUITE_VERSION:-unknown}" \
--suite-version-url "${SUITE_VERSION_URL:-}" \ --suite-version-url "${SUITE_VERSION_URL:-}" \
--suite-path "$SUITE_PATH" \ --suite-path "$SUITE_PATH" \
--page-label "$PAGE_LABEL" \ --page-label "$PAGE_LABEL" \
--path-err "$HTML_URL_ERR" --path-err "$HTML_URL_ERR"
then
echo "ERROR: mon HTML convert failed (redirect page) for $host" >&2
return 1 2>/dev/null || exit 1
fi
echo "html $host${HTML_URL_OK} stub + ${HTML_URL_ERR} (ec=$ec · ${COMMIT_SHORT:-?})" echo "html $host${HTML_URL_OK} stub + ${HTML_URL_ERR} (ec=$ec · ${COMMIT_SHORT:-?})"
fi fi
else else

View file

@ -1,48 +0,0 @@
#!/usr/bin/env bash
# run-max-ladder-monitoring.sh — GOA max-search amount ladder → public HTML
#
# Public (landing hosts, same Caddy /monitoring* static root):
# https://{bank,exchange,taler}.hacktivism.ch/monitoring-max-ladder/
# https://{bank,exchange,taler}.hacktivism.ch/monitoring-max-ladder_err/
#
# Timer: once daily (taler-monitoring-max-ladder.timer). Long wall clock.
#
set -uo pipefail
AGENT_DIR=$(cd "$(dirname "$0")" && pwd)
export AGENT_LABEL="${AGENT_LABEL:-max-ladder-host-agent}"
export STATE_NAME="${STATE_NAME:-taler-max-ladder-monitoring}"
export TALER_DOMAIN="${TALER_DOMAIN:-hacktivism.ch}"
export TALER_MON_LANG="${TALER_MON_LANG:-en}"
export TALER_MON_LANG_SET=1
export INSIDE_PODMAN="${INSIDE_PODMAN:-1}"
export INSIDE_MODE="${INSIDE_MODE:-local-podman}"
export LOCAL_STACK="${LOCAL_STACK:-1}"
export SKIP_SSH="${SKIP_SSH:-0}"
export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}"
# max-search probes + pay hunt — force high wall clock (koopa env often has RUN_TIMEOUT=600)
export RUN_TIMEOUT="${MAX_LADDER_RUN_TIMEOUT:-10800}"
export PHASES="max-ladder monpages"
export MON_HOSTS="${MON_HOSTS:-bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch}"
export HTML_OUT="${HTML_OUT:-$HOME/monitoring-sites-staging}"
export DEPLOY_WWW_ROOT="${DEPLOY_WWW_ROOT:-/var/www/monitoring-sites}"
export HTML_OK_DIR="monitoring-max-ladder"
export HTML_ERR_DIR="monitoring-max-ladder_err"
export HTML_URL_OK="/monitoring-max-ladder/"
export HTML_URL_ERR="/monitoring-max-ladder_err/"
export PAGE_LABEL="monitoring-max-ladder"
export MONPAGES_INVENTORY="job"
export LADDER_MODE=max
export LADDER_PAY="${LADDER_PAY:-1}"
export LADDER_PAY_WAIT_AVAILABLE_S="${LADDER_PAY_WAIT_AVAILABLE_S:-180}"
export APT_DEPLOY_ENSURE=0
# Prefer host wallet-cli install used by mon (find_wallet_cli / WALLET_CLI)
export PATH="${HOME}/.local/bin:${PATH:-}"
if [ -z "${WALLET_CLI:-}" ] && [ -f "${HOME}/.local/bin/taler-wallet-cli.mjs" ]; then
export WALLET_CLI="${HOME}/.local/bin/taler-wallet-cli.mjs"
fi
exec bash "$AGENT_DIR/run-host-report.sh" "$@"

View file

@ -6,7 +6,7 @@
# https://taler.hacktivism.ch/taler-monitoring-surface_err/ # https://taler.hacktivism.ch/taler-monitoring-surface_err/
# #
# This page covers remote ecosystem inventory: host/port probes, nmap OS # This page covers remote ecosystem inventory: host/port probes, nmap OS
# fingerprint, Mattermost, mail (firefly + mail.anastasis.lu/pixel), package/version signals. # fingerprint, Mattermost, mail (firefly + anastasis), package/version signals.
# There are NO separate public pages for mail / mattermost (folded here). # There are NO separate public pages for mail / mattermost (folded here).
# Apt-src deploy tests keep their own page + timer: # Apt-src deploy tests keep their own page + timer:
# run-aptdeploy-monitoring.sh → /taler-monitoring-aptdeploy(+_err)/ # run-aptdeploy-monitoring.sh → /taler-monitoring-aptdeploy(+_err)/

View file

@ -1,19 +0,0 @@
[Unit]
Description=Taler monitoring GOA max-search amount ladder → /monitoring-max-ladder/
Documentation=file:%h/src/taler-monitoring/host-agent/README.md
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
Environment=PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin
Environment=CONTINUE_ON_ERROR=1
Environment=RUN_TIMEOUT=10800
EnvironmentFile=-%h/.config/taler-monitoring/env
WorkingDirectory=%h/src/taler-monitoring
ExecStart=%h/src/taler-monitoring/host-agent/run-max-ladder-monitoring.sh
Nice=10
TimeoutStartSec=4h
[Install]
WantedBy=default.target

View file

@ -1,14 +0,0 @@
[Unit]
Description=Daily GOA max-search amount ladder mon page
Documentation=file:%h/src/taler-monitoring/host-agent/README.md
[Timer]
# Once per day, after classic ladder window
OnCalendar=*-*-* 06:30:00
Persistent=true
AccuracySec=15min
RandomizedDelaySec=30min
Unit=taler-monitoring-max-ladder.service
[Install]
WantedBy=timers.target

View file

@ -8,8 +8,7 @@
# SUITE_GIT_URL default https://git.hacktivism.ch/hernani/taler-monitoring.git # SUITE_GIT_URL default https://git.hacktivism.ch/hernani/taler-monitoring.git
# SUITE_GIT_REF default main # SUITE_GIT_REF default main
# SUITE_DIR default ~/src/taler-monitoring # SUITE_DIR default ~/src/taler-monitoring
# SUITE_UPDATE_MODE reset (default) | pull | keep # SUITE_UPDATE_MODE reset (default) | pull
# keep = no fetch/reset (local deploy / unpushed fixes)
# SUITE_UPDATE_STRICT 1 (default) = fetch/reset failure aborts the agent run # SUITE_UPDATE_STRICT 1 (default) = fetch/reset failure aborts the agent run
# 0 = warn and continue on old tree (emergency only) # 0 = warn and continue on old tree (emergency only)
# #
@ -56,41 +55,29 @@ _before_ver=$(git describe --tags --exact-match 2>/dev/null \
|| git describe --tags --abbrev=0 2>/dev/null \ || git describe --tags --abbrev=0 2>/dev/null \
|| echo unknown) || echo unknown)
case "$SUITE_UPDATE_MODE" in echo "fetch origin ($SUITE_GIT_REF) + tags …"
keep|skip|off|none) if ! git fetch --tags --force --prune origin 2>&1; then
echo "suite update skipped (SUITE_UPDATE_MODE=$SUITE_UPDATE_MODE) — keep local tree ${_before} (${_before_ver})" _upd_die "git fetch failed — suite would stay at ${_before} (${_before_ver})"
;; # fall through only if STRICT=0
*) else
echo "fetch origin ($SUITE_GIT_REF) + tags …" case "$SUITE_UPDATE_MODE" in
if ! git fetch --tags --force --prune origin 2>&1; then reset)
_upd_die "git fetch failed — suite would stay at ${_before} (${_before_ver})" if ! git checkout -B "$SUITE_GIT_REF" "origin/$SUITE_GIT_REF" 2>/dev/null \
# fall through only if STRICT=0 && ! git checkout -B "$SUITE_GIT_REF" FETCH_HEAD; then
else _upd_die "git checkout $SUITE_GIT_REF failed"
case "$SUITE_UPDATE_MODE" in fi
reset) if ! git reset --hard "origin/$SUITE_GIT_REF" 2>/dev/null \
if ! git checkout -B "$SUITE_GIT_REF" "origin/$SUITE_GIT_REF" 2>/dev/null \ && ! git reset --hard FETCH_HEAD; then
&& ! git checkout -B "$SUITE_GIT_REF" FETCH_HEAD; then _upd_die "git reset --hard origin/$SUITE_GIT_REF failed"
_upd_die "git checkout $SUITE_GIT_REF failed" fi
fi ;;
if ! git reset --hard "origin/$SUITE_GIT_REF" 2>/dev/null \ *)
&& ! git reset --hard FETCH_HEAD; then if ! git pull --ff-only origin "$SUITE_GIT_REF" 2>&1; then
_upd_die "git reset --hard origin/$SUITE_GIT_REF failed" _upd_die "git pull --ff-only failed"
fi fi
;; ;;
pull) esac
if ! git pull --ff-only origin "$SUITE_GIT_REF" 2>&1; then fi
_upd_die "git pull --ff-only failed"
fi
;;
*)
if ! git pull --ff-only origin "$SUITE_GIT_REF" 2>&1; then
_upd_die "git pull --ff-only failed (mode=$SUITE_UPDATE_MODE)"
fi
;;
esac
fi
;;
esac
COMMIT=$(git rev-parse HEAD) COMMIT=$(git rev-parse HEAD)
COMMIT_SHORT=$(git rev-parse --short=12 HEAD) COMMIT_SHORT=$(git rev-parse --short=12 HEAD)
@ -142,12 +129,4 @@ fi
ln -sfn "$SUITE_DIR" "$HOME/taler-monitoring" 2>/dev/null || true ln -sfn "$SUITE_DIR" "$HOME/taler-monitoring" 2>/dev/null || true
chmod +x taler-monitoring.sh check_*.sh host-agent/*.sh site-gen/*.sh site-gen/*.py 2>/dev/null || true chmod +x taler-monitoring.sh check_*.sh host-agent/*.sh site-gen/*.sh site-gen/*.py 2>/dev/null || true
# Drop bytecode so a stale site-gen never wins over the tree we just reset to.
rm -rf "$SUITE_DIR/site-gen/__pycache__" 2>/dev/null || true
# Refuse known-bad pre-1.15.8 generator (box-drawing JS regex → SyntaxWarning noise)
if [ -f "$SUITE_DIR/site-gen/console_to_html.py" ] \
&& grep -qE 'if \(/\^\[[╔╗╚╝║═]' "$SUITE_DIR/site-gen/console_to_html.py" 2>/dev/null; then
_upd_die "site-gen/console_to_html.py still has pre-1.15.8 box-drawing JS regex — refuse stale generator"
return 1 2>/dev/null || exit 1
fi
return 0 2>/dev/null || true return 0 2>/dev/null || true

254
i18n.sh
View file

@ -1,202 +1,104 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# i18n.sh — UI language + regional locale for taler-monitoring console + HTML # i18n.sh — language for taler-monitoring console + HTML chrome
# #
# UI language (mon badges / sticky chrome): # TALER_MON_LANG=en|fr (default en)
# TALER_MON_LANG=en|fr|de # Auto: domains *lefrancpaysan* → fr unless TALER_MON_LANG is set explicitly.
# TALER_MON_LANG_SET=1 → explicit (--lang); do not override.
# Auto (SET≠1): TALER_DOMAIN_LANG from domains.conf `lang` column.
# de is supported but never a stock profile default (no domains.conf row uses lang=de).
#
# Regional locale (l10n defaults, BCP 47 — not the same as UI lang):
# TALER_MON_LOCALE / TALER_DOMAIN_LOCALE e.g. fr-CH, de-CH
# From domains.conf `locale` column (fr-CH for FP, de-CH for others).
# Aliases ch-FR / ch-DE accepted and normalized to fr-CH / de-CH.
#
# domains.conf is the source of truth for both lang and locale.
: "${TALER_MON_LANG:=}" : "${TALER_MON_LANG:=}"
: "${TALER_MON_LANG_SET:=0}" : "${TALER_MON_LANG_SET:=0}"
: "${TALER_DOMAIN_LANG:=}"
: "${TALER_DOMAIN_LOCALE:=}"
: "${TALER_MON_LOCALE:=}"
i18n_init() { i18n_init() {
# Only TALER_MON_LANG_SET=1 is "explicit". Do not promote non-empty TALER_MON_LANG if [ -n "${TALER_MON_LANG:-}" ]; then
# to SET=1 — env files often ship LANG=en for the default GOA workstation. TALER_MON_LANG_SET=1
if [ "${TALER_MON_LANG_SET:-0}" != "1" ]; then fi
if [ -n "${TALER_DOMAIN_LANG:-}" ]; then if [ "${TALER_MON_LANG_SET}" != "1" ]; then
TALER_MON_LANG="$TALER_DOMAIN_LANG" case "${TALER_DOMAIN:-}" in
else *lefrancpaysan*|*francpaysan*)
case "${TALER_DOMAIN:-}" in TALER_MON_LANG=fr
*lefrancpaysan*|*francpaysan*) ;;
TALER_MON_LANG=fr *)
;; TALER_MON_LANG=en
*) ;;
if [ -z "${TALER_MON_LANG:-}" ]; then esac
TALER_MON_LANG=en
fi
;;
esac
fi
fi fi
case "${TALER_MON_LANG}" in case "${TALER_MON_LANG}" in
fr|FR|fra|french) TALER_MON_LANG=fr ;; fr|FR|fra|french) TALER_MON_LANG=fr ;;
de|DE|deu|ger|german|deutsch) TALER_MON_LANG=de ;;
*) TALER_MON_LANG=en ;; *) TALER_MON_LANG=en ;;
esac esac
# Regional locale from profile (or FP/other fallback)
if [ -n "${TALER_DOMAIN_LOCALE:-}" ]; then
TALER_MON_LOCALE="$TALER_DOMAIN_LOCALE"
elif [ -z "${TALER_MON_LOCALE:-}" ]; then
case "${TALER_DOMAIN:-}" in
*lefrancpaysan*|*francpaysan*) TALER_MON_LOCALE=fr-CH ;;
*) TALER_MON_LOCALE=de-CH ;;
esac
fi
# normalize aliases
case "$(printf '%s' "$TALER_MON_LOCALE" | tr '[:upper:]' '[:lower:]' | tr '_' '-')" in
fr-ch|ch-fr) TALER_MON_LOCALE=fr-CH ;;
de-ch|ch-de) TALER_MON_LOCALE=de-CH ;;
en-ch|ch-en) TALER_MON_LOCALE=en-CH ;;
esac
export TALER_MON_LANG export TALER_MON_LANG
export TALER_MON_LANG_SET
export TALER_DOMAIN_LANG
export TALER_DOMAIN_LOCALE
export TALER_MON_LOCALE
} }
i18n_tag() { i18n_tag() {
local t="$1" local t="$1"
case "${TALER_MON_LANG:-en}" in if [ "${TALER_MON_LANG:-en}" != "fr" ]; then
fr) printf '%s' "$t"
case "$t" in return 0
OK) printf 'OK' ;; fi
ERROR) printf 'ERREUR' ;; case "$t" in
WARN) printf 'AVERT' ;; OK) printf 'OK' ;;
INFO) printf 'INFO' ;; ERROR) printf 'ERREUR' ;;
BLOCKER) printf 'BLOCAGE' ;; WARN) printf 'AVERT' ;;
BLOCK) printf 'BLOC' ;; INFO) printf 'INFO' ;;
SUMMARY) printf 'RESUME' ;; BLOCKER) printf 'BLOCAGE' ;;
PROG|PROGRESS) printf 'PROG' ;; BLOCK) printf 'BLOC' ;;
*) printf '%s' "$t" ;; SUMMARY) printf 'RESUME' ;;
esac PROG|PROGRESS) printf 'PROG' ;;
;; *) printf '%s' "$t" ;;
de)
case "$t" in
OK) printf 'OK' ;;
ERROR) printf 'FEHLER' ;;
WARN) printf 'WARNUNG' ;;
INFO) printf 'INFO' ;;
BLOCKER) printf 'BLOCKER' ;;
BLOCK) printf 'BLOCK' ;;
SUMMARY) printf 'ZUSAMMENFASSUNG' ;;
PROG|PROGRESS) printf 'PROG' ;;
*) printf '%s' "$t" ;;
esac
;;
*)
printf '%s' "$t"
;;
esac esac
} }
i18n_text() { i18n_text() {
local s="$*" local s="$*"
[ -z "$s" ] && { printf '%s' "$s"; return 0; } if [ "${TALER_MON_LANG:-en}" != "fr" ] || [ -z "$s" ]; then
case "${TALER_MON_LANG:-en}" in printf '%s' "$s"
fr) return 0
case "$s" in fi
"numbered checks this run:") printf 'controles numerotes de cette execution :' ;; case "$s" in
"numbered checks:") printf 'controles numerotes :' ;; "numbered checks this run:") printf 'controles numerotes de cette execution :' ;;
"(global done/total)") printf '(fait/total global)' ;; "numbered checks:") printf 'controles numerotes :' ;;
"global checks:") printf 'controles globaux :' ;; "(global done/total)") printf '(fait/total global)' ;;
"failed — see list above"|"failed - see list above") printf 'echec — voir la liste ci-dessus' ;; "global checks:") printf 'controles globaux :' ;;
"BLOCKERS · pay/withdraw cannot finish") printf 'BLOCAGES · paiement/retrait impossible' ;; "failed — see list above"|"failed - see list above") printf 'echec — voir la liste ci-dessus' ;;
"BLOCKERS (pay/withdraw cannot finish)") printf 'BLOCAGES (paiement/retrait impossible)' ;; "BLOCKERS · pay/withdraw cannot finish") printf 'BLOCAGES · paiement/retrait impossible' ;;
"ERRORS · failed checks") printf 'ERREURS · controles en echec' ;; "BLOCKERS (pay/withdraw cannot finish)") printf 'BLOCAGES (paiement/retrait impossible)' ;;
"ERRORS (failed checks)") printf 'ERREURS (controles en echec)' ;; "ERRORS · failed checks") printf 'ERREURS · controles en echec' ;;
"pay/withdraw cannot finish") printf 'paiement/retrait impossible a terminer' ;; "ERRORS (failed checks)") printf 'ERREURS (controles en echec)' ;;
"SUMMARY") printf 'RESUME' ;; "pay/withdraw cannot finish") printf 'paiement/retrait impossible a terminer' ;;
"totals:") printf 'totaux :' ;; "SUMMARY") printf 'RESUME' ;;
"all clear") printf 'tout est clair' ;; "totals:") printf 'totaux :' ;;
"warnings only (no hard fail)") printf 'avertissements seulement (pas d'\''echec dur)' ;; "container") printf 'conteneur' ;;
"monitoring") printf 'surveillance' ;; "disk") printf 'disque' ;;
"surface") printf 'surface' ;; "package") printf 'paquet' ;;
"aptdeploy") printf 'aptdeploy' ;; "version") printf 'version' ;;
"container") printf 'conteneur' ;; "reachability") printf 'accessibilite' ;;
"disk") printf 'disque' ;; "server-header") printf 'en-tete Server' ;;
"package") printf 'paquet' ;; "inventory") printf 'inventaire' ;;
"version") printf 'version' ;; "flags") printf 'options' ;;
"reachability") printf 'accessibilite' ;; "target domain") printf 'domaine cible' ;;
"server-header") printf 'en-tete Server' ;; "currency") printf 'devise' ;;
"inventory") printf 'inventaire' ;; "phases") printf 'phases' ;;
"flags") printf 'options' ;; "access") printf 'acces' ;;
"target domain") printf 'domaine cible' ;;
"currency") printf 'devise' ;;
"phases") printf 'phases' ;;
"access") printf 'acces' ;;
*)
local out="$s"
out=${out//shared library missing/bibliotheque partagee manquante}
out=${out//not active/non actif}
out=${out//does not resolve/ne resout pas}
out=${out//from Server header/depuis en-tete Server}
out=${out//public HTTPS/HTTPS public}
out=${out//no SSH/sans SSH}
out=${out//failed — see list above/echec — voir la liste ci-dessus}
printf '%s' "$out"
;;
esac
;;
de)
case "$s" in
"numbered checks this run:") printf 'Nummerierte Pruefungen dieser Ausfuehrung :' ;;
"numbered checks:") printf 'Nummerierte Pruefungen :' ;;
"(global done/total)") printf '(fertig/gesamt global)' ;;
"global checks:") printf 'Globale Pruefungen :' ;;
"failed — see list above"|"failed - see list above") printf 'fehlgeschlagen — siehe Liste oben' ;;
"BLOCKERS · pay/withdraw cannot finish") printf 'BLOCKER · Zahlung/Abhebung nicht moeglich' ;;
"BLOCKERS (pay/withdraw cannot finish)") printf 'BLOCKER (Zahlung/Abhebung nicht moeglich)' ;;
"ERRORS · failed checks") printf 'FEHLER · fehlgeschlagene Pruefungen' ;;
"ERRORS (failed checks)") printf 'FEHLER (fehlgeschlagene Pruefungen)' ;;
"pay/withdraw cannot finish") printf 'Zahlung/Abhebung kann nicht beendet werden' ;;
"SUMMARY") printf 'ZUSAMMENFASSUNG' ;;
"totals:") printf 'Summen :' ;;
"all clear") printf 'alles in Ordnung' ;;
"warnings only (no hard fail)") printf 'nur Warnungen (kein harter Fehler)' ;;
"monitoring") printf 'Ueberwachung' ;;
"surface") printf 'Surface' ;;
"aptdeploy") printf 'aptdeploy' ;;
"container") printf 'Container' ;;
"disk") printf 'Disk' ;;
"package") printf 'Paket' ;;
"version") printf 'Version' ;;
"reachability") printf 'Erreichbarkeit' ;;
"server-header") printf 'Server-Header' ;;
"inventory") printf 'Inventar' ;;
"flags") printf 'Flags' ;;
"target domain") printf 'Zieldomaene' ;;
"currency") printf 'Waehrung' ;;
"phases") printf 'Phasen' ;;
"access") printf 'Zugang' ;;
*)
local outd="$s"
outd=${outd//shared library missing/gemeinsame Bibliothek fehlt}
outd=${outd//not active/nicht aktiv}
outd=${outd//does not resolve/loest nicht auf}
outd=${outd//from Server header/aus Server-Header}
outd=${outd//public HTTPS/oeffentliches HTTPS}
outd=${outd//no SSH/ohne SSH}
outd=${outd//failed — see list above/fehlgeschlagen — siehe Liste oben}
printf '%s' "$outd"
;;
esac
;;
*) *)
printf '%s' "$s" local out="$s"
out=${out//shared library missing/bibliotheque partagee manquante}
out=${out//shared library error/erreur de bibliotheque partagee}
out=${out//not active/non actif}
out=${out//after_start/apres_demarrage}
out=${out//catalogued but not reachable/catalogue mais inaccessible}
out=${out//does not resolve/ne resout pas}
out=${out//protocol confirmed/protocole confirme}
out=${out//catalogued service OK/service catalogue OK}
out=${out//from Server header/depuis en-tete Server}
out=${out//OSV clean/OSV sans vulnerabilite}
out=${out//actionable vuln/vuln. actionnable}
out=${out//header version · not Debian pkg/version en-tete · pas paquet Debian}
out=${out//RUN_TIMEOUT exceeded/RUN_TIMEOUT depasse}
out=${out//public HTTPS/HTTPS public}
out=${out//container status/etat du conteneur}
out=${out//remote-only/uniquement distant}
out=${out//no SSH/sans SSH}
out=${out//failed — see list above/echec — voir la liste ci-dessus}
printf '%s' "$out"
;; ;;
esac esac
} }

View file

@ -1,32 +0,0 @@
# Amount ladder modules
**Entry:** `../check_amount_ladder.sh` via:
```bash
./taler-monitoring.sh ladder # classic 23-rung withdraw + pay
./taler-monitoring.sh max-ladder # max-search withdraw + pay
./taler-monitoring.sh goa-ladder # legacy phase name = classic ladder
```
| Path | Role |
|------|------|
| `../check_amount_ladder.sh` | Orchestrator: classic / max **withdraw**, then pay phase |
| `lib_pay.sh` | **Pay** module: free-amount template, classic pay list, max **payable** ≤ available |
| `extract_rpubs.py` | Force-select: scrape `reservePub` from wallet dumps |
There is **no** `check_goa_ladder.sh` — removed; use the phases above.
## Modes
- **classic** (default): 23-rung withdraw 0→mids→max-1→max, then matching pay plan (never overspend).
- **max** (`max-ladder`): adaptive max withdraw, then adaptive max pay via public free template (hacktivism: `goa-free`).
## Env (pay)
| Variable | Default | Meaning |
|----------|---------|---------|
| `LADDER_PAY` | `1` | `0` = skip all pays |
| `LADDER_USE_FREE_TEMPLATE` | `1` | public POST template with `{"amount":…}` |
| `LADDER_FREE_TEMPLATE` | `goa-free` | template id (hacktivism) |
| `LADDER_FREE_TEMPLATE_INSTANCE` | merchant instance | e.g. `goa-demo-cp4zqk` |
| `LADDER_PAY_WAIT_AVAILABLE_S` | `90` | after withdraw, wait for spendable **available**>0; logs **withdraw board in start order** (OK/OK_BANK) with live `pendingIncoming``done` + timer (same wallet DB) |

View file

@ -1,163 +0,0 @@
#!/usr/bin/env python3
"""Extract reserve_pub candidates from wallet accept + transactions dumps.
Used by check_amount_ladder.sh force-select. Kept as a file (not a bash
heredoc) so shell refactors cannot turn Python ``continue`` into ``return``.
Usage:
ladder_extract_rpubs.py WID AMT accept.out tx.json used-rpubs.txt
Prints one reserve pub per line, preferred first.
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
def main(argv: list[str]) -> int:
if len(argv) < 6:
print(
"usage: ladder_extract_rpubs.py WID AMT accept.out tx.json used-rpubs.txt",
file=sys.stderr,
)
return 2
wid = argv[1]
amt = argv[2]
paths = argv[3:5]
used_path = argv[5]
used: set[str] = set()
if Path(used_path).is_file():
used = {ln.strip() for ln in open(used_path) if ln.strip()}
amt_num = amt.split(":", 1)[-1] if amt else ""
dec = json.JSONDecoder()
def walk_collect(o, bag, ctx=None):
ctx = dict(ctx or {})
if isinstance(o, dict):
for k, v in o.items():
if not isinstance(v, str):
continue
kl = str(k).lower().replace("-", "_")
if kl in (
"withdrawal_id",
"withdraw_id",
"wopid",
"id",
"transactionid",
"transaction_id",
):
ctx["id"] = v
elif kl in (
"amount",
"rawamount",
"instructedamount",
"amounteffective",
"amountraw",
"transferamount",
):
ctx["amount"] = v
elif kl in (
"taler_withdraw_uri",
"talerwithdrawuri",
"uri",
"bankconfirmationurl",
"confirmtransferurl",
):
ctx["uri"] = v
for k, v in o.items():
kl = str(k).lower().replace("-", "_")
if kl in ("reserve_pub", "reservepub") and isinstance(v, str) and len(v) >= 40:
bag.append((v, dict(ctx)))
elif not isinstance(v, (str, int, float, bool)) and v is not None:
walk_collect(v, bag, ctx)
elif isinstance(o, list):
for i in o:
walk_collect(i, bag, ctx)
def score_ctx(pub, ctx, window=""):
score = 0
cid = str(ctx.get("id") or "")
camt = str(ctx.get("amount") or "")
curi = str(ctx.get("uri") or "")
if wid and wid in curi:
score += 120
if wid and wid in cid:
score += 100
if wid and wid in window:
score += 80
if amt and (camt == amt or camt.endswith(amt_num)):
score += 50
if amt and amt in window:
score += 30
if pub in used:
score -= 200
return score
def iter_json_objects(text: str):
i, n = 0, len(text)
while i < n:
if text[i] == "{":
try:
o, end = dec.raw_decode(text, i)
yield o
i = end
continue
except Exception:
pass
i += 1
raw_pubs: list[str] = []
scored: list[tuple[int, str]] = []
for p in paths:
try:
t = open(p, errors="replace").read()
except Exception:
continue
for o in iter_json_objects(t):
bag = []
walk_collect(o, bag)
for pub, ctx in bag:
raw_pubs.append(pub)
scored.append((score_ctx(pub, ctx), pub))
rpub_pats = (
r"\"reservePub\"\s*:\s*\"([A-Z0-9]{40,})\"",
r"\"reserve_pub\"\s*:\s*\"([A-Z0-9]{40,})\"",
r"reserve[_ ]?pub[\"\s:=]+([A-Z0-9]{40,})",
)
for p in paths:
try:
blob = open(p, errors="replace").read()
except Exception:
continue
for pat in rpub_pats:
for m in re.finditer(pat, blob, re.I):
pub = m.group(1)
raw_pubs.append(pub)
window = blob[max(0, m.start() - 600) : m.end() + 600]
scored.append((score_ctx(pub, {}, window) + 10, pub))
order: list[str] = []
seen: set[str] = set()
for score, pub in sorted(scored, key=lambda x: x[0], reverse=True):
if pub in seen or pub in used:
continue
seen.add(pub)
order.append(pub)
for pub in reversed(raw_pubs):
if pub in seen or pub in used:
continue
seen.add(pub)
order.append(pub)
for pub in order:
print(pub)
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))

View file

@ -1,559 +0,0 @@
# ladder/lib_pay.sh — payment side of amount ladder (sourced by check_amount_ladder.sh)
# Expects: lib.sh + metrics.sh already sourced; CUR BANK MER INST EX SCRATCH WDB
# wcli wallet_avail format_amount_alt ladder_over now_ms elapsed_ms
# LADDER_* env, AUTH optional, PAY_TSV PAY_OK_N PAY_FAIL_N
#
# Public free-amount template (hacktivism): goa-free @ goa-demo-cp4zqk
# Never spends more than wallet available.
: "${LADDER_FREE_TEMPLATE:=goa-free}"
: "${LADDER_FREE_TEMPLATE_INSTANCE:=}"
: "${LADDER_USE_FREE_TEMPLATE:=1}"
: "${LADDER_PAY_WAIT_AVAILABLE_S:=180}"
: "${LADDER_PAY_SETTLE_ROUNDS:=6}"
MAX_PAY_BEST_AMT="${MAX_PAY_BEST_AMT:-}"
MAX_PAY_LO="${MAX_PAY_LO:-0}"
MAX_PAY_HI="${MAX_PAY_HI:-0}"
ladder_pay_resolve_merchant() {
# Sets AUTH (may be empty if free template only)
MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}"
if [ -z "$MPW" ]; then
MPW=$(read_secret "taler-merchant/merchant-${INST}-password.txt" 2>/dev/null || true)
fi
if [ -z "$MPW" ]; then
MPW=$(read_secret "taler-merchant/merchant-goa-demo-cp4zqk-password.txt" 2>/dev/null || true)
fi
if [ -z "$MPW" ] && [ "${CUR}" = "TESTPAYSAN" ]; then
if [ -n "${FRANCPAYSAN_SECRETS:-}" ] && [ -f "${FRANCPAYSAN_SECRETS}/stage/default-instance-token.txt" ]; then
MPW=$(tr -d '\n\r' <"${FRANCPAYSAN_SECRETS}/stage/default-instance-token.txt")
fi
fi
AUTH=""
if [ -n "$MPW" ]; then
case "$MPW" in
secret-token:*) AUTH="Authorization: Bearer ${MPW}" ;;
*) AUTH="Authorization: Bearer secret-token:${MPW}" ;;
esac
fi
: "${LADDER_FREE_TEMPLATE_INSTANCE:=${INST}}"
}
# Create order for PAMT → OID OTOK PAYURI. Prefers public free template.
ladder_create_order() {
local pamt="$1" ptag="$2"
local inst mh body tpl_https sum_json
OID=""; OTOK=""; PAYURI=""
inst="${LADDER_FREE_TEMPLATE_INSTANCE:-$INST}"
mh=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"${MER}"'").hostname or "taler.hacktivism.ch")' 2>/dev/null || echo "taler.hacktivism.ch")
if [ "${LADDER_USE_FREE_TEMPLATE}" = "1" ] && [ -n "${LADDER_FREE_TEMPLATE:-}" ]; then
tpl_https="${MER}/instances/${inst}/templates/${LADDER_FREE_TEMPLATE}"
body=$(python3 -c 'import json,sys; print(json.dumps({"amount":sys.argv[1]}))' "$pamt" 2>/dev/null || printf '{"amount":"%s"}' "$pamt")
curl -skS -m 20 -o "$SCRATCH/ord-$ptag.json" -X POST \
-H 'Content-Type: application/json' \
-d "$body" \
"$tpl_https" 2>"$SCRATCH/ord-$ptag.err" || true
OID=$(python3 -c 'import json,re,sys;t=open(sys.argv[1]).read()
try: print(json.loads(t).get("order_id") or "")
except Exception:
m=re.search(r"\"order_id\"\s*:\s*\"([^\"]+)\"",t); print(m.group(1) if m else "")
' "$SCRATCH/ord-$ptag.json" 2>/dev/null || true)
OTOK=$(python3 -c 'import json,re,sys;t=open(sys.argv[1]).read()
try: print(json.loads(t).get("token") or "")
except Exception:
m=re.search(r"\"token\"\s*:\s*\"([^\"]+)\"",t); print(m.group(1) if m else "")
' "$SCRATCH/ord-$ptag.json" 2>/dev/null || true)
if [ -n "$OID" ] && [ -n "$OTOK" ]; then
PAYURI="taler://pay/${mh}/instances/${inst}/${OID}/?c=${OTOK}"
PAYURI=$(normalize_taler_uri "$PAYURI")
return 0
fi
fi
[ -n "${AUTH:-}" ] || return 1
sum_json=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "ladder pay ${pamt}" 2>/dev/null || echo '"ladder pay"')
curl -skS -m 20 -o "$SCRATCH/ord-$ptag.json" -X POST \
-H "$AUTH" -H 'Content-Type: application/json' \
-d "{\"order\":{\"summary\":${sum_json},\"amount\":\"${pamt}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}" \
"${MER}/instances/${INST}/private/orders" 2>"$SCRATCH/ord-$ptag.err" || true
OID=$(python3 -c 'import json,re,sys;t=open(sys.argv[1]).read()
try: print(json.loads(t).get("order_id") or "")
except Exception:
m=re.search(r"\"order_id\"\s*:\s*\"([^\"]+)\"",t); print(m.group(1) if m else "")
' "$SCRATCH/ord-$ptag.json" 2>/dev/null || true)
OTOK=$(python3 -c 'import json,re,sys;t=open(sys.argv[1]).read()
try: print(json.loads(t).get("token") or "")
except Exception:
m=re.search(r"\"token\"\s*:\s*\"([^\"]+)\"",t); print(m.group(1) if m else "")
' "$SCRATCH/ord-$ptag.json" 2>/dev/null || true)
[ -n "$OID" ] || return 1
curl -skS -m 12 -o "$SCRATCH/ord-det-$ptag.json" -H "$AUTH" \
"${MER}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
PAYURI=$(python3 -c 'import json,sys
try: print(json.load(open(sys.argv[1])).get("taler_pay_uri") or "")
except Exception: print("")
' "$SCRATCH/ord-det-$ptag.json" 2>/dev/null || true)
if [ -z "$PAYURI" ] && [ -n "$OTOK" ]; then
PAYURI="taler://pay/${mh}/instances/${INST}/${OID}/?c=${OTOK}"
fi
PAYURI=$(normalize_taler_uri "$PAYURI")
[ -n "$PAYURI" ]
}
# One pay attempt; never overspends. Always soft (no hard ladder stop).
ladder_pay_attempt() {
local pamt="$1" prange="$2" prung="$3"
local ptag pnum bal t_pay t0 ms_order ms_handle ms_psettle after settled r pstatus pnote
ptag=$(printf '%s' "$pamt" | tr '.:' '__')
pnum=$(python3 -c 'import sys; print(sys.argv[1].split(":",1)[-1])' "$pamt")
t_pay=$(now_ms)
ms_order=0; ms_handle=0; ms_psettle=0
pnote=""; pstatus="FAIL"; OID="-"
bal=$(wallet_avail)
if ! python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])>0 and Decimal(sys.argv[1])>=Decimal(sys.argv[2]) else 1)' "$bal" "$pnum" 2>/dev/null; then
pstatus="SKIP_BALANCE"
pnote="skip: would spend more than available avail=${CUR}:${bal} need=${pamt}"
warn pay "pay $pamt skipped (balance)" \
"problem: never overspend. $(format_amount_alt "${CUR}:${bal}") free vs $(format_amount_alt "$pamt")"
echo -e "${prung}\t${prange}\t${pamt}\t${pstatus}\t0\t0\t0\t$(elapsed_ms "$t_pay")\t-\t${pnote}" >>"$PAY_TSV"
return 0
fi
t0=$(now_ms)
if ! ladder_create_order "$pamt" "$ptag"; then
ms_order=$(elapsed_ms "$t0")
pstatus="FAIL_ORDER"
pnote="order create failed $(head -c 100 "$SCRATCH/ord-$ptag.json" 2>/dev/null | tr '\n\"' ' ')"
warn pay "order $pamt" "$pnote"
echo -e "${prung}\t${prange}\t${pamt}\t${pstatus}\t${ms_order}\t0\t0\t$(elapsed_ms "$t_pay")\t-\t${pnote}" >>"$PAY_TSV"
return 0
fi
ms_order=$(elapsed_ms "$t0")
ok "order $OID ($pamt) ${ms_order}ms · $(format_amount_alt "$pamt")"
t0=$(now_ms)
if ! wcli handle-uri --yes "$PAYURI" >"$SCRATCH/pay-$ptag.out" 2>&1; then
ms_handle=$(elapsed_ms "$t0")
pstatus="FAIL_HANDLE"
pnote="handle-uri failed $(tail -c 100 "$SCRATCH/pay-$ptag.out" | tr '\n\"' ' ')"
warn pay "handle $pamt" "$pnote"
echo -e "${prung}\t${prange}\t${pamt}\t${pstatus}\t${ms_order}\t${ms_handle}\t0\t$(elapsed_ms "$t_pay")\t${OID}\t${pnote}" >>"$PAY_TSV"
return 0
fi
ms_handle=$(elapsed_ms "$t0")
ok "handle-uri $pamt ${ms_handle}ms"
t0=$(now_ms)
settled=0
r=0
while [ "$r" -lt "${LADDER_PAY_SETTLE_ROUNDS}" ]; do
r=$((r + 1))
wcli transactions >"$SCRATCH/tx-pay-$ptag.out" 2>&1 || true
if [ -n "${AUTH:-}" ]; then
curl -skS -m 8 -o "$SCRATCH/ord-paid-$ptag.json" -H "$AUTH" \
"${MER}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
fi
if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-pay-$ptag.out" 2>/dev/null \
|| python3 -c 'import json,sys
try:
d=json.load(open(sys.argv[1]))
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1)
except Exception:
sys.exit(1)
' "$SCRATCH/ord-paid-$ptag.json" 2>/dev/null; then
settled=1
break
fi
sleep 0.5
done
ms_psettle=$(elapsed_ms "$t0")
after=$(wallet_avail)
if [ "$settled" = "1" ]; then
pstatus="OK"
pnote="avail=${CUR}:${after}"
ok "pay settled $pamt · $(format_amount_alt "$pamt") → bal ${CUR}:${after}"
PAY_OK_N=$((PAY_OK_N + 1))
echo -e "${prung}\t${prange}\t${pamt}\t${pstatus}\t${ms_order}\t${ms_handle}\t${ms_psettle}\t$(elapsed_ms "$t_pay")\t${OID}\t${pnote}" >>"$PAY_TSV"
metrics_report_coins "after-pay-${ptag}" || true
metrics_record_flow spent "$pamt" || true
else
pstatus="FAIL_SETTLE"
pnote="not settled order=$OID avail=${CUR}:${after}"
warn pay "settle $pamt" "$pnote"
echo -e "${prung}\t${prange}\t${pamt}\t${pstatus}\t${ms_order}\t${ms_handle}\t${ms_psettle}\t$(elapsed_ms "$t_pay")\t${OID}\t${pnote}" >>"$PAY_TSV"
fi
return 0
}
# Print withdraw board: OK/OK_BANK rungs in *start order* + wallet tx state.
# Uses TSV (ladder order) + transactions dump. Call after refreshing tx dump.
# Args: left_secs available_num pendingIncoming_num
ladder_print_wd_board() {
local left="${1:-?}" bal="${2:-0}" pend="${3:-0}"
local tsv="${TSV:-}" txf="${SCRATCH:-/tmp}/tx-wait-pay.out"
local board
board=$(
python3 - "$tsv" "$txf" "${CUR:-GOA}" "$left" "$bal" "$pend" <<'PY' 2>/dev/null || true
import json, sys
from pathlib import Path
tsv, txf, cur, left, bal, pend = sys.argv[1:7]
dec = json.JSONDecoder()
def num(a: str) -> str:
a = (a or "").strip()
if ":" in a:
return a.split(":", 1)[1]
return a
def iter_json(text: str):
i, n = 0, len(text)
while i < n:
if text[i] == "{":
try:
o, end = dec.raw_decode(text, i)
yield o
i = end
continue
except Exception:
pass
i += 1
# TSV rows in start order (OK / OK_BANK only — successful bank path)
rows = []
if tsv and Path(tsv).is_file():
for ln in open(tsv, errors="replace"):
if not ln.strip() or ln.startswith("rung"):
continue
p = ln.rstrip("\n").split("\t")
if len(p) < 10:
continue
status = p[3]
if status not in ("OK", "OK_BANK"):
continue
rows.append({
"rung": p[0],
"amt": p[2],
"bank": status,
"wid": (p[9] if len(p) > 9 else "")[:12],
})
# Wallet withdrawal txs (chronological if timestamp present)
wds = []
if txf and Path(txf).is_file():
try:
text = open(txf, errors="replace").read()
except Exception:
text = ""
for o in iter_json(text):
txs = o.get("transactions") if isinstance(o, dict) else None
if not isinstance(txs, list):
continue
for t in txs:
if not isinstance(t, dict):
continue
typ = str(t.get("type") or t.get("Type") or "").lower()
if "withdraw" not in typ:
continue
amt = t.get("amountRaw") or t.get("amountEffective") or t.get("amount") or ""
st = (
t.get("txState")
or t.get("status")
or t.get("pendingMajor")
or t.get("txMajorState")
or ""
)
st = str(st).lower()
det = t.get("withdrawalDetails") or t.get("withdrawal_details") or {}
if not isinstance(det, dict):
det = {}
conf = det.get("confirmed")
if conf is True and not st:
st = "done"
elif conf is False and not st:
st = "pending"
# pending* / dialog / kyc → still incoming; done/abort final
if any(x in st for x in ("done", "abort", "fail", "delete")):
phase = "done" if "done" in st else st
elif st in ("", "none", "null"):
phase = "unknown"
else:
phase = "pendingIncoming"
ts = 0
for k in ("timestamp", "timestamp_ms", "age"):
v = t.get(k)
if isinstance(v, dict):
v = v.get("t_s") or v.get("t_ms") or v.get("t_usec")
try:
ts = int(v or 0)
except Exception:
ts = 0
if ts:
break
wds.append({"amt": str(amt), "n": num(str(amt)), "phase": phase, "st": st or "?", "ts": ts})
wds.sort(key=lambda x: x["ts"])
used = [False] * len(wds)
lines = [
f"withdraw board (start order) · timer {left}s left · available={cur}:{bal} · pendingIncoming={cur}:{pend}"
]
if not rows:
lines.append(" (no OK/OK_BANK rows in TSV yet)")
else:
for i, r in enumerate(rows, 1):
n = num(r["amt"])
match = None
for j, w in enumerate(wds):
if used[j]:
continue
if w["n"] == n or w["amt"] == r["amt"] or w["amt"].endswith(":" + n):
used[j] = True
match = w
break
if match is None:
wlabel = "wallet=? (no tx match)"
elif match["phase"] == "pendingIncoming":
wlabel = f"wallet=pendingIncoming ({match['st']})"
elif match["phase"] == "done":
wlabel = f"wallet=done ({match['st']})"
else:
wlabel = f"wallet={match['phase']} ({match['st']})"
wid = r["wid"] or "-"
lines.append(
f" #{i:<3} rung={r['rung']:<4} {r['amt']:<22} bank={r['bank']:<8} {wlabel} wid={wid}"
)
pending_n = sum(1 for w in wds if w["phase"] == "pendingIncoming")
done_n = sum(1 for w in wds if w["phase"] == "done")
lines.append(
f" summary: tsv_ok={len(rows)} · wallet_wd={len(wds)} (pendingIncoming={pending_n} done={done_n})"
)
print("\n".join(lines))
PY
)
if [ -n "$board" ]; then
while IFS= read -r line || [ -n "$line" ]; do
[ -n "$line" ] && info pay "$line"
done <<<"$board"
else
info pay "timer ${left}s left · available=${CUR}:${bal} · pendingIncoming=${CUR}:${pend} · same wallet (board n/a)"
fi
}
# Wait for spendable coins on the *same* wallet DB as withdraw (WDB).
# After OK_BANK, available is often still 0 while pendingIncoming > 0 — pay needs available.
# Shows withdraw board in start order + countdown (never looks hung).
ladder_wait_available() {
local wait_s="${1:-$LADDER_PAY_WAIT_AVAILABLE_S}"
local end now left bal pend tick=0
end=$(( $(date +%s) + wait_s ))
bal=$(wallet_avail)
pend=$(wallet_pending_in 2>/dev/null || echo "0")
if python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])>0 else 1)' "$bal" 2>/dev/null; then
printf '%s' "$bal"
return 0
fi
info pay "same wallet as withdraw (db=$(basename "${WDB:-wallet}")) · available=${CUR}:${bal} · pendingIncoming=${CUR}:${pend}"
info pay "waiting up to ${wait_s}s for spendable available>0 — board lists OK/OK_BANK withdraws in start order"
# initial board before loop
wcli transactions >"$SCRATCH/tx-wait-pay.out" 2>&1 || true
ladder_print_wd_board "$wait_s" "$bal" "$pend"
while python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])<=0 else 1)' "$bal" 2>/dev/null; do
now=$(date +%s)
left=$(( end - now ))
if [ "$left" -le 0 ]; then
wcli transactions >"$SCRATCH/tx-wait-pay.out" 2>&1 || true
ladder_print_wd_board "0" "$bal" "$pend"
info pay "timer 0s left — stop waiting (available still ${CUR}:0)"
break
fi
# nudge wallet (same DB); do not run-until-done (can hang)
wcli transactions >"$SCRATCH/tx-wait-pay.out" 2>&1 || true
if command -v timeout >/dev/null 2>&1 || command -v gtimeout >/dev/null 2>&1; then
local _to
_to=$(command -v gtimeout 2>/dev/null || command -v timeout)
"$_to" 8 wcli advanced run-pending >"$SCRATCH/run-pending.out" 2>&1 || true
fi
# progress board every ~5s (tick every 2s → every 3rd)
if [ $((tick % 3)) -eq 0 ]; then
ladder_print_wd_board "$left" "$bal" "$pend"
fi
sleep 2
tick=$((tick + 1))
bal=$(wallet_avail)
pend=$(wallet_pending_in 2>/dev/null || echo "0")
done
if python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])>0 else 1)' "$bal" 2>/dev/null; then
wcli transactions >"$SCRATCH/tx-wait-pay.out" 2>&1 || true
ladder_print_wd_board "ready" "$bal" "$pend"
ok pay "spendable coins ready available=${CUR}:${bal} · $(format_amount_alt "${CUR}:${bal}") (same wallet)"
else
warn pay "no spendable coins after ${wait_s}s" \
"problem: available still ${CUR}:0 (pendingIncoming=${CUR}:${pend}). Board above shows which withdraws still pendingIncoming. Pay will SKIP_BALANCE / skip max-pay until wirewatch+wallet finish. Same wallet as withdraw."
fi
printf '%s' "$bal"
}
# Classic planned pays (PAY_LIST)
ladder_run_pay_classic() {
# shellcheck disable=SC2086
set -- $PAY_LIST
local pay_n=$# prung=0 PAMT prange PNUM
if [ "$pay_n" -eq 0 ]; then
info pay "empty pay plan — skip"
return 0
fi
info "pay rungs" "$pay_n · $*"
for PAMT in "$@"; do
prung=$((prung + 1))
ladder_over && break
if [ "$prung" -eq 1 ]; then
prange="pin:0"
elif [ "$prung" -eq "$pay_n" ]; then
prange="pin:max"
elif [ "$prung" -eq $((pay_n - 1)) ] && [ "$pay_n" -ge 3 ]; then
prange="pin:max-1"
else
prange="random"
fi
section "ladder · pay rung $prung $PAMT · $(format_amount_alt "$PAMT")"
PNUM=$(python3 -c 'import sys; print(sys.argv[1].split(":",1)[-1])' "$PAMT")
if python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])==0 else 1)' "$PNUM" 2>/dev/null; then
echo -e "${prung}\t${prange}\t${PAMT}\tZERO_SKIP\t0\t0\t0\t0\t-\tzero" >>"$PAY_TSV"
PAY_OK_N=$((PAY_OK_N + 1))
continue
fi
ladder_pay_attempt "$PAMT" "$prange" "$prung"
done
}
# Max-search: highest payable ≤ available (uses max_search_next from withdraw module)
ladder_run_pay_max_search() {
local bal prung _next _anum prange _act _last _slo _shi
section "ladder · phase B · max-search pay (highest payable ≤ available)"
bal=$(ladder_wait_available "$LADDER_PAY_WAIT_AVAILABLE_S")
MAX_PAY_LO="0"
MAX_PAY_HI="$bal"
MAX_PAY_BEST_AMT=""
if ! python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])>0 else 1)' "$MAX_PAY_HI" 2>/dev/null; then
warn pay "max-search pay skipped" \
"problem: wallet available is ${CUR}:0 — need spendable coins (pendingIncoming is not enough)"
return 0
fi
info "max-search pay" "hi=available $(format_amount_alt "${CUR}:${MAX_PAY_HI}") · template=${LADDER_FREE_TEMPLATE}@${LADDER_FREE_TEMPLATE_INSTANCE}"
info "pay budget" "never order more than $(format_amount_alt "${CUR}:${MAX_PAY_HI}")"
prung=0
_last=""
while [ "${LADDER_MAX_PROBES}" = "0" ] || [ "$prung" -lt "${LADDER_MAX_PROBES}" ]; do
ladder_over && break
# reuse withdraw amount picker with pay bounds
_slo=${MAX_LO:-0}; _shi=${MAX_HI:-0}
MAX_LO=$MAX_PAY_LO
MAX_HI=$MAX_PAY_HI
if ! type max_search_next >/dev/null 2>&1; then
warn pay "max_search_next missing — cannot run pay max-search"
MAX_LO=$_slo; MAX_HI=$_shi
return 0
fi
_next=$(max_search_next)
MAX_LO=$_slo; MAX_HI=$_shi
_anum=$(printf '%s' "$_next" | cut -d'|' -f1)
prange=$(printf '%s' "$_next" | cut -d'|' -f2 | sed 's/^max:/pay:/')
_act=$(printf '%s' "$_next" | cut -d'|' -f3)
if [ "$_act" = "done" ]; then
info "max-search pay" "converged best=$(format_amount_alt "${MAX_PAY_BEST_AMT:-${CUR}:0}") · lo=$(format_amount_alt "${CUR}:${MAX_PAY_LO}") · hi=$(format_amount_alt "${CUR}:${MAX_PAY_HI}")"
break
fi
if ! python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])<=Decimal(sys.argv[2]) else 1)' "$_anum" "$MAX_PAY_HI" 2>/dev/null; then
_anum=$MAX_PAY_HI
fi
if ! python3 -c 'import sys; from decimal import Decimal; sys.exit(0 if Decimal(sys.argv[1])>0 else 1)' "$_anum" 2>/dev/null; then
break
fi
PAMT="${CUR}:${_anum}"
if [ -n "$_last" ] && [ "$PAMT" = "$_last" ]; then
MAX_PAY_HI=$(python3 -c 'from decimal import Decimal; a=Decimal("'"$_anum"'"); print(max(Decimal(0), a-(1 if a>=1 else Decimal("0.000001"))))')
continue
fi
_last=$PAMT
prung=$((prung + 1))
section "ladder · max pay probe $prung $PAMT · $(format_amount_alt "$PAMT") [lo=$(format_amount_alt "${CUR}:${MAX_PAY_LO}") · hi=$(format_amount_alt "${CUR}:${MAX_PAY_HI}")]"
ladder_pay_attempt "$PAMT" "$prange" "$prung"
_last_st=$(tail -1 "$PAY_TSV" 2>/dev/null | cut -f4)
case "$_last_st" in
OK)
MAX_PAY_LO=$_anum
MAX_PAY_BEST_AMT=$PAMT
info "max-search pay" "success bound lo=$(format_amount_alt "${CUR}:${MAX_PAY_LO}")"
;;
SKIP_BALANCE|FAIL_ORDER|FAIL_HANDLE|FAIL_SETTLE)
MAX_PAY_HI=$(python3 -c 'from decimal import Decimal; a=Decimal("'"$_anum"'"); h=Decimal("'"$MAX_PAY_HI"'"); print(min(a,h))')
info "max-search pay" "too-high/fail hi:=$(format_amount_alt "${CUR}:${MAX_PAY_HI}") status=${_last_st}"
;;
esac
if python3 -c 'from decimal import Decimal; import sys; sys.exit(0 if Decimal(sys.argv[1])>0 and Decimal(sys.argv[2])<=Decimal(sys.argv[1]) else 1)' "$MAX_PAY_LO" "$MAX_PAY_HI" 2>/dev/null; then
info "max-search pay" "bounds crossed — stop"
break
fi
done
if [ -n "${MAX_PAY_BEST_AMT:-}" ]; then
ok "max-search best payable $(format_amount_alt "$MAX_PAY_BEST_AMT") · lo=$(format_amount_alt "${CUR}:${MAX_PAY_LO}") · hi=$(format_amount_alt "${CUR}:${MAX_PAY_HI}")"
else
warn pay "max-search no successful pay" "available was ${CUR}:$(wallet_avail)"
fi
{
echo "best_pay=${MAX_PAY_BEST_AMT:-}"
echo "best_pay_alt=$(format_amount_alt "${MAX_PAY_BEST_AMT:-${CUR}:0}" 2>/dev/null || true)"
echo "pay_lo=${MAX_PAY_LO}"
echo "pay_hi=${MAX_PAY_HI}"
} >>"${SCRATCH}/ladder-plan.txt" 2>/dev/null || true
printf '%s\n' "${MAX_PAY_BEST_AMT:-}" >"${SCRATCH}/ladder-pay-plan.txt" 2>/dev/null || true
}
# Entry: glue from check_amount_ladder.sh after withdraw phase
ladder_run_pay_phase() {
PAY_OK_N=0
PAY_FAIL_N=0
if [ "${LADDER_PAY}" != "1" ]; then
info pay "LADDER_PAY=0 — pay phase skipped"
return 0
fi
if [ "${FAIL_N_L:-0}" -gt 0 ]; then
warn pay "skipped pay ladder" "withdraw phase already failed"
return 0
fi
set_group pay
section "ladder · phase B · pay"
metrics_report_coins "before-pay-ladder" || true
ladder_pay_resolve_merchant
if [ -z "${AUTH:-}" ] && ! { [ "${LADDER_USE_FREE_TEMPLATE}" = "1" ] && [ -n "${LADDER_FREE_TEMPLATE:-}" ]; }; then
warn pay "no merchant token and free template off — skip pay"
return 0
fi
if [ -n "${AUTH:-}" ]; then
ok "merchant token" "instance ${INST}"
else
info pay "public free template ${LADDER_FREE_TEMPLATE}@${LADDER_FREE_TEMPLATE_INSTANCE} (no merchant token)"
fi
# Same cumulative wallet as withdraw (WDB=wallet-main.sqlite3)
wallet_prepare "main"
info pay "using same wallet as withdraw · WDB=${WDB:-?}"
local bal0
bal0=$(ladder_wait_available "$LADDER_PAY_WAIT_AVAILABLE_S")
info "pay budget" "wallet available ${CUR}:${bal0} · $(format_amount_alt "${CUR}:${bal0}") — never order more than this"
if [ "${LADDER_MODE}" = "max" ]; then
ladder_run_pay_max_search
else
ladder_run_pay_classic
fi
metrics_report_coins "after-pay-ladder" || true
info "pay summary" "ok=$PAY_OK_N fail=$PAY_FAIL_N tsv=$PAY_TSV"
}

415
lib.sh
View file

@ -62,9 +62,9 @@ fi
# Merchant portal /private API: instance ids must be lowercase or you get odd 401s. # Merchant portal /private API: instance ids must be lowercase or you get odd 401s.
# (Display names may use capitals; path segment /instances/{id}/ must not.) # (Display names may use capitals; path segment /instances/{id}/ must not.)
MERCHANT_INSTANCE=$(printf '%s' "${MERCHANT_INSTANCE}" | tr '[:upper:]' '[:lower:]') MERCHANT_INSTANCE=$(printf '%s' "${MERCHANT_INSTANCE}" | tr '[:upper:]' '[:lower:]')
: "${WITHDRAW_AMT:=GOA:20}" # single-shot fallback; e2e ladder uses bank cash-note amounts : "${WITHDRAW_AMT:=GOA:20}" # single-shot fallback; e2e ladder uses ATM notes
: "${PAY_AMT:=GOA:0.01}" : "${PAY_AMT:=GOA:0.01}"
: "${CREDIT_AMT:=GOA:4700}" # covers bank withdraw ladder 20+50+100+200+4200 (paivana) : "${CREDIT_AMT:=GOA:4700}" # covers ATM ladder 20+50+100+200+4200 (paivana)
: "${TIMEOUT:=12}" : "${TIMEOUT:=12}"
: "${E2E_TIMEOUT:=55}" # whole e2e budget; skip rest when exceeded (e2e raises as needed) : "${E2E_TIMEOUT:=55}" # whole e2e budget; skip rest when exceeded (e2e raises as needed)
: "${E2E_PAY_SECS:=22}" # dedicated seconds for pay handle-uri (avoid Alarm clock) : "${E2E_PAY_SECS:=22}" # dedicated seconds for pay handle-uri (avoid Alarm clock)
@ -166,53 +166,13 @@ set_taler_stack() {
fi fi
} }
# Normalize domains.conf UI lang token → en|fr|de (empty if not a lang token).
# Note: de is valid for --lang / explicit profile, but no stock profile defaults to de.
_domain_lang_token() {
case "${1:-}" in
en|EN|eng|english) printf 'en' ;;
fr|FR|fra|french) printf 'fr' ;;
de|DE|deu|ger|german|deutsch) printf 'de' ;;
*) printf '' ;;
esac
}
# Normalize regional locale → BCP 47 (fr-CH, de-CH, en-CH, …). Empty if unknown.
# Accepts fr-CH, fr_CH, ch-FR / de-CH, ch-DE (user-friendly aliases).
_domain_locale_token() {
local t="${1:-}"
t=$(printf '%s' "$t" | tr '[:upper:]' '[:lower:]' | tr '_' '-')
case "$t" in
fr-ch|ch-fr|fr_ch) printf 'fr-CH' ;;
de-ch|ch-de|de_ch) printf 'de-CH' ;;
en-ch|ch-en|en_ch) printf 'en-CH' ;;
en-us|en-gb|fr-fr|de-de|it-ch|rm-ch)
# pass through other xx-YY
printf '%s' "$t" | awk -F- '{printf "%s-%s", tolower($1), toupper($2)}'
;;
*)
# bare xx-YY pattern
if printf '%s' "$t" | grep -qE '^[a-z]{2}-[a-z]{2}$'; then
printf '%s' "$t" | awk -F- '{printf "%s-%s", tolower($1), toupper($2)}'
else
printf ''
fi
;;
esac
}
# Load first matching profile from domains.conf. # Load first matching profile from domains.conf.
# Fields (whitespace-separated; # comments; blank lines ignored): # Fields (whitespace-separated; # comments; blank lines ignored):
# name bank exchange merchant currency local landing lang locale [canonical] # name bank exchange merchant currency local[0|1] landing[0|1] [canonical_domain]
# Legacy:
# … landing lang [canonical] → locale defaulted
# … landing [canonical] → lang=en, locale defaulted
# Sets TALER_DOMAIN_LANG + TALER_DOMAIN_LOCALE from the profile.
# Returns 0 if found, 1 if not. # Returns 0 if found, 1 if not.
load_domain_profile() { load_domain_profile() {
local want="$1" conf="${TALER_DOMAINS_CONF:-}" local want="$1" conf="${TALER_DOMAINS_CONF:-}"
local line name bank exchange merchant currency local_stack landing canon local line name bank exchange merchant currency local_stack landing canon
local profile_lang profile_locale f8 f9 f10
[ -n "$want" ] || return 1 [ -n "$want" ] || return 1
[ -n "$conf" ] && [ -f "$conf" ] || return 1 [ -n "$conf" ] && [ -f "$conf" ] || return 1
@ -229,55 +189,12 @@ load_domain_profile() {
currency="$5" currency="$5"
local_stack="${6:-0}" local_stack="${6:-0}"
landing="${7:-}" landing="${7:-}"
f8="${8:-}" canon="${8:-$name}"
f9="${9:-}"
f10="${10:-}"
canon="$name"
profile_lang=""
profile_locale=""
# Backward compat: old 7th field was canonical domain (contains a dot) # Backward compat: old 7th field was canonical domain (contains a dot)
if [ -n "$landing" ] && [[ "$landing" == *.* && "$landing" != "0" && "$landing" != "1" ]]; then if [ -n "$landing" ] && [[ "$landing" == *.* && "$landing" != "0" && "$landing" != "1" ]]; then
canon="$landing" canon="$landing"
landing="" landing=""
f8="${7:-}"
f9="${8:-}"
f10="${9:-}"
fi fi
if [ -n "$f8" ]; then
profile_lang=$(_domain_lang_token "$f8")
if [ -n "$profile_lang" ]; then
# modern: lang [locale] [canonical]
profile_locale=$(_domain_locale_token "$f9")
if [ -n "$profile_locale" ]; then
canon="${f10:-$name}"
else
# lang + canonical (no locale column)
if [ -n "$f9" ] && [ -z "$(_domain_lang_token "$f9")" ]; then
canon="$f9"
else
canon="${f9:-$name}"
fi
profile_locale=$(_domain_locale_token "$f10")
fi
else
# legacy: field 8 = canonical only
canon="$f8"
profile_lang=$(_domain_lang_token "$f9")
profile_locale=$(_domain_locale_token "${f10:-$f9}")
[ -z "$profile_lang" ] && profile_lang=en
fi
fi
[ -z "$profile_lang" ] && profile_lang=en
if [ -z "$profile_locale" ]; then
# default regional locale: FP → fr-CH, else de-CH
case "$name$canon$bank$merchant" in
*lefrancpaysan*|*francpaysan*) profile_locale=fr-CH ;;
*) profile_locale=de-CH ;;
esac
fi
TALER_DOMAIN_LANG="$profile_lang"
TALER_DOMAIN_LOCALE="$profile_locale"
export TALER_DOMAIN_LANG TALER_DOMAIN_LOCALE
set_taler_stack "$bank" "$exchange" "$merchant" "$currency" "$local_stack" "$landing" "$canon" set_taler_stack "$bank" "$exchange" "$merchant" "$currency" "$local_stack" "$landing" "$canon"
return 0 return 0
done <"$conf" done <"$conf"
@ -610,13 +527,7 @@ TEST_GROUP=""
TEST_N=0 TEST_N=0
LAST_TID="" LAST_TID=""
# Global / progress: may continue across check_*.sh processes via TALER_MON_STATE # Global / progress: may continue across check_*.sh processes via TALER_MON_STATE
# GLOBAL_*_N = whole-run status totals (multi-phase summary at parent end)
GLOBAL_N="${GLOBAL_N:-0}" GLOBAL_N="${GLOBAL_N:-0}"
GLOBAL_PASS_N="${GLOBAL_PASS_N:-0}"
GLOBAL_FAIL_N="${GLOBAL_FAIL_N:-0}"
GLOBAL_WARN_N="${GLOBAL_WARN_N:-0}"
GLOBAL_INFO_N="${GLOBAL_INFO_N:-0}"
GLOBAL_BLOCK_N="${GLOBAL_BLOCK_N:-0}"
LAST_GLOBAL="" LAST_GLOBAL=""
PROGRESS_DONE="${PROGRESS_DONE:-0}" PROGRESS_DONE="${PROGRESS_DONE:-0}"
PROGRESS_TOTAL="${PROGRESS_TOTAL:-0}" PROGRESS_TOTAL="${PROGRESS_TOTAL:-0}"
@ -626,28 +537,14 @@ _mon_state_load() {
[ -n "${TALER_MON_STATE:-}" ] && [ -f "${TALER_MON_STATE}" ] || return 0 [ -n "${TALER_MON_STATE:-}" ] && [ -f "${TALER_MON_STATE}" ] || return 0
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "${TALER_MON_STATE}" . "${TALER_MON_STATE}"
: "${GLOBAL_N:=0}"
: "${GLOBAL_PASS_N:=0}"
: "${GLOBAL_FAIL_N:=0}"
: "${GLOBAL_WARN_N:=0}"
: "${GLOBAL_INFO_N:=0}"
: "${GLOBAL_BLOCK_N:=0}"
: "${PROGRESS_DONE:=0}"
: "${PROGRESS_TOTAL:=0}"
: "${PROGRESS_LAST_SHOWN:=0}"
} }
_mon_state_save() { _mon_state_save() {
[ -n "${TALER_MON_STATE:-}" ] || return 0 [ -n "${TALER_MON_STATE:-}" ] || return 0
{ {
printf 'GLOBAL_N=%s\n' "${GLOBAL_N:-0}" printf 'GLOBAL_N=%s\n' "$GLOBAL_N"
printf 'GLOBAL_PASS_N=%s\n' "${GLOBAL_PASS_N:-0}" printf 'PROGRESS_DONE=%s\n' "$PROGRESS_DONE"
printf 'GLOBAL_FAIL_N=%s\n' "${GLOBAL_FAIL_N:-0}" printf 'PROGRESS_TOTAL=%s\n' "$PROGRESS_TOTAL"
printf 'GLOBAL_WARN_N=%s\n' "${GLOBAL_WARN_N:-0}" printf 'PROGRESS_LAST_SHOWN=%s\n' "$PROGRESS_LAST_SHOWN"
printf 'GLOBAL_INFO_N=%s\n' "${GLOBAL_INFO_N:-0}"
printf 'GLOBAL_BLOCK_N=%s\n' "${GLOBAL_BLOCK_N:-0}"
printf 'PROGRESS_DONE=%s\n' "${PROGRESS_DONE:-0}"
printf 'PROGRESS_TOTAL=%s\n' "${PROGRESS_TOTAL:-0}"
printf 'PROGRESS_LAST_SHOWN=%s\n' "${PROGRESS_LAST_SHOWN:-0}"
} >"${TALER_MON_STATE}" } >"${TALER_MON_STATE}"
} }
_mon_state_load _mon_state_load
@ -678,7 +575,7 @@ set_area() {
} }
# Start a logical sub-group. Short stable names: # Start a logical sub-group. Short stable names:
# exchange bank merchant perf stats landing paivana # exchange bank merchant perf stats landing paivana
# prereq wallet bankwd settle pay shop dig load report # prereq wallet atm settle pay shop dig load report
# ssh caddy outside inside compare withdraw # ssh caddy outside inside compare withdraw
# Counter continues if the same group is re-entered later in the area. # Counter continues if the same group is re-entered later in the area.
# Prints a compact group chip so log + issue text map cleanly (www.bank-03). # Prints a compact group chip so log + issue text map cleanly (www.bank-03).
@ -886,8 +783,6 @@ ok() {
_take_tid _take_tid
_msg_line "$BG_OK" "OK" "$G" "$label" "$detail" _msg_line "$BG_OK" "OK" "$G" "$label" "$detail"
PASS_N=$((PASS_N + 1)) PASS_N=$((PASS_N + 1))
GLOBAL_PASS_N=$((GLOBAL_PASS_N + 1))
_mon_state_save
} }
# component-scoped error: err bank "what failed" "why / HTTP / path" # component-scoped error: err bank "what failed" "why / HTTP / path"
err() { err() {
@ -895,9 +790,7 @@ err() {
_take_tid _take_tid
_msg_line "$BG_ERR" "ERROR" "$R" "${comp}: ${msg}" "$detail" _msg_line "$BG_ERR" "ERROR" "$R" "${comp}: ${msg}" "$detail"
FAIL_N=$((FAIL_N + 1)) FAIL_N=$((FAIL_N + 1))
GLOBAL_FAIL_N=$((GLOBAL_FAIL_N + 1))
ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ · $detail}") ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ · $detail}")
_mon_state_save
} }
# fail "what failed" ["why / HTTP code / path"] # fail "what failed" ["why / HTTP code / path"]
fail() { fail() {
@ -905,9 +798,7 @@ fail() {
_take_tid _take_tid
_msg_line "$BG_ERR" "ERROR" "$R" "$label" "$detail" _msg_line "$BG_ERR" "ERROR" "$R" "$label" "$detail"
FAIL_N=$((FAIL_N + 1)) FAIL_N=$((FAIL_N + 1))
GLOBAL_FAIL_N=$((GLOBAL_FAIL_N + 1))
ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ · $detail}") ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ · $detail}")
_mon_state_save
} }
warn() { warn() {
# warn "what is soft-bad" ["why still ok to continue"] # warn "what is soft-bad" ["why still ok to continue"]
@ -928,8 +819,6 @@ warn() {
fi fi
_msg_line "$BG_WARN" "WARN" "$Y" "$head" "$detail" _msg_line "$BG_WARN" "WARN" "$Y" "$head" "$detail"
WARN_N=$((WARN_N + 1)) WARN_N=$((WARN_N + 1))
GLOBAL_WARN_N=$((GLOBAL_WARN_N + 1))
_mon_state_save
} }
info() { info() {
# info "topic" ["concrete fact / value / next step"] # info "topic" ["concrete fact / value / next step"]
@ -937,8 +826,6 @@ info() {
_take_tid _take_tid
_msg_line "$BG_INFO" "INFO" "$C" "$label" "$detail" _msg_line "$BG_INFO" "INFO" "$C" "$label" "$detail"
INFO_N=$((INFO_N + 1)) INFO_N=$((INFO_N + 1))
GLOBAL_INFO_N=$((GLOBAL_INFO_N + 1))
_mon_state_save
} }
blocker() { blocker() {
# Hard stop on pay/withdraw path: blocker "step" "why it cannot continue" # Hard stop on pay/withdraw path: blocker "step" "why it cannot continue"
@ -947,10 +834,7 @@ blocker() {
_msg_line "$BG_BLK" "BLOCKER" "$M" "${step}" "$msg" _msg_line "$BG_BLK" "BLOCKER" "$M" "${step}" "$msg"
BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg") BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg")
FAIL_N=$((FAIL_N + 1)) FAIL_N=$((FAIL_N + 1))
GLOBAL_FAIL_N=$((GLOBAL_FAIL_N + 1))
GLOBAL_BLOCK_N=$((GLOBAL_BLOCK_N + 1))
ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg") ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg")
_mon_state_save
} }
section() { section() {
# Boxed section header (3 lines when colour on) # Boxed section header (3 lines when colour on)
@ -976,155 +860,31 @@ section() {
fi fi
} }
# Print SUMMARY totals box for given counts (phase-local or global). summary() {
# Each severity row is colour-distinct: badge + full-line bg tint + bold count (v1.14.0). local blk_n=${#BLOCKERS[@]}
# $1=pass $2=fail $3=warn $4=info $5=block echo ""
_summary_totals_box() { # Phase progress snapshot — keep GLOBAL total (do not snap N to phase-local done).
local pass_n="$1" fail_n="$2" warn_n="$3" info_n="$4" blk_n="$5" # Snapping here made mid-run bars like 34/34 then 38/47. Whole-run snap: progress_finish.
local hdr_bg hdr_fg _sum_lbl if [ "${PROGRESS_OFF:-0}" != "1" ] && [ "${PROGRESS_DONE:-0}" -gt 0 ]; then
_sum_lbl=$(i18n_tag SUMMARY) if [ "${PROGRESS_FINAL:-0}" = "1" ]; then
# Header colour follows overall verdict # last phase / explicit final: done/total exact
if [ "$fail_n" -gt 0 ] || [ "$blk_n" -gt 0 ]; then if [ "$PROGRESS_TOTAL" -gt 0 ] && [ "$PROGRESS_DONE" -ne "$PROGRESS_TOTAL" ]; then
hdr_bg="${BG_ERR}" PROGRESS_TOTAL=$PROGRESS_DONE
hdr_fg="${R}" _mon_state_save
elif [ "$warn_n" -gt 0 ]; then fi
hdr_bg="${BG_WARN}"
hdr_fg="${Y}"
else
hdr_bg="${BG_OK}"
hdr_fg="${G}"
fi
if [ "${BOX:-0}" = "1" ]; then
# Dim full-line tints (bold fg on soft bg) — distinct per severity
local ROW_OK=$'\e[1;32;48;5;22m' # bold green on dark green
local ROW_ERR=$'\e[1;97;48;5;52m' # bold white on dark red
local ROW_WARN=$'\e[1;30;48;5;178m' # bold black on gold
local ROW_INFO=$'\e[1;97;48;5;24m' # bold white on dark blue
local ROW_BLK=$'\e[1;97;48;5;53m' # bold white on dark magenta
local ROW_TOT=$'\e[1;37;48;5;236m' # bold white on dark gray
# Include SUMMARY on all three frame lines so HTML classify maps them to sum-header
# (phase section() boxes use the same ╔═║╚ glyphs but must not get sum-* paint).
printf -- '\n%s╔════════ %s ════════════════════╗%s\n' "$hdr_bg" "$_sum_lbl" "$N"
printf -- '%s║ %-34s ║%s\n' "$hdr_bg" "$_sum_lbl" "$N"
printf -- '%s╚════════ %s ════════════════════╝%s\n' "$hdr_bg" "$_sum_lbl" "$N"
# $1=row_bg $2=badge_bg $3=tag $4=n $5=label_fg — full-width-ish row
_sum_row() {
local row_bg="$1" badge="$2" tag="$3" n="$4" fg="$5" _tg pad
_tg=$(i18n_tag "$tag")
pad=$(printf '%-5s' "$_tg")
printf -- ' %s┌ %s┐%s %s %s%6d%s %s%-12s%s\n' \
"$badge" "$pad" "$N" \
"$row_bg" "$B" "$n" "$N" \
"$fg$B" "$_tg" "$N"
}
_sum_row "$ROW_OK" "$BG_OK" "OK" "$pass_n" "$G"
[ "$fail_n" -gt 0 ] && _sum_row "$ROW_ERR" "$BG_ERR" "ERROR" "$fail_n" "$R"
[ "$warn_n" -gt 0 ] && _sum_row "$ROW_WARN" "$BG_WARN" "WARN" "$warn_n" "$Y"
[ "$info_n" -gt 0 ] && _sum_row "$ROW_INFO" "$BG_INFO" "INFO" "$info_n" "$C"
[ "$blk_n" -gt 0 ] && _sum_row "$ROW_BLK" "$BG_BLK" "BLOCK" "$blk_n" "$M"
# totals rollup on tinted bar
printf -- ' %s %s %s %s%d OK%s' "$ROW_TOT" "$B" "$(i18n_text 'totals:')" "$G$B" "$pass_n" "$N$ROW_TOT"
[ "$fail_n" -gt 0 ] && printf -- ' %s·%s %s%d ERROR%s' "$D" "$N$ROW_TOT" "$R$B" "$fail_n" "$N$ROW_TOT"
[ "$warn_n" -gt 0 ] && printf -- ' %s·%s %s%d WARN%s' "$D" "$N$ROW_TOT" "$Y$B" "$warn_n" "$N$ROW_TOT"
[ "$info_n" -gt 0 ] && printf -- ' %s·%s %s%d INFO%s' "$D" "$N$ROW_TOT" "$C$B" "$info_n" "$N$ROW_TOT"
[ "$blk_n" -gt 0 ] && printf -- ' %s·%s %s%d BLOCKER%s' "$D" "$N$ROW_TOT" "$M$B" "$blk_n" "$N$ROW_TOT"
printf -- ' %s\n' "$N"
else
printf -- '\n[ %s ]\n' "$_sum_lbl"
printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag OK)" "$G$B" "$pass_n" "$N"
[ "$fail_n" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag ERROR)" "$R$B" "$fail_n" "$N"
[ "$warn_n" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag WARN)" "$Y$B" "$warn_n" "$N"
[ "$info_n" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag INFO)" "$C$B" "$info_n" "$N"
[ "$blk_n" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag BLOCK)" "$M$B" "$blk_n" "$N"
printf -- ' %s%s%s %s%d OK%s' "$B" "$(i18n_text 'totals:')" "$N" "$G$B" "$pass_n" "$N"
[ "$fail_n" -gt 0 ] && printf -- ' · %s%d ERROR%s' "$R$B" "$fail_n" "$N"
[ "$warn_n" -gt 0 ] && printf -- ' · %s%d WARN%s' "$Y$B" "$warn_n" "$N"
[ "$info_n" -gt 0 ] && printf -- ' · %s%d INFO%s' "$C$B" "$info_n" "$N"
[ "$blk_n" -gt 0 ] && printf -- ' · %s%d BLOCKER%s' "$M$B" "$blk_n" "$N"
printf '\n'
fi
# overall verdict — bold label, filled badge (i18n for FR console/HTML)
local _ok_msg _warn_msg _fail_msg
_ok_msg=$(i18n_text 'all clear')
_warn_msg=$(i18n_text 'warnings only (no hard fail)')
if [ "$fail_n" -eq 0 ] && [ "$blk_n" -eq 0 ] && [ "$warn_n" -eq 0 ]; then
if [ "${BOX:-0}" = "1" ]; then
printf -- ' %s┌ OK ┐%s %s%s%s%s\n' "$BG_OK" "$N" "$G" "$B" "$_ok_msg" "$N"
else
printf -- ' %s[ OK ] %s%s%s\n' "$G" "$B" "$_ok_msg" "$N"
fi
elif [ "$fail_n" -eq 0 ] && [ "$blk_n" -eq 0 ]; then
if [ "${BOX:-0}" = "1" ]; then
printf -- ' %s┌ WARN ┐%s %s%s%s%s\n' "$BG_WARN" "$N" "$Y" "$B" "$_warn_msg" "$N"
else
printf -- ' %s[ WARN ] %s%s%s\n' "$Y" "$B" "$_warn_msg" "$N"
fi
else
_fail_msg=$(i18n_text 'failed — see list above')
if [ "${BOX:-0}" = "1" ]; then
printf -- ' %s┌ ERROR ┐%s %s%s%s%s\n' "$BG_ERR" "$N" "$R" "$B" "$_fail_msg" "$N"
else
printf -- ' %s[ ERROR ] %s%s%s\n' "$R" "$B" "$_fail_msg" "$N"
fi fi
_progress_rebalance
_progress_bar_line "$PROGRESS_DONE" "$PROGRESS_TOTAL"
fi fi
} if [ "$GLOBAL_N" -gt 0 ]; then
# Always show global position: current # / expected total (if known)
# Whole-run SUMMARY from TALER_MON_STATE (parent after all phases).
# Multi-phase: phases never print SUMMARY totals; only this does.
summary_global() {
_mon_state_load
local pass_n="${GLOBAL_PASS_N:-0}"
local fail_n="${GLOBAL_FAIL_N:-0}"
local warn_n="${GLOBAL_WARN_N:-0}"
local info_n="${GLOBAL_INFO_N:-0}"
local blk_n="${GLOBAL_BLOCK_N:-0}"
# Progress bar + numbered checks already printed by progress_finish when present.
if [ "${GLOBAL_N:-0}" -gt 0 ]; then
if [ "${PROGRESS_TOTAL:-0}" -gt 0 ]; then if [ "${PROGRESS_TOTAL:-0}" -gt 0 ]; then
printf -- '%s %s #001…#%03d · %d/%d %s%s\n' \ printf -- '%s %s #001…#%03d · %d/%d %s%s\n' \
"$D" "$(i18n_text 'numbered checks:')" "$GLOBAL_N" \ "$D" "$(i18n_text 'numbered checks:')" "$GLOBAL_N" \
"${PROGRESS_DONE:-0}" "$PROGRESS_TOTAL" \ "$PROGRESS_DONE" "$PROGRESS_TOTAL" \
"$(i18n_text '(global done/total)')" "$N" "$(i18n_text '(global done/total)')" "$N"
else else
printf -- '%s %s #001…#%03d%s\n' \ printf -- '%s %s #001…#%03d%s\n' "$D" "$(i18n_text 'numbered checks this run:')" "$GLOBAL_N" "$N"
"$D" "$(i18n_text 'numbered checks this run:')" "$GLOBAL_N" "$N"
fi
fi
_summary_totals_box "$pass_n" "$fail_n" "$warn_n" "$info_n" "$blk_n"
[ "$fail_n" -eq 0 ]
}
summary() {
local blk_n=${#BLOCKERS[@]}
# Multi-phase runs: never print phase SUMMARY / progress standings (v1.13.10+).
# Parent prints one global SUMMARY via summary_global after progress_finish.
# Solo check_*.sh (TALER_MON_MULTI_PHASE unset/0) still full summary.
# Phase still lists ERRORS/BLOCKERS so mid-run failures stay visible.
local _full=1
if [ "${TALER_MON_MULTI_PHASE:-0}" = "1" ]; then
_full=0
fi
echo ""
if [ "$_full" = "1" ]; then
# Progress snapshot — whole-run snap also in progress_finish.
if [ "${PROGRESS_OFF:-0}" != "1" ] && [ "${PROGRESS_DONE:-0}" -gt 0 ]; then
_progress_rebalance
_progress_bar_line "$PROGRESS_DONE" "$PROGRESS_TOTAL"
fi
if [ "$GLOBAL_N" -gt 0 ]; then
if [ "${PROGRESS_TOTAL:-0}" -gt 0 ]; then
printf -- '%s %s #001…#%03d · %d/%d %s%s\n' \
"$D" "$(i18n_text 'numbered checks:')" "$GLOBAL_N" \
"$PROGRESS_DONE" "$PROGRESS_TOTAL" \
"$(i18n_text '(global done/total)')" "$N"
else
printf -- '%s %s #001…#%03d%s\n' "$D" "$(i18n_text 'numbered checks this run:')" "$GLOBAL_N" "$N"
fi
fi fi
fi fi
if [ "$blk_n" -gt 0 ]; then if [ "$blk_n" -gt 0 ]; then
@ -1151,13 +911,55 @@ summary() {
done done
fi fi
# Coloured totals — only at full phase summary (solo script / single-phase) # Coloured totals — same severity badges as check lines (always; NO_COLOR blanks ANSI)
if [ "$_full" != "1" ]; then if [ "${BOX:-0}" = "1" ]; then
# Still fail the phase exit when this phase had errors (set -e + trailing summary) printf -- '\n%s┌ %s ┐%s\n' "$BG_SEC" "$(i18n_tag SUMMARY)" "$N"
[ "$FAIL_N" -eq 0 ] _sum_badge() { # $1=bg $2=tag $3=fg $4=n
return local _tg
_tg=$(i18n_tag "$2")
printf -- ' %s┌ %-5s┐%s %s%4d%s\n' "$1" "$_tg" "$N" "$3" "$4" "$N"
}
_sum_badge "$BG_OK" "OK" "$G" "$PASS_N"
[ "$FAIL_N" -gt 0 ] && _sum_badge "$BG_ERR" "ERROR" "$R" "$FAIL_N"
[ "$WARN_N" -gt 0 ] && _sum_badge "$BG_WARN" "WARN" "$Y" "$WARN_N"
[ "$INFO_N" -gt 0 ] && _sum_badge "$BG_INFO" "INFO" "$C" "$INFO_N"
[ "$blk_n" -gt 0 ] && _sum_badge "$BG_BLK" "BLOCK" "$M" "$blk_n"
else
printf -- '\n[ %s ]\n' "$(i18n_tag SUMMARY)"
printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag OK)" "$G" "$PASS_N" "$N"
[ "$FAIL_N" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag ERROR)" "$R" "$FAIL_N" "$N"
[ "$WARN_N" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag WARN)" "$Y" "$WARN_N" "$N"
[ "$INFO_N" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag INFO)" "$C" "$INFO_N" "$N"
[ "$blk_n" -gt 0 ] && printf -- ' [ %-5s ] %s%4d%s\n' "$(i18n_tag BLOCK)" "$M" "$blk_n" "$N"
fi
# one-line rollup (bold label, severity-coloured counts)
printf -- ' %stotals:%s %s%d OK%s' "$B" "$N" "$G" "$PASS_N" "$N"
[ "$FAIL_N" -gt 0 ] && printf -- ' · %s%d ERROR%s' "$R" "$FAIL_N" "$N"
[ "$WARN_N" -gt 0 ] && printf -- ' · %s%d WARN%s' "$Y" "$WARN_N" "$N"
[ "$INFO_N" -gt 0 ] && printf -- ' · %s%d INFO%s' "$C" "$INFO_N" "$N"
[ "$blk_n" -gt 0 ] && printf -- ' · %s%d BLOCKER%s' "$M" "$blk_n" "$N"
printf '\n'
# overall verdict
if [ "$FAIL_N" -eq 0 ] && [ "$blk_n" -eq 0 ] && [ "$WARN_N" -eq 0 ]; then
if [ "${BOX:-0}" = "1" ]; then
printf -- ' %s┌ OK ┐%s %sall clear%s\n' "$BG_OK" "$N" "$G" "$N"
else
printf -- ' %s[ OK ] all clear%s\n' "$G" "$N"
fi
elif [ "$FAIL_N" -eq 0 ] && [ "$blk_n" -eq 0 ]; then
if [ "${BOX:-0}" = "1" ]; then
printf -- ' %s┌ WARN ┐%s %swarnings only (no hard fail)%s\n' "$BG_WARN" "$N" "$Y" "$N"
else
printf -- ' %s[ WARN ] warnings only (no hard fail)%s\n' "$Y" "$N"
fi
else
_fail_msg=$(i18n_text 'failed — see list above')
if [ "${BOX:-0}" = "1" ]; then
printf -- ' %s┌ ERROR ┐%s %s%s%s\n' "$BG_ERR" "$N" "$R" "$_fail_msg" "$N"
else
printf -- ' %s[ ERROR ] %s%s\n' "$R" "$_fail_msg" "$N"
fi
fi fi
_summary_totals_box "$PASS_N" "$FAIL_N" "$WARN_N" "$INFO_N" "$blk_n"
[ "$FAIL_N" -eq 0 ] [ "$FAIL_N" -eq 0 ]
} }
@ -1504,53 +1306,6 @@ load_monitoring_secrets_env() {
export E2E_BANK_ADMIN_PASS E2E_MERCHANT_TOKEN SECRETS_ROOT KOOPA_ADMIN_SECRETS export E2E_BANK_ADMIN_PASS E2E_MERCHANT_TOKEN SECRETS_ROOT KOOPA_ADMIN_SECRETS
} }
# Strip default HTTPS/HTTP ports from taler:// URIs (libeufin often emits host:443).
# Wallets and mon QR rules reject default :443/:80 in the authority.
# Usage: normalize_taler_uri "taler://withdraw/host:443/…" → prints cleaned URI
normalize_taler_uri() {
local u="${1-}"
if [ -z "$u" ]; then
return 0
fi
printf '%s' "$u" | python3 -c '
import re, sys
u = sys.stdin.read().strip()
# any taler://SCHEME/HOST:443|80/...
u = re.sub(
r"(taler://[A-Za-z0-9._-]+/)([^/?#:]+):443(?=/|$|\?|#)",
r"\1\2",
u,
flags=re.I,
)
u = re.sub(
r"(taler://[A-Za-z0-9._-]+/)([^/?#:]+):80(?=/|$|\?|#)",
r"\1\2",
u,
flags=re.I,
)
# leftover path/query forms
for a, b in (
(":443/", "/"),
(":443?", "?"),
(":443#", "#"),
(":80/", "/"),
(":80?", "?"),
(":80#", "#"),
):
u = u.replace(a, b)
# bare trailing :443 / :80 on host (no slash)
u = re.sub(r"(taler://[A-Za-z0-9._-]+/[^/?#:]+):443$", r"\1", u, flags=re.I)
u = re.sub(r"(taler://[A-Za-z0-9._-]+/[^/?#:]+):80$", r"\1", u, flags=re.I)
sys.stdout.write(u)
' 2>/dev/null || printf '%s' "$u"
}
# True (exit 0) if URI still has default port in taler:// authority.
taler_uri_has_default_port() {
# host:443 or host:80 before next path/query/end
python3 -c "import re,sys; u=sys.argv[1] if len(sys.argv)>1 else ''; sys.exit(0 if re.search(r'taler://\\S+:(443|80)(/|$|\\?|#)', u, re.I) else 1)" "${1-}" 2>/dev/null
}
find_wallet_cli() { find_wallet_cli() {
# Must return a path suitable for: node "$WALLET_CLI" … # Must return a path suitable for: node "$WALLET_CLI" …
# Prefer *.mjs / bundled entry; plain /usr/bin/taler-wallet-cli is often a # Prefer *.mjs / bundled entry; plain /usr/bin/taler-wallet-cli is often a
@ -1559,20 +1314,12 @@ find_wallet_cli() {
echo "$WALLET_CLI"; return 0 echo "$WALLET_CLI"; return 0
fi fi
local c cand local c cand
# WALLET_CLI / WALLET_CLI_CANDIDATES from env; then portable package paths. # WALLET_CLI / WALLET_CLI_CANDIDATES from env; then portable package paths only.
# Prefer local ts-core / mon-env builds (schema-compatible) over stale system
# wrappers (e.g. /usr/local 1.3.x vs DB schema 29 from 1.6.x).
# shellcheck disable=SC2086 # shellcheck disable=SC2086
for c in ${WALLET_CLI_CANDIDATES:-} \ for c in ${WALLET_CLI_CANDIDATES:-} \
"${HOME}/.local/bin/taler-wallet-cli.mjs" \ "${HOME}/.local/bin/taler-wallet-cli.mjs" \
"${HOME}/src/taler/taler-typescript-core/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs" \
"${HOME}/src/taler/taler-typescript-core/packages/taler-wallet-cli/dist/taler-wallet-cli.mjs" \
"${HOME}/src/taler/taler-typescript-core/packages/taler-wallet-cli/dist/taler-wallet-cli-local.mjs" \
"${HOME}/src/taler/taler-typescript-core/packages/taler-wallet-cli/dist/taler-wallet-cli-bundled.cjs" \
/usr/lib/taler-wallet-cli/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs \ /usr/lib/taler-wallet-cli/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs \
/usr/share/taler-wallet-cli/bin/taler-wallet-cli.mjs \ /usr/share/taler-wallet-cli/bin/taler-wallet-cli.mjs
/usr/local/lib/taler-wallet-cli/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs \
/usr/local/lib/taler-wallet-cli/node_modules/taler-wallet-cli/dist/taler-wallet-cli-bundled.cjs
do do
[ -n "$c" ] && [ -f "$c" ] && { echo "$c"; return 0; } [ -n "$c" ] && [ -f "$c" ] && { echo "$c"; return 0; }
done done
@ -1588,14 +1335,6 @@ find_wallet_cli() {
c=$(readlink -f "$cand" 2>/dev/null || true) c=$(readlink -f "$cand" 2>/dev/null || true)
[ -n "$c" ] && [ -f "$c" ] && { echo "$c"; return 0; } [ -n "$c" ] && [ -f "$c" ] && { echo "$c"; return 0; }
fi fi
# macOS /usr/local wrapper often runs an older bundle; prefer its bundled.cjs only
# when no newer candidate above matched (already returned).
for c in \
/usr/local/lib/taler-wallet-cli/node_modules/taler-wallet-cli/dist/taler-wallet-cli-bundled.cjs \
/usr/local/lib/node_modules/taler-wallet-cli/dist/taler-wallet-cli-bundled.cjs
do
[ -f "$c" ] && { echo "$c"; return 0; }
done
fi fi
return 1 return 1
} }

View file

@ -3,18 +3,16 @@
# Format (whitespace-separated; # comments): # Format (whitespace-separated; # comments):
# domain expected_mx (comma) mail_hosts (comma) smtp_ports imap_ports label # domain expected_mx (comma) mail_hosts (comma) smtp_ports imap_ports label
# #
# Mail planes (outside-in only — no host login): # Only these mail planes (no hacktivism / ad-hoc MX):
# • firefly.gnunet.org — taler.net + gnunet.org # • firefly.gnunet.org — taler.net + gnunet.org
# • mail.anastasis.lu — Anastasis / TSA (A → pixel.taler-systems.com) # • anastasis.taler-systems.com — Anastasis / TSA mail
# (legacy catalog host anastasis.taler-systems.com does NOT resolve)
# #
# Override path: MAIL_CATALOG=/path/to/file # Override path: MAIL_CATALOG=/path/to/file
# --- firefly.gnunet.org --- # --- firefly.gnunet.org ---
taler.net firefly.gnunet.org firefly.gnunet.org 25,465,587 143,993 firefly-taler taler.net firefly.gnunet.org firefly.gnunet.org 25,465,587 143,993 firefly-taler
gnunet.org firefly.gnunet.org firefly.gnunet.org 25,465,587 143,993 firefly-gnunet gnunet.org firefly.gnunet.org firefly.gnunet.org 25,465,587 143,993 firefly-gnunet
# --- Anastasis / TSA (MX mail.anastasis.lu → pixel) --- # --- anastasis.taler-systems.com ---
# Port 587 often closed/filtered on this plane → WARN only (see check_mail.sh). anastasis.taler-systems.com anastasis.taler-systems.com anastasis.taler-systems.com 25,465,587 143,993 anastasis-tsa
taler-systems.com mail.anastasis.lu mail.anastasis.lu,pixel.taler-systems.com 25,465,587 143,993 tsa-anastasis taler-systems.com anastasis.taler-systems.com anastasis.taler-systems.com 25,465,587 143,993 tsa-via-anastasis
anastasis.lu mail.anastasis.lu mail.anastasis.lu 25,465,587 143,993 anastasis-lu

View file

@ -70,15 +70,11 @@ path = sys.argv[2] if len(sys.argv) > 2 else ""
alt = {} alt = {}
if path: if path:
try: try:
_loaded = json.load(open(path)) alt = json.load(open(path))
if isinstance(_loaded, dict):
alt = {str(k): str(v) for k, v in _loaded.items()}
except Exception: except Exception:
alt = {} alt = {}
if not isinstance(alt, dict) or not alt: if not alt:
alt = {"0": "GOA"} alt = {"0": "GOA"}
elif "0" not in alt:
alt["0"] = "GOA"
def parse(s): def parse(s):
if ":" in s: if ":" in s:

View file

@ -5,7 +5,6 @@
# #
# CORRECT (site-level named matcher — preferred): # CORRECT (site-level named matcher — preferred):
# @mon_bare path /monitoring /monitoring_err \ # @mon_bare path /monitoring /monitoring_err \
# /monitoring-max-ladder /monitoring-max-ladder_err \
# /taler-monitoring-surface /taler-monitoring-surface_err \ # /taler-monitoring-surface /taler-monitoring-surface_err \
# /taler-monitoring-aptdeploy /taler-monitoring-aptdeploy_err # /taler-monitoring-aptdeploy /taler-monitoring-aptdeploy_err
# redir @mon_bare {path}/ 302 # redir @mon_bare {path}/ 302
@ -36,7 +35,6 @@
# --- only inside taler.hacktivism.ch { ... } --- # --- only inside taler.hacktivism.ch { ... } ---
@mon_bare path /monitoring /monitoring_err \ @mon_bare path /monitoring /monitoring_err \
/monitoring-max-ladder /monitoring-max-ladder_err \
/taler-monitoring-surface /taler-monitoring-surface_err \ /taler-monitoring-surface /taler-monitoring-surface_err \
/taler-monitoring-aptdeploy /taler-monitoring-aptdeploy_err \ /taler-monitoring-aptdeploy /taler-monitoring-aptdeploy_err \
/taler-monitoring-mail /taler-monitoring-mail_err \ /taler-monitoring-mail /taler-monitoring-mail_err \
@ -61,9 +59,7 @@
} }
# --- bank + exchange + taler (each site block; set root host dir) --- # --- bank + exchange + taler (each site block; set root host dir) ---
# /monitoring* already covers /monitoring-max-ladder* @mon_bare path /monitoring /monitoring_err
@mon_bare path /monitoring /monitoring_err \
/monitoring-max-ladder /monitoring-max-ladder_err
redir @mon_bare {path}/ 302 redir @mon_bare {path}/ 302
handle /monitoring_err* { handle /monitoring_err* {

File diff suppressed because it is too large Load diff

View file

@ -46,10 +46,7 @@ bugs.gnunet.org 443 https gnunet-bugs
# --- taler-systems.com --- # --- taler-systems.com ---
www.taler-systems.com 443 https tsa-www www.taler-systems.com 443 https tsa-www
taler-systems.com 443 https tsa-apex taler-systems.com 443 https tsa-apex
# Anastasis / TSA mail plane (MX mail.anastasis.lu → pixel). Do NOT list anastasis.taler-systems.com 443,25,465,587,143,993 https anastasis-mail
# anastasis.taler-systems.com — it does not resolve (was a catalog typo).
mail.anastasis.lu 443,25,465,587,143,993 https anastasis-mail
pixel.taler-systems.com 443,25,465,587,143,993 https pixel-mail
# --- mail (also covered by phase mail; listed for surface nmap/ports) --- # --- mail (also covered by phase mail; listed for surface nmap/ports) ---
firefly.gnunet.org 443,25,465,587,143,993 https firefly-mail firefly.gnunet.org 443,25,465,587,143,993 https firefly-mail

View file

@ -93,16 +93,14 @@ Phases:
sanity public + optional server sanity public + optional server
server server-side only (SSH) server server-side only (SSH)
e2e withdraw + pay (small amounts; remote aborts on login/KYC) e2e withdraw + pay (small amounts; remote aborts on login/KYC)
ladder amount ladder classic (default 23-rung withdraw + pay; any currency) ladder withdraw/pay amount ladder (GOA ceiling or stage TESTPAYSAN max_wire)
max-ladder max-search: highest withdrawable + highest payable (≤ available)
goa-ladder alias of ladder (legacy phase name; same check_amount_ladder.sh)
auth401 merchant Basic-auth / case matrix (HTTP 401 paths; may create throwaway instance) auth401 merchant Basic-auth / case matrix (HTTP 401 paths; may create throwaway instance)
aptdeploy koopa podman apt-src smoke: taler-merchant in aptdeploy koopa podman apt-src smoke: taler-merchant in
koopa-taler-deploy-test-apt-src-trixie{,-testing} koopa-taler-deploy-test-apt-src-trixie{,-testing}
surface REMOTE-ONLY public inventory (NOT in default/all/full): surface REMOTE-ONLY public inventory (NOT in default/all/full):
ecosystem hosts (taler.net, gnunet.org, taler-systems.com, mattermost, …) ecosystem hosts (taler.net, gnunet.org, taler-systems.com, mattermost, …)
or -d DOMAIN → that domains surface; port/protocol/TLS/CVE (OSV) or -d DOMAIN → that domains surface; port/protocol/TLS/CVE (OSV)
mattermost Mattermost chat health (SPA + ping + client floors: mobile/desktop/support ≥10.11) mattermost Mattermost chat health (default mattermost.taler.net; SPA + /api/v4/system/ping)
mail MX/SMTP/IMAP for Taler mail (firefly, pixel/TSA, catalogued domains) mail MX/SMTP/IMAP for Taler mail (firefly, pixel/TSA, catalogued domains)
monpages public monitoring HTML via FQDN (obligatory ERROR; GOA full inventory, FP only FP) monpages public monitoring HTML via FQDN (obligatory ERROR; GOA full inventory, FP only FP)
uses MON_HOSTS + HTML_URL_OK (same as host-agent); v1.3.1+ uses MON_HOSTS + HTML_URL_OK (same as host-agent); v1.3.1+
@ -124,7 +122,7 @@ Options:
--merchant URL merchant-backend base — overrides profile --merchant URL merchant-backend base — overrides profile
--currency CODE expected currency (GOA, KUDOS, CHF, …); empty = report only --currency CODE expected currency (GOA, KUDOS, CHF, …); empty = report only
--no-probe do not probe alternate hosts (unknown domains only) --no-probe do not probe alternate hosts (unknown domains only)
--lang en|fr|de UI/console language (sticky bar + badges; de available, not a profile default) --lang en|fr UI/console language (sticky bar + badges)
overrides auto (FrancPaysan→fr, else en) overrides auto (FrancPaysan→fr, else en)
--ver, --version, -V print suite version, git commit, and repo URLs --ver, --version, -V print suite version, git commit, and repo URLs
-h, --help -h, --help
@ -167,8 +165,7 @@ Env (same meaning):
SURFACE_CVE_LEVEL=warn|error bare Server-header versions default warn SURFACE_CVE_LEVEL=warn|error bare Server-header versions default warn
(Debian package versions default error) (Debian package versions default error)
SURFACE_CATALOG=path override surface-catalog.conf SURFACE_CATALOG=path override surface-catalog.conf
TALER_MON_LANG=en|fr|de same as --lang (profile lang from domains.conf; de never stock default) TALER_MON_LANG=en|fr same as --lang (default en; auto fr for *lefrancpaysan*)
TALER_MON_LOCALE=fr-CH|de-CH regional l10n from domains.conf locale column
Full load (GOA / hacktivism): Full load (GOA / hacktivism):
./taler-monitoring.sh -d hacktivism.ch full ./taler-monitoring.sh -d hacktivism.ch full
@ -228,13 +225,12 @@ NO_PROBE=0
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--lang) --lang)
[ $# -ge 2 ] || { echo "error: --lang needs en|fr|de" >&2; exit 2; } [ $# -ge 2 ] || { echo "error: --lang needs en|fr" >&2; exit 2; }
_lang="$2" _lang="$2"
case "$_lang" in case "$_lang" in
fr|FR|fra|french) TALER_MON_LANG=fr ;; fr|FR|fra|french) TALER_MON_LANG=fr ;;
de|DE|deu|ger|german|deutsch) TALER_MON_LANG=de ;;
en|EN|eng|english) TALER_MON_LANG=en ;; en|EN|eng|english) TALER_MON_LANG=en ;;
*) echo "error: unknown language '$_lang' (use en, fr, or de)" >&2; exit 2 ;; *) echo "error: unknown language '$_lang' (use en or fr)" >&2; exit 2 ;;
esac esac
TALER_MON_LANG_SET=1 TALER_MON_LANG_SET=1
export TALER_MON_LANG TALER_MON_LANG_SET export TALER_MON_LANG TALER_MON_LANG_SET
@ -267,7 +263,7 @@ while [ $# -gt 0 ]; do
CURRENCY_OVERRIDE="$2"; shift 2 CURRENCY_OVERRIDE="$2"; shift 2
;; ;;
--no-probe) NO_PROBE=1; shift ;; --no-probe) NO_PROBE=1; shift ;;
urls|inside|versions|sanity|server|e2e|ladder|goa-ladder|max-ladder|maxladder|auth401|aptdeploy|apt-deploy|apt_src|surface|ecosystem|mattermost|mail|monpages|pages|devtesting|franken|fake-franken|fake_franken|all|full) PHASES+=("$1"); shift ;; urls|inside|versions|sanity|server|e2e|ladder|goa-ladder|auth401|aptdeploy|apt-deploy|apt_src|surface|ecosystem|mattermost|mail|monpages|pages|devtesting|franken|fake-franken|fake_franken|all|full) PHASES+=("$1"); shift ;;
*) *)
# bare domain shorthand: ./taler-monitoring.sh taler.net # bare domain shorthand: ./taler-monitoring.sh taler.net
if [[ "$1" == *.* && "$1" != *://* && "$1" != -* ]]; then if [[ "$1" == *.* && "$1" != *://* && "$1" != -* ]]; then
@ -297,7 +293,7 @@ if [ "${LOCAL_STACK:-1}" != "1" ]; then
SKIP_SSH=1 SKIP_SSH=1
fi fi
# Remote: no SSH; bank withdraw ladder set in check_e2e (smaller notes) # Remote: no SSH; ATM withdraw ladder set in check_e2e (smaller notes)
if [ "${LOCAL_STACK}" != "1" ]; then if [ "${LOCAL_STACK}" != "1" ]; then
E2E_FAKE_INCOMING=0 E2E_FAKE_INCOMING=0
E2E_REMOTE=1 E2E_REMOTE=1
@ -311,15 +307,14 @@ export EXPECT_CURRENCY SKIP_SSH LOCAL_STACK TALER_DOMAIN_PROBE CHECK_LANDING
export TALER_DOMAIN_FROM_CLI="${TALER_DOMAIN_FROM_CLI:-0}" export TALER_DOMAIN_FROM_CLI="${TALER_DOMAIN_FROM_CLI:-0}"
export DOMAIN_SET="${DOMAIN_SET:-0}" export DOMAIN_SET="${DOMAIN_SET:-0}"
export WITHDRAW_AMT PAY_AMT CREDIT_AMT MERCHANT_INSTANCE export WITHDRAW_AMT PAY_AMT CREDIT_AMT MERCHANT_INSTANCE
export E2E_FAKE_INCOMING E2E_REMOTE E2E_VARIABLE E2E_ATM_MAX E2E_BANK_WITHDRAW_MAX export E2E_FAKE_INCOMING E2E_REMOTE E2E_VARIABLE E2E_ATM_MAX
export E2E_WITHDRAW_VALUES E2E_PAY_VALUES E2E_USE_TEMPLATES E2E_TEMPLATE_MAP export E2E_WITHDRAW_VALUES E2E_PAY_VALUES E2E_USE_TEMPLATES E2E_TEMPLATE_MAP
export PAIVANA_PUBLIC E2E_PAIVANA E2E_PAIVANA_TEMPLATE E2E_PAIVANA_AMOUNT E2E_PAIVANA_INSTANCE export PAIVANA_PUBLIC E2E_PAIVANA E2E_PAIVANA_TEMPLATE E2E_PAIVANA_AMOUNT E2E_PAIVANA_INSTANCE
export INSIDE_PROFILE INSIDE_SSH export INSIDE_PROFILE INSIDE_SSH
export INSIDE_BANK_CTR INSIDE_EXCHANGE_CTR INSIDE_MERCHANT_CTR export INSIDE_BANK_CTR INSIDE_EXCHANGE_CTR INSIDE_MERCHANT_CTR
export INSIDE_BANK_PORT INSIDE_EXCHANGE_PORT INSIDE_MERCHANT_PORT export INSIDE_BANK_PORT INSIDE_EXCHANGE_PORT INSIDE_MERCHANT_PORT
export INSIDE_DNS_BANK INSIDE_DNS_EXCHANGE INSIDE_DNS_MERCHANT export INSIDE_DNS_BANK INSIDE_DNS_EXCHANGE INSIDE_DNS_MERCHANT
# Amount ladder — check_amount_ladder.sh + ladder/lib_pay.sh + ladder/extract_rpubs.py # Ladder: withdraw then pay — 0 + random mids + max-1 + max (see check_goa_ladder.sh).
# Phases: ladder|goa-ladder (classic), max-ladder|maxladder (max-search).
# Defaults so set -u export is safe when vars were never set by caller. # Defaults so set -u export is safe when vars were never set by caller.
: "${LADDER_STEPS:=23}" : "${LADDER_STEPS:=23}"
: "${LADDER_MIN_AMOUNT:=0.000001}" : "${LADDER_MIN_AMOUNT:=0.000001}"
@ -333,23 +328,16 @@ export INSIDE_DNS_BANK INSIDE_DNS_EXCHANGE INSIDE_DNS_MERCHANT
: "${LADDER_INCLUDE_ZERO:=1}" : "${LADDER_INCLUDE_ZERO:=1}"
: "${LADDER_INCLUDE_MAX:=1}" : "${LADDER_INCLUDE_MAX:=1}"
: "${LADDER_HIGH_FROM:=1000000}" : "${LADDER_HIGH_FROM:=1000000}"
: "${LADDER_HIGH_RUNGS:=4}" : "${LADDER_HIGH_RUNGS:=12}"
: "${LADDER_LOAD:=1}" : "${LADDER_LOAD:=1}"
: "${LADDER_PAY:=1}" : "${LADDER_PAY:=1}"
: "${LADDER_WITHDRAW_SCALE:=1.5}" : "${LADDER_WITHDRAW_SCALE:=1.5}"
: "${LADDER_PAY_SETTLE_ROUNDS:=6}" : "${LADDER_PAY_SETTLE_ROUNDS:=6}"
: "${LADDER_MODE:=classic}"
: "${LADDER_MAX_PROBES:=32}"
: "${LADDER_MAX_TOL_REL:=0.001}"
: "${LADDER_MAX_SEED:=}"
: "${LADDER_PAY_WITH_MAX:=0}"
: "${LADDER_HIGH_RUNGS:=6}"
export LADDER_STEPS LADDER_MIN_AMOUNT LADDER_CONFIRM_POLLS LADDER_MAX_RUNGS LADDER_TIMEOUT_S LADDER_REPORT_DIR export LADDER_STEPS LADDER_MIN_AMOUNT LADDER_CONFIRM_POLLS LADDER_MAX_RUNGS LADDER_TIMEOUT_S LADDER_REPORT_DIR
export LADDER_SETTLE_ROUNDS LADDER_SETTLE_SLEEP EXP_PW_FILE EXP_USER export LADDER_SETTLE_ROUNDS LADDER_SETTLE_SLEEP EXP_PW_FILE EXP_USER
export LADDER_MAX_AMOUNT LADDER_INCLUDE_ZERO LADDER_INCLUDE_MAX export LADDER_MAX_AMOUNT LADDER_INCLUDE_ZERO LADDER_INCLUDE_MAX
export LADDER_HIGH_FROM LADDER_HIGH_RUNGS LADDER_LOAD export LADDER_HIGH_FROM LADDER_HIGH_RUNGS LADDER_LOAD
export LADDER_PAY LADDER_WITHDRAW_SCALE LADDER_PAY_SETTLE_ROUNDS export LADDER_PAY LADDER_WITHDRAW_SCALE LADDER_PAY_SETTLE_ROUNDS
export LADDER_MODE LADDER_MAX_PROBES LADDER_MAX_TOL_REL LADDER_MAX_SEED LADDER_PAY_WITH_MAX
export TALER_DOMAIN_APPLIED=1 export TALER_DOMAIN_APPLIED=1
i18n_init 2>/dev/null || true i18n_init 2>/dev/null || true
# Optional: auth401 and other phases may honor CONTINUE_ON_ERROR # Optional: auth401 and other phases may honor CONTINUE_ON_ERROR
@ -443,9 +431,9 @@ _progress_estimate_phase() {
versions) n=35 ;; versions) n=35 ;;
sanity) n=35 ;; sanity) n=35 ;;
server) n=20 ;; server) n=20 ;;
# e2e: bank withdraw ladder emits many INFO lines (coins before/after each note) # e2e: ATM ladder emits many INFO lines (coins before/after each note)
e2e) n=240 ;; e2e) n=240 ;;
ladder|goa-ladder|max-ladder|maxladder) n=120 ;; ladder|goa-ladder) n=120 ;;
auth401) n=70 ;; auth401) n=70 ;;
aptdeploy) n=40 ;; aptdeploy) n=40 ;;
surface|ecosystem) n=120 ;; surface|ecosystem) n=120 ;;
@ -460,14 +448,7 @@ _progress_estimate_phase() {
;; ;;
mattermost) n=25 ;; mattermost) n=25 ;;
mail) n=40 ;; mail) n=40 ;;
# baseline ~8 + ladder rungs (default 23 × ~2 lines) + summary devtesting|franken|fake-franken|fake_franken) n=10 ;;
devtesting|franken|fake-franken|fake_franken)
n=8
if [ "${DEVTESTING_LADDER:-1}" != "0" ]; then
_ls="${DEVTESTING_LADDER_STEPS:-23}"
n=$((8 + _ls * 2 + 6))
fi
;;
*) n=15 ;; *) n=15 ;;
esac esac
printf '%s' "$n" printf '%s' "$n"
@ -559,8 +540,6 @@ elif [ "${LOCAL_STACK:-0}" = "1" ] && command -v podman >/dev/null 2>&1 \
fi fi
printf 'target domain=%s\n' "${TALER_DOMAIN}" printf 'target domain=%s\n' "${TALER_DOMAIN}"
printf ' ui_lang %s\n' "${TALER_MON_LANG:-en}"
printf ' locale %s\n' "${TALER_MON_LOCALE:-${TALER_DOMAIN_LOCALE:-?}}"
printf ' bank %s\n' "$BANK_PUBLIC" printf ' bank %s\n' "$BANK_PUBLIC"
printf ' exchange %s\n' "$EXCHANGE_PUBLIC" printf ' exchange %s\n' "$EXCHANGE_PUBLIC"
printf ' merchant %s\n' "$MERCHANT_PUBLIC" printf ' merchant %s\n' "$MERCHANT_PUBLIC"
@ -591,14 +570,6 @@ chmod +x "$ROOT"/check_*.sh 2>/dev/null || true
ec=0 ec=0
_phase_idx=0 _phase_idx=0
_n_phases=${#PHASES[@]} _n_phases=${#PHASES[@]}
# Suppress all phase SUMMARY / progress standings when more than one phase;
# parent prints one global SUMMARY after progress_finish (v1.13.10+).
if [ "$_n_phases" -gt 1 ]; then
export TALER_MON_MULTI_PHASE=1
else
export TALER_MON_MULTI_PHASE=0
fi
_MON_WAS_MULTI="${TALER_MON_MULTI_PHASE}"
while [ "$_phase_idx" -lt "$_n_phases" ]; do while [ "$_phase_idx" -lt "$_n_phases" ]; do
p="${PHASES[$_phase_idx]}" p="${PHASES[$_phase_idx]}"
if [ "$RUN_TIMED_OUT" = "1" ]; then if [ "$RUN_TIMED_OUT" = "1" ]; then
@ -621,15 +592,7 @@ while [ "$_phase_idx" -lt "$_n_phases" ]; do
sanity) run_phase sanity "$ROOT/check_sanity.sh" || ec=1 ;; sanity) run_phase sanity "$ROOT/check_sanity.sh" || ec=1 ;;
server) run_phase server "$ROOT/check_server.sh" || ec=1 ;; server) run_phase server "$ROOT/check_server.sh" || ec=1 ;;
e2e) run_phase e2e "$ROOT/check_e2e.sh" || ec=1 ;; e2e) run_phase e2e "$ROOT/check_e2e.sh" || ec=1 ;;
ladder|goa-ladder) ladder|goa-ladder) run_phase ladder "$ROOT/check_goa_ladder.sh" || ec=1 ;;
run_phase ladder "$ROOT/check_amount_ladder.sh" || ec=1
;;
max-ladder|maxladder)
# Feature: hunt highest mint+confirm withdrawable amount (any currency)
LADDER_MODE=max
export LADDER_MODE
run_phase ladder "$ROOT/check_amount_ladder.sh" || ec=1
;;
auth401) run_phase auth401 "$ROOT/check_auth401.sh" || ec=1 ;; auth401) run_phase auth401 "$ROOT/check_auth401.sh" || ec=1 ;;
aptdeploy|apt-deploy|apt_src) run_phase aptdeploy "$ROOT/check_apt_deploy.sh" || ec=1 ;; aptdeploy|apt-deploy|apt_src) run_phase aptdeploy "$ROOT/check_apt_deploy.sh" || ec=1 ;;
surface|ecosystem) run_phase surface "$ROOT/check_surface.sh" || ec=1 ;; surface|ecosystem) run_phase surface "$ROOT/check_surface.sh" || ec=1 ;;
@ -655,7 +618,6 @@ while [ "$_phase_idx" -lt "$_n_phases" ]; do
_phase_idx=$((_phase_idx + 1)) _phase_idx=$((_phase_idx + 1))
done done
unset _phase_idx _n_phases unset _phase_idx _n_phases
unset PROGRESS_FINAL TALER_MON_MULTI_PHASE 2>/dev/null || true
# Extraordinary run-budget failure: always report at end; HTML links top → here. # Extraordinary run-budget failure: always report at end; HTML links top → here.
if [ "$RUN_TIMED_OUT" = "1" ]; then if [ "$RUN_TIMED_OUT" = "1" ]; then
@ -690,7 +652,7 @@ if [ "$RUN_TIMED_OUT" = "1" ]; then
fi fi
# Snap progress bar to real global count (done/total) once for the whole run. # Snap progress bar to real global count (done/total) once for the whole run.
# Reload TALER_MON_STATE — check_*.sh update done/total + GLOBAL_*_N in a separate process. # Reload TALER_MON_STATE — check_*.sh update done/total in a separate process.
if type _mon_state_load >/dev/null 2>&1; then if type _mon_state_load >/dev/null 2>&1; then
_mon_state_load _mon_state_load
fi fi
@ -698,11 +660,5 @@ if type progress_finish >/dev/null 2>&1; then
printf '\n' printf '\n'
progress_finish progress_finish
fi fi
# Multi-phase: one global SUMMARY (OK/ERROR/WARN/INFO/BLOCK across all phases).
# Single-phase already printed phase-local SUMMARY via check_*.sh.
if [ "${_MON_WAS_MULTI:-0}" = "1" ] && type summary_global >/dev/null 2>&1; then
summary_global || true
fi
unset _MON_WAS_MULTI 2>/dev/null || true
exit "$ec" exit "$ec"

View file

@ -1,73 +0,0 @@
#!/usr/bin/env bash
# Unit tests for normalize_taler_uri / taler_uri_has_default_port (lib.sh).
# Run: ./tests/test_normalize_taler_uri.sh
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
# shellcheck source=../lib.sh
source "$ROOT/lib.sh"
pass=0
fail=0
assert_eq() {
local name="$1" got="$2" want="$3"
if [ "$got" = "$want" ]; then
pass=$((pass + 1))
else
echo "FAIL $name" >&2
echo " got: $got" >&2
echo " want: $want" >&2
fail=$((fail + 1))
fi
}
assert_true() {
local name="$1"
if "$@"; then
# shift name
:
fi
}
# --- strip :443 / :80 ---
assert_eq "withdraw :443/" \
"$(normalize_taler_uri 'taler://withdraw/bank.hacktivism.ch:443/taler-integration/abc-123')" \
"taler://withdraw/bank.hacktivism.ch/taler-integration/abc-123"
assert_eq "withdraw :443 end" \
"$(normalize_taler_uri 'taler://withdraw/bank.example:443')" \
"taler://withdraw/bank.example"
assert_eq "pay :443" \
"$(normalize_taler_uri 'taler://pay/taler.hacktivism.ch:443/instances/x/orders/y?c=z')" \
"taler://pay/taler.hacktivism.ch/instances/x/orders/y?c=z"
assert_eq "pay-template :80" \
"$(normalize_taler_uri 'taler://pay-template/shop.example:80/instances/i/t')" \
"taler://pay-template/shop.example/instances/i/t"
assert_eq "already clean" \
"$(normalize_taler_uri 'taler://withdraw/bank.hacktivism.ch/taler-integration/uuid')" \
"taler://withdraw/bank.hacktivism.ch/taler-integration/uuid"
assert_eq "empty" "$(normalize_taler_uri '')" ""
# non-default port kept
assert_eq "keep :8443" \
"$(normalize_taler_uri 'taler://withdraw/bank.example:8443/taler-integration/x')" \
"taler://withdraw/bank.example:8443/taler-integration/x"
# has_default_port detector
if taler_uri_has_default_port 'taler://withdraw/h:443/p'; then
pass=$((pass + 1))
else
echo "FAIL detector should match :443" >&2
fail=$((fail + 1))
fi
if taler_uri_has_default_port 'taler://withdraw/h/p'; then
echo "FAIL detector should not match clean URI" >&2
fail=$((fail + 1))
else
pass=$((pass + 1))
fi
echo "normalize_taler_uri tests: pass=$pass fail=$fail"
[ "$fail" -eq 0 ]