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. /* Compact GOA / CUR:amount display using SI-style alt units (kGOA, MGOA, ).
* Large values "1.23 Mega-GOA" (title = full GOA:n) so landing tiles fit. * Large values "1.23 MGOA" (title = full GOA:n) so landing tiles fit.
*/ */
(function (global) { (function (global) {
"use strict"; "use strict";
var DEFAULT_ALT = { var DEFAULT_ALT = {
"24": "Yotta-GOA", "24": "YGOA",
"21": "Zetta-GOA", "21": "ZGOA",
"18": "Exa-GOA", "18": "EGOA",
"15": "Peta-GOA", "15": "PGOA",
"12": "Tera-GOA", "12": "TGOA",
"9": "Giga-GOA", "9": "GGOA",
"6": "Mega-GOA", "6": "MGOA",
"3": "Kilo-GOA", "3": "kGOA",
"0": "GOA", "0": "GOA",
"-1": "Deci-GOA", "-1": "dGOA",
"-2": "Centi-GOA", "-2": "cGOA",
"-3": "Milli-GOA", "-3": "mGOA",
"-6": "Micro-GOA", "-6": "µGOA",
"-7": "Deci-Micro-GOA", "-7": "dµGOA",
"-8": "Atomic-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; 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) { function parseAmount(s) {
if (s == null || s === "") return { cur: "GOA", val: 0 }; if (s == null || s === "") return { cur: "GOA", val: 0 };
if (typeof s === "number") return { cur: "GOA", val: s }; 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] * @param {object} [altMap]
* @returns {{ display: string, title: string, raw: string, usedAlt: boolean }} * @returns {{ display: string, title: string, raw: string, usedAlt: boolean }}
*/ */
@ -59,24 +93,17 @@
if (amount == null || amount === "") { if (amount == null || amount === "") {
return { display: "—", title: "", raw: "", usedAlt: false }; 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 (typeof amount === "object" && amount) {
if (amount.amount_alt) { if (amount.amount) {
var rawO = amount.amount || amount.amount_full || String(amount.amount_alt); return format(amount.amount, altMap);
return {
display: String(amount.amount_alt),
title: amount.amount_full || rawO,
raw: String(rawO),
usedAlt: true
};
} }
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 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 baseName = (alt && alt["0"]) || p.cur;
var curU = String(p.cur || "").toUpperCase(); var curU = String(p.cur || "").toUpperCase();
var baseU = String(baseName || "").toUpperCase(); var baseU = String(baseName || "").toUpperCase();
@ -95,7 +122,9 @@
var n = parseInt(k, 10); var n = parseInt(k, 10);
if (!isNaN(n)) scales.push({ sc: n, name: String(alt[k]) }); 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 abs = Math.abs(p.val);
var chosen = null; var chosen = null;
@ -136,33 +165,80 @@
setText(document.getElementById(id), amount, altMap); 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) { function pick(rowOrAmount, altMap) {
if (rowOrAmount && typeof rowOrAmount === "object") { 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) { if (rowOrAmount.amount_alt) {
return format({ return {
amount: rowOrAmount.amount, display: String(rowOrAmount.amount_alt),
amount_alt: rowOrAmount.amount_alt, title: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
amount_full: rowOrAmount.amount_full raw: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
}, altMap); usedAlt: true
};
} }
return format(rowOrAmount.amount || rowOrAmount.total_amount, altMap); return format("", altMap);
} }
return format(rowOrAmount, altMap); return format(rowOrAmount, altMap);
} }
function rememberAlt(stats) { function rememberAlt(stats) {
if (stats && stats.alt_unit_names && typeof stats.alt_unit_names === "object") { 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 = { global.GoaAmount = {
format: format, format: format,
setText: setText, setText: setText,
setById: setById, setById: setById,
pick: pick, pick: pick,
rememberAlt: rememberAlt, rememberAlt: rememberAlt,
compactAlt: compactAlt,
legendHtml: legendHtml,
fillLegend: fillLegend,
DEFAULT_ALT: DEFAULT_ALT, DEFAULT_ALT: DEFAULT_ALT,
THRESHOLD: THRESHOLD THRESHOLD: THRESHOLD
}; };

View file

@ -995,6 +995,27 @@
color: #f0a090; color: #f0a090;
text-align: center; text-align: center;
} }
.unit-legend {
margin: 0.65rem auto 0;
border-collapse: collapse;
font-size: 0.68rem;
color: var(--muted);
width: auto;
max-width: 100%;
}
.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; letter-spacing: 0.04em; text-transform: uppercase; font-size: 0.62rem; }
.unit-legend strong { color: #5eead4; font-weight: 750; }
.unit-legend-note {
margin: 0.35rem 0 0;
font-size: 0.68rem;
color: var(--muted);
text-align: center;
}
footer { footer {
margin-top: 2rem; margin-top: 2rem;
color: var(--muted); color: var(--muted);
@ -1392,39 +1413,67 @@ tw run-until-done &amp;&amp; tw balance</pre>
<!-- ========== LIVE STATS (before performance) ========== --> <!-- ========== LIVE STATS (before performance) ========== -->
<section class="stats" id="stats" aria-labelledby="stats-title" hidden> <section class="stats" id="stats" aria-labelledby="stats-title" hidden>
<h2 id="stats-title">GOA flow</h2> <h2 id="stats-title">GOA flow · all money</h2>
<div class="stats-grid"> <div class="stats-grid">
<div class="stat">
<span class="k">All money (explorer)</span>
<span class="v teal" id="st-all-money"></span>
</div>
<div class="stat"> <div class="stat">
<span class="k">Accounts</span> <span class="k">Accounts</span>
<span class="v teal" id="st-accounts"></span> <span class="v teal" id="st-accounts"></span>
</div> </div>
<div class="stat">
<span class="k">Bank credits</span>
<span class="v teal" id="st-incoming"></span>
</div>
<div class="stat">
<span class="k">Withdraws</span>
<span class="v" id="st-withdraw"></span>
</div>
<div class="stat"> <div class="stat">
<span class="k">Unique wallets</span> <span class="k">Unique wallets</span>
<span class="v teal" id="st-wallets"></span> <span class="v teal" id="st-wallets"></span>
</div> </div>
<div class="stat"> <div class="stat">
<span class="k">24h</span> <span class="k">All credits (in)</span>
<span class="v teal" id="st-incoming"></span>
</div>
<div class="stat">
<span class="k">Credits count</span>
<span class="v" id="st-incoming-n"></span>
</div>
<div class="stat">
<span class="k">Latest credit</span>
<span class="v sm" id="st-incoming-last"></span>
</div>
<div class="stat">
<span class="k">All withdraws</span>
<span class="v" id="st-withdraw"></span>
</div>
<div class="stat">
<span class="k">Withdraw count</span>
<span class="v" id="st-withdraw-n"></span>
</div>
<div class="stat">
<span class="k">Latest withdraw</span>
<span class="v sm" id="st-withdraw-last"></span>
</div>
<div class="stat">
<span class="k">Withdraws 24h</span>
<span class="v" id="st-24h"></span> <span class="v" id="st-24h"></span>
</div> </div>
<div class="stat"> <div class="stat">
<span class="k">7d</span> <span class="k">Withdraws 7d</span>
<span class="v" id="st-7d"></span> <span class="v" id="st-7d"></span>
</div> </div>
<div class="stat wide"> <div class="stat wide">
<span class="k">Recent withdraws (10)</span> <span class="k">Latest withdraws</span>
<ul class="wd-list" id="st-recent" aria-live="polite"> <ul class="wd-list" id="st-recent" aria-live="polite">
<li class="meta"></li> <li class="meta"></li>
</ul> </ul>
</div> </div>
<div class="stat wide">
<span class="k">Latest credits (incoming)</span>
<ul class="wd-list" id="st-recent-in" aria-live="polite">
<li class="meta"></li>
</ul>
</div>
</div> </div>
<p class="unit-legend-note">Large GOA → kGOA / MGOA / … (hover for exact GOA:n)</p>
<div id="st-unit-legend"></div>
<p class="stats-foot" id="st-foot" hidden></p> <p class="stats-foot" id="st-foot" hidden></p>
<p class="stats-run-note" id="st-run-note" hidden></p> <p class="stats-run-note" id="st-run-note" hidden></p>
</section> </section>
@ -1448,11 +1497,19 @@ tw run-until-done &amp;&amp; tw balance</pre>
<span class="v" id="st-webui-ms"></span> <span class="v" id="st-webui-ms"></span>
</div> </div>
<div class="stat"> <div class="stat">
<span class="k">Loadavg</span> <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" id="st-load"></span> <span class="v" id="st-load"></span>
</div> </div>
<div class="stat"> <div class="stat">
<span class="k">Container</span> <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> <span class="v teal" id="st-mem-ctr"></span>
</div> </div>
<div class="stat"> <div class="stat">
@ -1469,10 +1526,10 @@ tw run-until-done &amp;&amp; tw balance</pre>
</div> </div>
</div> </div>
<div style="margin-top:0.85rem;overflow-x:auto"> <div style="margin-top:0.85rem;overflow-x:auto">
<p class="note" style="text-align:center;margin-bottom:0.4rem">Top processes (RSS)</p> <p class="note" style="text-align:center;margin-bottom:0.4rem">Top processes (RSS inside container)</p>
<div id="st-mem-top"></div> <div id="st-mem-top"></div>
</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> </section>
<footer> <footer>
@ -1985,46 +2042,65 @@ tw run-until-done &amp;&amp; tw balance</pre>
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 : "—");
setAmt("st-incoming", fin.amount_alt ? fin : (fin.amount || flow.total_in || "—")); setAmt("st-all-money", d.balance_explorer
setAmt("st-withdraw", fwd.amount_alt ? fwd : (fwd.amount || w.total_amount || "—"));
setAmt("st-24h", h24.amount_alt ? h24 : (h24.amount || "GOA:0"));
setAmt("st-7d", h7.amount_alt ? h7 : (h7.amount || "GOA:0"));
setAmt("shared-balance", d.balance_explorer_alt
? { amount: d.balance_explorer, amount_alt: d.balance_explorer_alt, amount_full: d.balance_explorer_full } ? { amount: d.balance_explorer, amount_alt: d.balance_explorer_alt, amount_full: d.balance_explorer_full }
: (d.balance_explorer || "—")); : "—");
setAmt("st-incoming", fin.amount ? fin : (flow.total_in || "—"));
set("st-incoming-n", fin.count != null ? fin.count : "—");
setAmt("st-withdraw", fwd.amount ? fwd : (w.total_amount ? { amount: w.total_amount, amount_alt: w.total_amount_alt } : "—"));
set("st-withdraw-n", (fwd.count != null ? fwd.count : w.count) != null ? (fwd.count != null ? fwd.count : w.count) : "—");
setAmt("st-withdraw-last", w.last_amount
? { amount: w.last_amount, amount_alt: w.last_amount_alt, amount_full: w.last_amount_full }
: "—");
setAmt("st-24h", h24.amount ? h24 : (h24.amount || "GOA:0"));
setAmt("st-7d", h7.amount ? h7 : (h7.amount || "GOA:0"));
setAmt("shared-balance", d.balance_explorer
? { amount: d.balance_explorer, amount_alt: d.balance_explorer_alt, amount_full: d.balance_explorer_full }
: "—");
var list = document.getElementById("st-recent"); function fillMoneyList(listId, rows, emptyLabel) {
if (list) { var list = document.getElementById(listId);
if (!list) return;
list.innerHTML = ""; list.innerHTML = "";
/* Full list from stats.json (generator emits 10) */ rows = rows || [];
var rows = (d.recent_withdraws || []).slice(0, 10);
if (!rows.length) { if (!rows.length) {
var empty = document.createElement("li"); var empty = document.createElement("li");
empty.className = "meta"; empty.className = "meta";
empty.textContent = "—"; empty.textContent = emptyLabel || "—";
list.appendChild(empty); list.appendChild(empty);
} else { return;
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";
var when = fmtCest(row.at_unix, row.at || row.at_iso) || "";
var who = row.account ? " · " + row.account : "";
meta.textContent = when + who;
li.appendChild(amt);
li.appendChild(meta);
list.appendChild(li);
});
} }
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";
var when = fmtCest(row.at_unix, row.at || row.at_iso) || "";
var who = row.account ? " · " + row.account : "";
meta.textContent = when + who;
li.appendChild(amt);
li.appendChild(meta);
list.appendChild(li);
});
}
fillMoneyList("st-recent", (d.recent_withdraws || []).slice(0, 10));
var recentIn = (d.recent_incoming || []).slice(0, 10);
fillMoneyList("st-recent-in", recentIn);
if (recentIn.length) {
setAmt("st-incoming-last", recentIn[0]);
} else {
set("st-incoming-last", "—");
}
if (window.GoaAmount && GoaAmount.fillLegend) {
GoaAmount.fillLegend("st-unit-legend", d);
} }
if (foot) { if (foot) {
foot.hidden = false; foot.hidden = false;
@ -2037,19 +2113,30 @@ tw run-until-done &amp;&amp; tw balance</pre>
if (http && http !== "200") s += " (" + http + ")"; if (http && http !== "200") s += " (" + http + ")";
return s; return s;
} }
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) { function setMem(id, human, n, bytes) {
var el = document.getElementById(id); var el = document.getElementById(id);
if (!el) return; 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.textContent = "—";
el.removeAttribute("title"); el.removeAttribute("title");
return; return;
} }
var h = String(human); 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; el.textContent = h;
if (bytes != null && bytes !== "" && bytes !== 0) { if (isFinite(b) && b > 0) {
el.setAttribute("title", Number(bytes).toLocaleString("en-US") + " B"); el.setAttribute("title", b.toLocaleString("en-US") + " B");
} else { } else {
el.removeAttribute("title"); el.removeAttribute("title");
} }
@ -2058,7 +2145,9 @@ tw run-until-done &amp;&amp; tw balance</pre>
set("st-config-ms", msLabel(p.config_ms, p.config_http)); set("st-config-ms", msLabel(p.config_ms, p.config_http));
set("st-int-ms", msLabel(p.integration_ms, p.integration_http)); set("st-int-ms", msLabel(p.integration_ms, p.integration_http));
set("st-webui-ms", msLabel(p.webui_ms, p.webui_http)); set("st-webui-ms", msLabel(p.webui_ms, p.webui_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-load", p.loadavg || "—");
set("st-pids", p.pids != null ? p.pids : ((p.memory && p.memory.pids) != null ? p.memory.pids : "—"));
var mem = p.memory || {}; var mem = p.memory || {};
setMem("st-mem-ctr", mem.container_rss_label || mem.container_rss_human || "—", null, mem.container_rss_bytes); 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); setMem("st-mem-pg", mem.postgres_rss_human, mem.postgres_n, mem.postgres_rss_bytes);
@ -2079,12 +2168,12 @@ tw run-until-done &amp;&amp; tw balance</pre>
var cmdFull = t.cmd_full || cmd; var cmdFull = t.cmd_full || cmd;
if (cmd.length > 64) cmd = cmd.slice(0, 61) + "…"; if (cmd.length > 64) cmd = cmd.slice(0, 61) + "…";
th += '<tr><td style="padding:0.25rem;color:#5eead4" title="' + th += '<tr><td style="padding:0.25rem;color:#5eead4" title="' +
(t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + '">' + escHtml(t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + '">' +
(t.rss_human || "?") + escHtml(t.rss_human || "?") +
'</td><td style="padding:0.25rem">' + (t.comm || "") + '</td><td style="padding:0.25rem">' + escHtml(t.comm || "") +
'</td><td style="padding:0.25rem;word-break:break-all;color:var(--muted)" title="' + '</td><td style="padding:0.25rem;word-break:break-all;color:var(--muted)" title="' +
String(cmdFull).replace(/"/g, "&quot;") + '">' + escHtml(cmdFull) + '">' +
cmd + "</td></tr>"; escHtml(cmd) + "</td></tr>";
}); });
th += "</tbody></table>"; th += "</tbody></table>";
topBox.innerHTML = th; topBox.innerHTML = th;
@ -2092,9 +2181,11 @@ tw run-until-done &amp;&amp; tw balance</pre>
} }
var pf = document.getElementById("st-perf-foot"); var pf = document.getElementById("st-perf-foot");
if (pf) { if (pf) {
pf.textContent = "Container RSS + process groups" + var basis = mem.memory_basis === "cgroup" ? "cgroup" : "proc RSS sum";
(mem.source ? " · " + mem.source : "") + " · " + pf.textContent = "Memory: " + basis +
(d.generated_at_human || d.generated_at || ""); (mem.source ? " · " + mem.source : "") +
(p.loadavg_source ? " · loadavg=" + p.loadavg_source : "") +
" · " + (d.generated_at_human || d.generated_at || "");
} }
return fetch("stats-run.json", { cache: "no-store" }) return fetch("stats-run.json", { cache: "no-store" })
.then(function (r) { return r.ok ? r.json() : null; }) .then(function (r) { return r.ok ? r.json() : null; })

