Compare commits
7 commits
bf8a915a01
...
4685e370ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4685e370ac | ||
|
|
186fdc255c | ||
|
|
bdfdbfe7c8 | ||
|
|
1bbdc2d1ae | ||
|
|
d5eba02945 | ||
|
|
33749ed714 | ||
|
|
fb1a0b0b8e |
16 changed files with 1363 additions and 337 deletions
21
ISSUES.md
21
ISSUES.md
|
|
@ -1,6 +1,6 @@
|
|||
# Open issues
|
||||
|
||||
Stand **2026-07-16** (koopa, live). Keine Secrets.
|
||||
Stand **2026-07-17** (koopa, live). Keine Secrets.
|
||||
|
||||
## Index
|
||||
|
||||
|
|
@ -13,6 +13,7 @@ Stand **2026-07-16** (koopa, live). Keine Secrets.
|
|||
| [I-05](#i-2026-07-16-05) | low | open | Alte exited Podman-Container |
|
||||
| [I-06](#i-2026-07-16-06) | low–medium | open | Nym HTTP API `*:9080` |
|
||||
| [I-07](#i-2026-07-16-07) | medium | **fixed** | Tor host vs `koopa-tor-relay` |
|
||||
| [I-08](#i-2026-07-17-08) | medium | open | Landing-stats → standalone `taler-landing-stats` |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -131,6 +132,24 @@ podman ps --filter name=koopa-tor-relay
|
|||
|
||||
---
|
||||
|
||||
### I-2026-07-17-08
|
||||
|
||||
**Landing-stats / Resource-Monitoring: auf standalone `taler-landing-stats` umstellen**
|
||||
Severity: medium · Host: koopa · Status: open · **TODO (noch nicht umsetzen auf koopa)**
|
||||
|
||||
**Ist:** Collector + Timer leben unter `scripts/taler-landing/` in **diesem** admin-log, installiert nach `~/.local/bin` / user-systemd, Defaults stark **hacktivism.ch**-lastig (Container-Namen, URLs, `ADMIN_LOG`-Pfade).
|
||||
|
||||
**Soll:** Dieselbe **moderne, host-dynamische** Lösung wie geplant/gebaut für andere Stacks:
|
||||
|
||||
- Eigenes Repo **`taler-landing-stats`** auf **git.hacktivism.ch** (`hernani/taler-landing-stats`)
|
||||
- Profile/env pro Host (bank/exchange/merchant Container, Landing-Pfade, optional bank full-scan)
|
||||
- User-systemd timer (`taler-landing-stats.service` / `.timer`), Linger
|
||||
- Resource-Merge (RSS/loadavg via in-container mem-snapshot + podman stats) host-agnostisch
|
||||
|
||||
**Nicht jetzt auf koopa umbauen** — nur merken. Wenn das Repo steht und auf stage/etc. erprobt ist: koopa von `scripts/taler-landing/` auf Checkout+Install aus `taler-landing-stats` migrieren; hier ggf. nur noch Pointer/README.
|
||||
|
||||
---
|
||||
|
||||
## Kein Issue (Scan)
|
||||
|
||||
- Prod-Container mit Boot-Autostart: Up
|
||||
|
|
|
|||
|
|
@ -1,29 +1,63 @@
|
|||
/* Compact GOA / CUR:amount display using currency alt_unit_names.
|
||||
* Large values → "1.23 Mega-GOA" (title = full GOA:n) so landing tiles fit.
|
||||
/* Compact GOA / CUR:amount display using SI-style alt units (kGOA, MGOA, …).
|
||||
* Large values → "1.23 MGOA" (title = full GOA:n) so landing tiles fit.
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var DEFAULT_ALT = {
|
||||
"24": "Yotta-GOA",
|
||||
"21": "Zetta-GOA",
|
||||
"18": "Exa-GOA",
|
||||
"15": "Peta-GOA",
|
||||
"12": "Tera-GOA",
|
||||
"9": "Giga-GOA",
|
||||
"6": "Mega-GOA",
|
||||
"3": "Kilo-GOA",
|
||||
"24": "YGOA",
|
||||
"21": "ZGOA",
|
||||
"18": "EGOA",
|
||||
"15": "PGOA",
|
||||
"12": "TGOA",
|
||||
"9": "GGOA",
|
||||
"6": "MGOA",
|
||||
"3": "kGOA",
|
||||
"0": "GOA",
|
||||
"-1": "Deci-GOA",
|
||||
"-2": "Centi-GOA",
|
||||
"-3": "Milli-GOA",
|
||||
"-6": "Micro-GOA",
|
||||
"-7": "Deci-Micro-GOA",
|
||||
"-8": "Atomic-GOA"
|
||||
"-1": "dGOA",
|
||||
"-2": "cGOA",
|
||||
"-3": "mGOA",
|
||||
"-6": "µGOA",
|
||||
"-7": "dµGOA",
|
||||
"-8": "aGOA"
|
||||
};
|
||||
|
||||
var LONG_TO_COMPACT = {
|
||||
"yotta-goa": "YGOA",
|
||||
"zetta-goa": "ZGOA",
|
||||
"exa-goa": "EGOA",
|
||||
"peta-goa": "PGOA",
|
||||
"tera-goa": "TGOA",
|
||||
"giga-goa": "GGOA",
|
||||
"mega-goa": "MGOA",
|
||||
"kilo-goa": "kGOA",
|
||||
"goa": "GOA",
|
||||
"deci-goa": "dGOA",
|
||||
"centi-goa": "cGOA",
|
||||
"milli-goa": "mGOA",
|
||||
"micro-goa": "µGOA",
|
||||
"deci-micro-goa": "dµGOA",
|
||||
"atomic-goa": "aGOA"
|
||||
};
|
||||
|
||||
var THRESHOLD = 1000;
|
||||
|
||||
function compactAlt(altMap) {
|
||||
var out = {};
|
||||
var src = altMap || DEFAULT_ALT;
|
||||
Object.keys(DEFAULT_ALT).forEach(function (k) {
|
||||
out[k] = DEFAULT_ALT[k];
|
||||
});
|
||||
Object.keys(src).forEach(function (k) {
|
||||
var name = String(src[k] || "").trim();
|
||||
var low = name.toLowerCase().replace(/\s+/g, "");
|
||||
if (LONG_TO_COMPACT[low]) out[k] = LONG_TO_COMPACT[low];
|
||||
else if (DEFAULT_ALT[k] && /goa/i.test(name)) out[k] = DEFAULT_ALT[k];
|
||||
else out[k] = name || DEFAULT_ALT[k] || name;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseAmount(s) {
|
||||
if (s == null || s === "") return { cur: "GOA", val: 0 };
|
||||
if (typeof s === "number") return { cur: "GOA", val: s };
|
||||
|
|
@ -51,7 +85,7 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string|number} amount
|
||||
* @param {string|number|object} amount
|
||||
* @param {object} [altMap]
|
||||
* @returns {{ display: string, title: string, raw: string, usedAlt: boolean }}
|
||||
*/
|
||||
|
|
@ -59,24 +93,17 @@
|
|||
if (amount == null || amount === "") {
|
||||
return { display: "—", title: "", raw: "", usedAlt: false };
|
||||
}
|
||||
// Prefer server-provided alt if caller passes object {amount, amount_alt}
|
||||
// Prefer re-format from canonical amount (keeps kGOA even if server sent Kilo-GOA)
|
||||
if (typeof amount === "object" && amount) {
|
||||
if (amount.amount_alt) {
|
||||
var rawO = amount.amount || amount.amount_full || String(amount.amount_alt);
|
||||
return {
|
||||
display: String(amount.amount_alt),
|
||||
title: amount.amount_full || rawO,
|
||||
raw: String(rawO),
|
||||
usedAlt: true
|
||||
};
|
||||
if (amount.amount) {
|
||||
return format(amount.amount, altMap);
|
||||
}
|
||||
amount = amount.amount || amount.amount_full || "";
|
||||
amount = amount.amount_full || amount.amount_alt || "";
|
||||
}
|
||||
|
||||
var alt = altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT;
|
||||
var alt = compactAlt(altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT);
|
||||
var p = parseAmount(amount);
|
||||
var raw = baseStr(p.cur, p.val);
|
||||
// Foreign currencies must not pick up Kilo-GOA / Mega-GOA labels
|
||||
var baseName = (alt && alt["0"]) || p.cur;
|
||||
var curU = String(p.cur || "").toUpperCase();
|
||||
var baseU = String(baseName || "").toUpperCase();
|
||||
|
|
@ -95,7 +122,9 @@
|
|||
var n = parseInt(k, 10);
|
||||
if (!isNaN(n)) scales.push({ sc: n, name: String(alt[k]) });
|
||||
});
|
||||
scales.sort(function (a, b) { return b.sc - a.sc; });
|
||||
scales.sort(function (a, b) {
|
||||
return b.sc - a.sc;
|
||||
});
|
||||
|
||||
var abs = Math.abs(p.val);
|
||||
var chosen = null;
|
||||
|
|
@ -136,33 +165,80 @@
|
|||
setText(document.getElementById(id), amount, altMap);
|
||||
}
|
||||
|
||||
/** Prefer amount_alt from stats row, else format amount. */
|
||||
/** Prefer formatting from amount field; fall back to amount_alt string. */
|
||||
function pick(rowOrAmount, altMap) {
|
||||
if (rowOrAmount && typeof rowOrAmount === "object") {
|
||||
var a =
|
||||
rowOrAmount.amount ||
|
||||
rowOrAmount.total_amount ||
|
||||
rowOrAmount.amount_paid_sum ||
|
||||
rowOrAmount.amount_sum;
|
||||
if (a) return format(a, altMap);
|
||||
if (rowOrAmount.amount_alt) {
|
||||
return format({
|
||||
amount: rowOrAmount.amount,
|
||||
amount_alt: rowOrAmount.amount_alt,
|
||||
amount_full: rowOrAmount.amount_full
|
||||
}, altMap);
|
||||
return {
|
||||
display: String(rowOrAmount.amount_alt),
|
||||
title: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
|
||||
raw: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
|
||||
usedAlt: true
|
||||
};
|
||||
}
|
||||
return format(rowOrAmount.amount || rowOrAmount.total_amount, altMap);
|
||||
return format("", altMap);
|
||||
}
|
||||
return format(rowOrAmount, altMap);
|
||||
}
|
||||
|
||||
function rememberAlt(stats) {
|
||||
if (stats && stats.alt_unit_names && typeof stats.alt_unit_names === "object") {
|
||||
global.__GOA_ALT_UNITS = stats.alt_unit_names;
|
||||
global.__GOA_ALT_UNITS = compactAlt(stats.alt_unit_names);
|
||||
}
|
||||
}
|
||||
|
||||
/** Small legend HTML (positive scales). */
|
||||
function legendHtml(stats) {
|
||||
var alt = compactAlt((stats && stats.alt_unit_names) || global.__GOA_ALT_UNITS || DEFAULT_ALT);
|
||||
var rows = [];
|
||||
Object.keys(alt)
|
||||
.map(function (k) {
|
||||
return parseInt(k, 10);
|
||||
})
|
||||
.filter(function (n) {
|
||||
return !isNaN(n) && n > 0;
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return a - b;
|
||||
})
|
||||
.forEach(function (sc) {
|
||||
rows.push(
|
||||
"<tr><td>10<sup>" +
|
||||
sc +
|
||||
"</sup></td><td><strong>" +
|
||||
alt[String(sc)] +
|
||||
"</strong></td></tr>"
|
||||
);
|
||||
});
|
||||
if (!rows.length) return "";
|
||||
return (
|
||||
'<table class="unit-legend" aria-label="GOA unit names"><thead><tr><th>×base</th><th>name</th></tr></thead><tbody>' +
|
||||
rows.join("") +
|
||||
"</tbody></table>"
|
||||
);
|
||||
}
|
||||
|
||||
function fillLegend(elOrId, stats) {
|
||||
var el = typeof elOrId === "string" ? document.getElementById(elOrId) : elOrId;
|
||||
if (!el) return;
|
||||
el.innerHTML = legendHtml(stats);
|
||||
}
|
||||
|
||||
global.GoaAmount = {
|
||||
format: format,
|
||||
setText: setText,
|
||||
setById: setById,
|
||||
pick: pick,
|
||||
rememberAlt: rememberAlt,
|
||||
compactAlt: compactAlt,
|
||||
legendHtml: legendHtml,
|
||||
fillLegend: fillLegend,
|
||||
DEFAULT_ALT: DEFAULT_ALT,
|
||||
THRESHOLD: THRESHOLD
|
||||
};
|
||||
|
|
|
|||
|
|
@ -995,6 +995,27 @@
|
|||
color: #f0a090;
|
||||
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 {
|
||||
margin-top: 2rem;
|
||||
color: var(--muted);
|
||||
|
|
@ -1081,7 +1102,17 @@
|
|||
border-radius: 10px;
|
||||
}
|
||||
.shop-foot a:hover { filter: brightness(1.06); }
|
||||
</style>
|
||||
.landing-rev {
|
||||
margin: 0.75rem 0 0;
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted, #9a8fb0);
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
}
|
||||
.landing-rev a { color: inherit; text-decoration: underline; text-underline-offset: 2px; opacity: 0.95; }
|
||||
.landing-rev strong { font-weight: 750; }
|
||||
|
||||
</style>
|
||||
<base href="/intro/" />
|
||||
<link rel="stylesheet" href="/intro/shop-pay.css" />
|
||||
<script src="/intro/qrcode.min.js"></script>
|
||||
|
|
@ -1392,39 +1423,67 @@ tw run-until-done && tw balance</pre>
|
|||
|
||||
<!-- ========== LIVE STATS (before performance) ========== -->
|
||||
<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="stat">
|
||||
<span class="k">All money (explorer)</span>
|
||||
<span class="v teal" id="st-all-money">—</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="k">Accounts</span>
|
||||
<span class="v teal" id="st-accounts">—</span>
|
||||
</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">
|
||||
<span class="k">Unique wallets</span>
|
||||
<span class="v teal" id="st-wallets">—</span>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="k">7d</span>
|
||||
<span class="k">Withdraws 7d</span>
|
||||
<span class="v" id="st-7d">—</span>
|
||||
</div>
|
||||
<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">
|
||||
<li class="meta">…</li>
|
||||
</ul>
|
||||
</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>
|
||||
<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-run-note" id="st-run-note" hidden></p>
|
||||
</section>
|
||||
|
|
@ -1448,11 +1507,19 @@ tw run-until-done && tw balance</pre>
|
|||
<span class="v" id="st-webui-ms">—</span>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="stat">
|
||||
|
|
@ -1469,10 +1536,10 @@ tw run-until-done && tw balance</pre>
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<footer>
|
||||
|
|
@ -1488,6 +1555,13 @@ tw run-until-done && tw balance</pre>
|
|||
Privacy: processing under Swiss FADP (revDSG). What is retained is listed on
|
||||
<a href="https://bank.hacktivism.ch/privacy" style="color:#5eead4">/privacy</a>.
|
||||
</p>
|
||||
<!-- landing-rev -->
|
||||
<p class="landing-rev" data-landing-site="bank" data-landing-version="42" data-landing-commit="921a4f1">
|
||||
Bank landing <strong>v42</strong>
|
||||
· content <time datetime="2026-07-17T13:32:59+02:00">2026-07-17 13:32 CEST</time>
|
||||
· <a href="https://git.hacktivism.ch/hernani/koopa-admin-log/commit/921a4f1572dbf4391b69899287b4c0280bb0e271" title="koopa-admin-log 921a4f1572dbf4391b69899287b4c0280bb0e271">921a4f1</a>
|
||||
</p>
|
||||
<!-- /landing-rev -->
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
|
|
@ -1985,46 +2059,65 @@ tw run-until-done && tw balance</pre>
|
|||
var fwd = flow.withdraw || {};
|
||||
set("st-accounts", ba.total != null ? ba.total : "—");
|
||||
set("st-wallets", wl.unique_reserves != null ? wl.unique_reserves : "—");
|
||||
setAmt("st-incoming", fin.amount_alt ? fin : (fin.amount || flow.total_in || "—"));
|
||||
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
|
||||
setAmt("st-all-money", d.balance_explorer
|
||||
? { 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");
|
||||
if (list) {
|
||||
function fillMoneyList(listId, rows, emptyLabel) {
|
||||
var list = document.getElementById(listId);
|
||||
if (!list) return;
|
||||
list.innerHTML = "";
|
||||
/* Full list from stats.json (generator emits 10) */
|
||||
var rows = (d.recent_withdraws || []).slice(0, 10);
|
||||
rows = rows || [];
|
||||
if (!rows.length) {
|
||||
var empty = document.createElement("li");
|
||||
empty.className = "meta";
|
||||
empty.textContent = "—";
|
||||
empty.textContent = emptyLabel || "—";
|
||||
list.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";
|
||||
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);
|
||||
});
|
||||
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);
|
||||
});
|
||||
}
|
||||
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) {
|
||||
foot.hidden = false;
|
||||
|
|
@ -2037,19 +2130,30 @@ tw run-until-done && tw balance</pre>
|
|||
if (http && http !== "200") s += " (" + http + ")";
|
||||
return s;
|
||||
}
|
||||
function escHtml(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/&/g, "&").replace(/</g, "<")
|
||||
.replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
function setMem(id, human, n, bytes) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
if (!human && human !== 0) {
|
||||
var b = bytes != null && bytes !== "" ? Number(bytes) : NaN;
|
||||
var nn = n != null && n !== "" ? Number(n) : NaN;
|
||||
var empty =
|
||||
human == null || human === "" || human === "—" ||
|
||||
human === "0 B" || human === "0B" ||
|
||||
(isFinite(b) && b <= 0 && (!isFinite(nn) || nn <= 0));
|
||||
if (empty) {
|
||||
el.textContent = "—";
|
||||
el.removeAttribute("title");
|
||||
return;
|
||||
}
|
||||
var h = String(human);
|
||||
if (n != null && n !== "" && Number(n) > 0) h += " · " + n + "p";
|
||||
if (isFinite(nn) && nn > 0) h += " · " + nn + "p";
|
||||
el.textContent = h;
|
||||
if (bytes != null && bytes !== "" && bytes !== 0) {
|
||||
el.setAttribute("title", Number(bytes).toLocaleString("en-US") + " B");
|
||||
if (isFinite(b) && b > 0) {
|
||||
el.setAttribute("title", b.toLocaleString("en-US") + " B");
|
||||
} else {
|
||||
el.removeAttribute("title");
|
||||
}
|
||||
|
|
@ -2058,7 +2162,9 @@ tw run-until-done && tw balance</pre>
|
|||
set("st-config-ms", msLabel(p.config_ms, p.config_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-cpu", p.cpu_percent_label || (p.cpu_percent != null ? (Number(p.cpu_percent).toFixed(2) + "%") : "—"));
|
||||
set("st-load", p.loadavg || "—");
|
||||
set("st-pids", p.pids != null ? p.pids : ((p.memory && p.memory.pids) != null ? p.memory.pids : "—"));
|
||||
var mem = p.memory || {};
|
||||
setMem("st-mem-ctr", mem.container_rss_label || mem.container_rss_human || "—", null, mem.container_rss_bytes);
|
||||
setMem("st-mem-pg", mem.postgres_rss_human, mem.postgres_n, mem.postgres_rss_bytes);
|
||||
|
|
@ -2079,12 +2185,12 @@ tw run-until-done && tw balance</pre>
|
|||
var cmdFull = t.cmd_full || cmd;
|
||||
if (cmd.length > 64) cmd = cmd.slice(0, 61) + "…";
|
||||
th += '<tr><td style="padding:0.25rem;color:#5eead4" title="' +
|
||||
(t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + '">' +
|
||||
(t.rss_human || "?") +
|
||||
'</td><td style="padding:0.25rem">' + (t.comm || "") +
|
||||
escHtml(t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + '">' +
|
||||
escHtml(t.rss_human || "?") +
|
||||
'</td><td style="padding:0.25rem">' + escHtml(t.comm || "") +
|
||||
'</td><td style="padding:0.25rem;word-break:break-all;color:var(--muted)" title="' +
|
||||
String(cmdFull).replace(/"/g, """) + '">' +
|
||||
cmd + "</td></tr>";
|
||||
escHtml(cmdFull) + '">' +
|
||||
escHtml(cmd) + "</td></tr>";
|
||||
});
|
||||
th += "</tbody></table>";
|
||||
topBox.innerHTML = th;
|
||||
|
|
@ -2092,9 +2198,11 @@ tw run-until-done && tw balance</pre>
|
|||
}
|
||||
var pf = document.getElementById("st-perf-foot");
|
||||
if (pf) {
|
||||
pf.textContent = "Container RSS + process groups" +
|
||||
(mem.source ? " · " + mem.source : "") + " · " +
|
||||
(d.generated_at_human || d.generated_at || "");
|
||||
var basis = mem.memory_basis === "cgroup" ? "cgroup" : "proc RSS sum";
|
||||
pf.textContent = "Memory: " + basis +
|
||||
(mem.source ? " · " + mem.source : "") +
|
||||
(p.loadavg_source ? " · loadavg=" + p.loadavg_source : "") +
|
||||
" · " + (d.generated_at_human || d.generated_at || "");
|
||||
}
|
||||
return fetch("stats-run.json", { cache: "no-store" })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
|
|
|
|||
|
|
@ -1,29 +1,63 @@
|
|||
/* Compact GOA / CUR:amount display using currency alt_unit_names.
|
||||
* Large values → "1.23 Mega-GOA" (title = full GOA:n) so landing tiles fit.
|
||||
/* Compact GOA / CUR:amount display using SI-style alt units (kGOA, MGOA, …).
|
||||
* Large values → "1.23 MGOA" (title = full GOA:n) so landing tiles fit.
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var DEFAULT_ALT = {
|
||||
"24": "Yotta-GOA",
|
||||
"21": "Zetta-GOA",
|
||||
"18": "Exa-GOA",
|
||||
"15": "Peta-GOA",
|
||||
"12": "Tera-GOA",
|
||||
"9": "Giga-GOA",
|
||||
"6": "Mega-GOA",
|
||||
"3": "Kilo-GOA",
|
||||
"24": "YGOA",
|
||||
"21": "ZGOA",
|
||||
"18": "EGOA",
|
||||
"15": "PGOA",
|
||||
"12": "TGOA",
|
||||
"9": "GGOA",
|
||||
"6": "MGOA",
|
||||
"3": "kGOA",
|
||||
"0": "GOA",
|
||||
"-1": "Deci-GOA",
|
||||
"-2": "Centi-GOA",
|
||||
"-3": "Milli-GOA",
|
||||
"-6": "Micro-GOA",
|
||||
"-7": "Deci-Micro-GOA",
|
||||
"-8": "Atomic-GOA"
|
||||
"-1": "dGOA",
|
||||
"-2": "cGOA",
|
||||
"-3": "mGOA",
|
||||
"-6": "µGOA",
|
||||
"-7": "dµGOA",
|
||||
"-8": "aGOA"
|
||||
};
|
||||
|
||||
var LONG_TO_COMPACT = {
|
||||
"yotta-goa": "YGOA",
|
||||
"zetta-goa": "ZGOA",
|
||||
"exa-goa": "EGOA",
|
||||
"peta-goa": "PGOA",
|
||||
"tera-goa": "TGOA",
|
||||
"giga-goa": "GGOA",
|
||||
"mega-goa": "MGOA",
|
||||
"kilo-goa": "kGOA",
|
||||
"goa": "GOA",
|
||||
"deci-goa": "dGOA",
|
||||
"centi-goa": "cGOA",
|
||||
"milli-goa": "mGOA",
|
||||
"micro-goa": "µGOA",
|
||||
"deci-micro-goa": "dµGOA",
|
||||
"atomic-goa": "aGOA"
|
||||
};
|
||||
|
||||
var THRESHOLD = 1000;
|
||||
|
||||
function compactAlt(altMap) {
|
||||
var out = {};
|
||||
var src = altMap || DEFAULT_ALT;
|
||||
Object.keys(DEFAULT_ALT).forEach(function (k) {
|
||||
out[k] = DEFAULT_ALT[k];
|
||||
});
|
||||
Object.keys(src).forEach(function (k) {
|
||||
var name = String(src[k] || "").trim();
|
||||
var low = name.toLowerCase().replace(/\s+/g, "");
|
||||
if (LONG_TO_COMPACT[low]) out[k] = LONG_TO_COMPACT[low];
|
||||
else if (DEFAULT_ALT[k] && /goa/i.test(name)) out[k] = DEFAULT_ALT[k];
|
||||
else out[k] = name || DEFAULT_ALT[k] || name;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseAmount(s) {
|
||||
if (s == null || s === "") return { cur: "GOA", val: 0 };
|
||||
if (typeof s === "number") return { cur: "GOA", val: s };
|
||||
|
|
@ -51,7 +85,7 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string|number} amount
|
||||
* @param {string|number|object} amount
|
||||
* @param {object} [altMap]
|
||||
* @returns {{ display: string, title: string, raw: string, usedAlt: boolean }}
|
||||
*/
|
||||
|
|
@ -59,24 +93,17 @@
|
|||
if (amount == null || amount === "") {
|
||||
return { display: "—", title: "", raw: "", usedAlt: false };
|
||||
}
|
||||
// Prefer server-provided alt if caller passes object {amount, amount_alt}
|
||||
// Prefer re-format from canonical amount (keeps kGOA even if server sent Kilo-GOA)
|
||||
if (typeof amount === "object" && amount) {
|
||||
if (amount.amount_alt) {
|
||||
var rawO = amount.amount || amount.amount_full || String(amount.amount_alt);
|
||||
return {
|
||||
display: String(amount.amount_alt),
|
||||
title: amount.amount_full || rawO,
|
||||
raw: String(rawO),
|
||||
usedAlt: true
|
||||
};
|
||||
if (amount.amount) {
|
||||
return format(amount.amount, altMap);
|
||||
}
|
||||
amount = amount.amount || amount.amount_full || "";
|
||||
amount = amount.amount_full || amount.amount_alt || "";
|
||||
}
|
||||
|
||||
var alt = altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT;
|
||||
var alt = compactAlt(altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT);
|
||||
var p = parseAmount(amount);
|
||||
var raw = baseStr(p.cur, p.val);
|
||||
// Foreign currencies must not pick up Kilo-GOA / Mega-GOA labels
|
||||
var baseName = (alt && alt["0"]) || p.cur;
|
||||
var curU = String(p.cur || "").toUpperCase();
|
||||
var baseU = String(baseName || "").toUpperCase();
|
||||
|
|
@ -95,7 +122,9 @@
|
|||
var n = parseInt(k, 10);
|
||||
if (!isNaN(n)) scales.push({ sc: n, name: String(alt[k]) });
|
||||
});
|
||||
scales.sort(function (a, b) { return b.sc - a.sc; });
|
||||
scales.sort(function (a, b) {
|
||||
return b.sc - a.sc;
|
||||
});
|
||||
|
||||
var abs = Math.abs(p.val);
|
||||
var chosen = null;
|
||||
|
|
@ -136,33 +165,80 @@
|
|||
setText(document.getElementById(id), amount, altMap);
|
||||
}
|
||||
|
||||
/** Prefer amount_alt from stats row, else format amount. */
|
||||
/** Prefer formatting from amount field; fall back to amount_alt string. */
|
||||
function pick(rowOrAmount, altMap) {
|
||||
if (rowOrAmount && typeof rowOrAmount === "object") {
|
||||
var a =
|
||||
rowOrAmount.amount ||
|
||||
rowOrAmount.total_amount ||
|
||||
rowOrAmount.amount_paid_sum ||
|
||||
rowOrAmount.amount_sum;
|
||||
if (a) return format(a, altMap);
|
||||
if (rowOrAmount.amount_alt) {
|
||||
return format({
|
||||
amount: rowOrAmount.amount,
|
||||
amount_alt: rowOrAmount.amount_alt,
|
||||
amount_full: rowOrAmount.amount_full
|
||||
}, altMap);
|
||||
return {
|
||||
display: String(rowOrAmount.amount_alt),
|
||||
title: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
|
||||
raw: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
|
||||
usedAlt: true
|
||||
};
|
||||
}
|
||||
return format(rowOrAmount.amount || rowOrAmount.total_amount, altMap);
|
||||
return format("", altMap);
|
||||
}
|
||||
return format(rowOrAmount, altMap);
|
||||
}
|
||||
|
||||
function rememberAlt(stats) {
|
||||
if (stats && stats.alt_unit_names && typeof stats.alt_unit_names === "object") {
|
||||
global.__GOA_ALT_UNITS = stats.alt_unit_names;
|
||||
global.__GOA_ALT_UNITS = compactAlt(stats.alt_unit_names);
|
||||
}
|
||||
}
|
||||
|
||||
/** Small legend HTML (positive scales). */
|
||||
function legendHtml(stats) {
|
||||
var alt = compactAlt((stats && stats.alt_unit_names) || global.__GOA_ALT_UNITS || DEFAULT_ALT);
|
||||
var rows = [];
|
||||
Object.keys(alt)
|
||||
.map(function (k) {
|
||||
return parseInt(k, 10);
|
||||
})
|
||||
.filter(function (n) {
|
||||
return !isNaN(n) && n > 0;
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return a - b;
|
||||
})
|
||||
.forEach(function (sc) {
|
||||
rows.push(
|
||||
"<tr><td>10<sup>" +
|
||||
sc +
|
||||
"</sup></td><td><strong>" +
|
||||
alt[String(sc)] +
|
||||
"</strong></td></tr>"
|
||||
);
|
||||
});
|
||||
if (!rows.length) return "";
|
||||
return (
|
||||
'<table class="unit-legend" aria-label="GOA unit names"><thead><tr><th>×base</th><th>name</th></tr></thead><tbody>' +
|
||||
rows.join("") +
|
||||
"</tbody></table>"
|
||||
);
|
||||
}
|
||||
|
||||
function fillLegend(elOrId, stats) {
|
||||
var el = typeof elOrId === "string" ? document.getElementById(elOrId) : elOrId;
|
||||
if (!el) return;
|
||||
el.innerHTML = legendHtml(stats);
|
||||
}
|
||||
|
||||
global.GoaAmount = {
|
||||
format: format,
|
||||
setText: setText,
|
||||
setById: setById,
|
||||
pick: pick,
|
||||
rememberAlt: rememberAlt,
|
||||
compactAlt: compactAlt,
|
||||
legendHtml: legendHtml,
|
||||
fillLegend: fillLegend,
|
||||
DEFAULT_ALT: DEFAULT_ALT,
|
||||
THRESHOLD: THRESHOLD
|
||||
};
|
||||
|
|
|
|||
|
|
@ -88,6 +88,33 @@
|
|||
.stat .v.sm { font-size: 0.92rem; }
|
||||
.stats-foot { margin: 0.75rem 0 0; font-size: 0.72rem; color: var(--muted); text-align: center; }
|
||||
.stats-foot.err { color: #f0a090; }
|
||||
.unit-legend {
|
||||
margin: 0.65rem auto 0;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.68rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.unit-legend th, .unit-legend td {
|
||||
padding: 0.12rem 0.45rem;
|
||||
border-bottom: 1px solid rgba(201, 184, 160, 0.12);
|
||||
text-align: left;
|
||||
}
|
||||
.unit-legend th { font-weight: 700; font-size: 0.62rem; text-transform: uppercase; }
|
||||
.unit-legend strong { color: #5eead4; }
|
||||
.unit-legend-note { margin: 0.35rem 0 0; font-size: 0.68rem; color: var(--muted); text-align: center; }
|
||||
.wd-list {
|
||||
list-style: none; margin: 0.15rem 0 0; padding: 0;
|
||||
display: flex; flex-direction: column; gap: 0.35rem;
|
||||
}
|
||||
.wd-list li {
|
||||
display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.25rem 0.75rem;
|
||||
font-size: 0.88rem; font-weight: 650; color: #f0d090; font-variant-numeric: tabular-nums;
|
||||
border-bottom: 1px solid rgba(201, 184, 160, 0.12); padding-bottom: 0.3rem;
|
||||
}
|
||||
.wd-list li:last-child { border-bottom: none; padding-bottom: 0; }
|
||||
.wd-list .amt { color: #5eead4; font-weight: 750; }
|
||||
.wd-list .meta { color: var(--muted); font-size: 0.78rem; font-weight: 600; }
|
||||
.stat.wide { grid-column: 1 / -1; }
|
||||
.stats-run-note {
|
||||
margin: 0.35rem 0 0; font-size: 0.7rem; color: #f0a090; text-align: center;
|
||||
}
|
||||
|
|
@ -247,7 +274,17 @@
|
|||
footer { margin-top: 1.75rem; text-align: center; color: var(--muted); font-size: 0.85rem; }
|
||||
footer a { color: #e8c878; }
|
||||
code { font-family: ui-monospace, Menlo, monospace; font-size: 0.86em; background: rgba(0,0,0,0.35); padding: 0.1em 0.35em; border-radius: 4px; color: #f0d090; }
|
||||
</style>
|
||||
.landing-rev {
|
||||
margin: 0.75rem 0 0;
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted, #9a8fb0);
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
}
|
||||
.landing-rev a { color: inherit; text-decoration: underline; text-underline-offset: 2px; opacity: 0.95; }
|
||||
.landing-rev strong { font-weight: 750; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p class="stack-bar" role="note" title="Taler software on this exchange service (links → git.taler.net tags)">
|
||||
|
|
@ -277,8 +314,8 @@
|
|||
<div class="stat"><span class="k">Live (remaining)</span><span class="v" id="st-coins-live">—</span></div>
|
||||
<div class="stat"><span class="k">Spent / zero</span><span class="v" id="st-coins-spent">—</span></div>
|
||||
<div class="stat"><span class="k">Remaining amt</span><span class="v teal sm" id="st-remaining">—</span></div>
|
||||
<div class="stat"><span class="k">Withdraw ops</span><span class="v" id="st-withdraw">—</span></div>
|
||||
<div class="stat"><span class="k">Withdraw amt</span><span class="v teal sm" id="st-withdraw-amt">—</span></div>
|
||||
<div class="stat"><span class="k">Withdraw ops</span><span class="v" id="st-withdraw-coins">—</span></div>
|
||||
<div class="stat"><span class="k">All withdraw amt</span><span class="v teal sm" id="st-withdraw-amt-coins">—</span></div>
|
||||
<div class="stat"><span class="k">Refresh</span><span class="v" id="st-refresh">—</span></div>
|
||||
<div class="stat"><span class="k">Coin deposits</span><span class="v" id="st-coin-dep">—</span></div>
|
||||
<div class="stat"><span class="k">Batch deposits</span><span class="v" id="st-batch-dep">—</span></div>
|
||||
|
|
@ -307,14 +344,22 @@
|
|||
</section>
|
||||
|
||||
<section class="stats" id="stats-wire" aria-labelledby="wire-title">
|
||||
<h2 id="wire-title">Reserves · wire</h2>
|
||||
<p class="note">Backing money movement</p>
|
||||
<h2 id="wire-title">Reserves · wire · all money</h2>
|
||||
<p class="note">Backing money movement · large GOA as kGOA / MGOA / …</p>
|
||||
<div class="stats-grid">
|
||||
<div class="stat"><span class="k">Reserves</span><span class="v teal" id="st-reserves">—</span></div>
|
||||
<div class="stat"><span class="k">Wire in</span><span class="v" id="st-wire-in-n">—</span></div>
|
||||
<div class="stat"><span class="k">Wire in amt</span><span class="v teal sm" id="st-wire-in-amt">—</span></div>
|
||||
<div class="stat"><span class="k">Wire-in count</span><span class="v" id="st-wire-in-n">—</span></div>
|
||||
<div class="stat"><span class="k">All wire-in money</span><span class="v teal sm" id="st-wire-in-amt">—</span></div>
|
||||
<div class="stat"><span class="k">Wire out</span><span class="v" id="st-wire-out">—</span></div>
|
||||
<div class="stat"><span class="k">All withdraws</span><span class="v teal sm" id="st-withdraw-amt">—</span></div>
|
||||
<div class="stat"><span class="k">Withdraw ops</span><span class="v" id="st-withdraw">—</span></div>
|
||||
<div class="stat wide">
|
||||
<span class="k">Latest wire-in & withdraws</span>
|
||||
<ul class="wd-list" id="st-recent" aria-live="polite"><li class="meta">…</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="unit-legend-note">Unit names (hover amounts for exact GOA:n)</p>
|
||||
<div id="st-unit-legend"></div>
|
||||
<p class="stats-foot" id="st-foot">Loading…</p>
|
||||
<p class="stats-run-note" id="st-run-note" hidden></p>
|
||||
</section>
|
||||
|
|
@ -325,17 +370,19 @@
|
|||
<div class="stats-grid">
|
||||
<div class="stat"><span class="k">/keys</span><span class="v teal" id="st-keys-ms">—</span></div>
|
||||
<div class="stat"><span class="k">/config</span><span class="v" id="st-config-ms">—</span></div>
|
||||
<div class="stat"><span class="k">Loadavg</span><span class="v sm" id="st-load">—</span></div>
|
||||
<div class="stat"><span class="k">Container</span><span class="v teal" id="st-mem-ctr">—</span></div>
|
||||
<div class="stat"><span class="k">CPU (container)</span><span class="v teal" id="st-cpu">—</span></div>
|
||||
<div class="stat"><span class="k">Loadavg (host)</span><span class="v sm" id="st-load">—</span></div>
|
||||
<div class="stat"><span class="k">PIDs</span><span class="v" id="st-pids">—</span></div>
|
||||
<div class="stat"><span class="k">Container RSS</span><span class="v teal" id="st-mem-ctr">—</span></div>
|
||||
<div class="stat"><span class="k">PostgreSQL</span><span class="v" id="st-mem-pg">—</span></div>
|
||||
<div class="stat"><span class="k">Taler procs</span><span class="v" id="st-mem-taler">—</span></div>
|
||||
<div class="stat"><span class="k">Nginx</span><span class="v sm" id="st-mem-nginx">—</span></div>
|
||||
</div>
|
||||
<div class="sub-block" style="margin-top:0.85rem">
|
||||
<h3>Top processes (RSS)</h3>
|
||||
<h3>Top processes (RSS inside container)</h3>
|
||||
<div id="st-mem-top" class="by-denom"></div>
|
||||
</div>
|
||||
<p class="stats-foot" id="st-perf-foot">Memory from /proc + cgroup inside podman container.</p>
|
||||
<p class="stats-foot" id="st-perf-foot">RSS from /proc inside container (no memory cgroup in rootless podman). CPU from podman stats.</p>
|
||||
</section>
|
||||
|
||||
<div class="wallet-fold" id="wallet-fold">
|
||||
|
|
@ -442,6 +489,13 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
Privacy: processing under Swiss FADP (revDSG). What is retained is listed on
|
||||
<a href="https://exchange.hacktivism.ch/privacy" style="color:#5eead4">/privacy</a>.
|
||||
</p>
|
||||
<!-- landing-rev -->
|
||||
<p class="landing-rev" data-landing-site="exchange" data-landing-version="42" data-landing-commit="921a4f1">
|
||||
Exchange landing <strong>v42</strong>
|
||||
· content <time datetime="2026-07-17T13:32:59+02:00">2026-07-17 13:32 CEST</time>
|
||||
· <a href="https://git.hacktivism.ch/hernani/koopa-admin-log/commit/921a4f1572dbf4391b69899287b4c0280bb0e271" title="koopa-admin-log 921a4f1572dbf4391b69899287b4c0280bb0e271">921a4f1</a>
|
||||
</p>
|
||||
<!-- /landing-rev -->
|
||||
</footer>
|
||||
</main>
|
||||
<script src="/intro/goa-amount.js"></script>
|
||||
|
|
@ -513,6 +567,8 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
set("st-coins-live", d.coins_live);
|
||||
set("st-coins-spent", d.coins_spent);
|
||||
setAmt("st-remaining", d.coins_remaining_amount, "coins_remaining_amount_alt", d);
|
||||
set("st-withdraw-coins", d.withdraw_ops);
|
||||
setAmt("st-withdraw-amt-coins", d.withdraw_amount, "withdraw_amount_alt", d);
|
||||
set("st-withdraw", d.withdraw_ops);
|
||||
setAmt("st-withdraw-amt", d.withdraw_amount, "withdraw_amount_alt", d);
|
||||
set("st-refresh", d.refresh_ops);
|
||||
|
|
@ -529,16 +585,48 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
setAmt("st-wire-in-amt", d.wire_in_amount, "wire_in_amount_alt", d);
|
||||
set("st-wire-out", d.wire_out);
|
||||
|
||||
var recent = document.getElementById("st-recent");
|
||||
if (recent) {
|
||||
recent.innerHTML = "";
|
||||
var rows = (d.recent_activity || []).slice(0, 12);
|
||||
if (!rows.length) {
|
||||
var empty = document.createElement("li");
|
||||
empty.className = "meta";
|
||||
empty.textContent = "—";
|
||||
recent.appendChild(empty);
|
||||
} else {
|
||||
rows.forEach(function (row) {
|
||||
var li = document.createElement("li");
|
||||
var amt = document.createElement("span");
|
||||
amt.className = "amt";
|
||||
if (window.GoaAmount) {
|
||||
var af = GoaAmount.pick(row, d.alt_unit_names);
|
||||
amt.textContent = af.display;
|
||||
if (af.title && af.title !== af.display) amt.setAttribute("title", af.title);
|
||||
} else {
|
||||
amt.textContent = row.amount_alt || row.amount || "?";
|
||||
}
|
||||
var meta = document.createElement("span");
|
||||
meta.className = "meta";
|
||||
meta.textContent = (row.ts_human || "") + (row.kind ? " · " + row.kind : "");
|
||||
li.appendChild(amt);
|
||||
li.appendChild(meta);
|
||||
recent.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (window.GoaAmount && GoaAmount.fillLegend) GoaAmount.fillLegend("st-unit-legend", d);
|
||||
|
||||
var ladder = document.getElementById("st-ladder");
|
||||
if (ladder) {
|
||||
ladder.innerHTML = "";
|
||||
(d.denom_ladder || []).forEach(function (x) {
|
||||
var s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
var lab = x.value_alt || x.value || "?";
|
||||
if (window.GoaAmount && !x.value_alt) {
|
||||
var lab = x.value || "?";
|
||||
if (window.GoaAmount) {
|
||||
lab = GoaAmount.format(x.value, d.alt_unit_names).display;
|
||||
}
|
||||
} else if (x.value_alt) lab = x.value_alt;
|
||||
s.textContent = lab + (x.keys > 1 ? " ×" + x.keys : "");
|
||||
if (x.value && lab !== x.value) s.title = x.value;
|
||||
ladder.appendChild(s);
|
||||
|
|
@ -553,10 +641,10 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
wrap.hidden = false;
|
||||
var html = "<table><thead><tr><th>Value</th><th>Coins</th><th>Live</th></tr></thead><tbody>";
|
||||
by.forEach(function (r) {
|
||||
var v = r.value_alt || r.value || "?";
|
||||
if (window.GoaAmount && !r.value_alt) {
|
||||
var v = r.value || "?";
|
||||
if (window.GoaAmount) {
|
||||
v = GoaAmount.format(r.value, d.alt_unit_names).display;
|
||||
}
|
||||
} else if (r.value_alt) v = r.value_alt;
|
||||
html += "<tr><td title=\"" + (r.value || "") + "\">" + v + "</td><td>" + r.coins + "</td><td>" + r.live + "</td></tr>";
|
||||
});
|
||||
html += "</tbody></table>";
|
||||
|
|
@ -569,25 +657,38 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
(d.generated_at_human || d.generated_at || "");
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/&/g, "&").replace(/</g, "<")
|
||||
.replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
function setMem(id, human, n, bytes) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
if (!human && human !== 0) {
|
||||
var b = bytes != null && bytes !== "" ? Number(bytes) : NaN;
|
||||
var nn = n != null && n !== "" ? Number(n) : NaN;
|
||||
var empty =
|
||||
human == null || human === "" || human === "—" ||
|
||||
human === "0 B" || human === "0B" ||
|
||||
(isFinite(b) && b <= 0 && (!isFinite(nn) || nn <= 0));
|
||||
if (empty) {
|
||||
el.textContent = "—";
|
||||
el.removeAttribute("title");
|
||||
return;
|
||||
}
|
||||
var h = String(human);
|
||||
if (n != null && n !== "" && Number(n) > 0) h += " · " + n + "p";
|
||||
if (isFinite(nn) && nn > 0) h += " · " + nn + "p";
|
||||
el.textContent = h;
|
||||
if (bytes != null && bytes !== "" && Number(bytes) > 0) {
|
||||
el.setAttribute("title", Number(bytes).toLocaleString("en-US") + " B");
|
||||
if (isFinite(b) && b > 0) {
|
||||
el.setAttribute("title", b.toLocaleString("en-US") + " B");
|
||||
} else el.removeAttribute("title");
|
||||
}
|
||||
var p = d.performance || {};
|
||||
set("st-keys-ms", msLabel(p.keys_ms, p.keys_http));
|
||||
set("st-config-ms", msLabel(p.config_ms, p.config_http));
|
||||
set("st-cpu", p.cpu_percent_label || (p.cpu_percent != null ? (Number(p.cpu_percent).toFixed(2) + "%") : "—"));
|
||||
set("st-load", p.loadavg || "—");
|
||||
set("st-pids", p.pids != null ? p.pids : ((p.memory && p.memory.pids) != null ? p.memory.pids : "—"));
|
||||
var mem = p.memory || {};
|
||||
setMem("st-mem-ctr", mem.container_rss_label || mem.container_rss_human || "—", null, mem.container_rss_bytes);
|
||||
setMem("st-mem-pg", mem.postgres_rss_human, mem.postgres_n, mem.postgres_rss_bytes);
|
||||
|
|
@ -604,11 +705,11 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
var cmd = t.cmd || "";
|
||||
var cmdFull = t.cmd_full || cmd;
|
||||
if (cmd.length > 64) cmd = cmd.slice(0, 61) + "…";
|
||||
th += "<tr><td title=\"" + (t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + "\">" +
|
||||
(t.rss_human || "?") + "</td><td>" +
|
||||
(t.comm || "") + "</td><td style=\"font-size:0.72rem;word-break:break-all\" title=\"" +
|
||||
String(cmdFull).replace(/"/g, """) + "\">" +
|
||||
cmd + "</td></tr>";
|
||||
th += "<tr><td title=\"" + escHtml(t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + "\">" +
|
||||
escHtml(t.rss_human || "?") + "</td><td>" +
|
||||
escHtml(t.comm || "") + "</td><td style=\"font-size:0.72rem;word-break:break-all\" title=\"" +
|
||||
escHtml(cmdFull) + "\">" +
|
||||
escHtml(cmd) + "</td></tr>";
|
||||
});
|
||||
th += "</tbody></table>";
|
||||
topBox.innerHTML = th;
|
||||
|
|
@ -616,9 +717,11 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
}
|
||||
var pf = document.getElementById("st-perf-foot");
|
||||
if (pf) {
|
||||
pf.textContent = "Container RSS + process groups" +
|
||||
(mem.source ? " · " + mem.source : "") + " · " +
|
||||
(d.generated_at_human || d.generated_at || "");
|
||||
var basis = mem.memory_basis === "cgroup" ? "cgroup" : "proc RSS sum";
|
||||
pf.textContent = "Memory: " + basis +
|
||||
(mem.source ? " · " + mem.source : "") +
|
||||
(p.loadavg_source ? " · loadavg=" + p.loadavg_source : "") +
|
||||
" · " + (d.generated_at_human || d.generated_at || "");
|
||||
}
|
||||
return fetch("/intro/stats-run.json", { cache: "no-store" })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
|
|
|
|||
|
|
@ -1,29 +1,63 @@
|
|||
/* Compact GOA / CUR:amount display using currency alt_unit_names.
|
||||
* Large values → "1.23 Mega-GOA" (title = full GOA:n) so landing tiles fit.
|
||||
/* Compact GOA / CUR:amount display using SI-style alt units (kGOA, MGOA, …).
|
||||
* Large values → "1.23 MGOA" (title = full GOA:n) so landing tiles fit.
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var DEFAULT_ALT = {
|
||||
"24": "Yotta-GOA",
|
||||
"21": "Zetta-GOA",
|
||||
"18": "Exa-GOA",
|
||||
"15": "Peta-GOA",
|
||||
"12": "Tera-GOA",
|
||||
"9": "Giga-GOA",
|
||||
"6": "Mega-GOA",
|
||||
"3": "Kilo-GOA",
|
||||
"24": "YGOA",
|
||||
"21": "ZGOA",
|
||||
"18": "EGOA",
|
||||
"15": "PGOA",
|
||||
"12": "TGOA",
|
||||
"9": "GGOA",
|
||||
"6": "MGOA",
|
||||
"3": "kGOA",
|
||||
"0": "GOA",
|
||||
"-1": "Deci-GOA",
|
||||
"-2": "Centi-GOA",
|
||||
"-3": "Milli-GOA",
|
||||
"-6": "Micro-GOA",
|
||||
"-7": "Deci-Micro-GOA",
|
||||
"-8": "Atomic-GOA"
|
||||
"-1": "dGOA",
|
||||
"-2": "cGOA",
|
||||
"-3": "mGOA",
|
||||
"-6": "µGOA",
|
||||
"-7": "dµGOA",
|
||||
"-8": "aGOA"
|
||||
};
|
||||
|
||||
var LONG_TO_COMPACT = {
|
||||
"yotta-goa": "YGOA",
|
||||
"zetta-goa": "ZGOA",
|
||||
"exa-goa": "EGOA",
|
||||
"peta-goa": "PGOA",
|
||||
"tera-goa": "TGOA",
|
||||
"giga-goa": "GGOA",
|
||||
"mega-goa": "MGOA",
|
||||
"kilo-goa": "kGOA",
|
||||
"goa": "GOA",
|
||||
"deci-goa": "dGOA",
|
||||
"centi-goa": "cGOA",
|
||||
"milli-goa": "mGOA",
|
||||
"micro-goa": "µGOA",
|
||||
"deci-micro-goa": "dµGOA",
|
||||
"atomic-goa": "aGOA"
|
||||
};
|
||||
|
||||
var THRESHOLD = 1000;
|
||||
|
||||
function compactAlt(altMap) {
|
||||
var out = {};
|
||||
var src = altMap || DEFAULT_ALT;
|
||||
Object.keys(DEFAULT_ALT).forEach(function (k) {
|
||||
out[k] = DEFAULT_ALT[k];
|
||||
});
|
||||
Object.keys(src).forEach(function (k) {
|
||||
var name = String(src[k] || "").trim();
|
||||
var low = name.toLowerCase().replace(/\s+/g, "");
|
||||
if (LONG_TO_COMPACT[low]) out[k] = LONG_TO_COMPACT[low];
|
||||
else if (DEFAULT_ALT[k] && /goa/i.test(name)) out[k] = DEFAULT_ALT[k];
|
||||
else out[k] = name || DEFAULT_ALT[k] || name;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseAmount(s) {
|
||||
if (s == null || s === "") return { cur: "GOA", val: 0 };
|
||||
if (typeof s === "number") return { cur: "GOA", val: s };
|
||||
|
|
@ -51,7 +85,7 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string|number} amount
|
||||
* @param {string|number|object} amount
|
||||
* @param {object} [altMap]
|
||||
* @returns {{ display: string, title: string, raw: string, usedAlt: boolean }}
|
||||
*/
|
||||
|
|
@ -59,24 +93,17 @@
|
|||
if (amount == null || amount === "") {
|
||||
return { display: "—", title: "", raw: "", usedAlt: false };
|
||||
}
|
||||
// Prefer server-provided alt if caller passes object {amount, amount_alt}
|
||||
// Prefer re-format from canonical amount (keeps kGOA even if server sent Kilo-GOA)
|
||||
if (typeof amount === "object" && amount) {
|
||||
if (amount.amount_alt) {
|
||||
var rawO = amount.amount || amount.amount_full || String(amount.amount_alt);
|
||||
return {
|
||||
display: String(amount.amount_alt),
|
||||
title: amount.amount_full || rawO,
|
||||
raw: String(rawO),
|
||||
usedAlt: true
|
||||
};
|
||||
if (amount.amount) {
|
||||
return format(amount.amount, altMap);
|
||||
}
|
||||
amount = amount.amount || amount.amount_full || "";
|
||||
amount = amount.amount_full || amount.amount_alt || "";
|
||||
}
|
||||
|
||||
var alt = altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT;
|
||||
var alt = compactAlt(altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT);
|
||||
var p = parseAmount(amount);
|
||||
var raw = baseStr(p.cur, p.val);
|
||||
// Foreign currencies must not pick up Kilo-GOA / Mega-GOA labels
|
||||
var baseName = (alt && alt["0"]) || p.cur;
|
||||
var curU = String(p.cur || "").toUpperCase();
|
||||
var baseU = String(baseName || "").toUpperCase();
|
||||
|
|
@ -95,7 +122,9 @@
|
|||
var n = parseInt(k, 10);
|
||||
if (!isNaN(n)) scales.push({ sc: n, name: String(alt[k]) });
|
||||
});
|
||||
scales.sort(function (a, b) { return b.sc - a.sc; });
|
||||
scales.sort(function (a, b) {
|
||||
return b.sc - a.sc;
|
||||
});
|
||||
|
||||
var abs = Math.abs(p.val);
|
||||
var chosen = null;
|
||||
|
|
@ -136,33 +165,80 @@
|
|||
setText(document.getElementById(id), amount, altMap);
|
||||
}
|
||||
|
||||
/** Prefer amount_alt from stats row, else format amount. */
|
||||
/** Prefer formatting from amount field; fall back to amount_alt string. */
|
||||
function pick(rowOrAmount, altMap) {
|
||||
if (rowOrAmount && typeof rowOrAmount === "object") {
|
||||
var a =
|
||||
rowOrAmount.amount ||
|
||||
rowOrAmount.total_amount ||
|
||||
rowOrAmount.amount_paid_sum ||
|
||||
rowOrAmount.amount_sum;
|
||||
if (a) return format(a, altMap);
|
||||
if (rowOrAmount.amount_alt) {
|
||||
return format({
|
||||
amount: rowOrAmount.amount,
|
||||
amount_alt: rowOrAmount.amount_alt,
|
||||
amount_full: rowOrAmount.amount_full
|
||||
}, altMap);
|
||||
return {
|
||||
display: String(rowOrAmount.amount_alt),
|
||||
title: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
|
||||
raw: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
|
||||
usedAlt: true
|
||||
};
|
||||
}
|
||||
return format(rowOrAmount.amount || rowOrAmount.total_amount, altMap);
|
||||
return format("", altMap);
|
||||
}
|
||||
return format(rowOrAmount, altMap);
|
||||
}
|
||||
|
||||
function rememberAlt(stats) {
|
||||
if (stats && stats.alt_unit_names && typeof stats.alt_unit_names === "object") {
|
||||
global.__GOA_ALT_UNITS = stats.alt_unit_names;
|
||||
global.__GOA_ALT_UNITS = compactAlt(stats.alt_unit_names);
|
||||
}
|
||||
}
|
||||
|
||||
/** Small legend HTML (positive scales). */
|
||||
function legendHtml(stats) {
|
||||
var alt = compactAlt((stats && stats.alt_unit_names) || global.__GOA_ALT_UNITS || DEFAULT_ALT);
|
||||
var rows = [];
|
||||
Object.keys(alt)
|
||||
.map(function (k) {
|
||||
return parseInt(k, 10);
|
||||
})
|
||||
.filter(function (n) {
|
||||
return !isNaN(n) && n > 0;
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return a - b;
|
||||
})
|
||||
.forEach(function (sc) {
|
||||
rows.push(
|
||||
"<tr><td>10<sup>" +
|
||||
sc +
|
||||
"</sup></td><td><strong>" +
|
||||
alt[String(sc)] +
|
||||
"</strong></td></tr>"
|
||||
);
|
||||
});
|
||||
if (!rows.length) return "";
|
||||
return (
|
||||
'<table class="unit-legend" aria-label="GOA unit names"><thead><tr><th>×base</th><th>name</th></tr></thead><tbody>' +
|
||||
rows.join("") +
|
||||
"</tbody></table>"
|
||||
);
|
||||
}
|
||||
|
||||
function fillLegend(elOrId, stats) {
|
||||
var el = typeof elOrId === "string" ? document.getElementById(elOrId) : elOrId;
|
||||
if (!el) return;
|
||||
el.innerHTML = legendHtml(stats);
|
||||
}
|
||||
|
||||
global.GoaAmount = {
|
||||
format: format,
|
||||
setText: setText,
|
||||
setById: setById,
|
||||
pick: pick,
|
||||
rememberAlt: rememberAlt,
|
||||
compactAlt: compactAlt,
|
||||
legendHtml: legendHtml,
|
||||
fillLegend: fillLegend,
|
||||
DEFAULT_ALT: DEFAULT_ALT,
|
||||
THRESHOLD: THRESHOLD
|
||||
};
|
||||
|
|
|
|||
|
|
@ -273,6 +273,15 @@
|
|||
.stats-run-note {
|
||||
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 {
|
||||
background: var(--card); border: 1px solid var(--border); border-radius: 18px;
|
||||
padding: 1.3rem 1.25rem 1.4rem; margin-bottom: 1.2rem;
|
||||
|
|
@ -508,7 +517,17 @@
|
|||
.sym-gold path[data-ring], .sym-gold circle[data-ring] { fill: none; stroke: #f0c86a; }
|
||||
.sym-blue path, .sym-blue circle { fill: #3ecfbf; stroke: #3ecfbf; }
|
||||
.sym-blue path[data-ring], .sym-blue circle[data-ring] { fill: none; stroke: #3ecfbf; }
|
||||
</style>
|
||||
.landing-rev {
|
||||
margin: 0.75rem 0 0;
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted, #9a8fb0);
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
}
|
||||
.landing-rev a { color: inherit; text-decoration: underline; text-underline-offset: 2px; opacity: 0.95; }
|
||||
.landing-rev strong { font-weight: 750; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p class="stack-bar" role="note" title="Taler software on this merchant service (links → git.taler.net tags)">
|
||||
|
|
@ -704,12 +723,12 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
|
||||
|
||||
<section class="stats" id="stats" aria-labelledby="stats-title">
|
||||
<h2 id="stats-title">Merchant stats</h2>
|
||||
<p class="note">From merchant DB · dual currency side by side</p>
|
||||
<h2 id="stats-title">Merchant stats · all payments</h2>
|
||||
<p class="note">From merchant DB · dual currency · large GOA as kGOA / MGOA / …</p>
|
||||
<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">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 orders</span><span class="v" id="st-orders">—</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">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>
|
||||
|
|
@ -721,29 +740,31 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
<div class="cur-name">GOA</div>
|
||||
<div class="cur-sub">explorational · hacktivism</div>
|
||||
<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><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">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>
|
||||
<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 class="cur-panel chf" id="panel-chf">
|
||||
<div class="cur-name">CHF</div>
|
||||
<div class="cur-sub">taler-ops</div>
|
||||
<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><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">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>
|
||||
<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>
|
||||
<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-run-note" id="st-run-note" hidden></p>
|
||||
</section>
|
||||
|
|
@ -755,22 +776,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">/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">Loadavg</span><span class="v sm" id="st-load">—</span></div>
|
||||
<div class="stat"><span class="k">Container</span><span class="v teal" id="st-mem-ctr">—</span></div>
|
||||
<div class="stat"><span class="k">CPU (container)</span><span class="v teal" id="st-cpu">—</span></div>
|
||||
<div class="stat"><span class="k">Loadavg (host)</span><span class="v sm" id="st-load">—</span></div>
|
||||
<div class="stat"><span class="k">PIDs</span><span class="v" id="st-pids">—</span></div>
|
||||
<div class="stat"><span class="k">Container RSS</span><span class="v teal" id="st-mem-ctr">—</span></div>
|
||||
<div class="stat"><span class="k">PostgreSQL</span><span class="v" id="st-mem-pg">—</span></div>
|
||||
<div class="stat"><span class="k">Taler procs</span><span class="v" id="st-mem-taler">—</span></div>
|
||||
<div class="stat"><span class="k">Nginx</span><span class="v sm" id="st-mem-nginx">—</span></div>
|
||||
</div>
|
||||
<div 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>
|
||||
<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 class="stats" id="activity" aria-labelledby="act-title">
|
||||
<h2 id="act-title">Recent activity</h2>
|
||||
<p class="note">Latest 5 payments & refunds per currency</p>
|
||||
<h2 id="act-title">Latest payments</h2>
|
||||
<p class="note">Latest payments & refunds per currency (all paid totals above)</p>
|
||||
<div class="act-pair" id="st-activity-pair">
|
||||
<div class="act-col goa" aria-labelledby="act-goa-h">
|
||||
<h3 class="act-col-h" id="act-goa-h">GOA</h3>
|
||||
|
|
@ -809,9 +832,17 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
Privacy: processing under Swiss FADP (revDSG). What is retained is listed on
|
||||
<a href="https://taler.hacktivism.ch/privacy" style="color:#5eead4">/privacy</a>.
|
||||
</p>
|
||||
<!-- landing-rev -->
|
||||
<p class="landing-rev" data-landing-site="merchant" data-landing-version="42" data-landing-commit="921a4f1">
|
||||
Merchant landing <strong>v42</strong>
|
||||
· content <time datetime="2026-07-17T13:32:59+02:00">2026-07-17 13:32 CEST</time>
|
||||
· <a href="https://git.hacktivism.ch/hernani/koopa-admin-log/commit/921a4f1572dbf4391b69899287b4c0280bb0e271" title="koopa-admin-log 921a4f1572dbf4391b69899287b4c0280bb0e271">921a4f1</a>
|
||||
</p>
|
||||
<!-- /landing-rev -->
|
||||
</footer>
|
||||
</main>
|
||||
<script src="/intro/goa-amount.js"></script>
|
||||
<script src="/intro/goa-ui.js" defer></script>
|
||||
<script>
|
||||
/* Fallback if goa-amount.js not yet deployed (uses amount_alt from stats.json) */
|
||||
(function () {
|
||||
|
|
@ -903,7 +934,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 (d) {
|
||||
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-orders", d.orders);
|
||||
set("st-paid", d.paid);
|
||||
|
|
@ -931,11 +965,16 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
var li = document.createElement("li");
|
||||
li.className = "act-item";
|
||||
// no merchant names / ids — amount, optional summary, time only
|
||||
var amtShow = a.amount_alt || a.amount || "—";
|
||||
if (window.GoaAmount && !a.amount_alt) {
|
||||
amtShow = GoaAmount.format(a.amount, d.alt_unit_names).display;
|
||||
var amtShow = a.amount || "—";
|
||||
var amtTitle = a.amount || "";
|
||||
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 =
|
||||
'<span class="act-kind ' + esc(kind) + '">' + esc(kind) + "</span>" +
|
||||
'<div class="act-main">' +
|
||||
|
|
@ -963,8 +1002,8 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
if (cur === "GOA") byCur.GOA.push(a);
|
||||
else if (cur === "CHF") byCur.CHF.push(a);
|
||||
});
|
||||
byCur.GOA = byCur.GOA.slice(0, 5);
|
||||
byCur.CHF = byCur.CHF.slice(0, 5);
|
||||
byCur.GOA = byCur.GOA.slice(0, 10);
|
||||
byCur.CHF = byCur.CHF.slice(0, 10);
|
||||
}
|
||||
fillActivityList(document.getElementById("st-activity-goa"), byCur.GOA);
|
||||
fillActivityList(document.getElementById("st-activity-chf"), byCur.CHF);
|
||||
|
|
@ -973,26 +1012,39 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
foot.textContent = (d.dual_currency ? "Dual-currency · " : "") +
|
||||
(d.generated_at_human || d.generated_at || "");
|
||||
}
|
||||
function escHtml(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/&/g, "&").replace(/</g, "<")
|
||||
.replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
function setMem(id, human, n, bytes) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
if (!human && human !== 0) {
|
||||
var b = bytes != null && bytes !== "" ? Number(bytes) : NaN;
|
||||
var nn = n != null && n !== "" ? Number(n) : NaN;
|
||||
var empty =
|
||||
human == null || human === "" || human === "—" ||
|
||||
human === "0 B" || human === "0B" ||
|
||||
(isFinite(b) && b <= 0 && (!isFinite(nn) || nn <= 0));
|
||||
if (empty) {
|
||||
el.textContent = "—";
|
||||
el.removeAttribute("title");
|
||||
return;
|
||||
}
|
||||
var h = String(human);
|
||||
if (n != null && n !== "" && Number(n) > 0) h += " · " + n + "p";
|
||||
if (isFinite(nn) && nn > 0) h += " · " + nn + "p";
|
||||
el.textContent = h;
|
||||
if (bytes != null && bytes !== "" && Number(bytes) > 0) {
|
||||
el.setAttribute("title", Number(bytes).toLocaleString("en-US") + " B");
|
||||
if (isFinite(b) && b > 0) {
|
||||
el.setAttribute("title", b.toLocaleString("en-US") + " B");
|
||||
} else el.removeAttribute("title");
|
||||
}
|
||||
var p = d.performance || {};
|
||||
set("st-config-ms", msLabel(p.config_ms, p.config_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-cpu", p.cpu_percent_label || (p.cpu_percent != null ? (Number(p.cpu_percent).toFixed(2) + "%") : "—"));
|
||||
set("st-load", p.loadavg || "—");
|
||||
set("st-pids", p.pids != null ? p.pids : ((p.memory && p.memory.pids) != null ? p.memory.pids : "—"));
|
||||
var mem = p.memory || {};
|
||||
setMem("st-mem-ctr", mem.container_rss_label || mem.container_rss_human || "—", null, mem.container_rss_bytes);
|
||||
setMem("st-mem-pg", mem.postgres_rss_human, mem.postgres_n, mem.postgres_rss_bytes);
|
||||
|
|
@ -1013,12 +1065,12 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
var cmdFull = t.cmd_full || cmd;
|
||||
if (cmd.length > 64) cmd = cmd.slice(0, 61) + "…";
|
||||
th += '<tr><td style="padding:0.25rem;color:#5eead4" title="' +
|
||||
(t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + '">' +
|
||||
(t.rss_human || "?") +
|
||||
'</td><td style="padding:0.25rem">' + (t.comm || "") +
|
||||
escHtml(t.rss_bytes != null ? Number(t.rss_bytes).toLocaleString("en-US") + " B" : "") + '">' +
|
||||
escHtml(t.rss_human || "?") +
|
||||
'</td><td style="padding:0.25rem">' + escHtml(t.comm || "") +
|
||||
'</td><td style="padding:0.25rem;word-break:break-all;color:var(--muted)" title="' +
|
||||
String(cmdFull).replace(/"/g, """) + '">' +
|
||||
cmd + "</td></tr>";
|
||||
escHtml(cmdFull) + '">' +
|
||||
escHtml(cmd) + "</td></tr>";
|
||||
});
|
||||
th += "</tbody></table>";
|
||||
topBox.innerHTML = th;
|
||||
|
|
@ -1026,9 +1078,11 @@ sudo apt-get install -y taler-wallet-cli</pre>
|
|||
}
|
||||
var pf = document.getElementById("st-perf-foot");
|
||||
if (pf) {
|
||||
pf.textContent = "Container RSS + process groups" +
|
||||
(mem.source ? " · " + mem.source : "") + " · " +
|
||||
(d.generated_at_human || d.generated_at || "");
|
||||
var basis = mem.memory_basis === "cgroup" ? "cgroup" : "proc RSS sum";
|
||||
pf.textContent = "Memory: " + basis +
|
||||
(mem.source ? " · " + mem.source : "") +
|
||||
(p.loadavg_source ? " · loadavg=" + p.loadavg_source : "") +
|
||||
" · " + (d.generated_at_human || d.generated_at || "");
|
||||
}
|
||||
return fetch("/intro/stats-run.json", { cache: "no-store" })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
|
|
|
|||
|
|
@ -1,29 +1,63 @@
|
|||
/* Compact GOA / CUR:amount display using currency alt_unit_names.
|
||||
* Large values → "1.23 Mega-GOA" (title = full GOA:n) so landing tiles fit.
|
||||
/* Compact GOA / CUR:amount display using SI-style alt units (kGOA, MGOA, …).
|
||||
* Large values → "1.23 MGOA" (title = full GOA:n) so landing tiles fit.
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var DEFAULT_ALT = {
|
||||
"24": "Yotta-GOA",
|
||||
"21": "Zetta-GOA",
|
||||
"18": "Exa-GOA",
|
||||
"15": "Peta-GOA",
|
||||
"12": "Tera-GOA",
|
||||
"9": "Giga-GOA",
|
||||
"6": "Mega-GOA",
|
||||
"3": "Kilo-GOA",
|
||||
"24": "YGOA",
|
||||
"21": "ZGOA",
|
||||
"18": "EGOA",
|
||||
"15": "PGOA",
|
||||
"12": "TGOA",
|
||||
"9": "GGOA",
|
||||
"6": "MGOA",
|
||||
"3": "kGOA",
|
||||
"0": "GOA",
|
||||
"-1": "Deci-GOA",
|
||||
"-2": "Centi-GOA",
|
||||
"-3": "Milli-GOA",
|
||||
"-6": "Micro-GOA",
|
||||
"-7": "Deci-Micro-GOA",
|
||||
"-8": "Atomic-GOA"
|
||||
"-1": "dGOA",
|
||||
"-2": "cGOA",
|
||||
"-3": "mGOA",
|
||||
"-6": "µGOA",
|
||||
"-7": "dµGOA",
|
||||
"-8": "aGOA"
|
||||
};
|
||||
|
||||
var LONG_TO_COMPACT = {
|
||||
"yotta-goa": "YGOA",
|
||||
"zetta-goa": "ZGOA",
|
||||
"exa-goa": "EGOA",
|
||||
"peta-goa": "PGOA",
|
||||
"tera-goa": "TGOA",
|
||||
"giga-goa": "GGOA",
|
||||
"mega-goa": "MGOA",
|
||||
"kilo-goa": "kGOA",
|
||||
"goa": "GOA",
|
||||
"deci-goa": "dGOA",
|
||||
"centi-goa": "cGOA",
|
||||
"milli-goa": "mGOA",
|
||||
"micro-goa": "µGOA",
|
||||
"deci-micro-goa": "dµGOA",
|
||||
"atomic-goa": "aGOA"
|
||||
};
|
||||
|
||||
var THRESHOLD = 1000;
|
||||
|
||||
function compactAlt(altMap) {
|
||||
var out = {};
|
||||
var src = altMap || DEFAULT_ALT;
|
||||
Object.keys(DEFAULT_ALT).forEach(function (k) {
|
||||
out[k] = DEFAULT_ALT[k];
|
||||
});
|
||||
Object.keys(src).forEach(function (k) {
|
||||
var name = String(src[k] || "").trim();
|
||||
var low = name.toLowerCase().replace(/\s+/g, "");
|
||||
if (LONG_TO_COMPACT[low]) out[k] = LONG_TO_COMPACT[low];
|
||||
else if (DEFAULT_ALT[k] && /goa/i.test(name)) out[k] = DEFAULT_ALT[k];
|
||||
else out[k] = name || DEFAULT_ALT[k] || name;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseAmount(s) {
|
||||
if (s == null || s === "") return { cur: "GOA", val: 0 };
|
||||
if (typeof s === "number") return { cur: "GOA", val: s };
|
||||
|
|
@ -51,7 +85,7 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string|number} amount
|
||||
* @param {string|number|object} amount
|
||||
* @param {object} [altMap]
|
||||
* @returns {{ display: string, title: string, raw: string, usedAlt: boolean }}
|
||||
*/
|
||||
|
|
@ -59,24 +93,17 @@
|
|||
if (amount == null || amount === "") {
|
||||
return { display: "—", title: "", raw: "", usedAlt: false };
|
||||
}
|
||||
// Prefer server-provided alt if caller passes object {amount, amount_alt}
|
||||
// Prefer re-format from canonical amount (keeps kGOA even if server sent Kilo-GOA)
|
||||
if (typeof amount === "object" && amount) {
|
||||
if (amount.amount_alt) {
|
||||
var rawO = amount.amount || amount.amount_full || String(amount.amount_alt);
|
||||
return {
|
||||
display: String(amount.amount_alt),
|
||||
title: amount.amount_full || rawO,
|
||||
raw: String(rawO),
|
||||
usedAlt: true
|
||||
};
|
||||
if (amount.amount) {
|
||||
return format(amount.amount, altMap);
|
||||
}
|
||||
amount = amount.amount || amount.amount_full || "";
|
||||
amount = amount.amount_full || amount.amount_alt || "";
|
||||
}
|
||||
|
||||
var alt = altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT;
|
||||
var alt = compactAlt(altMap || global.__GOA_ALT_UNITS || DEFAULT_ALT);
|
||||
var p = parseAmount(amount);
|
||||
var raw = baseStr(p.cur, p.val);
|
||||
// Foreign currencies must not pick up Kilo-GOA / Mega-GOA labels
|
||||
var baseName = (alt && alt["0"]) || p.cur;
|
||||
var curU = String(p.cur || "").toUpperCase();
|
||||
var baseU = String(baseName || "").toUpperCase();
|
||||
|
|
@ -95,7 +122,9 @@
|
|||
var n = parseInt(k, 10);
|
||||
if (!isNaN(n)) scales.push({ sc: n, name: String(alt[k]) });
|
||||
});
|
||||
scales.sort(function (a, b) { return b.sc - a.sc; });
|
||||
scales.sort(function (a, b) {
|
||||
return b.sc - a.sc;
|
||||
});
|
||||
|
||||
var abs = Math.abs(p.val);
|
||||
var chosen = null;
|
||||
|
|
@ -136,33 +165,80 @@
|
|||
setText(document.getElementById(id), amount, altMap);
|
||||
}
|
||||
|
||||
/** Prefer amount_alt from stats row, else format amount. */
|
||||
/** Prefer formatting from amount field; fall back to amount_alt string. */
|
||||
function pick(rowOrAmount, altMap) {
|
||||
if (rowOrAmount && typeof rowOrAmount === "object") {
|
||||
var a =
|
||||
rowOrAmount.amount ||
|
||||
rowOrAmount.total_amount ||
|
||||
rowOrAmount.amount_paid_sum ||
|
||||
rowOrAmount.amount_sum;
|
||||
if (a) return format(a, altMap);
|
||||
if (rowOrAmount.amount_alt) {
|
||||
return format({
|
||||
amount: rowOrAmount.amount,
|
||||
amount_alt: rowOrAmount.amount_alt,
|
||||
amount_full: rowOrAmount.amount_full
|
||||
}, altMap);
|
||||
return {
|
||||
display: String(rowOrAmount.amount_alt),
|
||||
title: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
|
||||
raw: String(rowOrAmount.amount_full || rowOrAmount.amount_alt),
|
||||
usedAlt: true
|
||||
};
|
||||
}
|
||||
return format(rowOrAmount.amount || rowOrAmount.total_amount, altMap);
|
||||
return format("", altMap);
|
||||
}
|
||||
return format(rowOrAmount, altMap);
|
||||
}
|
||||
|
||||
function rememberAlt(stats) {
|
||||
if (stats && stats.alt_unit_names && typeof stats.alt_unit_names === "object") {
|
||||
global.__GOA_ALT_UNITS = stats.alt_unit_names;
|
||||
global.__GOA_ALT_UNITS = compactAlt(stats.alt_unit_names);
|
||||
}
|
||||
}
|
||||
|
||||
/** Small legend HTML (positive scales). */
|
||||
function legendHtml(stats) {
|
||||
var alt = compactAlt((stats && stats.alt_unit_names) || global.__GOA_ALT_UNITS || DEFAULT_ALT);
|
||||
var rows = [];
|
||||
Object.keys(alt)
|
||||
.map(function (k) {
|
||||
return parseInt(k, 10);
|
||||
})
|
||||
.filter(function (n) {
|
||||
return !isNaN(n) && n > 0;
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return a - b;
|
||||
})
|
||||
.forEach(function (sc) {
|
||||
rows.push(
|
||||
"<tr><td>10<sup>" +
|
||||
sc +
|
||||
"</sup></td><td><strong>" +
|
||||
alt[String(sc)] +
|
||||
"</strong></td></tr>"
|
||||
);
|
||||
});
|
||||
if (!rows.length) return "";
|
||||
return (
|
||||
'<table class="unit-legend" aria-label="GOA unit names"><thead><tr><th>×base</th><th>name</th></tr></thead><tbody>' +
|
||||
rows.join("") +
|
||||
"</tbody></table>"
|
||||
);
|
||||
}
|
||||
|
||||
function fillLegend(elOrId, stats) {
|
||||
var el = typeof elOrId === "string" ? document.getElementById(elOrId) : elOrId;
|
||||
if (!el) return;
|
||||
el.innerHTML = legendHtml(stats);
|
||||
}
|
||||
|
||||
global.GoaAmount = {
|
||||
format: format,
|
||||
setText: setText,
|
||||
setById: setById,
|
||||
pick: pick,
|
||||
rememberAlt: rememberAlt,
|
||||
compactAlt: compactAlt,
|
||||
legendHtml: legendHtml,
|
||||
fillLegend: fillLegend,
|
||||
DEFAULT_ALT: DEFAULT_ALT,
|
||||
THRESHOLD: THRESHOLD
|
||||
};
|
||||
|
|
|
|||
35
configs/shared/landing-version.json
Normal file
35
configs/shared/landing-version.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"note": "Per-site landing content versions (start 42). Bump: scripts/taler-landing/stamp-landing-version.sh --bump bank|exchange|merchant",
|
||||
"sites": {
|
||||
"bank": {
|
||||
"version": 42,
|
||||
"label": "Bank landing",
|
||||
"path": "configs/bank-landing",
|
||||
"updated_iso": "2026-07-17T13:32:59+02:00",
|
||||
"updated_human": "2026-07-17 13:32 CEST",
|
||||
"commit": "921a4f1572dbf4391b69899287b4c0280bb0e271",
|
||||
"commit_short": "921a4f1",
|
||||
"repo_commit_url": "https://git.hacktivism.ch/hernani/koopa-admin-log/commit/921a4f1572dbf4391b69899287b4c0280bb0e271"
|
||||
},
|
||||
"exchange": {
|
||||
"version": 42,
|
||||
"label": "Exchange landing",
|
||||
"path": "configs/exchange-landing",
|
||||
"updated_iso": "2026-07-17T13:32:59+02:00",
|
||||
"updated_human": "2026-07-17 13:32 CEST",
|
||||
"commit": "921a4f1572dbf4391b69899287b4c0280bb0e271",
|
||||
"commit_short": "921a4f1",
|
||||
"repo_commit_url": "https://git.hacktivism.ch/hernani/koopa-admin-log/commit/921a4f1572dbf4391b69899287b4c0280bb0e271"
|
||||
},
|
||||
"merchant": {
|
||||
"version": 42,
|
||||
"label": "Merchant landing",
|
||||
"path": "configs/merchant-landing",
|
||||
"updated_iso": "2026-07-17T13:32:59+02:00",
|
||||
"updated_human": "2026-07-17 13:32 CEST",
|
||||
"commit": "921a4f1572dbf4391b69899287b4c0280bb0e271",
|
||||
"commit_short": "921a4f1",
|
||||
"repo_commit_url": "https://git.hacktivism.ch/hernani/koopa-admin-log/commit/921a4f1572dbf4391b69899287b4c0280bb0e271"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -146,3 +146,18 @@ podman exec taler-hacktivism-bank crontab -l # inspect
|
|||
|
||||
curl -sS https://bank.hacktivism.ch/intro/stats.json | python3 -m json.tool | head
|
||||
```
|
||||
|
||||
## Landing content version (per site)
|
||||
|
||||
Each landing footer shows **Bank|Exchange|Merchant landing vN** + last content
|
||||
change + linkified git commit. Versions are **independent** (start **v42** each).
|
||||
|
||||
- State: `configs/shared/landing-version.json` → `sites.bank|exchange|merchant`
|
||||
- After editing one site (example bank):
|
||||
```bash
|
||||
./scripts/taler-landing/stamp-landing-version.sh --bump bank
|
||||
git add configs/ && git commit -m "landing(bank): …"
|
||||
./scripts/taler-landing/stamp-landing-version.sh bank
|
||||
git add configs/ && git commit --amend --no-edit # optional pin
|
||||
```
|
||||
- Re-stamp all without bump: `./scripts/taler-landing/stamp-landing-version.sh --all`
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
#!/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]
|
||||
# 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
|
||||
|
||||
CTR="${1:-}"
|
||||
|
|
@ -11,7 +16,10 @@ if [ -z "$CTR" ]; then
|
|||
exit 2
|
||||
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_DST="${MEM_SNAPSHOT_DST:-/usr/local/lib/landing-mem-snapshot.sh}"
|
||||
|
||||
|
|
@ -22,10 +30,10 @@ fi
|
|||
|
||||
if [ -f "$MEM_SRC" ]; then
|
||||
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
|
||||
|
||||
# Run emit inside container (needs /proc + cgroup of that container)
|
||||
set +e
|
||||
raw=$(podman exec "$CTR" bash -c '
|
||||
set -e
|
||||
|
|
@ -37,7 +45,6 @@ raw=$(podman exec "$CTR" bash -c '
|
|||
# shellcheck disable=SC1091
|
||||
. /usr/local/lib/landing-mem-snapshot.sh
|
||||
if ! declare -F mem_snapshot_emit >/dev/null 2>&1; then
|
||||
# older helper without emit — synthesize
|
||||
mem_snapshot_json
|
||||
loadavg=""
|
||||
[ -r /proc/loadavg ] && loadavg=$(awk "{print \$1\",\"\$2\",\"\$3}" /proc/loadavg)
|
||||
|
|
@ -58,13 +65,115 @@ if [ "$ec" -ne 0 ] || [ -z "$raw" ]; then
|
|||
fi
|
||||
rm -f /tmp/landing-mem-err.$$
|
||||
|
||||
# Validate JSON
|
||||
if ! printf '%s' "$raw" | python3 -c 'import json,sys; json.load(sys.stdin)' 2>/dev/null; then
|
||||
printf '{"ok":false,"error":"invalid json from container"}\n'
|
||||
exit 1
|
||||
# Host-side: CPU% + PIDs via podman stats (works without memory controller)
|
||||
stats_json=$(podman stats --no-stream --format json "$CTR" 2>/dev/null || true)
|
||||
|
||||
# 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
|
||||
|
||||
if [ -n "$OUT" ]; then
|
||||
printf '%s\n' "$raw" >"$OUT"
|
||||
printf '%s\n' "$merged" >"$OUT"
|
||||
fi
|
||||
printf '%s\n' "$raw"
|
||||
printf '%s\n' "$merged"
|
||||
|
|
|
|||
|
|
@ -1,38 +1,81 @@
|
|||
#!/usr/bin/env python3
|
||||
"""GOA (and generic CUR:amount) alt-unit formatting for landing stats.
|
||||
|
||||
Matches exchange currency_specification.alt_unit_names:
|
||||
0→GOA, 3→Kilo-GOA, 6→Mega-GOA, … and fractional scales.
|
||||
Display prefers compact alt names for large values so landing tiles fit.
|
||||
Display uses compact SI-style names (kGOA, MGOA, GGOA, …) so tiles stay short.
|
||||
Exchange /config may still send long names (Kilo-GOA); we compact them.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
||||
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] = {
|
||||
"24": "Yotta-GOA",
|
||||
"21": "Zetta-GOA",
|
||||
"18": "Exa-GOA",
|
||||
"15": "Peta-GOA",
|
||||
"12": "Tera-GOA",
|
||||
"9": "Giga-GOA",
|
||||
"6": "Mega-GOA",
|
||||
"3": "Kilo-GOA",
|
||||
"24": "YGOA",
|
||||
"21": "ZGOA",
|
||||
"18": "EGOA",
|
||||
"15": "PGOA",
|
||||
"12": "TGOA",
|
||||
"9": "GGOA",
|
||||
"6": "MGOA",
|
||||
"3": "kGOA",
|
||||
"0": "GOA",
|
||||
"-1": "Deci-GOA",
|
||||
"-2": "Centi-GOA",
|
||||
"-3": "Milli-GOA",
|
||||
"-6": "Micro-GOA",
|
||||
"-7": "Deci-Micro-GOA",
|
||||
"-8": "Atomic-GOA",
|
||||
"-1": "dGOA",
|
||||
"-2": "cGOA",
|
||||
"-3": "mGOA",
|
||||
"-6": "µGOA",
|
||||
"-7": "dµGOA",
|
||||
"-8": "aGOA",
|
||||
}
|
||||
|
||||
# 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")
|
||||
|
||||
|
||||
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]:
|
||||
"""Parse 'GOA:12.5' or bare number → (currency, value)."""
|
||||
if s is None:
|
||||
|
|
@ -51,7 +94,6 @@ def parse_amount(s: Any) -> Tuple[str, Decimal]:
|
|||
def fmt_coeff(v: Decimal) -> str:
|
||||
if v == v.to_integral_value():
|
||||
return format(int(v), "d")
|
||||
# up to 4 significant fractional digits, strip trailing zeros
|
||||
q = v.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
|
||||
s = format(q, "f").rstrip("0").rstrip(".")
|
||||
return s or "0"
|
||||
|
|
@ -60,7 +102,6 @@ def fmt_coeff(v: Decimal) -> str:
|
|||
def fmt_base(cur: str, val: Decimal) -> str:
|
||||
if val == val.to_integral_value():
|
||||
return f"{cur}:{int(val)}"
|
||||
# preserve up to 8 fractional digits (Taler style)
|
||||
s = format(val, "f").rstrip("0").rstrip(".")
|
||||
return f"{cur}:{s}"
|
||||
|
||||
|
|
@ -76,12 +117,12 @@ def format_amount_alt(
|
|||
|
||||
Keys:
|
||||
amount canonical CUR:n
|
||||
amount_alt short display (e.g. "1.23 Mega-GOA")
|
||||
amount_full "1.23 Mega-GOA (GOA:1230000)" when alt used
|
||||
value float for JSON (may lose precision above 2^53 — also value_str)
|
||||
amount_alt short display (e.g. "1.23 MGOA")
|
||||
amount_full "1.23 MGOA (GOA:1230000)" when alt used
|
||||
value float for JSON
|
||||
value_str exact decimal string
|
||||
"""
|
||||
alt = alt or DEFAULT_ALT
|
||||
alt = compact_alt_map(alt)
|
||||
try:
|
||||
cur, val = parse_amount(amount)
|
||||
except (InvalidOperation, ValueError, ArithmeticError):
|
||||
|
|
@ -95,17 +136,21 @@ def format_amount_alt(
|
|||
}
|
||||
|
||||
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:
|
||||
value_f = float(val)
|
||||
except Exception:
|
||||
value_f = 0.0
|
||||
|
||||
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()
|
||||
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 {
|
||||
"amount": base,
|
||||
"amount_alt": base,
|
||||
|
|
@ -115,7 +160,6 @@ def format_amount_alt(
|
|||
}
|
||||
|
||||
if val == 0:
|
||||
# Keep canonical CUR:0 (avoids "0 GOA" for empty foreign balances)
|
||||
return {
|
||||
"amount": base,
|
||||
"amount_alt": base,
|
||||
|
|
@ -133,9 +177,7 @@ def format_amount_alt(
|
|||
scales.sort(key=lambda x: -x[0])
|
||||
|
||||
absval = abs(val)
|
||||
# Below threshold: short base form (saves noise on small demo amounts)
|
||||
if absval < threshold:
|
||||
# Prefer "GOA:12.5" style (matches existing landings) when small
|
||||
return {
|
||||
"amount": base,
|
||||
"amount_alt": base,
|
||||
|
|
@ -165,7 +207,7 @@ def format_amount_alt(
|
|||
|
||||
sc, name, coeff = chosen
|
||||
short = f"{fmt_coeff(coeff)} {name}"
|
||||
full = f"{short} ({base})" if with_base or True else short
|
||||
full = f"{short} ({base})"
|
||||
return {
|
||||
"amount": base,
|
||||
"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."""
|
||||
if not isinstance(obj, dict):
|
||||
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]:
|
||||
"""GET exchange/bank /config → alt_unit_names (fallback DEFAULT_ALT)."""
|
||||
"""GET exchange/bank /config → compact alt_unit_names (fallback DEFAULT_ALT)."""
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
|
|
@ -215,16 +259,16 @@ def load_alt_from_config(url: str, timeout: float = 8.0) -> Dict[str, str]:
|
|||
break
|
||||
if not isinstance(au, dict) or "0" not in au:
|
||||
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]:
|
||||
"""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):
|
||||
return data
|
||||
|
||||
# bank-style
|
||||
# bank-style nested blocks
|
||||
for path in (
|
||||
("flow", "incoming"),
|
||||
("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)
|
||||
cur["last_amount"] = f["amount"]
|
||||
cur["last_amount_alt"] = f["amount_alt"]
|
||||
cur["last_amount_full"] = f["amount_full"]
|
||||
|
||||
if isinstance(data.get("flow"), dict):
|
||||
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)
|
||||
row["value"] = f["amount"]
|
||||
row["value_alt"] = f["amount_alt"]
|
||||
row["value_full"] = f["amount_full"]
|
||||
|
||||
# merchant dual currency
|
||||
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)
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -63,12 +63,46 @@ def merge(stats: Dict[str, Any], resources: Dict[str, Any]) -> Dict[str, Any]:
|
|||
|
||||
if resources.get("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")
|
||||
if isinstance(mem, dict) and mem:
|
||||
perf["memory"] = compact_memory(dict(mem))
|
||||
perf["memory"]["source"] = resources.get("source") or "mem-snapshot"
|
||||
m = compact_memory(dict(mem))
|
||||
# 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
|
||||
|
||||
|
||||
|
|
|
|||
193
scripts/taler-landing/stamp-landing-version.sh
Executable file
193
scripts/taler-landing/stamp-landing-version.sh
Executable file
|
|
@ -0,0 +1,193 @@
|
|||
#!/usr/bin/env bash
|
||||
# Stamp landing footers per site: version + last content change + linkified commit.
|
||||
#
|
||||
# Sites: bank | exchange | merchant
|
||||
#
|
||||
# Usage (from koopa-admin-log root):
|
||||
# ./scripts/taler-landing/stamp-landing-version.sh bank
|
||||
# re-stamp bank with current version + HEAD
|
||||
# ./scripts/taler-landing/stamp-landing-version.sh --bump bank
|
||||
# bank version += 1, then stamp HEAD
|
||||
# ./scripts/taler-landing/stamp-landing-version.sh --set 42 exchange
|
||||
# force exchange version to 42
|
||||
# ./scripts/taler-landing/stamp-landing-version.sh --all
|
||||
# re-stamp all sites (no bump)
|
||||
# ./scripts/taler-landing/stamp-landing-version.sh --bump --all
|
||||
# bump every site by 1 (rare)
|
||||
#
|
||||
# Typical flow after editing one site:
|
||||
# 1) edit configs/bank-landing/…
|
||||
# 2) ./scripts/taler-landing/stamp-landing-version.sh --bump bank
|
||||
# 3) git add configs/ && git commit -m "landing(bank): …"
|
||||
# 4) ./scripts/taler-landing/stamp-landing-version.sh bank
|
||||
# git add configs/ && git commit --amend --no-edit # optional pin
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
VER_FILE="$ROOT/configs/shared/landing-version.json"
|
||||
REPO_WEB="${LANDING_REPO_WEB:-https://git.hacktivism.ch/hernani/koopa-admin-log}"
|
||||
|
||||
declare -A SITE_HTML=(
|
||||
[bank]="$ROOT/configs/bank-landing/index.html"
|
||||
[exchange]="$ROOT/configs/exchange-landing/index.html"
|
||||
[merchant]="$ROOT/configs/merchant-landing/index.html"
|
||||
)
|
||||
declare -A SITE_LABEL=(
|
||||
[bank]="Bank landing"
|
||||
[exchange]="Exchange landing"
|
||||
[merchant]="Merchant landing"
|
||||
)
|
||||
declare -A SITE_PATH=(
|
||||
[bank]="configs/bank-landing"
|
||||
[exchange]="configs/exchange-landing"
|
||||
[merchant]="configs/merchant-landing"
|
||||
)
|
||||
|
||||
export TZ="${TZ:-Europe/Zurich}"
|
||||
|
||||
bump=0
|
||||
set_ver=""
|
||||
do_all=0
|
||||
sites=()
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--bump) bump=1; shift ;;
|
||||
--set) set_ver="${2:-}"; shift 2 ;;
|
||||
--all) do_all=1; shift ;;
|
||||
bank|exchange|merchant) sites+=("$1"); shift ;;
|
||||
-h|--help)
|
||||
sed -n '2,28p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "unknown arg: $1 (sites: bank exchange merchant)" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$do_all" = 1 ]; then
|
||||
sites=(bank exchange merchant)
|
||||
fi
|
||||
if [ "${#sites[@]}" -eq 0 ]; then
|
||||
echo "usage: $0 [--bump|--set N] bank|exchange|merchant|--all" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -f "$VER_FILE" ]; then
|
||||
cat >"$VER_FILE" <<'EOF'
|
||||
{"note":"per-site","sites":{"bank":{"version":42},"exchange":{"version":42},"merchant":{"version":42}}}
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ -n "${OVERRIDE_COMMIT:-}" ]; then
|
||||
full="$OVERRIDE_COMMIT"
|
||||
else
|
||||
full=$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||
fi
|
||||
short=$(git -C "$ROOT" rev-parse --short=7 "$full" 2>/dev/null || echo "${full:0:7}")
|
||||
iso=$(date +%Y-%m-%dT%H:%M:%S%z | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/')
|
||||
human=$(date +"%Y-%m-%d %H:%M %Z")
|
||||
url="${REPO_WEB}/commit/${full}"
|
||||
|
||||
css_snip=' .landing-rev {
|
||||
margin: 0.75rem 0 0;
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted, #9a8fb0);
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
}
|
||||
.landing-rev a { color: inherit; text-decoration: underline; text-underline-offset: 2px; opacity: 0.95; }
|
||||
.landing-rev strong { font-weight: 750; }
|
||||
'
|
||||
|
||||
for site in "${sites[@]}"; do
|
||||
html="${SITE_HTML[$site]:-}"
|
||||
label="${SITE_LABEL[$site]:-}"
|
||||
relpath="${SITE_PATH[$site]:-}"
|
||||
[ -n "$html" ] && [ -f "$html" ] || { echo "missing site/html: $site" >&2; exit 1; }
|
||||
|
||||
# resolve version for this site
|
||||
version=$(python3 - "$VER_FILE" "$site" "$bump" "$set_ver" <<'PY'
|
||||
import json, sys
|
||||
path, site, bump, set_ver = sys.argv[1:5]
|
||||
data = json.load(open(path, encoding="utf-8"))
|
||||
sites = data.setdefault("sites", {})
|
||||
entry = sites.setdefault(site, {"version": 42})
|
||||
v = int(entry.get("version", 42))
|
||||
if set_ver:
|
||||
v = int(set_ver)
|
||||
elif bump == "1":
|
||||
v += 1
|
||||
print(v)
|
||||
PY
|
||||
)
|
||||
|
||||
# update JSON entry for this site (preserve others)
|
||||
python3 - "$VER_FILE" "$site" "$version" "$label" "$relpath" "$iso" "$human" "$full" "$short" "$url" <<'PY'
|
||||
import json, sys
|
||||
path, site, version, label, relpath, iso, human, full, short, url = sys.argv[1:]
|
||||
data = json.load(open(path, encoding="utf-8"))
|
||||
data["note"] = (
|
||||
"Per-site landing content versions (start 42). "
|
||||
"Bump: scripts/taler-landing/stamp-landing-version.sh --bump bank|exchange|merchant"
|
||||
)
|
||||
sites = data.setdefault("sites", {})
|
||||
sites[site] = {
|
||||
"version": int(version),
|
||||
"label": label,
|
||||
"path": relpath,
|
||||
"updated_iso": iso,
|
||||
"updated_human": human,
|
||||
"commit": full,
|
||||
"commit_short": short,
|
||||
"repo_commit_url": url,
|
||||
}
|
||||
open(path, "w", encoding="utf-8").write(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
||||
print(f"{site}: v{version} commit={short} at={human}")
|
||||
PY
|
||||
|
||||
block=$(cat <<EOF
|
||||
<!-- landing-rev -->
|
||||
<p class="landing-rev" data-landing-site="${site}" data-landing-version="${version}" data-landing-commit="${short}">
|
||||
${label} <strong>v${version}</strong>
|
||||
· content <time datetime="${iso}">${human}</time>
|
||||
· <a href="${url}" title="koopa-admin-log ${full}">${short}</a>
|
||||
</p>
|
||||
<!-- /landing-rev -->
|
||||
EOF
|
||||
)
|
||||
|
||||
# ensure CSS once
|
||||
if ! grep -q '\.landing-rev' "$html"; then
|
||||
python3 - "$html" "$css_snip" <<'PY'
|
||||
import sys
|
||||
path, snip = sys.argv[1], sys.argv[2]
|
||||
t = open(path, encoding="utf-8").read()
|
||||
if ".landing-rev" in t:
|
||||
raise SystemExit(0)
|
||||
idx = t.find("</style>")
|
||||
if idx < 0:
|
||||
raise SystemExit(f"no </style> in {path}")
|
||||
open(path, "w", encoding="utf-8").write(t[:idx] + snip + "\n" + t[idx:])
|
||||
print(f"css → {path}")
|
||||
PY
|
||||
fi
|
||||
|
||||
python3 - "$html" "$block" <<'PY'
|
||||
import sys, re
|
||||
path, block = sys.argv[1], sys.argv[2]
|
||||
t = open(path, encoding="utf-8").read()
|
||||
block = block.rstrip() + "\n"
|
||||
pat = re.compile(r"[ \t]*<!-- landing-rev -->.*?<!-- /landing-rev -->\n?", re.S)
|
||||
if pat.search(t):
|
||||
t = pat.sub(block, t, count=1)
|
||||
else:
|
||||
t2, n = re.subn(r"(\n)([ \t]*</footer>)", r"\1" + block + r"\2", t, count=1)
|
||||
if n != 1:
|
||||
raise SystemExit(f"could not insert landing-rev in {path}")
|
||||
t = t2
|
||||
open(path, "w", encoding="utf-8").write(t)
|
||||
print(f"stamp → {path}")
|
||||
PY
|
||||
done
|
||||
|
||||
echo "OK stamped: ${sites[*]}"
|
||||
|
|
@ -66,8 +66,8 @@ bank = {
|
|||
"recent_withdraws": [{"amount": "GOA:4503599627370495"}],
|
||||
}
|
||||
enrich_stats_tree(bank, DEFAULT_ALT)
|
||||
assert "Kilo-GOA" in bank["withdraws"]["total_amount_alt"]
|
||||
assert "Peta-GOA" in bank["recent_withdraws"][0]["amount_alt"]
|
||||
assert "kGOA" in bank["withdraws"]["total_amount_alt"]
|
||||
assert "PGOA" in bank["recent_withdraws"][0]["amount_alt"]
|
||||
|
||||
ex = {
|
||||
"ok": True,
|
||||
|
|
@ -77,8 +77,8 @@ ex = {
|
|||
"by_denom": [{"value": "GOA:1000"}],
|
||||
}
|
||||
enrich_stats_tree(ex, DEFAULT_ALT)
|
||||
assert "Peta-GOA" in ex["wire_in_amount_alt"]
|
||||
assert "Mega-GOA" in ex["withdraw_amount_alt"]
|
||||
assert "PGOA" in ex["wire_in_amount_alt"]
|
||||
assert "MGOA" in ex["withdraw_amount_alt"]
|
||||
assert ex["coins_remaining_amount_alt"] in ("GOA:0", "0 GOA")
|
||||
|
||||
mer = {
|
||||
|
|
@ -92,10 +92,10 @@ mer = {
|
|||
],
|
||||
}
|
||||
enrich_stats_tree(mer, DEFAULT_ALT)
|
||||
# CHF must NOT become Kilo-GOA
|
||||
assert "GOA" not in mer["by_currency"][1]["amount_paid_sum_alt"] or mer["by_currency"][1]["amount_paid_sum_alt"].startswith("CHF")
|
||||
# CHF must NOT become kGOA
|
||||
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")
|
||||
PY
|
||||
then ok "enrich shapes bank/exchange/merchant"
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ for item in $SCHEMAS; do
|
|||
done
|
||||
|
||||
# --- 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).
|
||||
# Writes TSV rows: ts kind order_id amount summary status
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue