openvibers/openvibe.network Commits

Internal: site-wide OpenCoins totals for public stat displays

OP OpenVibers committed 16 Sept 2026 · a58ee73160 · 1 file, +24 −0

GET /internal/coins/stats returns earned, spent, circulating, holders and transaction count
across the whole ledger, held for a minute. OpenVibe.Live's home hero shows OpenCoins beside
per-channel points, and the coin ledger lives here, so this is the only way it can.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/server/internal/routes.js b/server/internal/routes.js
iindex 6586cd40..cf29e78a 100644
---- a/server/internal/routes.js
++++ b/server/internal/routes.js
@@@ -174,6 +174,30 @@ function handleWalletError(res, err) {
return res.status(500).json({ error: 'wallet_internal_error' });
}
++// ── GET /internal/coins/stats ────────────────────────────────
++// Site-wide OpenCoins totals for public stat displays (OpenVibe.Live's home hero).
++// Summing the whole ledger is cheap at our size but pointless to repeat per pageview,
++// so the answer is held for a minute.
++let _coinStats = { at: 0, data: null };
++router.get('/coins/stats', (req, res) => {
++ try {
++ if (_coinStats.data && Date.now() - _coinStats.at < 60_000) return res.json(_coinStats.data);
++ const db = getDb(req);
++ const one = (sql) => { try { return db.prepare(sql).get()?.n || 0; } catch { return 0; } };
++ const data = {
++ earned: one('SELECT COALESCE(SUM(delta), 0) AS n FROM coin_transactions WHERE delta > 0'),
++ spent: one('SELECT COALESCE(-SUM(delta), 0) AS n FROM coin_transactions WHERE delta < 0'),
++ circulating: one('SELECT COALESCE(SUM(balance), 0) AS n FROM wallets'),
++ holders: one('SELECT COUNT(*) AS n FROM wallets WHERE balance > 0'),
++ transactions: one('SELECT COUNT(*) AS n FROM coin_transactions'),
++ };
++ _coinStats = { at: Date.now(), data };
++ res.json(data);
++ } catch (err) {
++ res.status(500).json({ error: String(err.message || err) });
++ }
++});
++
// ── POST /internal/coins/credit ──────────────────────────────
// Body: { user_id, app_id, amount (positive int), reason, ref?, idempotency_key }
// → { balance }