View file

@ -1,29 +1,63 @@
/* Compact GOA / CUR:amount display using currency alt_unit_names. /* Compact GOA / CUR:amount display using SI-style alt units (kGOA, MGOA, ).
* Large values "1.23 Mega-GOA" (title = full GOA:n) so landing tiles fit. * Large values "1.23 MGOA" (title = full GOA:n) so landing tiles fit.
*/ */
(function (global) { (function (global) {
"use strict"; "use strict";
var DEFAULT_ALT = { var DEFAULT_ALT = {
"24": "Yotta-GOA", "24": "YGOA",
"21": "Zetta-GOA", "21": "ZGOA",
"18": "Exa-GOA", "18": "EGOA",
"15": "Peta-GOA", "15": "PGOA",
"12": "Tera-GOA", "12": "TGOA",
"9": "Giga-GOA", "9": "GGOA",
"6": "Mega-GOA", "6": "MGOA",
"3": "Kilo-GOA", "3": "kGOA",
"0": "GOA", "0": "GOA",
"-1": "Deci-GOA", "-1": "dGOA",
"-2": "Centi-GOA", "-2": "cGOA",
"-3": "Milli-GOA", "-3": "mGOA",
"-6": "Micro-GOA", "-6": "µGOA",
"-7": "Deci-Micro-GOA", "-7": "dµGOA",
"-8": "Atomic-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; 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) { function parseAmount(s) {
if (s == null || s === "") return { cur: "GOA", val: 0 }; if (s == null || s === "") return { cur: "GOA", val: 0 };
if (typeof s === "number") return { cur: "GOA", val: s }; 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] * @param {object} [altMap]
* @returns {{ display: string, title: string, raw: string, usedAlt: boolean }} * @returns {{ display: string, title: string, raw: string, usedAlt: boolean }}
*/ */
@ -59,24 +93,17 @@
if (amount == null || amount === "") { if (amount == null || amount === "") {
return { display: "—", title: "", raw: "", usedAlt: false }; 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 (typeof amount === "object" && amount) {
if (amount.amount_alt) { if (amount.amount) {
var rawO = amount.amount || amount.amount_full || String(amount.amount_alt); return format(amount.amount, altMap);
return {
display: String(amount.amount_alt),
title: amount.amount_full || rawO,
raw: String(rawO),
usedAlt: true
};
} }
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 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 baseName = (alt && alt["0"]) || p.cur;
var curU = String(p.cur || "").toUpperCase(); var curU = String(p.cur || "").toUpperCase();
var baseU = String(baseName || "").toUpperCase(); var baseU = String(baseName || "").toUpperCase();
@ -95,7 +122,9 @@
var n = parseInt(k, 10); var n = parseInt(k, 10);
if (!isNaN(n)) scales.push({ sc: n, name: String(alt[k]) }); 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 abs = Math.abs(p.val);
var chosen = null; var chosen = null;
@ -136,33 +165,80 @@
setText(document.getElementById(id), amount, altMap); 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) { function pick(rowOrAmount, altMap) {
if (rowOrAmount && typeof rowOrAmount === "object") { 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) { if (rowOrAmount.amount_alt) {
return format({ return {
amount: rowOrAmount.amount, display: String(rowOrAmount.amount_alt),
amount_alt: rowOrAmount.amount_alt, title: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
amount_full: rowOrAmount.amount_full raw: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
}, altMap); usedAlt: true
};
} }
return format(rowOrAmount.amount || rowOrAmount.total_amount, altMap); return format("", altMap);
} }
return format(rowOrAmount, altMap); return format(rowOrAmount, altMap);
} }
function rememberAlt(stats) { function rememberAlt(stats) {
if (stats && stats.alt_unit_names && typeof stats.alt_unit_names === "object") { 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 = { global.GoaAmount = {
format: format, format: format,
setText: setText, setText: setText,
setById: setById, setById: setById,
pick: pick, pick: pick,
rememberAlt: rememberAlt, rememberAlt: rememberAlt,
compactAlt: compactAlt,
legendHtml: legendHtml,
fillLegend: fillLegend,
DEFAULT_ALT: DEFAULT_ALT, DEFAULT_ALT: DEFAULT_ALT,
THRESHOLD: THRESHOLD THRESHOLD: THRESHOLD
}; };

View file

@ -88,6 +88,33 @@
.stat .v.sm { font-size: 0.92rem; } .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 { margin: 0.75rem 0 0; font-size: 0.72rem; color: var(--muted); text-align: center; }
.stats-foot.err { color: #f0a090; } .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 { .stats-run-note {
margin: 0.35rem 0 0; font-size: 0.7rem; color: #f0a090; text-align: center; 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">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">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">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 ops</span><span class="v" id="st-withdraw-coins"></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">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">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">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> <div class="stat"><span class="k">Batch deposits</span><span class="v" id="st-batch-dep"></span></div>
@ -307,14 +334,22 @@
</section> </section>
<section class="stats" id="stats-wire" aria-labelledby="wire-title"> <section class="stats" id="stats-wire" aria-labelledby="wire-title">
<h2 id="wire-title">Reserves · wire</h2> <h2 id="wire-title">Reserves · wire · all money</h2>
<p class="note">Backing money movement</p> <p class="note">Backing money movement · large GOA as kGOA / MGOA / …</p>
<div class="stats-grid"> <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">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 count</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">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">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> </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-foot" id="st-foot">Loading…</p>
<p class="stats-run-note" id="st-run-note" hidden></p> <p class="stats-run-note" id="st-run-note" hidden></p>
</section> </section>
@ -325,17 +360,19 @@
<div class="stats-grid"> <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">/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">/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">CPU (container)</span><span class="v teal" id="st-cpu"></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">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">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">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 class="stat"><span class="k">Nginx</span><span class="v sm" id="st-mem-nginx"></span></div>
</div> </div>
<div class="sub-block" style="margin-top:0.85rem"> <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 id="st-mem-top" class="by-denom"></div>
</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> </section>
<div class="wallet-fold" id="wallet-fold"> <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-live", d.coins_live);
set("st-coins-spent", d.coins_spent); set("st-coins-spent", d.coins_spent);
setAmt("st-remaining", d.coins_remaining_amount, "coins_remaining_amount_alt", d); 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); set("st-withdraw", d.withdraw_ops);
setAmt("st-withdraw-amt", d.withdraw_amount, "withdraw_amount_alt", d); setAmt("st-withdraw-amt", d.withdraw_amount, "withdraw_amount_alt", d);
set("st-refresh", d.refresh_ops); 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); 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 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"); var ladder = document.getElementById("st-ladder");
if (ladder) { if (ladder) {
ladder.innerHTML = ""; ladder.innerHTML = "";
(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";
var lab = x.value_alt || x.value || "?"; var lab = x.value || "?";
if (window.GoaAmount && !x.value_alt) { if (window.GoaAmount) {
lab = GoaAmount.format(x.value, d.alt_unit_names).display; 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 : ""); s.textContent = lab + (x.keys > 1 ? " ×" + x.keys : "");
if (x.value && lab !== x.value) s.title = x.value; if (x.value && lab !== x.value) s.title = x.value;
ladder.appendChild(s); ladder.appendChild(s);
@ -553,10 +624,10 @@ 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) {
var v = r.value_alt || r.value || "?"; var v = r.value || "?";
if (window.GoaAmount && !r.value_alt) { if (window.GoaAmount) {
v = GoaAmount.format(r.value, d.alt_unit_names).display; 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 += "<tr><td title=\"" + (r.value || "") + "\">" + v + "</td><td>" + r.coins + "</td><td>" + r.live + "</td></tr>";
}); });
html += "</tbody></table>"; html += "</tbody></table>";
@ -569,25 +640,38 @@ sudo apt-get install -y taler-wallet-cli</pre>
(d.generated_at_human || d.generated_at || ""); (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) { function setMem(id, human, n, bytes) {
var el = document.getElementById(id); var el = document.getElementById(id);
if (!el) return; 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.textContent = "—";
el.removeAttribute("title"); el.removeAttribute("title");
return; return;
} }
var h = String(human); 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; el.textContent = h;
if (bytes != null && bytes !== "" && Number(bytes) > 0) { if (isFinite(b) && b > 0) {
el.setAttribute("title", Number(bytes).toLocaleString("en-US") + " B"); el.setAttribute("title", b.toLocaleString("en-US") + " B");
} else el.removeAttribute("title"); } else el.removeAttribute("title");
} }
var p = d.performance || {}; var p = d.performance || {};
set("st-keys-ms", msLabel(p.keys_ms, p.keys_http)); set("st-keys-ms", msLabel(p.keys_ms, p.keys_http));
set("st-config-ms", msLabel(p.config_ms, p.config_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-load", p.loadavg || "—");
set("st-pids", p.pids != null ? p.pids : ((p.memory && p.memory.pids) != null ? p.memory.pids : "—"));
var mem = p.memory || {}; var mem = p.memory || {};
setMem("st-mem-ctr", mem.container_rss_label || mem.container_rss_human || "—", null, mem.container_rss_bytes); 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); 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 cmd = t.cmd || "";
var cmdFull = t.cmd_full || cmd; var cmdFull = t.cmd_full || cmd;
if (cmd.length > 64) cmd = cmd.slice(0, 61) + "…"; 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" : "") + "\">" + th += "<tr><td title=\"" + escHtml(t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + "\">" +
(t.rss_human || "?") + "</td><td>" + escHtml(t.rss_human || "?") + "</td><td>" +
(t.comm || "") + "</td><td style=\"font-size:0.72rem;word-break:break-all\" title=\"" + escHtml(t.comm || "") + "</td><td style=\"font-size:0.72rem;word-break:break-all\" title=\"" +
String(cmdFull).replace(/"/g, "&quot;") + "\">" + escHtml(cmdFull) + "\">" +
cmd + "</td></tr>"; escHtml(cmd) + "</td></tr>";
}); });
th += "</tbody></table>"; th += "</tbody></table>";
topBox.innerHTML = th; topBox.innerHTML = th;
@ -616,9 +700,11 @@ sudo apt-get install -y taler-wallet-cli</pre>
} }
var pf = document.getElementById("st-perf-foot"); var pf = document.getElementById("st-perf-foot");
if (pf) { if (pf) {
pf.textContent = "Container RSS + process groups" + var basis = mem.memory_basis === "cgroup" ? "cgroup" : "proc RSS sum";
(mem.source ? " · " + mem.source : "") + " · " + pf.textContent = "Memory: " + basis +
(d.generated_at_human || d.generated_at || ""); (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" }) return fetch("/intro/stats-run.json", { cache: "no-store" })
.then(function (r) { return r.ok ? r.json() : null; }) .then(function (r) { return r.ok ? r.json() : null; })

View file

@ -1,29 +1,63 @@
/* Compact GOA / CUR:amount display using currency alt_unit_names. /* Compact GOA / CUR:amount display using SI-style alt units (kGOA, MGOA, ).
* Large values "1.23 Mega-GOA" (title = full GOA:n) so landing tiles fit. * Large values "1.23 MGOA" (title = full GOA:n) so landing tiles fit.
*/ */
(function (global) { (function (global) {
"use strict"; "use strict";
var DEFAULT_ALT = { var DEFAULT_ALT = {
"24": "Yotta-GOA", "24": "YGOA",
"21": "Zetta-GOA", "21": "ZGOA",
"18": "Exa-GOA", "18": "EGOA",
"15": "Peta-GOA", "15": "PGOA",
"12": "Tera-GOA", "12": "TGOA",
"9": "Giga-GOA", "9": "GGOA",
"6": "Mega-GOA", "6": "MGOA",
"3": "Kilo-GOA", "3": "kGOA",
"0": "GOA", "0": "GOA",
"-1": "Deci-GOA", "-1": "dGOA",
"-2": "Centi-GOA", "-2": "cGOA",
"-3": "Milli-GOA", "-3": "mGOA",
"-6": "Micro-GOA", "-6": "µGOA",
"-7": "Deci-Micro-GOA", "-7": "dµGOA",
"-8": "Atomic-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; 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) { function parseAmount(s) {
if (s == null || s === "") return { cur: "GOA", val: 0 }; if (s == null || s === "") return { cur: "GOA", val: 0 };
if (typeof s === "number") return { cur: "GOA", val: s }; 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] * @param {object} [altMap]
* @returns {{ display: string, title: string, raw: string, usedAlt: boolean }} * @returns {{ display: string, title: string, raw: string, usedAlt: boolean }}
*/ */
@ -59,24 +93,17 @@
if (amount == null || amount === "") { if (amount == null || amount === "") {
return { display: "—", title: "", raw: "", usedAlt: false }; 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 (typeof amount === "object" && amount) {
if (amount.amount_alt) { if (amount.amount) {
var rawO = amount.amount || amount.amount_full || String(amount.amount_alt); return format(amount.amount, altMap);
return {
display: String(amount.amount_alt),
title: amount.amount_full || rawO,
raw: String(rawO),
usedAlt: true
};
} }
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 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 baseName = (alt && alt["0"]) || p.cur;
var curU = String(p.cur || "").toUpperCase(); var curU = String(p.cur || "").toUpperCase();
var baseU = String(baseName || "").toUpperCase(); var baseU = String(baseName || "").toUpperCase();
@ -95,7 +122,9 @@
var n = parseInt(k, 10); var n = parseInt(k, 10);
if (!isNaN(n)) scales.push({ sc: n, name: String(alt[k]) }); 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 abs = Math.abs(p.val);
var chosen = null; var chosen = null;
@ -136,33 +165,80 @@
setText(document.getElementById(id), amount, altMap); 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) { function pick(rowOrAmount, altMap) {
if (rowOrAmount && typeof rowOrAmount === "object") { 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) { if (rowOrAmount.amount_alt) {
return format({ return {
amount: rowOrAmount.amount, display: String(rowOrAmount.amount_alt),
amount_alt: rowOrAmount.amount_alt, title: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
amount_full: rowOrAmount.amount_full raw: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
}, altMap); usedAlt: true
};
} }
return format(rowOrAmount.amount || rowOrAmount.total_amount, altMap); return format("", altMap);
} }
return format(rowOrAmount, altMap); return format(rowOrAmount, altMap);
} }
function rememberAlt(stats) { function rememberAlt(stats) {
if (stats && stats.alt_unit_names && typeof stats.alt_unit_names === "object") { 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 = { global.GoaAmount = {
format: format, format: format,
setText: setText, setText: setText,
setById: setById, setById: setById,
pick: pick, pick: pick,
rememberAlt: rememberAlt, rememberAlt: rememberAlt,
compactAlt: compactAlt,
legendHtml: legendHtml,
fillLegend: fillLegend,
DEFAULT_ALT: DEFAULT_ALT, DEFAULT_ALT: DEFAULT_ALT,
THRESHOLD: THRESHOLD THRESHOLD: THRESHOLD
}; };

View file

@ -273,6 +273,15 @@
.stats-run-note { .stats-run-note {
margin: 0.35rem 0 0; font-size: 0.7rem; color: #f0a090; text-align: center; margin: 0.35rem 0 0; font-size: 0.7rem; color: #f0a090; text-align: center;
} }
.unit-legend {
margin: 0.45rem auto 0; border-collapse: collapse; font-size: 0.68rem; color: var(--muted);
display: inline-table;
}
.unit-legend th, .unit-legend td {
padding: 0.1rem 0.4rem; border-bottom: 1px solid rgba(167, 139, 250, 0.15); text-align: left;
}
.unit-legend th { font-weight: 700; font-size: 0.6rem; text-transform: uppercase; }
.unit-legend strong { color: var(--goa); }
.step-card { .step-card {
background: var(--card); border: 1px solid var(--border); border-radius: 18px; background: var(--card); border: 1px solid var(--border); border-radius: 18px;
padding: 1.3rem 1.25rem 1.4rem; margin-bottom: 1.2rem; padding: 1.3rem 1.25rem 1.4rem; margin-bottom: 1.2rem;
@ -704,12 +713,12 @@ sudo apt-get install -y taler-wallet-cli</pre>
<section class="stats" id="stats" aria-labelledby="stats-title"> <section class="stats" id="stats" aria-labelledby="stats-title">
<h2 id="stats-title">Merchant stats</h2> <h2 id="stats-title">Merchant stats · all payments</h2>
<p class="note">From merchant DB · dual currency side by side</p> <p class="note">From merchant DB · dual currency · large GOA as kGOA / MGOA / …</p>
<div class="stats-grid"> <div class="stats-grid">
<div class="stat"><span class="k">Instances</span><span class="v teal" id="st-instances"></span></div> <div class="stat"><span class="k">Instances</span><span class="v teal" id="st-instances"></span></div>
<div class="stat"><span class="k">Orders</span><span class="v" id="st-orders"></span></div> <div class="stat"><span class="k">All orders</span><span class="v" id="st-orders"></span></div>
<div class="stat"><span class="k">Paid</span><span class="v teal" id="st-paid"></span></div> <div class="stat"><span class="k">All paid</span><span class="v teal" id="st-paid"></span></div>
<div class="stat"><span class="k">Unpaid</span><span class="v" id="st-unpaid"></span></div> <div class="stat"><span class="k">Unpaid</span><span class="v" id="st-unpaid"></span></div>
<div class="stat"><span class="k">Wired</span><span class="v" id="st-wired"></span></div> <div class="stat"><span class="k">Wired</span><span class="v" id="st-wired"></span></div>
<div class="stat"><span class="k">Refunds</span><span class="v" id="st-refunds"></span></div> <div class="stat"><span class="k">Refunds</span><span class="v" id="st-refunds"></span></div>
@ -721,29 +730,31 @@ sudo apt-get install -y taler-wallet-cli</pre>
<div class="cur-name">GOA</div> <div class="cur-name">GOA</div>
<div class="cur-sub">explorational · hacktivism</div> <div class="cur-sub">explorational · hacktivism</div>
<span class="amt-big" id="goa-paid-amt"></span> <span class="amt-big" id="goa-paid-amt"></span>
<span class="amt-label">paid volume</span> <span class="amt-label">all paid volume</span>
<div class="cur-meta"> <div class="cur-meta">
<div><span class="mk">Orders</span><span class="mv" id="goa-orders"></span></div> <div><span class="mk">Orders</span><span class="mv" id="goa-orders"></span></div>
<div><span class="mk">Paid</span><span class="mv" id="goa-paid"></span></div> <div><span class="mk">Paid</span><span class="mv" id="goa-paid"></span></div>
<div><span class="mk">Wired</span><span class="mv" id="goa-wired"></span></div> <div><span class="mk">Wired</span><span class="mv" id="goa-wired"></span></div>
<div><span class="mk">Unpaid</span><span class="mv" id="goa-unpaid"></span></div> <div><span class="mk">Unpaid</span><span class="mv" id="goa-unpaid"></span></div>
</div> </div>
<div class="amt-secondary">all orders <strong id="goa-all-amt"></strong></div> <div class="amt-secondary">all orders volume <strong id="goa-all-amt"></strong></div>
</div> </div>
<div class="cur-panel chf" id="panel-chf"> <div class="cur-panel chf" id="panel-chf">
<div class="cur-name">CHF</div> <div class="cur-name">CHF</div>
<div class="cur-sub">taler-ops</div> <div class="cur-sub">taler-ops</div>
<span class="amt-big" id="chf-paid-amt"></span> <span class="amt-big" id="chf-paid-amt"></span>
<span class="amt-label">paid volume</span> <span class="amt-label">all paid volume</span>
<div class="cur-meta"> <div class="cur-meta">
<div><span class="mk">Orders</span><span class="mv" id="chf-orders"></span></div> <div><span class="mk">Orders</span><span class="mv" id="chf-orders"></span></div>
<div><span class="mk">Paid</span><span class="mv" id="chf-paid"></span></div> <div><span class="mk">Paid</span><span class="mv" id="chf-paid"></span></div>
<div><span class="mk">Wired</span><span class="mv" id="chf-wired"></span></div> <div><span class="mk">Wired</span><span class="mv" id="chf-wired"></span></div>
<div><span class="mk">Unpaid</span><span class="mv" id="chf-unpaid"></span></div> <div><span class="mk">Unpaid</span><span class="mv" id="chf-unpaid"></span></div>
</div> </div>
<div class="amt-secondary">all orders <strong id="chf-all-amt"></strong></div> <div class="amt-secondary">all orders volume <strong id="chf-all-amt"></strong></div>
</div> </div>
</div> </div>
<p class="note" style="margin-top:0.65rem;text-align:center;font-size:0.68rem">Unit names for large GOA</p>
<div id="st-unit-legend" style="text-align:center"></div>
<p class="stats-foot" id="st-foot">Loading…</p> <p class="stats-foot" id="st-foot">Loading…</p>
<p class="stats-run-note" id="st-run-note" hidden></p> <p class="stats-run-note" id="st-run-note" hidden></p>
</section> </section>
@ -755,22 +766,24 @@ sudo apt-get install -y taler-wallet-cli</pre>
<div class="stat"><span class="k">/config</span><span class="v teal" id="st-config-ms"></span></div> <div class="stat"><span class="k">/config</span><span class="v teal" id="st-config-ms"></span></div>
<div class="stat"><span class="k">/terms</span><span class="v" id="st-terms-ms"></span></div> <div class="stat"><span class="k">/terms</span><span class="v" id="st-terms-ms"></span></div>
<div class="stat"><span class="k">/webui/</span><span class="v" id="st-webui-ms"></span></div> <div class="stat"><span class="k">/webui/</span><span class="v" id="st-webui-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">CPU (container)</span><span class="v teal" id="st-cpu"></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">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">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">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 class="stat"><span class="k">Nginx</span><span class="v sm" id="st-mem-nginx"></span></div>
</div> </div>
<div style="margin-top:0.85rem;overflow-x:auto"> <div style="margin-top:0.85rem;overflow-x:auto">
<p class="note" style="margin-bottom:0.4rem">Top processes (RSS)</p> <p class="note" style="margin-bottom:0.4rem">Top processes (RSS inside container)</p>
<div id="st-mem-top"></div> <div id="st-mem-top"></div>
</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> </section>
<section class="stats" id="activity" aria-labelledby="act-title"> <section class="stats" id="activity" aria-labelledby="act-title">
<h2 id="act-title">Recent activity</h2> <h2 id="act-title">Latest payments</h2>
<p class="note">Latest 5 payments &amp; refunds per currency</p> <p class="note">Latest payments &amp; refunds per currency (all paid totals above)</p>
<div class="act-pair" id="st-activity-pair"> <div class="act-pair" id="st-activity-pair">
<div class="act-col goa" aria-labelledby="act-goa-h"> <div class="act-col goa" aria-labelledby="act-goa-h">
<h3 class="act-col-h" id="act-goa-h">GOA</h3> <h3 class="act-col-h" id="act-goa-h">GOA</h3>
@ -812,6 +825,7 @@ sudo apt-get install -y taler-wallet-cli</pre>
</footer> </footer>
</main> </main>
<script src="/intro/goa-amount.js"></script> <script src="/intro/goa-amount.js"></script>
<script src="/intro/goa-ui.js" defer></script>
<script> <script>
/* Fallback if goa-amount.js not yet deployed (uses amount_alt from stats.json) */ /* Fallback if goa-amount.js not yet deployed (uses amount_alt from stats.json) */
(function () { (function () {
@ -903,7 +917,10 @@ 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); if (window.GoaAmount) {
GoaAmount.rememberAlt(d);
if (GoaAmount.fillLegend) GoaAmount.fillLegend("st-unit-legend", 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);
@ -931,11 +948,16 @@ 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 || "—"; var amtShow = a.amount || "—";
if (window.GoaAmount && !a.amount_alt) { var amtTitle = a.amount || "";
amtShow = GoaAmount.format(a.amount, d.alt_unit_names).display; if (window.GoaAmount) {
var af = GoaAmount.pick(a, d.alt_unit_names);
amtShow = af.display;
amtTitle = af.title || a.amount || "";
} else if (a.amount_alt) {
amtShow = a.amount_alt;
amtTitle = a.amount_full || a.amount || "";
} }
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">' +
@ -963,8 +985,8 @@ sudo apt-get install -y taler-wallet-cli</pre>
if (cur === "GOA") byCur.GOA.push(a); if (cur === "GOA") byCur.GOA.push(a);
else if (cur === "CHF") byCur.CHF.push(a); else if (cur === "CHF") byCur.CHF.push(a);
}); });
byCur.GOA = byCur.GOA.slice(0, 5); byCur.GOA = byCur.GOA.slice(0, 10);
byCur.CHF = byCur.CHF.slice(0, 5); byCur.CHF = byCur.CHF.slice(0, 10);
} }
fillActivityList(document.getElementById("st-activity-goa"), byCur.GOA); fillActivityList(document.getElementById("st-activity-goa"), byCur.GOA);
fillActivityList(document.getElementById("st-activity-chf"), byCur.CHF); fillActivityList(document.getElementById("st-activity-chf"), byCur.CHF);
@ -973,26 +995,39 @@ sudo apt-get install -y taler-wallet-cli</pre>
foot.textContent = (d.dual_currency ? "Dual-currency · " : "") + foot.textContent = (d.dual_currency ? "Dual-currency · " : "") +
(d.generated_at_human || d.generated_at || ""); (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) { function setMem(id, human, n, bytes) {
var el = document.getElementById(id); var el = document.getElementById(id);
if (!el) return; 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.textContent = "—";
el.removeAttribute("title"); el.removeAttribute("title");
return; return;
} }
var h = String(human); 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; el.textContent = h;
if (bytes != null && bytes !== "" && Number(bytes) > 0) { if (isFinite(b) && b > 0) {
el.setAttribute("title", Number(bytes).toLocaleString("en-US") + " B"); el.setAttribute("title", b.toLocaleString("en-US") + " B");
} else el.removeAttribute("title"); } else el.removeAttribute("title");
} }
var p = d.performance || {}; var p = d.performance || {};
set("st-config-ms", msLabel(p.config_ms, p.config_http)); set("st-config-ms", msLabel(p.config_ms, p.config_http));
set("st-terms-ms", msLabel(p.terms_ms, p.terms_http)); set("st-terms-ms", msLabel(p.terms_ms, p.terms_http));
set("st-webui-ms", msLabel(p.webui_ms, p.webui_http)); set("st-webui-ms", msLabel(p.webui_ms, p.webui_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-load", p.loadavg || "—");
set("st-pids", p.pids != null ? p.pids : ((p.memory && p.memory.pids) != null ? p.memory.pids : "—"));
var mem = p.memory || {}; var mem = p.memory || {};
setMem("st-mem-ctr", mem.container_rss_label || mem.container_rss_human || "—", null, mem.container_rss_bytes); 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); setMem("st-mem-pg", mem.postgres_rss_human, mem.postgres_n, mem.postgres_rss_bytes);
@ -1013,12 +1048,12 @@ sudo apt-get install -y taler-wallet-cli</pre>
var cmdFull = t.cmd_full || cmd; var cmdFull = t.cmd_full || cmd;
if (cmd.length > 64) cmd = cmd.slice(0, 61) + "…"; if (cmd.length > 64) cmd = cmd.slice(0, 61) + "…";
th += '<tr><td style="padding:0.25rem;color:#5eead4" title="' + th += '<tr><td style="padding:0.25rem;color:#5eead4" title="' +
(t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + '">' + escHtml(t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + '">' +
(t.rss_human || "?") + escHtml(t.rss_human || "?") +
'</td><td style="padding:0.25rem">' + (t.comm || "") + '</td><td style="padding:0.25rem">' + escHtml(t.comm || "") +
'</td><td style="padding:0.25rem;word-break:break-all;color:var(--muted)" title="' + '</td><td style="padding:0.25rem;word-break:break-all;color:var(--muted)" title="' +
String(cmdFull).replace(/"/g, "&quot;") + '">' + escHtml(cmdFull) + '">' +
cmd + "</td></tr>"; escHtml(cmd) + "</td></tr>";
}); });
th += "</tbody></table>"; th += "</tbody></table>";
topBox.innerHTML = th; topBox.innerHTML = th;
@ -1026,9 +1061,11 @@ sudo apt-get install -y taler-wallet-cli</pre>
} }
var pf = document.getElementById("st-perf-foot"); var pf = document.getElementById("st-perf-foot");
if (pf) { if (pf) {
pf.textContent = "Container RSS + process groups" + var basis = mem.memory_basis === "cgroup" ? "cgroup" : "proc RSS sum";
(mem.source ? " · " + mem.source : "") + " · " + pf.textContent = "Memory: " + basis +
(d.generated_at_human || d.generated_at || ""); (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" }) return fetch("/intro/stats-run.json", { cache: "no-store" })
.then(function (r) { return r.ok ? r.json() : null; }) .then(function (r) { return r.ok ? r.json() : null; })

View file

@ -1,29 +1,63 @@
/* Compact GOA / CUR:amount display using currency alt_unit_names. /* Compact GOA / CUR:amount display using SI-style alt units (kGOA, MGOA, ).
* Large values "1.23 Mega-GOA" (title = full GOA:n) so landing tiles fit. * Large values "1.23 MGOA" (title = full GOA:n) so landing tiles fit.
*/ */
(function (global) { (function (global) {
"use strict"; "use strict";
var DEFAULT_ALT = { var DEFAULT_ALT = {
"24": "Yotta-GOA", "24": "YGOA",
"21": "Zetta-GOA", "21": "ZGOA",
"18": "Exa-GOA", "18": "EGOA",
"15": "Peta-GOA", "15": "PGOA",
"12": "Tera-GOA", "12": "TGOA",
"9": "Giga-GOA", "9": "GGOA",
"6": "Mega-GOA", "6": "MGOA",
"3": "Kilo-GOA", "3": "kGOA",
"0": "GOA", "0": "GOA",
"-1": "Deci-GOA", "-1": "dGOA",
"-2": "Centi-GOA", "-2": "cGOA",
"-3": "Milli-GOA", "-3": "mGOA",
"-6": "Micro-GOA", "-6": "µGOA",
"-7": "Deci-Micro-GOA", "-7": "dµGOA",
"-8": "Atomic-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; 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) { function parseAmount(s) {
if (s == null || s === "") return { cur: "GOA", val: 0 }; if (s == null || s === "") return { cur: "GOA", val: 0 };
if (typeof s === "number") return { cur: "GOA", val: s }; 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] * @param {object} [altMap]
* @returns {{ display: string, title: string, raw: string, usedAlt: boolean }} * @returns {{ display: string, title: string, raw: string, usedAlt: boolean }}
*/ */
@ -59,24 +93,17 @@
if (amount == null || amount === "") { if (amount == null || amount === "") {
return { display: "—", title: "", raw: "", usedAlt: false }; 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 (typeof amount === "object" && amount) {
if (amount.amount_alt) { if (amount.amount) {
var rawO = amount.amount || amount.amount_full || String(amount.amount_alt); return format(amount.amount, altMap);
return {
display: String(amount.amount_alt),
title: amount.amount_full || rawO,
raw: String(rawO),
usedAlt: true
};
} }
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 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 baseName = (alt && alt["0"]) || p.cur;
var curU = String(p.cur || "").toUpperCase(); var curU = String(p.cur || "").toUpperCase();
var baseU = String(baseName || "").toUpperCase(); var baseU = String(baseName || "").toUpperCase();
@ -95,7 +122,9 @@
var n = parseInt(k, 10); var n = parseInt(k, 10);
if (!isNaN(n)) scales.push({ sc: n, name: String(alt[k]) }); 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 abs = Math.abs(p.val);
var chosen = null; var chosen = null;
@ -136,33 +165,80 @@
setText(document.getElementById(id), amount, altMap); 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) { function pick(rowOrAmount, altMap) {
if (rowOrAmount && typeof rowOrAmount === "object") { 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) { if (rowOrAmount.amount_alt) {
return format({ return {
amount: rowOrAmount.amount, display: String(rowOrAmount.amount_alt),
amount_alt: rowOrAmount.amount_alt, title: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
amount_full: rowOrAmount.amount_full raw: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
}, altMap); usedAlt: true
};
} }
return format(rowOrAmount.amount || rowOrAmount.total_amount, altMap); return format("", altMap);
} }
return format(rowOrAmount, altMap); return format(rowOrAmount, altMap);
} }
function rememberAlt(stats) { function rememberAlt(stats) {
if (stats && stats.alt_unit_names && typeof stats.alt_unit_names === "object") { 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 = { global.GoaAmount = {
format: format, format: format,
setText: setText, setText: setText,
setById: setById, setById: setById,
pick: pick, pick: pick,
rememberAlt: rememberAlt, rememberAlt: rememberAlt,
compactAlt: compactAlt,
legendHtml: legendHtml,
fillLegend: fillLegend,
DEFAULT_ALT: DEFAULT_ALT, DEFAULT_ALT: DEFAULT_ALT,
THRESHOLD: THRESHOLD THRESHOLD: THRESHOLD
}; };

View file

@ -1,7 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Snapshot loadavg + RSS groups inside a podman container (for landing stats). # Snapshot RSS process groups inside a podman container + host-side CPU/PIDs.
# Usage: collect_container_resources.sh CONTAINER [OUT.json] # Usage: collect_container_resources.sh CONTAINER [OUT.json]
# Prints JSON to stdout (and writes OUT when given). # Prints JSON to stdout (and writes OUT when given).
#
# Note: rootless podman often has no memory cgroup controller delegated
# (memory.current missing → podman stats shows 0B). We therefore use
# /proc VmRSS sums inside the container for memory, and podman stats for
# CPU% + PID count. /proc/loadavg inside the container is the *host* load.
set -euo pipefail set -euo pipefail
CTR="${1:-}" CTR="${1:-}"
@ -11,7 +16,10 @@ if [ -z "$CTR" ]; then
exit 2 exit 2
fi fi
ADMIN_LOG="${ADMIN_LOG:-${HOME}/src/koopa/koopa-admin-log}" ADMIN_LOG="${ADMIN_LOG:-${HOME}/koopa-admin-log}"
if [ ! -d "$ADMIN_LOG" ] && [ -d "${HOME}/src/koopa/koopa-admin-log" ]; then
ADMIN_LOG="${HOME}/src/koopa/koopa-admin-log"
fi
MEM_SRC="${MEM_SNAPSHOT_SRC:-$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh}" MEM_SRC="${MEM_SNAPSHOT_SRC:-$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh}"
MEM_DST="${MEM_SNAPSHOT_DST:-/usr/local/lib/landing-mem-snapshot.sh}" MEM_DST="${MEM_SNAPSHOT_DST:-/usr/local/lib/landing-mem-snapshot.sh}"
@ -22,10 +30,10 @@ fi
if [ -f "$MEM_SRC" ]; then if [ -f "$MEM_SRC" ]; then
podman exec "$CTR" mkdir -p "$(dirname "$MEM_DST")" 2>/dev/null || true podman exec "$CTR" mkdir -p "$(dirname "$MEM_DST")" 2>/dev/null || true
podman cp "$MEM_SRC" "${CTR}:${MEM_DST}" # Always refresh helper (fixes stale copies missing json_str / emit)
podman cp "$MEM_SRC" "${CTR}:${MEM_DST}" 2>/dev/null || true
fi fi
# Run emit inside container (needs /proc + cgroup of that container)
set +e set +e
raw=$(podman exec "$CTR" bash -c ' raw=$(podman exec "$CTR" bash -c '
set -e set -e
@ -37,7 +45,6 @@ raw=$(podman exec "$CTR" bash -c '
# shellcheck disable=SC1091 # shellcheck disable=SC1091
. /usr/local/lib/landing-mem-snapshot.sh . /usr/local/lib/landing-mem-snapshot.sh
if ! declare -F mem_snapshot_emit >/dev/null 2>&1; then if ! declare -F mem_snapshot_emit >/dev/null 2>&1; then
# older helper without emit — synthesize
mem_snapshot_json mem_snapshot_json
loadavg="" loadavg=""
[ -r /proc/loadavg ] && loadavg=$(awk "{print \$1\",\"\$2\",\"\$3}" /proc/loadavg) [ -r /proc/loadavg ] && loadavg=$(awk "{print \$1\",\"\$2\",\"\$3}" /proc/loadavg)
@ -58,13 +65,115 @@ if [ "$ec" -ne 0 ] || [ -z "$raw" ]; then
fi fi
rm -f /tmp/landing-mem-err.$$ rm -f /tmp/landing-mem-err.$$
# Validate JSON # Host-side: CPU% + PIDs via podman stats (works without memory controller)
if ! printf '%s' "$raw" | python3 -c 'import json,sys; json.load(sys.stdin)' 2>/dev/null; then stats_json=$(podman stats --no-stream --format json "$CTR" 2>/dev/null || true)
printf '{"ok":false,"error":"invalid json from container"}\n'
exit 1 # Merge + normalize with Python
merged=$(
RAW_JSON="$raw" STATS_JSON="$stats_json" CTR_NAME="$CTR" python3 - <<'PY'
import json, os, re
raw = os.environ.get("RAW_JSON") or ""
stats_raw = os.environ.get("STATS_JSON") or ""
ctr = os.environ.get("CTR_NAME") or ""
try:
data = json.loads(raw)
except Exception as e:
print(json.dumps({"ok": False, "error": f"invalid mem json: {e}"}))
raise SystemExit(0)
if not isinstance(data, dict) or not data.get("ok"):
print(json.dumps(data if isinstance(data, dict) else {"ok": False}))
raise SystemExit(0)
mem = data.get("memory") if isinstance(data.get("memory"), dict) else {}
data["memory"] = mem
# Zero-group cleanup: UI should not show "0 B"
for key in ("taler", "java", "nginx", "postgres", "redis", "other"):
n = mem.get(f"{key}_n")
b = mem.get(f"{key}_rss_bytes")
try:
n = int(n) if n is not None else 0
except Exception:
n = 0
try:
b = int(b) if b is not None else 0
except Exception:
b = 0
if n <= 0 or b <= 0:
mem[f"{key}_n"] = 0
mem[f"{key}_rss_bytes"] = 0
mem[f"{key}_rss_human"] = None # UI → "—"
mem[f"{key}_rss_label"] = "—"
# Prefer process-sum label when cgroup missing
if not mem.get("cgroup_bytes") and mem.get("proc_sum_rss_human"):
hum = mem.get("container_rss_human") or mem.get("proc_sum_rss_human")
mem["container_rss_label"] = f"{hum} (proc)"
mem["memory_basis"] = "proc_rss_sum"
elif mem.get("cgroup_limit_human") and mem.get("container_rss_human"):
mem["container_rss_label"] = f"{mem['container_rss_human']} / {mem['cgroup_limit_human']}"
mem["memory_basis"] = "cgroup"
else:
mem["memory_basis"] = mem.get("memory_basis") or "proc_rss_sum"
if mem.get("container_rss_human") and not mem.get("container_rss_label"):
mem["container_rss_label"] = mem["container_rss_human"]
# loadavg inside rootless containers is usually the host's
data["loadavg_note"] = "host (shared; rootless container /proc/loadavg)"
data["loadavg_source"] = "host_via_container_proc"
# podman stats enrichment
cpu = None
pids = None
if stats_raw.strip():
try:
st = json.loads(stats_raw)
if isinstance(st, list) and st:
st = st[0]
if isinstance(st, dict):
cp = st.get("CPUPerc") or st.get("cpu_percent") or st.get("CPU")
if cp is not None:
s = str(cp).strip().rstrip("%")
try:
cpu = float(s)
except Exception:
cpu = None
p = st.get("PIDs") or st.get("pids")
if p is not None:
try:
pids = int(str(p).strip())
except Exception:
pids = None
# mem from stats only if non-zero (usually 0B without memory controller)
mu = st.get("MemUsage") or st.get("mem_usage") or ""
m = re.match(r"^\s*([0-9.]+)\s*([KMGT]?i?B)\s*/\s*([0-9.]+)\s*([KMGT]?i?B)", str(mu), re.I)
if m and float(m.group(1)) > 0:
mem["podman_stats_mem"] = str(mu).strip()
except Exception:
pass
if cpu is not None:
data["cpu_percent"] = cpu
data["cpu_percent_label"] = f"{cpu:.2f}%"
if pids is not None:
data["pids"] = pids
mem["pids"] = pids
data["container"] = ctr
data["source"] = "mem-snapshot+podman-stats"
print(json.dumps(data, ensure_ascii=False))
PY
)
if ! printf '%s' "$merged" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("ok")' 2>/dev/null; then
# fall back to raw if merge failed
merged="$raw"
fi fi
if [ -n "$OUT" ]; then if [ -n "$OUT" ]; then
printf '%s\n' "$raw" >"$OUT" printf '%s\n' "$merged" >"$OUT"
fi fi
printf '%s\n' "$raw" printf '%s\n' "$merged"

View file

@ -1,38 +1,81 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""GOA (and generic CUR:amount) alt-unit formatting for landing stats. """GOA (and generic CUR:amount) alt-unit formatting for landing stats.
Matches exchange currency_specification.alt_unit_names: Display uses compact SI-style names (kGOA, MGOA, GGOA, ) so tiles stay short.
0GOA, 3Kilo-GOA, 6Mega-GOA, and fractional scales. Exchange /config may still send long names (Kilo-GOA); we compact them.
Display prefers compact alt names for large values so landing tiles fit.
""" """
from __future__ import annotations from __future__ import annotations
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Optional, Tuple
# Default GOA ladder (same shape as exchange /config) # Compact display ladder (preferred on landings)
DEFAULT_ALT: Dict[str, str] = { DEFAULT_ALT: Dict[str, str] = {
"24": "Yotta-GOA", "24": "YGOA",
"21": "Zetta-GOA", "21": "ZGOA",
"18": "Exa-GOA", "18": "EGOA",
"15": "Peta-GOA", "15": "PGOA",
"12": "Tera-GOA", "12": "TGOA",
"9": "Giga-GOA", "9": "GGOA",
"6": "Mega-GOA", "6": "MGOA",
"3": "Kilo-GOA", "3": "kGOA",
"0": "GOA", "0": "GOA",
"-1": "Deci-GOA", "-1": "dGOA",
"-2": "Centi-GOA", "-2": "cGOA",
"-3": "Milli-GOA", "-3": "mGOA",
"-6": "Micro-GOA", "-6": "µGOA",
"-7": "Deci-Micro-GOA", "-7": "GOA",
"-8": "Atomic-GOA", "-8": "aGOA",
} }
# Use alt unit when |value| >= this (base units). Below: keep compact CUR:n. # Long names from exchange /config → compact
_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",
}
# Use alt unit when |value| >= this (base units). Below: keep CUR:n.
ALT_THRESHOLD = Decimal("1000") ALT_THRESHOLD = Decimal("1000")
def compact_alt_map(alt: Optional[Dict[str, str]] = None) -> Dict[str, str]:
"""Normalize any alt_unit_names map to compact display labels."""
base = dict(DEFAULT_ALT)
if not alt:
return base
out: Dict[str, str] = {}
for k, v in alt.items():
sk = str(k)
name = str(v).strip()
low = name.lower().replace(" ", "")
if low in _LONG_TO_COMPACT:
out[sk] = _LONG_TO_COMPACT[low]
elif sk in DEFAULT_ALT and (
"goa" in low or name == DEFAULT_ALT.get(sk) or name in DEFAULT_ALT.values()
):
# keep scale key, prefer compact for GOA family
out[sk] = DEFAULT_ALT.get(sk, name)
else:
out[sk] = name
# ensure full default ladder for missing scales
for sk, name in DEFAULT_ALT.items():
out.setdefault(sk, name)
return out
def parse_amount(s: Any) -> Tuple[str, Decimal]: def parse_amount(s: Any) -> Tuple[str, Decimal]:
"""Parse 'GOA:12.5' or bare number → (currency, value).""" """Parse 'GOA:12.5' or bare number → (currency, value)."""
if s is None: if s is None:
@ -51,7 +94,6 @@ def parse_amount(s: Any) -> Tuple[str, Decimal]:
def fmt_coeff(v: Decimal) -> str: def fmt_coeff(v: Decimal) -> str:
if v == v.to_integral_value(): if v == v.to_integral_value():
return format(int(v), "d") return format(int(v), "d")
# up to 4 significant fractional digits, strip trailing zeros
q = v.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP) q = v.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
s = format(q, "f").rstrip("0").rstrip(".") s = format(q, "f").rstrip("0").rstrip(".")
return s or "0" return s or "0"
@ -60,7 +102,6 @@ def fmt_coeff(v: Decimal) -> str:
def fmt_base(cur: str, val: Decimal) -> str: def fmt_base(cur: str, val: Decimal) -> str:
if val == val.to_integral_value(): if val == val.to_integral_value():
return f"{cur}:{int(val)}" return f"{cur}:{int(val)}"
# preserve up to 8 fractional digits (Taler style)
s = format(val, "f").rstrip("0").rstrip(".") s = format(val, "f").rstrip("0").rstrip(".")
return f"{cur}:{s}" return f"{cur}:{s}"
@ -76,12 +117,12 @@ def format_amount_alt(
Keys: Keys:
amount canonical CUR:n amount canonical CUR:n
amount_alt short display (e.g. "1.23 Mega-GOA") amount_alt short display (e.g. "1.23 MGOA")
amount_full "1.23 Mega-GOA (GOA:1230000)" when alt used amount_full "1.23 MGOA (GOA:1230000)" when alt used
value float for JSON (may lose precision above 2^53 also value_str) value float for JSON
value_str exact decimal string value_str exact decimal string
""" """
alt = alt or DEFAULT_ALT alt = compact_alt_map(alt)
try: try:
cur, val = parse_amount(amount) cur, val = parse_amount(amount)
except (InvalidOperation, ValueError, ArithmeticError): except (InvalidOperation, ValueError, ArithmeticError):
@ -95,17 +136,21 @@ def format_amount_alt(
} }
base = fmt_base(cur, val) base = fmt_base(cur, val)
value_str = format(val, "f").rstrip("0").rstrip(".") if val != val.to_integral_value() else str(int(val)) value_str = (
format(val, "f").rstrip("0").rstrip(".")
if val != val.to_integral_value()
else str(int(val))
)
try: try:
value_f = float(val) value_f = float(val)
except Exception: except Exception:
value_f = 0.0 value_f = 0.0
base_name = alt.get("0") or cur base_name = alt.get("0") or cur
# Never rename foreign currencies with a GOA alt map (merchant dual CHF+GOA).
cur_u = (cur or "").upper() cur_u = (cur or "").upper()
base_u = str(base_name).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): # Foreign currencies must not pick up kGOA / MGOA labels
if cur_u and cur_u != "GOA" and ("GOA" in base_u or base_u.endswith("GOA")):
return { return {
"amount": base, "amount": base,
"amount_alt": base, "amount_alt": base,
@ -115,7 +160,6 @@ def format_amount_alt(
} }
if val == 0: if val == 0:
# Keep canonical CUR:0 (avoids "0 GOA" for empty foreign balances)
return { return {
"amount": base, "amount": base,
"amount_alt": base, "amount_alt": base,
@ -133,9 +177,7 @@ def format_amount_alt(
scales.sort(key=lambda x: -x[0]) scales.sort(key=lambda x: -x[0])
absval = abs(val) absval = abs(val)
# Below threshold: short base form (saves noise on small demo amounts)
if absval < threshold: if absval < threshold:
# Prefer "GOA:12.5" style (matches existing landings) when small
return { return {
"amount": base, "amount": base,
"amount_alt": base, "amount_alt": base,
@ -165,7 +207,7 @@ def format_amount_alt(
sc, name, coeff = chosen sc, name, coeff = chosen
short = f"{fmt_coeff(coeff)} {name}" short = f"{fmt_coeff(coeff)} {name}"
full = f"{short} ({base})" if with_base or True else short full = f"{short} ({base})"
return { return {
"amount": base, "amount": base,
"amount_alt": short, "amount_alt": short,
@ -175,7 +217,9 @@ def format_amount_alt(
} }
def attach_alt(obj: Dict[str, Any], amount_key: str = "amount", alt: Optional[Dict[str, str]] = None) -> Dict[str, Any]: def attach_alt(
obj: Dict[str, Any], amount_key: str = "amount", alt: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Mutate obj: ensure amount_alt / amount_full from amount_key.""" """Mutate obj: ensure amount_alt / amount_full from amount_key."""
if not isinstance(obj, dict): if not isinstance(obj, dict):
return obj return obj
@ -193,7 +237,7 @@ def attach_alt(obj: Dict[str, Any], amount_key: str = "amount", alt: Optional[Di
def load_alt_from_config(url: str, timeout: float = 8.0) -> Dict[str, str]: def load_alt_from_config(url: str, timeout: float = 8.0) -> Dict[str, str]:
"""GET exchange/bank /config → alt_unit_names (fallback DEFAULT_ALT).""" """GET exchange/bank /config → compact alt_unit_names (fallback DEFAULT_ALT)."""
import json import json
import urllib.request import urllib.request
@ -215,16 +259,16 @@ def load_alt_from_config(url: str, timeout: float = 8.0) -> Dict[str, str]:
break break
if not isinstance(au, dict) or "0" not in au: if not isinstance(au, dict) or "0" not in au:
return dict(DEFAULT_ALT) return dict(DEFAULT_ALT)
return {str(k): str(v) for k, v in au.items()} return compact_alt_map({str(k): str(v) for k, v in au.items()})
def enrich_stats_tree(data: Dict[str, Any], alt: Optional[Dict[str, str]] = None) -> Dict[str, Any]: def enrich_stats_tree(data: Dict[str, Any], alt: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
"""Walk common landing stats.json shapes and add amount_alt fields.""" """Walk common landing stats.json shapes and add amount_alt fields."""
alt = alt or DEFAULT_ALT alt = compact_alt_map(alt)
if not isinstance(data, dict): if not isinstance(data, dict):
return data return data
# bank-style # bank-style nested blocks
for path in ( for path in (
("flow", "incoming"), ("flow", "incoming"),
("flow", "withdraw"), ("flow", "withdraw"),
@ -252,6 +296,7 @@ def enrich_stats_tree(data: Dict[str, Any], alt: Optional[Dict[str, str]] = None
f = format_amount_alt(cur["last_amount"], alt) f = format_amount_alt(cur["last_amount"], alt)
cur["last_amount"] = f["amount"] cur["last_amount"] = f["amount"]
cur["last_amount_alt"] = f["amount_alt"] cur["last_amount_alt"] = f["amount_alt"]
cur["last_amount_full"] = f["amount_full"]
if isinstance(data.get("flow"), dict): if isinstance(data.get("flow"), dict):
fl = data["flow"] fl = data["flow"]
@ -295,6 +340,7 @@ def enrich_stats_tree(data: Dict[str, Any], alt: Optional[Dict[str, str]] = None
f = format_amount_alt(row["value"], alt) f = format_amount_alt(row["value"], alt)
row["value"] = f["amount"] row["value"] = f["amount"]
row["value_alt"] = f["amount_alt"] row["value_alt"] = f["amount_alt"]
row["value_full"] = f["amount_full"]
# merchant dual currency # merchant dual currency
for block in data.get("by_currency") or []: for block in data.get("by_currency") or []:
@ -315,4 +361,10 @@ def enrich_stats_tree(data: Dict[str, Any], alt: Optional[Dict[str, str]] = None
attach_alt(row, "amount", alt) attach_alt(row, "amount", alt)
data["alt_unit_names"] = alt data["alt_unit_names"] = alt
# small legend for UI (positive scales only, compact)
data["alt_unit_legend"] = [
{"scale": int(k), "name": alt[k], "factor": f"10^{k}"}
for k in sorted(alt.keys(), key=lambda x: -int(x))
if int(k) >= 0
]
return data return data

View file

@ -63,12 +63,46 @@ def merge(stats: Dict[str, Any], resources: Dict[str, Any]) -> Dict[str, Any]:
if resources.get("loadavg"): if resources.get("loadavg"):
perf["loadavg"] = resources["loadavg"] perf["loadavg"] = resources["loadavg"]
perf["loadavg_source"] = "container" # rootless: /proc/loadavg is almost always the host's
perf["loadavg_source"] = resources.get("loadavg_source") or "host_via_container_proc"
if resources.get("loadavg_note"):
perf["loadavg_note"] = resources["loadavg_note"]
if resources.get("cpu_percent") is not None:
perf["cpu_percent"] = resources["cpu_percent"]
perf["cpu_percent_label"] = resources.get("cpu_percent_label") or (
f'{resources["cpu_percent"]:.2f}%'
)
if resources.get("pids") is not None:
perf["pids"] = resources["pids"]
mem = resources.get("memory") mem = resources.get("memory")
if isinstance(mem, dict) and mem: if isinstance(mem, dict) and mem:
perf["memory"] = compact_memory(dict(mem)) m = compact_memory(dict(mem))
perf["memory"]["source"] = resources.get("source") or "mem-snapshot" # normalize empty groups for UI
for key in ("taler", "java", "nginx", "postgres", "redis", "other"):
try:
n = int(m.get(f"{key}_n") or 0)
b = int(m.get(f"{key}_rss_bytes") or 0)
except Exception:
n, b = 0, 0
if n <= 0 or b <= 0:
m[f"{key}_n"] = 0
m[f"{key}_rss_bytes"] = 0
m[f"{key}_rss_human"] = None
h = m.get("container_rss_human") or ""
has_cgroup = bool(m.get("cgroup_bytes"))
if m.get("cgroup_limit_human") and h != "":
m["container_rss_label"] = f"{h} / {m['cgroup_limit_human']}"
m["memory_basis"] = "cgroup"
elif not has_cgroup and h != "":
m["container_rss_label"] = f"{h} (proc)"
m["memory_basis"] = m.get("memory_basis") or "proc_rss_sum"
elif not m.get("container_rss_label"):
m["container_rss_label"] = h
m["source"] = resources.get("source") or "mem-snapshot"
perf["memory"] = m
return stats return stats

View file

@ -66,8 +66,8 @@ bank = {
"recent_withdraws": [{"amount": "GOA:4503599627370495"}], "recent_withdraws": [{"amount": "GOA:4503599627370495"}],
} }
enrich_stats_tree(bank, DEFAULT_ALT) enrich_stats_tree(bank, DEFAULT_ALT)
assert "Kilo-GOA" in bank["withdraws"]["total_amount_alt"] assert "kGOA" in bank["withdraws"]["total_amount_alt"]
assert "Peta-GOA" in bank["recent_withdraws"][0]["amount_alt"] assert "PGOA" in bank["recent_withdraws"][0]["amount_alt"]
ex = { ex = {
"ok": True, "ok": True,
@ -77,8 +77,8 @@ ex = {
"by_denom": [{"value": "GOA:1000"}], "by_denom": [{"value": "GOA:1000"}],
} }
enrich_stats_tree(ex, DEFAULT_ALT) enrich_stats_tree(ex, DEFAULT_ALT)
assert "Peta-GOA" in ex["wire_in_amount_alt"] assert "PGOA" in ex["wire_in_amount_alt"]
assert "Mega-GOA" in ex["withdraw_amount_alt"] assert "MGOA" in ex["withdraw_amount_alt"]
assert ex["coins_remaining_amount_alt"] in ("GOA:0", "0 GOA") assert ex["coins_remaining_amount_alt"] in ("GOA:0", "0 GOA")
mer = { mer = {
@ -92,10 +92,10 @@ mer = {
], ],
} }
enrich_stats_tree(mer, DEFAULT_ALT) enrich_stats_tree(mer, DEFAULT_ALT)
# CHF must NOT become Kilo-GOA # CHF must NOT become kGOA
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 mer["by_currency"][1]["amount_paid_sum_alt"].startswith("CHF")
assert "Kilo-GOA" in mer["recent_activity_by_currency"][0]["items"][0]["amount_alt"] assert "kGOA" in mer["recent_activity_by_currency"][0]["items"][0]["amount_alt"]
assert mer.get("alt_unit_legend")
print("unit ok") print("unit ok")
PY PY
then ok "enrich shapes bank/exchange/merchant" then ok "enrich shapes bank/exchange/merchant"

View file

@ -207,7 +207,7 @@ for item in $SCHEMAS; do
done done
# --- recent activity: 5 latest events per currency (GOA + CHF), payments + refunds --- # --- recent activity: 5 latest events per currency (GOA + CHF), payments + refunds ---
ACT_PER_CURRENCY="${ACT_PER_CURRENCY:-5}" ACT_PER_CURRENCY="${ACT_PER_CURRENCY:-10}"
# Query latest ACT_PER_CURRENCY events for one currency code (payments refunds). # Query latest ACT_PER_CURRENCY events for one currency code (payments refunds).
# Writes TSV rows: ts kind order_id amount summary status # Writes TSV rows: ts kind order_id amount summary status