landing: harden GOA alt display for all three sites

Ship goa-amount.js inside each landing tree, add an inline fallback when
the asset is not yet deployed, keep CHF free of GOA unit names, fix
exchange amount_full wiring, and add test-landing-stats.sh (local + live).
This commit is contained in:
Hernâni Marques 2026-07-17 08:48:00 +02:00
parent b65018cd0b
commit 9e0f85024b
No known key found for this signature in database
10 changed files with 796 additions and 16 deletions

View file

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

View file

@ -1493,6 +1493,33 @@ tw run-until-done &amp;&amp; tw balance</pre>
<script src="/intro/goa-amount.js"></script> <script src="/intro/goa-amount.js"></script>
<script> <script>
/* Fallback if goa-amount.js not yet deployed (uses amount_alt from stats.json) */
(function () {
if (window.GoaAmount) return;
window.GoaAmount = {
rememberAlt: function () {},
format: function (a) {
var s = a == null || a === "" ? "—" : String(a);
return { display: s, title: s === "—" ? "" : s, raw: s, usedAlt: false };
},
pick: function (row) {
if (row && typeof row === "object") {
if (row.amount_alt) {
return {
display: String(row.amount_alt),
title: String(row.amount_full || row.amount || row.amount_alt),
raw: String(row.amount || ""),
usedAlt: true
};
}
return this.format(row.amount || row.total_amount || "—");
}
return this.format(row);
}
};
})();
</script>
<script>
(function () { (function () {
var WEBUI = "https://bank.hacktivism.ch/webui/"; var WEBUI = "https://bank.hacktivism.ch/webui/";
/* Secondary: own-account path (register first) */ /* Secondary: own-account path (register first) */

View file

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

View file

@ -446,6 +446,33 @@ sudo apt-get install -y taler-wallet-cli</pre>
</main> </main>
<script src="/intro/goa-amount.js"></script> <script src="/intro/goa-amount.js"></script>
<script> <script>
/* Fallback if goa-amount.js not yet deployed (uses amount_alt from stats.json) */
(function () {
if (window.GoaAmount) return;
window.GoaAmount = {
rememberAlt: function () {},
format: function (a) {
var s = a == null || a === "" ? "—" : String(a);
return { display: s, title: s === "—" ? "" : s, raw: s, usedAlt: false };
},
pick: function (row) {
if (row && typeof row === "object") {
if (row.amount_alt) {
return {
display: String(row.amount_alt),
title: String(row.amount_full || row.amount || row.amount_alt),
raw: String(row.amount || ""),
usedAlt: true
};
}
return this.format(row.amount || row.total_amount || "—");
}
return this.format(row);
}
};
})();
</script>
<script>
(function () { (function () {
function set(id, v) { function set(id, v) {
var el = document.getElementById(id); var el = document.getElementById(id);
@ -454,17 +481,21 @@ sudo apt-get install -y taler-wallet-cli</pre>
function setAmt(id, amount, altKey, d) { function setAmt(id, amount, altKey, d) {
var el = document.getElementById(id); var el = document.getElementById(id);
if (!el) return; if (!el) return;
var row = null; var row = { amount: amount };
if (altKey && d && d[altKey]) { if (altKey && d && d[altKey]) {
row = { amount: amount, amount_alt: d[altKey], amount_full: d[altKey.replace(/_alt$/, "_full")] || d[amount + "_full"] }; row.amount_alt = d[altKey];
var fullKey = String(altKey).replace(/_alt$/, "_full");
if (d[fullKey]) row.amount_full = d[fullKey];
} }
if (window.GoaAmount) { if (window.GoaAmount) {
var f = row ? GoaAmount.pick(row, d && d.alt_unit_names) : GoaAmount.format(amount, d && d.alt_unit_names); var f = (row.amount_alt || (d && d.alt_unit_names))
? GoaAmount.pick(row, d && d.alt_unit_names)
: GoaAmount.format(amount, d && d.alt_unit_names);
el.textContent = f.display; el.textContent = f.display;
if (f.title && f.title !== f.display) el.setAttribute("title", f.title); if (f.title && f.title !== f.display) el.setAttribute("title", f.title);
else el.removeAttribute("title"); else el.removeAttribute("title");
} else { } else {
el.textContent = (row && row.amount_alt) || amount || "—"; el.textContent = row.amount_alt || amount || "—";
} }
} }
function msLabel(ms, http) { function msLabel(ms, http) {

View file

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

View file

@ -813,6 +813,33 @@ sudo apt-get install -y taler-wallet-cli</pre>
</main> </main>
<script src="/intro/goa-amount.js"></script> <script src="/intro/goa-amount.js"></script>
<script> <script>
/* Fallback if goa-amount.js not yet deployed (uses amount_alt from stats.json) */
(function () {
if (window.GoaAmount) return;
window.GoaAmount = {
rememberAlt: function () {},
format: function (a) {
var s = a == null || a === "" ? "—" : String(a);
return { display: s, title: s === "—" ? "" : s, raw: s, usedAlt: false };
},
pick: function (row) {
if (row && typeof row === "object") {
if (row.amount_alt) {
return {
display: String(row.amount_alt),
title: String(row.amount_full || row.amount || row.amount_alt),
raw: String(row.amount || ""),
usedAlt: true
};
}
return this.format(row.amount || row.amount_paid_sum || row.amount_sum || "—");
}
return this.format(row);
}
};
})();
</script>
<script>
(function () { (function () {
function set(id, v) { function set(id, v) {
var el = document.getElementById(id); var el = document.getElementById(id);

View file

@ -76,9 +76,15 @@
var alt = altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT; var alt = altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT;
var p = parseAmount(amount); var p = parseAmount(amount);
var raw = baseStr(p.cur, p.val); var raw = baseStr(p.cur, p.val);
// Foreign currencies must not pick up Kilo-GOA / Mega-GOA labels
var baseName = (alt && alt["0"]) || p.cur;
var curU = String(p.cur || "").toUpperCase();
var baseU = String(baseName || "").toUpperCase();
if (curU && curU !== "GOA" && (baseU === "GOA" || baseU.indexOf("GOA") !== -1)) {
return { display: raw, title: raw, raw: raw, usedAlt: false };
}
if (p.val === 0) { if (p.val === 0) {
var zname = (alt && alt["0"]) || p.cur; return { display: raw, title: raw, raw: raw, usedAlt: false };
return { display: "0 " + zname, title: raw, raw: raw, usedAlt: false };
} }
if (Math.abs(p.val) < THRESHOLD) { if (Math.abs(p.val) < THRESHOLD) {
return { display: raw, title: raw, raw: raw, usedAlt: false }; return { display: raw, title: raw, raw: raw, usedAlt: false };

View file

@ -22,10 +22,26 @@ C_EX=taler-hacktivism-exchange-ansible
C_MER=taler-hacktivism C_MER=taler-hacktivism
C_BANK="${C_BANK:-taler-hacktivism-bank}" C_BANK="${C_BANK:-taler-hacktivism-bank}"
# Prefer per-landing copy, then shared (keeps bank/exchange/merchant in sync)
resolve_goa_amount() {
local local_copy="$1"
if [ -f "$local_copy" ]; then
printf '%s' "$local_copy"
elif [ -f "$SRC_GOA_AMT" ]; then
printf '%s' "$SRC_GOA_AMT"
else
printf ''
fi
}
copy_goa_amount() { copy_goa_amount() {
local ctr="$1" dest="$2" local ctr="$1" dest="$2" src="$3"
if [ -f "$SRC_GOA_AMT" ]; then src=$(resolve_goa_amount "$src")
podman cp "$SRC_GOA_AMT" "${ctr}:${dest}/goa-amount.js" if [ -n "$src" ] && [ -f "$src" ]; then
podman cp "$src" "${ctr}:${dest}/goa-amount.js"
echo " goa-amount.js ← $src"
else
echo " WARN: goa-amount.js missing for $ctr"
fi fi
} }
@ -35,7 +51,7 @@ if podman inspect -f '{{.State.Running}}' "$C_BANK" 2>/dev/null | grep -qx true;
if [ -f "$SRC_BANK/index.html" ]; then if [ -f "$SRC_BANK/index.html" ]; then
podman cp "$SRC_BANK/index.html" "$C_BANK:/var/www/bank-landing/index.html" podman cp "$SRC_BANK/index.html" "$C_BANK:/var/www/bank-landing/index.html"
fi fi
copy_goa_amount "$C_BANK" /var/www/bank-landing copy_goa_amount "$C_BANK" /var/www/bank-landing "$SRC_BANK/goa-amount.js"
else else
echo "WARN: $C_BANK not running — skip bank landing files" echo "WARN: $C_BANK not running — skip bank landing files"
fi fi
@ -51,7 +67,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 copy_goa_amount "$C_EX" /var/www/exchange-landing "$SRC_EX/goa-amount.js"
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
@ -72,7 +88,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 copy_goa_amount "$C_MER" /var/www/merchant-landing "$SRC_MER/goa-amount.js"
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

View file

@ -102,12 +102,24 @@ def format_amount_alt(
value_f = 0.0 value_f = 0.0
base_name = alt.get("0") or cur base_name = alt.get("0") or cur
if val == 0: # Never rename foreign currencies with a GOA alt map (merchant dual CHF+GOA).
disp = f"0 {base_name}" cur_u = (cur or "").upper()
base_u = str(base_name).upper()
if cur_u and cur_u != "GOA" and (base_u == "GOA" or base_u.endswith("-GOA") or "GOA" in base_u):
return { return {
"amount": base, "amount": base,
"amount_alt": disp, "amount_alt": base,
"amount_full": disp, "amount_full": base,
"value": value_f,
"value_str": value_str,
}
if val == 0:
# Keep canonical CUR:0 (avoids "0 GOA" for empty foreign balances)
return {
"amount": base,
"amount_alt": base,
"amount_full": base,
"value": 0.0, "value": 0.0,
"value_str": "0", "value_str": "0",
} }

View file

@ -0,0 +1,154 @@
#!/usr/bin/env bash
# Outside-in checks for bank / exchange / merchant landing stats + alt UI assets.
# Usage: ./test-landing-stats.sh
# Exit 0 only if all critical checks pass.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
PY="${PYTHON:-python3}"
BANK="${BANK_PUBLIC:-https://bank.hacktivism.ch}"
EX="${EXCHANGE_PUBLIC:-https://exchange.hacktivism.ch}"
MER="${MERCHANT_PUBLIC:-https://taler.hacktivism.ch}"
fail=0
pass=0
note() { printf ' %s\n' "$*"; }
ok() { pass=$((pass + 1)); printf ' [OK] %s\n' "$*"; }
bad() { fail=$((fail + 1)); printf ' [FAIL] %s\n' "$*"; }
http_code() {
curl -skS -m 15 -o /tmp/landing-test.body -w '%{http_code}' "$1" 2>/dev/null || echo 000
}
echo "=== local repo checks ==="
for f in \
configs/shared/goa-amount.js \
configs/bank-landing/goa-amount.js \
configs/exchange-landing/goa-amount.js \
configs/merchant-landing/goa-amount.js \
configs/bank-landing/index.html \
configs/exchange-landing/index.html \
configs/merchant-landing/index.html \
scripts/taler-landing/goa_amounts.py \
scripts/taler-landing/collect_bank_stats.py
do
if [ -f "$ROOT/$f" ]; then ok "file $f"
else bad "missing $f"
fi
done
for html in bank-landing exchange-landing merchant-landing; do
if grep -q 'goa-amount.js' "$ROOT/configs/$html/index.html" \
&& grep -q 'GoaAmount' "$ROOT/configs/$html/index.html"; then
ok "$html index wires GoaAmount"
else
bad "$html index missing goa-amount / GoaAmount"
fi
if grep -q 'Fallback if goa-amount.js' "$ROOT/configs/$html/index.html"; then
ok "$html has offline fallback"
else
bad "$html missing offline GoaAmount fallback"
fi
done
echo
echo "=== unit: goa_amounts enrich (all three shapes) ==="
if "$PY" - <<PY
import sys
sys.path.insert(0, "$ROOT/scripts/taler-landing")
from goa_amounts import enrich_stats_tree, DEFAULT_ALT
bank = {
"ok": True,
"balance_explorer": "GOA:40614.67",
"withdraws": {"total_amount": "GOA:120944.66", "last_24h": {"amount": "GOA:9494.66"}},
"flow": {"withdraw": {"amount": "GOA:120944.66"}, "incoming": {"amount": "GOA:1000"}},
"recent_withdraws": [{"amount": "GOA:4503599627370495"}],
}
enrich_stats_tree(bank, DEFAULT_ALT)
assert "Kilo-GOA" in bank["withdraws"]["total_amount_alt"]
assert "Peta-GOA" in bank["recent_withdraws"][0]["amount_alt"]
ex = {
"ok": True,
"wire_in_amount": "GOA:1267008731299764.85",
"withdraw_amount": "GOA:1783661.57",
"coins_remaining_amount": "GOA:0",
"by_denom": [{"value": "GOA:1000"}],
}
enrich_stats_tree(ex, DEFAULT_ALT)
assert "Peta-GOA" in ex["wire_in_amount_alt"]
assert "Mega-GOA" in ex["withdraw_amount_alt"]
assert ex["coins_remaining_amount_alt"] in ("GOA:0", "0 GOA")
mer = {
"ok": True,
"by_currency": [
{"currency": "GOA", "amount_sum": "GOA:294.05", "amount_paid_sum": "GOA:294.05"},
{"currency": "CHF", "amount_sum": "CHF:64000", "amount_paid_sum": "CHF:64000"},
],
"recent_activity_by_currency": [
{"currency": "GOA", "items": [{"amount": "GOA:2500"}]},
],
}
enrich_stats_tree(mer, DEFAULT_ALT)
# CHF must NOT become Kilo-GOA
assert "GOA" not in mer["by_currency"][1]["amount_paid_sum_alt"] or mer["by_currency"][1]["amount_paid_sum_alt"].startswith("CHF")
assert mer["by_currency"][1]["amount_paid_sum_alt"].startswith("CHF")
assert "Kilo-GOA" in mer["recent_activity_by_currency"][0]["items"][0]["amount_alt"]
print("unit ok")
PY
then ok "enrich shapes bank/exchange/merchant"
else bad "enrich unit failed"
fi
echo
echo "=== public HTTPS (live stack) ==="
for name_base in "bank|$BANK" "exchange|$EX" "merchant|$MER"; do
name="${name_base%%|*}"
base="${name_base#*|}"
echo "-- $name ($base) --"
code=$(http_code "$base/intro/")
if [ "$code" = "200" ]; then ok "$name /intro/ HTTP 200"
else bad "$name /intro/ HTTP $code"
fi
# live HTML may lag deploy — warn only if missing script after we expect it
if grep -q 'goa-amount.js' /tmp/landing-test.body 2>/dev/null; then
ok "$name live HTML references goa-amount.js"
else
note "[WARN] $name live HTML has no goa-amount.js yet (deploy pending)"
fi
code=$(http_code "$base/intro/stats.json")
if [ "$code" = "200" ] && "$PY" -c 'import json,sys; d=json.load(open("/tmp/landing-test.body")); sys.exit(0 if d.get("ok") else 1)'; then
gen=$("$PY" -c 'import json; d=json.load(open("/tmp/landing-test.body")); print(d.get("generated_at_human") or d.get("generated_at") or "?")')
ok "$name stats.json ok (gen=$gen)"
# enrich live payload
if "$PY" "$ROOT/scripts/taler-landing/enrich_stats_alt.py" /tmp/landing-test.body -o /tmp/landing-test.enriched.json >/dev/null 2>&1; then
ok "$name live stats enrichable with amount_alt"
else
bad "$name live stats enrich failed"
fi
else
bad "$name stats.json HTTP $code / not ok"
fi
code=$(http_code "$base/intro/stats-run.json")
if [ "$code" = "200" ]; then ok "$name stats-run.json HTTP 200"
else bad "$name stats-run.json HTTP $code"
fi
code=$(http_code "$base/intro/goa-amount.js")
if [ "$code" = "200" ] && grep -q 'GoaAmount' /tmp/landing-test.body; then
ok "$name goa-amount.js live"
else
note "[WARN] $name goa-amount.js not live yet (HTTP $code) — deploy-landings.sh needed"
fi
done
echo
echo "=== result: pass=$pass fail=$fail ==="
if [ "$fail" -gt 0 ]; then
exit 1
fi
exit 0