landing: kGOA units, full money stats, rootless resource display

Public bank/exchange/merchant landing stats and performance UI only.
Secret goa-ui overlay stays on local/merchant-secret-ui (never push).
This commit is contained in:
Hernâni Marques 2026-07-17 13:30:23 +02:00
parent bf8a915a01
commit fb1a0b0b8e
No known key found for this signature in database
GPG key ID: CB5738652768F7E9
12 changed files with 1046 additions and 333 deletions

View file

@ -1,29 +1,63 @@
/* Compact GOA / CUR:amount display using currency alt_unit_names.
* Large values "1.23 Mega-GOA" (title = full GOA:n) so landing tiles fit.
/* Compact GOA / CUR:amount display using SI-style alt units (kGOA, MGOA, ).
* Large values "1.23 MGOA" (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",
"24": "YGOA",
"21": "ZGOA",
"18": "EGOA",
"15": "PGOA",
"12": "TGOA",
"9": "GGOA",
"6": "MGOA",
"3": "kGOA",
"0": "GOA",
"-1": "Deci-GOA",
"-2": "Centi-GOA",
"-3": "Milli-GOA",
"-6": "Micro-GOA",
"-7": "Deci-Micro-GOA",
"-8": "Atomic-GOA"
"-1": "dGOA",
"-2": "cGOA",
"-3": "mGOA",
"-6": "µGOA",
"-7": "dµGOA",
"-8": "aGOA"
};
var LONG_TO_COMPACT = {
"yotta-goa": "YGOA",
"zetta-goa": "ZGOA",
"exa-goa": "EGOA",
"peta-goa": "PGOA",
"tera-goa": "TGOA",
"giga-goa": "GGOA",
"mega-goa": "MGOA",
"kilo-goa": "kGOA",
"goa": "GOA",
"deci-goa": "dGOA",
"centi-goa": "cGOA",
"milli-goa": "mGOA",
"micro-goa": "µGOA",
"deci-micro-goa": "dµGOA",
"atomic-goa": "aGOA"
};
var THRESHOLD = 1000;
function compactAlt(altMap) {
var out = {};
var src = altMap || DEFAULT_ALT;
Object.keys(DEFAULT_ALT).forEach(function (k) {
out[k] = DEFAULT_ALT[k];
});
Object.keys(src).forEach(function (k) {
var name = String(src[k] || "").trim();
var low = name.toLowerCase().replace(/\s+/g, "");
if (LONG_TO_COMPACT[low]) out[k] = LONG_TO_COMPACT[low];
else if (DEFAULT_ALT[k] && /goa/i.test(name)) out[k] = DEFAULT_ALT[k];
else out[k] = name || DEFAULT_ALT[k] || name;
});
return out;
}
function parseAmount(s) {
if (s == null || s === "") return { cur: "GOA", val: 0 };
if (typeof s === "number") return { cur: "GOA", val: s };
@ -51,7 +85,7 @@
}
/**
* @param {string|number} amount
* @param {string|number|object} amount
* @param {object} [altMap]
* @returns {{ display: string, title: string, raw: string, usedAlt: boolean }}
*/
@ -59,24 +93,17 @@
if (amount == null || amount === "") {
return { display: "—", title: "", raw: "", usedAlt: false };
}
// Prefer server-provided alt if caller passes object {amount, amount_alt}
// Prefer re-format from canonical amount (keeps kGOA even if server sent Kilo-GOA)
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
};
if (amount.amount) {
return format(amount.amount, altMap);
}
amount = amount.amount || amount.amount_full || "";
amount = amount.amount_full || amount.amount_alt || "";
}
var alt = altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT;
var alt = compactAlt(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();
@ -95,7 +122,9 @@
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; });
scales.sort(function (a, b) {
return b.sc - a.sc;
});
var abs = Math.abs(p.val);
var chosen = null;
@ -136,33 +165,80 @@
setText(document.getElementById(id), amount, altMap);
}
/** Prefer amount_alt from stats row, else format amount. */
/** Prefer formatting from amount field; fall back to amount_alt string. */
function pick(rowOrAmount, altMap) {
if (rowOrAmount && typeof rowOrAmount === "object") {
var a =
rowOrAmount.amount ||
rowOrAmount.total_amount ||
rowOrAmount.amount_paid_sum ||
rowOrAmount.amount_sum;
if (a) return format(a, altMap);
if (rowOrAmount.amount_alt) {
return format({
amount: rowOrAmount.amount,
amount_alt: rowOrAmount.amount_alt,
amount_full: rowOrAmount.amount_full
}, altMap);
return {
display: String(rowOrAmount.amount_alt),
title: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
raw: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
usedAlt: true
};
}
return format(rowOrAmount.amount || rowOrAmount.total_amount, altMap);
return format("", 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.__GOA_ALT_UNITS = compactAlt(stats.alt_unit_names);
}
}
/** Small legend HTML (positive scales). */
function legendHtml(stats) {
var alt = compactAlt((stats && stats.alt_unit_names) || global.__GOA_ALT_UNITS || DEFAULT_ALT);
var rows = [];
Object.keys(alt)
.map(function (k) {
return parseInt(k, 10);
})
.filter(function (n) {
return !isNaN(n) && n > 0;
})
.sort(function (a, b) {
return a - b;
})
.forEach(function (sc) {
rows.push(
"<tr><td>10<sup>" +
sc +
"</sup></td><td><strong>" +
alt[String(sc)] +
"</strong></td></tr>"
);
});
if (!rows.length) return "";
return (
'<table class="unit-legend" aria-label="GOA unit names"><thead><tr><th>×base</th><th>name</th></tr></thead><tbody>' +
rows.join("") +
"</tbody></table>"
);
}
function fillLegend(elOrId, stats) {
var el = typeof elOrId === "string" ? document.getElementById(elOrId) : elOrId;
if (!el) return;
el.innerHTML = legendHtml(stats);
}
global.GoaAmount = {
format: format,
setText: setText,
setById: setById,
pick: pick,
rememberAlt: rememberAlt,
compactAlt: compactAlt,
legendHtml: legendHtml,
fillLegend: fillLegend,
DEFAULT_ALT: DEFAULT_ALT,
THRESHOLD: THRESHOLD
};

View file

@ -88,6 +88,33 @@
.stat .v.sm { font-size: 0.92rem; }
.stats-foot { margin: 0.75rem 0 0; font-size: 0.72rem; color: var(--muted); text-align: center; }
.stats-foot.err { color: #f0a090; }
.unit-legend {
margin: 0.65rem auto 0;
border-collapse: collapse;
font-size: 0.68rem;
color: var(--muted);
}
.unit-legend th, .unit-legend td {
padding: 0.12rem 0.45rem;
border-bottom: 1px solid rgba(201, 184, 160, 0.12);
text-align: left;
}
.unit-legend th { font-weight: 700; font-size: 0.62rem; text-transform: uppercase; }
.unit-legend strong { color: #5eead4; }
.unit-legend-note { margin: 0.35rem 0 0; font-size: 0.68rem; color: var(--muted); text-align: center; }
.wd-list {
list-style: none; margin: 0.15rem 0 0; padding: 0;
display: flex; flex-direction: column; gap: 0.35rem;
}
.wd-list li {
display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.25rem 0.75rem;
font-size: 0.88rem; font-weight: 650; color: #f0d090; font-variant-numeric: tabular-nums;
border-bottom: 1px solid rgba(201, 184, 160, 0.12); padding-bottom: 0.3rem;
}
.wd-list li:last-child { border-bottom: none; padding-bottom: 0; }
.wd-list .amt { color: #5eead4; font-weight: 750; }
.wd-list .meta { color: var(--muted); font-size: 0.78rem; font-weight: 600; }
.stat.wide { grid-column: 1 / -1; }
.stats-run-note {
margin: 0.35rem 0 0; font-size: 0.7rem; color: #f0a090; text-align: center;
}
@ -277,8 +304,8 @@
<div class="stat"><span class="k">Live (remaining)</span><span class="v" id="st-coins-live"></span></div>
<div class="stat"><span class="k">Spent / zero</span><span class="v" id="st-coins-spent"></span></div>
<div class="stat"><span class="k">Remaining amt</span><span class="v teal sm" id="st-remaining"></span></div>
<div class="stat"><span class="k">Withdraw ops</span><span class="v" id="st-withdraw"></span></div>
<div class="stat"><span class="k">Withdraw amt</span><span class="v teal sm" id="st-withdraw-amt"></span></div>
<div class="stat"><span class="k">Withdraw ops</span><span class="v" id="st-withdraw-coins"></span></div>
<div class="stat"><span class="k">All withdraw amt</span><span class="v teal sm" id="st-withdraw-amt-coins"></span></div>
<div class="stat"><span class="k">Refresh</span><span class="v" id="st-refresh"></span></div>
<div class="stat"><span class="k">Coin deposits</span><span class="v" id="st-coin-dep"></span></div>
<div class="stat"><span class="k">Batch deposits</span><span class="v" id="st-batch-dep"></span></div>
@ -307,14 +334,22 @@
</section>
<section class="stats" id="stats-wire" aria-labelledby="wire-title">
<h2 id="wire-title">Reserves · wire</h2>
<p class="note">Backing money movement</p>
<h2 id="wire-title">Reserves · wire · all money</h2>
<p class="note">Backing money movement · large GOA as kGOA / MGOA / …</p>
<div class="stats-grid">
<div class="stat"><span class="k">Reserves</span><span class="v teal" id="st-reserves"></span></div>
<div class="stat"><span class="k">Wire in</span><span class="v" id="st-wire-in-n"></span></div>
<div class="stat"><span class="k">Wire in amt</span><span class="v teal sm" id="st-wire-in-amt"></span></div>
<div class="stat"><span class="k">Wire-in count</span><span class="v" id="st-wire-in-n"></span></div>
<div class="stat"><span class="k">All wire-in money</span><span class="v teal sm" id="st-wire-in-amt"></span></div>
<div class="stat"><span class="k">Wire out</span><span class="v" id="st-wire-out"></span></div>
<div class="stat"><span class="k">All withdraws</span><span class="v teal sm" id="st-withdraw-amt"></span></div>
<div class="stat"><span class="k">Withdraw ops</span><span class="v" id="st-withdraw"></span></div>
<div class="stat wide">
<span class="k">Latest wire-in &amp; withdraws</span>
<ul class="wd-list" id="st-recent" aria-live="polite"><li class="meta"></li></ul>
</div>
</div>
<p class="unit-legend-note">Unit names (hover amounts for exact GOA:n)</p>
<div id="st-unit-legend"></div>
<p class="stats-foot" id="st-foot">Loading…</p>
<p class="stats-run-note" id="st-run-note" hidden></p>
</section>
@ -325,17 +360,19 @@
<div class="stats-grid">
<div class="stat"><span class="k">/keys</span><span class="v teal" id="st-keys-ms"></span></div>
<div class="stat"><span class="k">/config</span><span class="v" id="st-config-ms"></span></div>
<div class="stat"><span class="k">Loadavg</span><span class="v sm" id="st-load"></span></div>
<div class="stat"><span class="k">Container</span><span class="v teal" id="st-mem-ctr"></span></div>
<div class="stat"><span class="k">CPU (container)</span><span class="v teal" id="st-cpu"></span></div>
<div class="stat"><span class="k">Loadavg (host)</span><span class="v sm" id="st-load"></span></div>
<div class="stat"><span class="k">PIDs</span><span class="v" id="st-pids"></span></div>
<div class="stat"><span class="k">Container RSS</span><span class="v teal" id="st-mem-ctr"></span></div>
<div class="stat"><span class="k">PostgreSQL</span><span class="v" id="st-mem-pg"></span></div>
<div class="stat"><span class="k">Taler procs</span><span class="v" id="st-mem-taler"></span></div>
<div class="stat"><span class="k">Nginx</span><span class="v sm" id="st-mem-nginx"></span></div>
</div>
<div class="sub-block" style="margin-top:0.85rem">
<h3>Top processes (RSS)</h3>
<h3>Top processes (RSS inside container)</h3>
<div id="st-mem-top" class="by-denom"></div>
</div>
<p class="stats-foot" id="st-perf-foot">Memory from /proc + cgroup inside podman container.</p>
<p class="stats-foot" id="st-perf-foot">RSS from /proc inside container (no memory cgroup in rootless podman). CPU from podman stats.</p>
</section>
<div class="wallet-fold" id="wallet-fold">
@ -513,6 +550,8 @@ sudo apt-get install -y taler-wallet-cli</pre>
set("st-coins-live", d.coins_live);
set("st-coins-spent", d.coins_spent);
setAmt("st-remaining", d.coins_remaining_amount, "coins_remaining_amount_alt", d);
set("st-withdraw-coins", d.withdraw_ops);
setAmt("st-withdraw-amt-coins", d.withdraw_amount, "withdraw_amount_alt", d);
set("st-withdraw", d.withdraw_ops);
setAmt("st-withdraw-amt", d.withdraw_amount, "withdraw_amount_alt", d);
set("st-refresh", d.refresh_ops);
@ -529,16 +568,48 @@ sudo apt-get install -y taler-wallet-cli</pre>
setAmt("st-wire-in-amt", d.wire_in_amount, "wire_in_amount_alt", d);
set("st-wire-out", d.wire_out);
var recent = document.getElementById("st-recent");
if (recent) {
recent.innerHTML = "";
var rows = (d.recent_activity || []).slice(0, 12);
if (!rows.length) {
var empty = document.createElement("li");
empty.className = "meta";
empty.textContent = "—";
recent.appendChild(empty);
} else {
rows.forEach(function (row) {
var li = document.createElement("li");
var amt = document.createElement("span");
amt.className = "amt";
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");
meta.className = "meta";
meta.textContent = (row.ts_human || "") + (row.kind ? " · " + row.kind : "");
li.appendChild(amt);
li.appendChild(meta);
recent.appendChild(li);
});
}
}
if (window.GoaAmount && GoaAmount.fillLegend) GoaAmount.fillLegend("st-unit-legend", d);
var ladder = document.getElementById("st-ladder");
if (ladder) {
ladder.innerHTML = "";
(d.denom_ladder || []).forEach(function (x) {
var s = document.createElement("span");
s.className = "pill";
var lab = x.value_alt || x.value || "?";
if (window.GoaAmount && !x.value_alt) {
var lab = x.value || "?";
if (window.GoaAmount) {
lab = GoaAmount.format(x.value, d.alt_unit_names).display;
}
} else if (x.value_alt) lab = x.value_alt;
s.textContent = lab + (x.keys > 1 ? " ×" + x.keys : "");
if (x.value && lab !== x.value) s.title = x.value;
ladder.appendChild(s);
@ -553,10 +624,10 @@ sudo apt-get install -y taler-wallet-cli</pre>
wrap.hidden = false;
var html = "<table><thead><tr><th>Value</th><th>Coins</th><th>Live</th></tr></thead><tbody>";
by.forEach(function (r) {
var v = r.value_alt || r.value || "?";
if (window.GoaAmount && !r.value_alt) {
var v = r.value || "?";
if (window.GoaAmount) {
v = GoaAmount.format(r.value, d.alt_unit_names).display;
}
} else if (r.value_alt) v = r.value_alt;
html += "<tr><td title=\"" + (r.value || "") + "\">" + v + "</td><td>" + r.coins + "</td><td>" + r.live + "</td></tr>";
});
html += "</tbody></table>";
@ -569,25 +640,38 @@ sudo apt-get install -y taler-wallet-cli</pre>
(d.generated_at_human || d.generated_at || "");
}
function escHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;").replace(/</g, "&lt;")
.replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function setMem(id, human, n, bytes) {
var el = document.getElementById(id);
if (!el) return;
if (!human && human !== 0) {
var b = bytes != null && bytes !== "" ? Number(bytes) : NaN;
var nn = n != null && n !== "" ? Number(n) : NaN;
var empty =
human == null || human === "" || human === "—" ||
human === "0 B" || human === "0B" ||
(isFinite(b) && b <= 0 && (!isFinite(nn) || nn <= 0));
if (empty) {
el.textContent = "—";
el.removeAttribute("title");
return;
}
var h = String(human);
if (n != null && n !== "" && Number(n) > 0) h += " · " + n + "p";
if (isFinite(nn) && nn > 0) h += " · " + nn + "p";
el.textContent = h;
if (bytes != null && bytes !== "" && Number(bytes) > 0) {
el.setAttribute("title", Number(bytes).toLocaleString("en-US") + " B");
if (isFinite(b) && b > 0) {
el.setAttribute("title", b.toLocaleString("en-US") + " B");
} else el.removeAttribute("title");
}
var p = d.performance || {};
set("st-keys-ms", msLabel(p.keys_ms, p.keys_http));
set("st-config-ms", msLabel(p.config_ms, p.config_http));
set("st-cpu", p.cpu_percent_label || (p.cpu_percent != null ? (Number(p.cpu_percent).toFixed(2) + "%") : "—"));
set("st-load", p.loadavg || "—");
set("st-pids", p.pids != null ? p.pids : ((p.memory && p.memory.pids) != null ? p.memory.pids : "—"));
var mem = p.memory || {};
setMem("st-mem-ctr", mem.container_rss_label || mem.container_rss_human || "—", null, mem.container_rss_bytes);
setMem("st-mem-pg", mem.postgres_rss_human, mem.postgres_n, mem.postgres_rss_bytes);
@ -604,11 +688,11 @@ sudo apt-get install -y taler-wallet-cli</pre>
var cmd = t.cmd || "";
var cmdFull = t.cmd_full || cmd;
if (cmd.length > 64) cmd = cmd.slice(0, 61) + "…";
th += "<tr><td title=\"" + (t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + "\">" +
(t.rss_human || "?") + "</td><td>" +
(t.comm || "") + "</td><td style=\"font-size:0.72rem;word-break:break-all\" title=\"" +
String(cmdFull).replace(/"/g, "&quot;") + "\">" +
cmd + "</td></tr>";
th += "<tr><td title=\"" + escHtml(t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + "\">" +
escHtml(t.rss_human || "?") + "</td><td>" +
escHtml(t.comm || "") + "</td><td style=\"font-size:0.72rem;word-break:break-all\" title=\"" +
escHtml(cmdFull) + "\">" +
escHtml(cmd) + "</td></tr>";
});
th += "</tbody></table>";
topBox.innerHTML = th;
@ -616,9 +700,11 @@ sudo apt-get install -y taler-wallet-cli</pre>
}
var pf = document.getElementById("st-perf-foot");
if (pf) {
pf.textContent = "Container RSS + process groups" +
(mem.source ? " · " + mem.source : "") + " · " +
(d.generated_at_human || d.generated_at || "");
var basis = mem.memory_basis === "cgroup" ? "cgroup" : "proc RSS sum";
pf.textContent = "Memory: " + basis +
(mem.source ? " · " + mem.source : "") +
(p.loadavg_source ? " · loadavg=" + p.loadavg_source : "") +
" · " + (d.generated_at_human || d.generated_at || "");
}
return fetch("/intro/stats-run.json", { cache: "no-store" })
.then(function (r) { return r.ok ? r.json() : null; })