landing: compact GOA alt display and all-account bank fallback

Wire goa-amount.js into bank/exchange/merchant intros, deploy it with the
landings, and deepen the in-container bank stats fallback to scan all
accounts (skip only exchange) when the host timer is down.
This commit is contained in:
Hernâni Marques 2026-07-17 08:12:00 +02:00
parent 746d7a41af
commit 48f77139c5
No known key found for this signature in database
8 changed files with 363 additions and 39 deletions

View file

@ -103,11 +103,31 @@ taler-wallet-cli exchanges accept-tos https://exchange.hacktivism.ch/
--- ---
## Live stats (in bank container) ## Live stats (central host collector · hernani systemd)
Stats are **not** computed on the laptop or host browser. A small script runs **Primary:** user **hernani** on koopa runs `taler-landing-stats.timer`
**inside the bank container**, queries libeufin for the demo funding account (`scripts/taler-landing/`). That process full-scans **all bank accounts**
(`explorer`), and writes a public JSON file next to the landing assets. (except `exchange` double-count), paginates every ledger, writes
`stats.json` with **GOA alt units** (`amount_alt` / Kilo-GOA / Mega-GOA / …),
and refreshes exchange + merchant stats the same way.
```bash
# on koopa as hernani
./scripts/taler-landing/install-landing-stats-host.sh
sudo loginctl enable-linger hernani
systemctl --user start taler-landing-stats.service
```
See **`scripts/taler-landing/README.md`**.
**Fallback:** in-container `landing-stats.sh` (below) if the host timer is down.
It now also scans admin + all users (skips only `exchange`).
## Live stats (in bank container · fallback)
Stats are **not** computed in the browser. A small script can still run
**inside the bank container**, query libeufin (all accounts), and write a
public JSON file next to the landing assets.
| Piece | Path / role | | Piece | Path / role |
|-------|-------------| |-------|-------------|
@ -131,9 +151,9 @@ Stats are **not** computed on the laptop or host browser. A small script runs
| Env | Default | Meaning | | Env | Default | Meaning |
|-----|---------|---------| |-----|---------|---------|
| `TX_DELTA` | `-50000` | per-account ledger window (`GET …/transactions?delta=`) | | `TX_DELTA` | `-200000` | per-account ledger window (`GET …/transactions?delta=`) |
| `ACCOUNTS_DELTA` | `-500` | account-list window (`GET /accounts?delta=`) | | `ACCOUNTS_DELTA` | `-10000` | account-list window (`GET /accounts?delta=`) |
| `MAX_SCAN_ACCOUNTS` | `500` | max usernames to scan | | `MAX_SCAN_ACCOUNTS` | `0` (unlimited) | max usernames to scan; `0` = all |
Older defaults (`TX_DELTA=-100`, `MAX_SCAN_ACCOUNTS=80`) **undercounted** credits/withdraws Older defaults (`TX_DELTA=-100`, `MAX_SCAN_ACCOUNTS=80`) **undercounted** credits/withdraws
and account totals on this stack. Empty accounts often return **HTTP 204** (no body) — and account totals on this stack. Empty accounts often return **HTTP 204** (no body) —

View file

@ -1491,6 +1491,7 @@ tw run-until-done &amp;&amp; tw balance</pre>
</footer> </footer>
</main> </main>
<script src="/intro/goa-amount.js"></script>
<script> <script>
(function () { (function () {
var WEBUI = "https://bank.hacktivism.ch/webui/"; var WEBUI = "https://bank.hacktivism.ch/webui/";
@ -1936,16 +1937,34 @@ tw run-until-done &amp;&amp; tw balance</pre>
var el = document.getElementById(id); var el = document.getElementById(id);
if (el) el.textContent = v == null || v === "" ? "—" : String(v); if (el) el.textContent = v == null || v === "" ? "—" : String(v);
} }
function setAmt(id, rowOrAmt) {
var el = document.getElementById(id);
if (!el) return;
if (window.GoaAmount) {
var f = GoaAmount.pick(rowOrAmt, d.alt_unit_names);
el.textContent = f.display;
if (f.title && f.title !== f.display) el.setAttribute("title", f.title);
else el.removeAttribute("title");
} else {
var raw = rowOrAmt && typeof rowOrAmt === "object"
? (rowOrAmt.amount_alt || rowOrAmt.amount || rowOrAmt.total_amount || "—")
: (rowOrAmt == null || rowOrAmt === "" ? "—" : String(rowOrAmt));
el.textContent = raw;
}
}
if (window.GoaAmount) GoaAmount.rememberAlt(d);
var flow = d.flow || {}; var flow = d.flow || {};
var fin = flow.incoming || {}; var fin = flow.incoming || {};
var fwd = flow.withdraw || {}; var fwd = flow.withdraw || {};
set("st-accounts", ba.total != null ? ba.total : "—"); set("st-accounts", ba.total != null ? ba.total : "—");
set("st-wallets", wl.unique_reserves != null ? wl.unique_reserves : "—"); set("st-wallets", wl.unique_reserves != null ? wl.unique_reserves : "—");
set("st-incoming", fin.amount || flow.total_in || "—"); setAmt("st-incoming", fin.amount_alt ? fin : (fin.amount || flow.total_in || "—"));
set("st-withdraw", fwd.amount || w.total_amount || "—"); setAmt("st-withdraw", fwd.amount_alt ? fwd : (fwd.amount || w.total_amount || "—"));
set("st-24h", h24.amount || "GOA:0"); setAmt("st-24h", h24.amount_alt ? h24 : (h24.amount || "GOA:0"));
set("st-7d", h7.amount || "GOA:0"); setAmt("st-7d", h7.amount_alt ? h7 : (h7.amount || "GOA:0"));
set("shared-balance", d.balance_explorer || "—"); setAmt("shared-balance", d.balance_explorer_alt
? { amount: d.balance_explorer, amount_alt: d.balance_explorer_alt, amount_full: d.balance_explorer_full }
: (d.balance_explorer || "—"));
var list = document.getElementById("st-recent"); var list = document.getElementById("st-recent");
if (list) { if (list) {
@ -1962,7 +1981,13 @@ tw run-until-done &amp;&amp; tw balance</pre>
var li = document.createElement("li"); var li = document.createElement("li");
var amt = document.createElement("span"); var amt = document.createElement("span");
amt.className = "amt"; amt.className = "amt";
amt.textContent = row.amount || "?"; if (window.GoaAmount) {
var af = GoaAmount.pick(row, d.alt_unit_names);
amt.textContent = af.display;
if (af.title && af.title !== af.display) amt.setAttribute("title", af.title);
} else {
amt.textContent = row.amount_alt || row.amount || "?";
}
var meta = document.createElement("span"); var meta = document.createElement("span");
meta.className = "meta"; meta.className = "meta";
var when = fmtCest(row.at_unix, row.at || row.at_iso) || ""; var when = fmtCest(row.at_unix, row.at || row.at_iso) || "";

View file

@ -444,12 +444,29 @@ sudo apt-get install -y taler-wallet-cli</pre>
</p> </p>
</footer> </footer>
</main> </main>
<script src="/intro/goa-amount.js"></script>
<script> <script>
(function () { (function () {
function set(id, v) { function set(id, v) {
var el = document.getElementById(id); var el = document.getElementById(id);
if (el) el.textContent = v == null || v === "" ? "—" : String(v); if (el) el.textContent = v == null || v === "" ? "—" : String(v);
} }
function setAmt(id, amount, altKey, d) {
var el = document.getElementById(id);
if (!el) return;
var row = null;
if (altKey && d && d[altKey]) {
row = { amount: amount, amount_alt: d[altKey], amount_full: d[altKey.replace(/_alt$/, "_full")] || d[amount + "_full"] };
}
if (window.GoaAmount) {
var f = row ? GoaAmount.pick(row, d && d.alt_unit_names) : GoaAmount.format(amount, d && d.alt_unit_names);
el.textContent = f.display;
if (f.title && f.title !== f.display) el.setAttribute("title", f.title);
else el.removeAttribute("title");
} else {
el.textContent = (row && row.amount_alt) || amount || "—";
}
}
function msLabel(ms, http) { function msLabel(ms, http) {
if (ms == null || ms === "") return "—"; if (ms == null || ms === "") return "—";
var s = ms + " ms"; var s = ms + " ms";
@ -460,12 +477,13 @@ sudo apt-get install -y taler-wallet-cli</pre>
.then(function (r) { if (!r.ok) throw new Error("x"); return r.json(); }) .then(function (r) { if (!r.ok) throw new Error("x"); return r.json(); })
.then(function (d) { .then(function (d) {
if (!d || !d.ok) throw new Error("bad"); if (!d || !d.ok) throw new Error("bad");
if (window.GoaAmount) GoaAmount.rememberAlt(d);
set("st-coins", d.known_coins); set("st-coins", d.known_coins);
set("st-coins-live", d.coins_live); set("st-coins-live", d.coins_live);
set("st-coins-spent", d.coins_spent); set("st-coins-spent", d.coins_spent);
set("st-remaining", d.coins_remaining_amount); setAmt("st-remaining", d.coins_remaining_amount, "coins_remaining_amount_alt", d);
set("st-withdraw", d.withdraw_ops); set("st-withdraw", d.withdraw_ops);
set("st-withdraw-amt", d.withdraw_amount); setAmt("st-withdraw-amt", d.withdraw_amount, "withdraw_amount_alt", d);
set("st-refresh", d.refresh_ops); set("st-refresh", d.refresh_ops);
set("st-coin-dep", d.coin_deposits); set("st-coin-dep", d.coin_deposits);
set("st-batch-dep", d.batch_deposits); set("st-batch-dep", d.batch_deposits);
@ -477,7 +495,7 @@ sudo apt-get install -y taler-wallet-cli</pre>
set("st-denoms-wd", d.denoms_withdrawable); set("st-denoms-wd", d.denoms_withdrawable);
set("st-reserves", d.reserves); set("st-reserves", d.reserves);
set("st-wire-in-n", d.wire_in_count); set("st-wire-in-n", d.wire_in_count);
set("st-wire-in-amt", d.wire_in_amount); setAmt("st-wire-in-amt", d.wire_in_amount, "wire_in_amount_alt", d);
set("st-wire-out", d.wire_out); set("st-wire-out", d.wire_out);
var ladder = document.getElementById("st-ladder"); var ladder = document.getElementById("st-ladder");
@ -486,7 +504,12 @@ sudo apt-get install -y taler-wallet-cli</pre>
(d.denom_ladder || []).forEach(function (x) { (d.denom_ladder || []).forEach(function (x) {
var s = document.createElement("span"); var s = document.createElement("span");
s.className = "pill"; s.className = "pill";
s.textContent = x.value + (x.keys > 1 ? " ×" + x.keys : ""); var lab = x.value_alt || x.value || "?";
if (window.GoaAmount && !x.value_alt) {
lab = GoaAmount.format(x.value, d.alt_unit_names).display;
}
s.textContent = lab + (x.keys > 1 ? " ×" + x.keys : "");
if (x.value && lab !== x.value) s.title = x.value;
ladder.appendChild(s); ladder.appendChild(s);
}); });
if (!(d.denom_ladder || []).length) ladder.textContent = "—"; if (!(d.denom_ladder || []).length) ladder.textContent = "—";
@ -499,7 +522,11 @@ sudo apt-get install -y taler-wallet-cli</pre>
wrap.hidden = false; wrap.hidden = false;
var html = "<table><thead><tr><th>Value</th><th>Coins</th><th>Live</th></tr></thead><tbody>"; var html = "<table><thead><tr><th>Value</th><th>Coins</th><th>Live</th></tr></thead><tbody>";
by.forEach(function (r) { by.forEach(function (r) {
html += "<tr><td>" + r.value + "</td><td>" + r.coins + "</td><td>" + r.live + "</td></tr>"; var v = r.value_alt || r.value || "?";
if (window.GoaAmount && !r.value_alt) {
v = GoaAmount.format(r.value, d.alt_unit_names).display;
}
html += "<tr><td title=\"" + (r.value || "") + "\">" + v + "</td><td>" + r.coins + "</td><td>" + r.live + "</td></tr>";
}); });
html += "</tbody></table>"; html += "</tbody></table>";
box.innerHTML = html; box.innerHTML = html;

View file

@ -811,19 +811,36 @@ sudo apt-get install -y taler-wallet-cli</pre>
</p> </p>
</footer> </footer>
</main> </main>
<script src="/intro/goa-amount.js"></script>
<script> <script>
(function () { (function () {
function set(id, v) { function set(id, v) {
var el = document.getElementById(id); var el = document.getElementById(id);
if (el) el.textContent = v == null || v === "" ? "—" : String(v); if (el) el.textContent = v == null || v === "" ? "—" : String(v);
} }
function setAmt(id, rowOrAmt, altMap) {
var el = document.getElementById(id);
if (!el) return;
if (window.GoaAmount) {
var f = GoaAmount.pick(rowOrAmt, altMap);
el.textContent = f.display;
if (f.title && f.title !== f.display) el.setAttribute("title", f.title);
else el.removeAttribute("title");
} else {
var raw = rowOrAmt && typeof rowOrAmt === "object"
? (rowOrAmt.amount_alt || rowOrAmt.amount_paid_sum_alt || rowOrAmt.amount_sum_alt
|| rowOrAmt.amount_paid_sum || rowOrAmt.amount_sum || rowOrAmt.amount || "—")
: (rowOrAmt == null || rowOrAmt === "" ? "—" : String(rowOrAmt));
el.textContent = raw;
}
}
function msLabel(ms, http) { function msLabel(ms, http) {
if (ms == null || ms === "") return "—"; if (ms == null || ms === "") return "—";
var s = ms + " ms"; var s = ms + " ms";
if (http && http !== "200") s += " (" + http + ")"; if (http && http !== "200") s += " (" + http + ")";
return s; return s;
} }
function fillCurrency(prefix, c) { function fillCurrency(prefix, c, altMap) {
if (!c) { if (!c) {
set(prefix + "-paid-amt", "—"); set(prefix + "-paid-amt", "—");
set(prefix + "-all-amt", "—"); set(prefix + "-all-amt", "—");
@ -833,8 +850,16 @@ sudo apt-get install -y taler-wallet-cli</pre>
set(prefix + "-unpaid", "—"); set(prefix + "-unpaid", "—");
return; return;
} }
set(prefix + "-paid-amt", c.amount_paid_sum || "—"); setAmt(prefix + "-paid-amt", {
set(prefix + "-all-amt", c.amount_sum || "—"); amount: c.amount_paid_sum,
amount_alt: c.amount_paid_sum_alt,
amount_full: c.amount_paid_sum_full
}, altMap);
setAmt(prefix + "-all-amt", {
amount: c.amount_sum,
amount_alt: c.amount_sum_alt,
amount_full: c.amount_sum_full
}, altMap);
set(prefix + "-orders", c.contracts); set(prefix + "-orders", c.contracts);
set(prefix + "-paid", c.paid); set(prefix + "-paid", c.paid);
set(prefix + "-wired", c.wired); set(prefix + "-wired", c.wired);
@ -851,6 +876,7 @@ sudo apt-get install -y taler-wallet-cli</pre>
.then(function (r) { if (!r.ok) throw new Error("http"); return r.json(); }) .then(function (r) { if (!r.ok) throw new Error("http"); return r.json(); })
.then(function (d) { .then(function (d) {
if (!d || !d.ok) throw new Error("bad"); if (!d || !d.ok) throw new Error("bad");
if (window.GoaAmount) GoaAmount.rememberAlt(d);
set("st-instances", d.instances); set("st-instances", d.instances);
set("st-orders", d.orders); set("st-orders", d.orders);
set("st-paid", d.paid); set("st-paid", d.paid);
@ -862,8 +888,8 @@ sudo apt-get install -y taler-wallet-cli</pre>
(d.by_currency || []).forEach(function (c) { (d.by_currency || []).forEach(function (c) {
by[(c.currency || "").toUpperCase()] = c; by[(c.currency || "").toUpperCase()] = c;
}); });
fillCurrency("goa", by.GOA); fillCurrency("goa", by.GOA, d.alt_unit_names);
fillCurrency("chf", by.CHF); fillCurrency("chf", by.CHF, d.alt_unit_names);
function fillActivityList(el, acts) { function fillActivityList(el, acts) {
if (!el) return; if (!el) return;
@ -878,10 +904,15 @@ sudo apt-get install -y taler-wallet-cli</pre>
var li = document.createElement("li"); var li = document.createElement("li");
li.className = "act-item"; li.className = "act-item";
// no merchant names / ids — amount, optional summary, time only // no merchant names / ids — amount, optional summary, time only
var amtShow = a.amount_alt || a.amount || "—";
if (window.GoaAmount && !a.amount_alt) {
amtShow = GoaAmount.format(a.amount, d.alt_unit_names).display;
}
var amtTitle = a.amount_full || a.amount || "";
li.innerHTML = li.innerHTML =
'<span class="act-kind ' + esc(kind) + '">' + esc(kind) + "</span>" + '<span class="act-kind ' + esc(kind) + '">' + esc(kind) + "</span>" +
'<div class="act-main">' + '<div class="act-main">' +
'<div class="act-amt">' + esc(a.amount || "—") + "</div>" + '<div class="act-amt" title="' + esc(amtTitle) + '">' + esc(amtShow) + "</div>" +
(a.summary (a.summary
? '<div class="act-sum">' + esc(a.summary) + "</div>" ? '<div class="act-sum">' + esc(a.summary) + "</div>"
: "") + : "") +

View file

@ -0,0 +1,163 @@
/* Compact GOA / CUR:amount display using currency alt_unit_names.
* Large values "1.23 Mega-GOA" (title = full GOA:n) so landing tiles fit.
*/
(function (global) {
"use strict";
var DEFAULT_ALT = {
"24": "Yotta-GOA",
"21": "Zetta-GOA",
"18": "Exa-GOA",
"15": "Peta-GOA",
"12": "Tera-GOA",
"9": "Giga-GOA",
"6": "Mega-GOA",
"3": "Kilo-GOA",
"0": "GOA",
"-1": "Deci-GOA",
"-2": "Centi-GOA",
"-3": "Milli-GOA",
"-6": "Micro-GOA",
"-7": "Deci-Micro-GOA",
"-8": "Atomic-GOA"
};
var THRESHOLD = 1000;
function parseAmount(s) {
if (s == null || s === "") return { cur: "GOA", val: 0 };
if (typeof s === "number") return { cur: "GOA", val: s };
var t = String(s).trim();
var i = t.indexOf(":");
if (i < 0) return { cur: "GOA", val: parseFloat(t) || 0 };
return {
cur: t.slice(0, i) || "GOA",
val: parseFloat(t.slice(i + 1)) || 0
};
}
function fmtCoeff(v) {
if (!isFinite(v)) return "?";
var a = Math.abs(v);
if (Math.abs(a - Math.round(a)) < 1e-9) return String(Math.round(v));
var s = v.toFixed(4).replace(/\.?0+$/, "");
return s;
}
function baseStr(cur, val) {
if (!isFinite(val)) return String(cur) + ":?";
if (Math.abs(val - Math.round(val)) < 1e-9) return cur + ":" + Math.round(val);
return cur + ":" + String(val).replace(/(\.\d*?[1-9])0+$/, "$1").replace(/\.0+$/, "");
}
/**
* @param {string|number} amount
* @param {object} [altMap]
* @returns {{ display: string, title: string, raw: string, usedAlt: boolean }}
*/
function format(amount, altMap) {
if (amount == null || amount === "") {
return { display: "—", title: "", raw: "", usedAlt: false };
}
// Prefer server-provided alt if caller passes object {amount, amount_alt}
if (typeof amount === "object" && amount) {
if (amount.amount_alt) {
var rawO = amount.amount || amount.amount_full || String(amount.amount_alt);
return {
display: String(amount.amount_alt),
title: amount.amount_full || rawO,
raw: String(rawO),
usedAlt: true
};
}
amount = amount.amount || amount.amount_full || "";
}
var alt = altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT;
var p = parseAmount(amount);
var raw = baseStr(p.cur, p.val);
if (p.val === 0) {
var zname = (alt && alt["0"]) || p.cur;
return { display: "0 " + zname, title: raw, raw: raw, usedAlt: false };
}
if (Math.abs(p.val) < THRESHOLD) {
return { display: raw, title: raw, raw: raw, usedAlt: false };
}
var scales = [];
Object.keys(alt || {}).forEach(function (k) {
var n = parseInt(k, 10);
if (!isNaN(n)) scales.push({ sc: n, name: String(alt[k]) });
});
scales.sort(function (a, b) { return b.sc - a.sc; });
var abs = Math.abs(p.val);
var chosen = null;
for (var i = 0; i < scales.length; i++) {
var unit = Math.pow(10, scales[i].sc);
if (!(unit > 0)) continue;
var coeff = abs / unit;
if (coeff >= 1) {
chosen = {
sc: scales[i].sc,
name: scales[i].name,
coeff: p.val >= 0 ? coeff : -coeff
};
break;
}
}
if (!chosen || chosen.sc === 0) {
return { display: raw, title: raw, raw: raw, usedAlt: false };
}
var short = fmtCoeff(chosen.coeff) + " " + chosen.name;
return {
display: short,
title: short + " (" + raw + ")",
raw: raw,
usedAlt: true
};
}
function setText(el, amount, altMap) {
if (!el) return;
var f = format(amount, altMap);
el.textContent = f.display;
if (f.title && f.title !== f.display) el.setAttribute("title", f.title);
else el.removeAttribute("title");
}
function setById(id, amount, altMap) {
setText(document.getElementById(id), amount, altMap);
}
/** Prefer amount_alt from stats row, else format amount. */
function pick(rowOrAmount, altMap) {
if (rowOrAmount && typeof rowOrAmount === "object") {
if (rowOrAmount.amount_alt) {
return format({
amount: rowOrAmount.amount,
amount_alt: rowOrAmount.amount_alt,
amount_full: rowOrAmount.amount_full
}, altMap);
}
return format(rowOrAmount.amount || rowOrAmount.total_amount, altMap);
}
return format(rowOrAmount, altMap);
}
function rememberAlt(stats) {
if (stats && stats.alt_unit_names && typeof stats.alt_unit_names === "object") {
global.__GOA_ALT_UNITS = stats.alt_unit_names;
}
}
global.GoaAmount = {
format: format,
setText: setText,
setById: setById,
pick: pick,
rememberAlt: rememberAlt,
DEFAULT_ALT: DEFAULT_ALT,
THRESHOLD: THRESHOLD
};
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -30,6 +30,22 @@
if (el) el.textContent = v == null || v === "" ? "—" : String(v); if (el) el.textContent = v == null || v === "" ? "—" : String(v);
} }
function setAmt(id, rowOrAmt, altMap) {
var el = document.getElementById(id);
if (!el) return;
if (window.GoaAmount) {
var f = GoaAmount.pick(rowOrAmt, altMap);
el.textContent = f.display;
if (f.title && f.title !== f.display) el.setAttribute("title", f.title);
else el.removeAttribute("title");
} else {
var raw = rowOrAmt && typeof rowOrAmt === "object"
? (rowOrAmt.amount_alt || rowOrAmt.amount || "—")
: (rowOrAmt == null || rowOrAmt === "" ? "—" : String(rowOrAmt));
el.textContent = raw;
}
}
function showFallback(msg) { function showFallback(msg) {
var box = document.getElementById("stats"); var box = document.getElementById("stats");
var foot = document.getElementById("st-foot"); var foot = document.getElementById("st-foot");
@ -68,6 +84,7 @@
}) })
.then(function (d) { .then(function (d) {
if (!d || !d.ok) throw new Error("bad stats"); if (!d || !d.ok) throw new Error("bad stats");
if (window.GoaAmount) GoaAmount.rememberAlt(d);
var w = d.withdraws || {}; var w = d.withdraws || {};
var h24 = w.last_24h || {}; var h24 = w.last_24h || {};
var h7 = w.last_7d || {}; var h7 = w.last_7d || {};
@ -78,10 +95,10 @@
var fwd = flow.withdraw || {}; var fwd = flow.withdraw || {};
set("st-accounts", ba.total != null ? ba.total : "—"); set("st-accounts", ba.total != null ? ba.total : "—");
set("st-wallets", wl.unique_reserves != null ? wl.unique_reserves : "—"); set("st-wallets", wl.unique_reserves != null ? wl.unique_reserves : "—");
set("st-incoming", fin.amount || flow.total_in || "—"); setAmt("st-incoming", fin.amount_alt ? fin : (fin.amount || flow.total_in || "—"), d.alt_unit_names);
set("st-withdraw", fwd.amount || w.total_amount || "—"); setAmt("st-withdraw", fwd.amount_alt ? fwd : (fwd.amount || w.total_amount || "—"), d.alt_unit_names);
set("st-24h", h24.amount || "GOA:0"); setAmt("st-24h", h24.amount_alt ? h24 : (h24.amount || "GOA:0"), d.alt_unit_names);
set("st-7d", h7.amount || "GOA:0"); setAmt("st-7d", h7.amount_alt ? h7 : (h7.amount || "GOA:0"), d.alt_unit_names);
var list = document.getElementById("st-recent"); var list = document.getElementById("st-recent");
if (list) { if (list) {
@ -97,7 +114,13 @@
var li = document.createElement("li"); var li = document.createElement("li");
var amt = document.createElement("span"); var amt = document.createElement("span");
amt.className = "amt"; amt.className = "amt";
amt.textContent = row.amount || "?"; if (window.GoaAmount) {
var af = GoaAmount.pick(row, d.alt_unit_names);
amt.textContent = af.display;
if (af.title && af.title !== af.display) amt.setAttribute("title", af.title);
} else {
amt.textContent = row.amount_alt || row.amount || "?";
}
var meta = document.createElement("span"); var meta = document.createElement("span");
meta.className = "meta"; meta.className = "meta";
meta.textContent = fmtCest(row.at_unix, row.at || row.at_iso) || ""; meta.textContent = fmtCest(row.at_unix, row.at || row.at_iso) || "";
@ -112,7 +135,8 @@
foot.className = "stats-foot"; foot.className = "stats-foot";
foot.innerHTML = foot.innerHTML =
'Source: <a href="' + BANK_INTRO + '">bank.hacktivism.ch</a>' + 'Source: <a href="' + BANK_INTRO + '">bank.hacktivism.ch</a>' +
(d.generated_at_human ? " · " + d.generated_at_human : ""); (d.generated_at_human ? " · " + d.generated_at_human : "") +
(d.source ? " · " + d.source : "");
} }
}) })
.catch(function () { .catch(function () {

View file

@ -16,13 +16,16 @@ BANK_USER="${BANK_USER:-explorer}"
ADMIN_USER="${ADMIN_USER:-admin}" ADMIN_USER="${ADMIN_USER:-admin}"
# Per-account transaction window (libeufin delta). Was -100 → systematically # Per-account transaction window (libeufin delta). Was -100 → systematically
# undercounted credits/withdraws on active accounts (broken public stats). # undercounted credits/withdraws on active accounts (broken public stats).
TX_DELTA="${TX_DELTA:--50000}" # Prefer the host collector (hernani systemd taler-landing-stats) for full
# pagination; these defaults keep the in-container fallback deep enough.
TX_DELTA="${TX_DELTA:--200000}"
# Max accounts to list + scan (was 80 → missed later accounts; bank has 100+). # Max accounts to list + scan (was 80 → missed later accounts; bank has 100+).
MAX_SCAN_ACCOUNTS="${MAX_SCAN_ACCOUNTS:-500}" # 0 or empty → no head limit (scan every listed account).
MAX_SCAN_ACCOUNTS="${MAX_SCAN_ACCOUNTS:-0}"
# Account-list page size for GET /accounts?delta=… (must cover all users) # Account-list page size for GET /accounts?delta=… (must cover all users)
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--500}" ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}"
# curl timeout per account (deeper history needs more headroom) # curl timeout per account (deeper history needs more headroom)
TX_CURL_TIMEOUT="${TX_CURL_TIMEOUT:-25}" TX_CURL_TIMEOUT="${TX_CURL_TIMEOUT:-45}"
export TZ="${TZ:-Europe/Zurich}" export TZ="${TZ:-Europe/Zurich}"
PASS="${BANK_PASS:-}" PASS="${BANK_PASS:-}"
@ -218,15 +221,23 @@ fi
SCAN_OK=0 SCAN_OK=0
SCAN_EMPTY=0 SCAN_EMPTY=0
SCAN_FAIL=0 SCAN_FAIL=0
# Scan customer accounts only (not exchange/admin — exchange credits would double-count) # Scan ALL accounts except exchange (withdraw debits live on customers + admin
# top-ups; exchange credits would double-count the same GOA).
# Prefer host collect_bank_stats.py (hernani timer) for full pagination.
{ {
echo "$BANK_USER" echo "$BANK_USER"
grep -Eve "^(admin|exchange|${BANK_USER})$" "$WORKDIR/usernames.txt" 2>/dev/null || true # include admin + every auto-account; drop only exchange
} | awk 'NF && !seen[$0]++' | head -n "$MAX_SCAN_ACCOUNTS" >"$WORKDIR/scan-users.txt" grep -Eve "^(exchange|${BANK_USER})$" "$WORKDIR/usernames.txt" 2>/dev/null || true
} | awk 'NF && !seen[$0]++' >"$WORKDIR/scan-users.all"
if [ -n "${MAX_SCAN_ACCOUNTS}" ] && [ "${MAX_SCAN_ACCOUNTS}" -gt 0 ] 2>/dev/null; then
head -n "$MAX_SCAN_ACCOUNTS" "$WORKDIR/scan-users.all" >"$WORKDIR/scan-users.txt"
else
cp "$WORKDIR/scan-users.all" "$WORKDIR/scan-users.txt"
fi
while IFS= read -r uname; do while IFS= read -r uname; do
[ -n "$uname" ] || continue [ -n "$uname" ] || continue
case "$uname" in admin|exchange) continue ;; esac case "$uname" in exchange) continue ;; esac
# Safe filename (usernames are mostly [A-Za-z0-9_-]) # Safe filename (usernames are mostly [A-Za-z0-9_-])
safe=$(printf '%s' "$uname" | tr -c 'A-Za-z0-9._-' '_') safe=$(printf '%s' "$uname" | tr -c 'A-Za-z0-9._-' '_')
code=$(curl -sS -m "${TX_CURL_TIMEOUT}" -o "$WORKDIR/tx-${safe}.json" -w '%{http_code}' \ code=$(curl -sS -m "${TX_CURL_TIMEOUT}" -o "$WORKDIR/tx-${safe}.json" -w '%{http_code}' \
@ -568,7 +579,7 @@ cat >"$TMP" <<EOF
"total_in_value": ${TOTAL_IN_N:-0}, "total_in_value": ${TOTAL_IN_N:-0},
"total_out": $(json_str "$TOTAL_OUT_AMT"), "total_out": $(json_str "$TOTAL_OUT_AMT"),
"total_out_value": ${TOTAL_OUT_N:-0}, "total_out_value": ${TOTAL_OUT_N:-0},
"note": "incoming=credits; withdraw=Taler withdrawal debits; excl. admin+exchange accounts" "note": "incoming=credits; withdraw=Taler withdrawal debits; excl. exchange only (admin+explorer+all users scanned)"
}, },
"withdraws": { "withdraws": {
"count": ${N_WD:-0}, "count": ${N_WD:-0},

23
scripts/taler-landing/deploy-landings.sh Normal file → Executable file
View file

@ -14,10 +14,31 @@ ROOT=$(cd "$(dirname "$0")/../.." && pwd)
# allow override when files already on host /tmp # allow override when files already on host /tmp
SRC_EX="${SRC_EX:-$ROOT/configs/exchange-landing}" SRC_EX="${SRC_EX:-$ROOT/configs/exchange-landing}"
SRC_MER="${SRC_MER:-$ROOT/configs/merchant-landing}" SRC_MER="${SRC_MER:-$ROOT/configs/merchant-landing}"
SRC_BANK="${SRC_BANK:-$ROOT/configs/bank-landing}"
SRC_QR="${SRC_QR:-$ROOT/configs/bank-landing/qrcode.min.js}" SRC_QR="${SRC_QR:-$ROOT/configs/bank-landing/qrcode.min.js}"
SRC_GOA_AMT="${SRC_GOA_AMT:-$ROOT/configs/shared/goa-amount.js}"
C_EX=taler-hacktivism-exchange-ansible C_EX=taler-hacktivism-exchange-ansible
C_MER=taler-hacktivism C_MER=taler-hacktivism
C_BANK="${C_BANK:-taler-hacktivism-bank}"
copy_goa_amount() {
local ctr="$1" dest="$2"
if [ -f "$SRC_GOA_AMT" ]; then
podman cp "$SRC_GOA_AMT" "${ctr}:${dest}/goa-amount.js"
fi
}
echo "=== bank landing → $C_BANK :9013 (html + goa-amount) ==="
if podman inspect -f '{{.State.Running}}' "$C_BANK" 2>/dev/null | grep -qx true; then
podman exec "$C_BANK" mkdir -p /var/www/bank-landing
if [ -f "$SRC_BANK/index.html" ]; then
podman cp "$SRC_BANK/index.html" "$C_BANK:/var/www/bank-landing/index.html"
fi
copy_goa_amount "$C_BANK" /var/www/bank-landing
else
echo "WARN: $C_BANK not running — skip bank landing files"
fi
echo "=== exchange landing → $C_EX :9014 ===" echo "=== exchange landing → $C_EX :9014 ==="
# nginx may be missing on exchange image # nginx may be missing on exchange image
@ -30,6 +51,7 @@ podman cp "$SRC_EX/index.html" "$C_EX:/var/www/exchange-landing/index.html"
if [ -f "$SRC_QR" ]; then if [ -f "$SRC_QR" ]; then
podman cp "$SRC_QR" "$C_EX:/var/www/exchange-landing/qrcode.min.js" podman cp "$SRC_QR" "$C_EX:/var/www/exchange-landing/qrcode.min.js"
fi fi
copy_goa_amount "$C_EX" /var/www/exchange-landing
podman cp "$SRC_EX/nginx-landing.conf" "$C_EX:/etc/nginx/sites-available/exchange-landing" podman cp "$SRC_EX/nginx-landing.conf" "$C_EX:/etc/nginx/sites-available/exchange-landing"
podman exec "$C_EX" bash -c ' podman exec "$C_EX" bash -c '
ln -sfn /etc/nginx/sites-available/exchange-landing /etc/nginx/sites-enabled/exchange-landing ln -sfn /etc/nginx/sites-available/exchange-landing /etc/nginx/sites-enabled/exchange-landing
@ -50,6 +72,7 @@ podman exec "$C_EX" bash -c '
echo "=== merchant landing → $C_MER :9015 ===" echo "=== merchant landing → $C_MER :9015 ==="
podman exec "$C_MER" mkdir -p /var/www/merchant-landing podman exec "$C_MER" mkdir -p /var/www/merchant-landing
podman cp "$SRC_MER/index.html" "$C_MER:/var/www/merchant-landing/index.html" podman cp "$SRC_MER/index.html" "$C_MER:/var/www/merchant-landing/index.html"
copy_goa_amount "$C_MER" /var/www/merchant-landing
podman cp "$SRC_MER/nginx-landing.conf" "$C_MER:/etc/nginx/sites-available/merchant-landing" podman cp "$SRC_MER/nginx-landing.conf" "$C_MER:/etc/nginx/sites-available/merchant-landing"
podman exec "$C_MER" bash -c ' podman exec "$C_MER" bash -c '
ln -sfn /etc/nginx/sites-available/merchant-landing /etc/nginx/sites-enabled/merchant-landing ln -sfn /etc/nginx/sites-available/merchant-landing /etc/nginx/sites-enabled/merchant-landing