merchant landing: dual-currency intro + nginx :9015

This commit is contained in:
Hernâni Marques 2026-07-09 12:18:40 +02:00
parent dd4a984912
commit 8bd0ec24a0
No known key found for this signature in database
3 changed files with 504 additions and 0 deletions

View file

@ -0,0 +1,15 @@
# Merchant landing (`taler.hacktivism.ch` intro)
Static intro for the **GOA merchant** backend.
| Item | Value |
|------|--------|
| Container | `taler-hacktivism` |
| In-container path | `/var/www/merchant-landing/` |
| nginx | listen **9015** (`nginx-landing.conf`) |
| **Host port to publish / wire** | **`9015`** → `127.0.0.1:9015` |
| Public path (once Caddy wired) | e.g. `https://taler.hacktivism.ch/intro/` |
## Deploy
See `scripts/taler-landing/deploy-landings.sh`.

View file

@ -0,0 +1,465 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GOA Merchant · dual currency</title>
<base href="/intro/" />
<style>
:root {
--bg: #120f18;
--card: rgba(36, 28, 48, 0.88);
--text: #f4eefc;
--muted: #b8a8c9;
--accent: #c084fc;
--accent2: #a78bfa;
--border: rgba(167, 139, 250, 0.32);
--glow-violet: rgba(168, 85, 247, 0.38);
--glow-indigo: rgba(99, 102, 241, 0.28);
--chf: #93c5fd;
--goa: #c4b5fd;
--val: #e9d5ff;
--pay: #86efac;
--ref: #fca5a5;
}
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--text); line-height: 1.55;
background-color: var(--bg);
background-image:
radial-gradient(ellipse 90% 55% at 50% 108%, rgba(76, 29, 149, 0.45) 0%, transparent 55%),
radial-gradient(circle 420px at 12% 18%, var(--glow-violet) 0%, transparent 62%),
radial-gradient(circle 380px at 88% 14%, var(--glow-indigo) 0%, transparent 58%),
linear-gradient(165deg, #1e1530 0%, #120f18 40%, #0f1220 72%, #0c0e18 100%);
background-attachment: fixed;
}
main { position: relative; z-index: 1; max-width: 42rem; margin: 0 auto; padding: 2.25rem 1.25rem 3.5rem; }
.hero { text-align: center; margin: 0 0 1.25rem; }
h1 {
font-size: 1.85rem; font-weight: 750; margin: 0 0 0.45rem; letter-spacing: -0.02em;
background: linear-gradient(120deg, #faf5ff 0%, #d8b4fe 40%, #818cf8 100%);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.sub { color: var(--muted); margin: 0 auto; max-width: 30rem; font-size: 1.02rem; }
.sub a {
color: #5eead4; text-decoration: none; border-bottom: 1px solid rgba(94, 234, 212, 0.35);
font-weight: 650;
}
.sub a:hover { border-bottom-color: #5eead4; }
.badge {
display: inline-block;
background: linear-gradient(135deg, rgba(168, 85, 247, 0.22), rgba(99, 102, 241, 0.18));
color: #e9d5ff; font-size: 0.75rem; font-weight: 700;
padding: 0.2rem 0.7rem; border-radius: 999px; margin: 0 0 0.75rem;
border: 1px solid rgba(167, 139, 250, 0.45); letter-spacing: 0.04em;
}
.stats {
background: rgba(24, 18, 40, 0.65);
border: 1px solid rgba(167, 139, 250, 0.32);
border-radius: 16px; padding: 1.1rem 1.15rem 1.2rem; margin: 0 0 1.15rem;
}
.stats h2 {
margin: 0 0 0.35rem; font-size: 0.78rem; font-weight: 700;
letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); text-align: center;
}
.stats .note {
text-align: center; font-size: 0.82rem; color: var(--muted); margin: 0 0 0.85rem;
}
.stats-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 0.55rem 0.7rem;
}
@media (min-width: 420px) { .stats-grid { grid-template-columns: 1fr 1fr 1fr; } }
.stat {
background: rgba(0, 0, 0, 0.22); border-radius: 10px; padding: 0.5rem 0.6rem;
}
.stat .k {
display: block; font-size: 0.68rem; color: var(--muted);
letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 0.15rem; text-align: center;
}
.stat .v {
display: block; font-size: 1.05rem; font-weight: 750; color: var(--val);
text-align: center; font-variant-numeric: tabular-nums;
}
.stat .v.teal { color: var(--accent2); }
/* Side-by-side currencies — static panels, never links */
.cur-pair {
display: grid; grid-template-columns: 1fr 1fr; gap: 0.65rem; margin-top: 0.85rem;
}
@media (max-width: 380px) { .cur-pair { grid-template-columns: 1fr; } }
.cur-panel {
border-radius: 14px; padding: 0.85rem 0.75rem 0.9rem;
background: rgba(0, 0, 0, 0.28);
border: 1px solid rgba(167, 139, 250, 0.25);
pointer-events: none; user-select: text;
}
.cur-panel.chf { border-color: rgba(147, 197, 253, 0.45); }
.cur-panel.goa { border-color: rgba(196, 181, 253, 0.5); }
.cur-panel .cur-name {
text-align: center; font-size: 0.78rem; font-weight: 800; letter-spacing: 0.08em;
text-transform: uppercase; margin: 0 0 0.15rem;
}
.cur-panel.chf .cur-name { color: var(--chf); }
.cur-panel.goa .cur-name { color: var(--goa); }
.cur-panel .cur-sub {
text-align: center; font-size: 0.68rem; color: var(--muted); margin: 0 0 0.55rem;
}
.cur-panel .amt-big {
display: block; text-align: center; font-size: 1.28rem; font-weight: 800;
font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
color: var(--val); margin: 0 0 0.15rem; word-break: break-word;
}
.cur-panel.chf .amt-big { color: #bfdbfe; }
.cur-panel.goa .amt-big { color: #ddd6fe; }
.cur-panel .amt-label {
display: block; text-align: center; font-size: 0.68rem; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.55rem;
}
.cur-meta {
display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem 0.4rem;
font-size: 0.8rem; text-align: center;
}
.cur-meta .mk { display: block; font-size: 0.64rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.cur-meta .mv { display: block; font-weight: 700; color: var(--val); font-variant-numeric: tabular-nums; }
.cur-panel .amt-secondary {
margin-top: 0.55rem; text-align: center; font-size: 0.78rem; color: var(--muted);
}
.cur-panel .amt-secondary strong { color: var(--val); font-weight: 700; }
/* Recent activity */
.act-list { list-style: none; margin: 0; padding: 0; }
.act-item {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.35rem 0.65rem;
align-items: center;
padding: 0.55rem 0.35rem;
border-bottom: 1px solid rgba(255,255,255,0.06);
font-size: 0.88rem;
}
.act-item:last-child { border-bottom: 0; }
.act-kind {
font-size: 0.65rem; font-weight: 800; letter-spacing: 0.06em;
text-transform: uppercase; padding: 0.18rem 0.4rem; border-radius: 6px;
align-self: center;
}
.act-kind.payment { color: #14532d; background: rgba(134, 239, 172, 0.85); }
.act-kind.refund { color: #7f1d1d; background: rgba(252, 165, 165, 0.9); }
.act-kind.other { color: #1e1b4b; background: rgba(196, 181, 253, 0.85); }
.act-main { min-width: 0; }
.act-amt {
font-weight: 800; font-variant-numeric: tabular-nums; color: var(--val);
font-size: 0.95rem;
}
.act-sum {
color: var(--muted); font-size: 0.78rem; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
.act-meta {
text-align: right; font-size: 0.68rem; color: var(--muted);
font-variant-numeric: tabular-nums; white-space: nowrap;
}
.empty { text-align: center; color: var(--muted); font-size: 0.85rem; padding: 0.5rem; }
.stats-foot {
margin: 0.75rem 0 0; font-size: 0.72rem; color: var(--muted); text-align: center;
}
.stats-foot.err { color: #f0a090; }
.stats-run-note {
margin: 0.35rem 0 0; font-size: 0.7rem; color: #f0a090; text-align: center;
}
.step-card {
background: var(--card); border: 1px solid var(--border); border-radius: 18px;
padding: 1.3rem 1.25rem 1.4rem; margin-bottom: 1.2rem;
}
.step-card h2 {
font-size: 1.2rem; font-weight: 800; margin: 0 0 0.4rem; text-align: center;
}
.step-tagline { margin: 0 0 1rem; color: var(--muted); text-align: center; font-size: 0.95rem; }
.links { display: flex; flex-direction: column; gap: 0.55rem; }
.cta {
display: block; text-align: center; text-decoration: none; font-weight: 750;
padding: 0.7rem 1.1rem; border-radius: 12px;
background: linear-gradient(135deg, #c084fc, #7c3aed); color: #1a0b2e;
box-shadow: 0 8px 22px rgba(124, 58, 237, 0.35);
}
.cta.teal {
background: linear-gradient(135deg, #a78bfa, #6366f1); color: #0f0a1a;
}
.cta-sec {
display: block; text-align: center; text-decoration: none; color: var(--accent2);
border: 1px solid rgba(167, 139, 250, 0.45); padding: 0.55rem 0.85rem;
border-radius: 10px; font-weight: 600; font-size: 0.92rem;
}
footer { margin-top: 1.75rem; color: var(--muted); font-size: 0.85rem; text-align: center; }
footer a { color: #d8b4fe; }
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: #e9d5ff;
}
</style>
</head>
<body>
<main>
<header class="hero">
<span class="badge">Merchant · dual currency</span>
<h1>GOA Merchant</h1>
<p class="sub">
Accepts payments in <strong>GOA</strong>
(<a href="https://taler.hacktivism.ch/terms" title="Terms: GOA explorational currency">explorational</a>)
and <strong>CHF</strong>
(<a href="https://taler.hacktivism.ch/terms" title="Terms: CHF via taler-ops">taler-ops</a>).
</p>
</header>
<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>
<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">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>
</div>
<!-- Static dual panels (not links) -->
<div class="cur-pair" id="st-currencies" aria-label="Amounts by currency">
<div class="cur-panel goa" id="panel-goa">
<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>
<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>
<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>
<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>
</div>
<p class="stats-foot" id="st-foot">Loading…</p>
<p class="stats-run-note" id="st-run-note" hidden></p>
</section>
<section class="stats" id="stats-perf" aria-labelledby="perf-title">
<h2 id="perf-title">Performance</h2>
<p class="note">Live HTTP probes + container memory (RSS)</p>
<div class="stats-grid">
<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">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>
<div id="st-mem-top"></div>
</div>
<p class="stats-foot" id="st-perf-foot">Memory from /proc + cgroup inside podman container.</p>
</section>
<section class="stats" id="activity" aria-labelledby="act-title">
<h2 id="act-title">Recent activity</h2>
<p class="note">Latest payments &amp; refunds across instances</p>
<ul class="act-list" id="st-activity">
<li class="empty">Loading…</li>
</ul>
</section>
<section class="step-card">
<h2>Merchant SPA</h2>
<p class="step-tagline">Backend <code>taler.hacktivism.ch</code> · GOA + CHF</p>
<div class="links">
<a class="cta teal" href="https://taler.hacktivism.ch/webui/">Open /webui/ →</a>
<a class="cta-sec" href="https://taler.hacktivism.ch/config">/config</a>
<a class="cta-sec" href="https://taler.hacktivism.ch/terms">/terms</a>
<a class="cta-sec" href="https://taler.hacktivism.ch/privacy">/privacy</a>
<a class="cta-sec" href="https://bank.hacktivism.ch/intro/">Bank (GOA withdraw)</a>
<a class="cta-sec" href="https://exchange.hacktivism.ch/intro/">Exchange</a>
</div>
</section>
<footer>
<a href="https://bank.hacktivism.ch/intro/">Bank</a> ·
<a href="https://exchange.hacktivism.ch/intro/">Exchange</a> ·
<a href="https://taler.hacktivism.ch/intro/">Merchant</a> ·
<a href="https://taler.hacktivism.ch/terms">Terms</a> ·
<a href="https://taler.hacktivism.ch/privacy">Privacy</a>
<p class="privacy-note" style="margin:0.65rem 0 0;font-size:0.78rem;color:var(--muted);line-height:1.4">
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>
</footer>
</main>
<script>
(function () {
function set(id, v) {
var el = document.getElementById(id);
if (el) el.textContent = v == null || v === "" ? "—" : String(v);
}
function msLabel(ms, http) {
if (ms == null || ms === "") return "—";
var s = ms + " ms";
if (http && http !== "200") s += " (" + http + ")";
return s;
}
function fillCurrency(prefix, c) {
if (!c) {
set(prefix + "-paid-amt", "—");
set(prefix + "-all-amt", "—");
set(prefix + "-orders", "—");
set(prefix + "-paid", "—");
set(prefix + "-wired", "—");
set(prefix + "-unpaid", "—");
return;
}
set(prefix + "-paid-amt", c.amount_paid_sum || "—");
set(prefix + "-all-amt", c.amount_sum || "—");
set(prefix + "-orders", c.contracts);
set(prefix + "-paid", c.paid);
set(prefix + "-wired", c.wired);
set(prefix + "-unpaid", c.unpaid);
}
function esc(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function load() {
var foot = document.getElementById("st-foot");
fetch("/intro/stats.json", { cache: "no-store" })
.then(function (r) { if (!r.ok) throw new Error("http"); return r.json(); })
.then(function (d) {
if (!d || !d.ok) throw new Error("bad");
set("st-instances", d.instances);
set("st-orders", d.orders);
set("st-paid", d.paid);
set("st-unpaid", d.unpaid);
set("st-wired", d.wired);
set("st-refunds", d.refunds);
var by = {};
(d.by_currency || []).forEach(function (c) {
by[(c.currency || "").toUpperCase()] = c;
});
fillCurrency("goa", by.GOA);
fillCurrency("chf", by.CHF);
var list = document.getElementById("st-activity");
list.innerHTML = "";
var acts = d.recent_activity || [];
if (!acts.length) {
list.innerHTML = '<li class="empty">No recent payments or refunds</li>';
} else {
acts.forEach(function (a) {
var kind = (a.kind || "other").toLowerCase();
var li = document.createElement("li");
li.className = "act-item";
// no merchant names / ids — amount, optional summary, time only
li.innerHTML =
'<span class="act-kind ' + esc(kind) + '">' + esc(kind) + "</span>" +
'<div class="act-main">' +
'<div class="act-amt">' + esc(a.amount || "—") + "</div>" +
(a.summary
? '<div class="act-sum">' + esc(a.summary) + "</div>"
: "") +
"</div>" +
'<div class="act-meta">' +
"<span>" + esc(a.ts_human || a.ts || "") + "</span>" +
"</div>";
list.appendChild(li);
});
}
if (foot) {
foot.textContent = (d.dual_currency ? "Dual-currency · " : "") +
(d.generated_at_human || d.generated_at || "");
}
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-load", p.loadavg || "—");
var mem = p.memory || {};
set("st-mem-ctr", mem.container_rss_human || "—");
set("st-mem-pg", mem.postgres_rss_human
? mem.postgres_rss_human + (mem.postgres_n ? " · " + mem.postgres_n + "p" : "")
: "—");
set("st-mem-taler", mem.taler_rss_human
? mem.taler_rss_human + (mem.taler_n ? " · " + mem.taler_n + "p" : "")
: "—");
set("st-mem-nginx", mem.nginx_rss_human || "—");
var topBox = document.getElementById("st-mem-top");
if (topBox) {
var tops = mem.top || [];
if (!tops.length) {
topBox.innerHTML = '<p class="note"></p>';
} else {
var th = '<table style="width:100%;font-size:0.78rem;border-collapse:collapse"><thead><tr>' +
'<th style="text-align:left;padding:0.25rem;border-bottom:1px solid var(--border)">RSS</th>' +
'<th style="text-align:left;padding:0.25rem;border-bottom:1px solid var(--border)">Comm</th>' +
'<th style="text-align:left;padding:0.25rem;border-bottom:1px solid var(--border)">Command</th></tr></thead><tbody>';
tops.forEach(function (t) {
th += '<tr><td style="padding:0.25rem;color:#5eead4">' + (t.rss_human || "?") +
'</td><td style="padding:0.25rem">' + (t.comm || "") +
'</td><td style="padding:0.25rem;word-break:break-all;color:var(--muted)">' +
(t.cmd || "") + "</td></tr>";
});
th += "</tbody></table>";
topBox.innerHTML = th;
}
}
var pf = document.getElementById("st-perf-foot");
if (pf) {
pf.textContent = "Container RSS + process groups · " +
(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; })
.catch(function () { return null; });
})
.then(function (run) {
var note = document.getElementById("st-run-note");
if (!note) return;
if (run && run.ok === false) {
note.hidden = false;
note.textContent = "Last refresh failed" +
(run.at_human ? " · " + run.at_human : "") +
" · showing last good data";
} else {
note.hidden = true;
note.textContent = "";
}
})
.catch(function () {
if (foot) foot.innerHTML = 'Stats offline · <a href="https://bank.hacktivism.ch/intro/" style="color:#e8c878">bank.hacktivism.ch</a>';
var list = document.getElementById("st-activity");
if (list) list.innerHTML = '<li class="empty">Stats offline</li>';
});
}
load();
})();
</script>
</body>
</html>

View file

@ -0,0 +1,24 @@
# Merchant landing — :9015 (behind Caddy — no port in Location)
server {
listen 9015;
listen [::]:9015;
server_name taler.hacktivism.ch _;
root /var/www/merchant-landing;
index index.html;
absolute_redirect off;
port_in_redirect off;
location = / {
return 302 /intro/;
}
location = /intro {
return 302 /intro/;
}
location /intro/ {
alias /var/www/merchant-landing/;
}
location / {
try_files $uri $uri/ =404;
}
}