openvibers/openvibe.live Commits

Stat board: a comparison on every chip, and money copy that isn't about being a nonprofit

OP OpenVibers committed 16 Sept 2026 · 557a989a96 · 4 files, +34 −21

Most of the expanded board had no comparison at all — Live, Watching, Streamers, Hours watched,
Supporters, all four OpenCoins figures, Goals and Emotes were bare numbers. Each now carries
something useful: the two instantaneous readings compare against their 24-hour average, and the
rest gained rolling windows. New on the server: distinct-supporter and distinct-streamer
headcounts per window, emote and goal creation windows, and OpenCoins earned/spent/holders
windows from the Network wallet, all with the previous seven days alongside for the trend.

The "not-for-profit plan" section is now "Where the money goes", and says what actually happens
rather than promising a legal status we have not filed for: hosting and development first, then
creator payouts, community events and prize pools, with the whole thing open source and the
analytics public. Framed around being open and not being evil, which is true today, instead of
around paperwork that may never happen.

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

Changes

ddiff --git a/public/index.html b/public/index.html
iindex f2191106..de04b25d 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -520,24 +520,24 @@
</a>
</div>
-- <!-- The not-for-profit plan — part of the same About section -->
-- <div class="cta-section-label cta-diff-label"><i class="fa-solid fa-seedling"></i> The not-for-profit plan</div>
-- <p class="cta-lede">The goal is not maximum revenue; it&rsquo;s long-term sustainability. Servers, storage, bandwidth, and development all cost money — so the plan is to keep the network transparent, fund operations responsibly, and formalize it as a real not-for-profit / public-benefit project once it can stand on its own.</p>
++ <!-- Where the money goes — part of the same About section -->
++ <div class="cta-section-label cta-diff-label"><i class="fa-solid fa-seedling"></i> Where the money goes</div>
++ <p class="cta-lede">Open source, run in the open, and built to stay that way. Whatever the network earns goes back into it &mdash; keeping the servers on, paying for the storage and bandwidth that free streaming actually costs, funding new features, and putting money into the creators and events that make this place worth showing up to.</p>
<div class="home-cta-features">
<div class="home-cta-feature">
<i class="fa-solid fa-plug-circle-bolt"></i>
-- <strong>Phase 1 — Make it sustainable</strong>
-- <span>Use optional support, memberships, cosmetics, or tasteful monetization to cover hosting, bandwidth, storage, and development.</span>
++ <strong>Keep it running</strong>
++ <span>Hosting, bandwidth, storage and development come first. Free streaming is not free to host, and being honest about that is how it stays free to use.</span>
</div>
<div class="home-cta-feature">
<i class="fa-solid fa-heart"></i>
-- <strong>Phase 2 — Protect the mission</strong>
-- <span>No investors, no surprise corporate pivot, and no turning the community into a product. Any money goes to operations first.</span>
++ <strong>Back the creators</strong>
++ <span>What is left over goes to the people who fill the place: creator payouts, community events, prize pools and the tools streamers ask for.</span>
</div>
<div class="home-cta-feature">
-- <i class="fa-solid fa-landmark"></i>
-- <strong>Phase 3 — Formalize it</strong>
-- <span>Once the network is stable enough, file the not-for-profit paperwork and build this into the real community project it was always meant to be.</span>
++ <i class="fa-solid fa-code-branch"></i>
++ <strong>Stay open, stay decent</strong>
++ <span>Every line is open source and the analytics are public. No selling the community out, no surprise pivot, no dark patterns &mdash; the code is right there if you want to check.</span>
</div>
</div>
</div><!-- /home-about-body -->
@@@ -5130,7 +5130,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/live-notify.js?v=3" defer></script>
<script src="/js/ov-skeleton.js?v=1" defer></script>
<script src="/js/ov-odometer.js?v=3" defer></script>
--<script src="/js/app.js?v=161" defer></script>
++<script src="/js/app.js?v=162" defer></script>
<script src="/js/stream-player.js?v=22" defer></script>
<script src="/js/stream-pip.js?v=1" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
ddiff --git a/public/js/app.js b/public/js/app.js
iindex 71894f0a..3dafab26 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -1369,8 +1369,8 @@ function renderHeroStats(stats) {
// ── Right now ────────────────────────────────────────────────
const now = [
-- { key: 'liveNow', cls: stats.liveNow > 0 ? 'hero-stat--live' : '', icon: stats.liveNow > 0 ? 'fa-circle' : 'fa-circle-dot', num: stats.liveNow, label: 'Live', title: stats.liveNow > 0 ? 'Streams live right now' : 'Nobody is live right now — check Recently Online below' },
-- { key: 'viewersNow', cls: stats.viewersNow > 0 ? 'hero-stat--live' : '', icon: 'fa-eye', num: stats.viewersNow, label: 'Watching', title: 'Viewers watching right now' },
++ { key: 'liveNow', deltaHTML: baselineDelta(stats.liveNow, CC.liveAvg24h, CC.livePeak24h), cls: stats.liveNow > 0 ? 'hero-stat--live' : '', icon: stats.liveNow > 0 ? 'fa-circle' : 'fa-circle-dot', num: stats.liveNow, label: 'Live', title: stats.liveNow > 0 ? 'Streams live right now' : 'Nobody is live right now — check Recently Online below' },
++ { key: 'viewersNow', deltaHTML: baselineDelta(stats.viewersNow, CC.viewersAvg24h, CC.viewersPeak24h), cls: stats.viewersNow > 0 ? 'hero-stat--live' : '', icon: 'fa-eye', num: stats.viewersNow, label: 'Watching', title: 'Viewers watching right now' },
{ key: 'weeklyActive', icon: 'fa-fire', num: stats.weeklyActive, label: 'Active', title: 'People who chatted in the last 7 days', desc: 'Distinct chatters in the last 7 days — signed-in users, anonymous chatters and relayed (Twitch/Kick/YouTube) chatters, each counted once.', metric: 'active' },
{ key: 'weeklyVisitors', icon: 'fa-user-plus', num: stats.weeklyVisitors, label: 'Visitors', title: 'First-time visitors in the last 7 days', desc: 'Browsers seen on the site for the first time in the last 7 days (a privacy-safe fingerprint, no account needed). A proxy for new people showing up, not just chatting.', metric: 'visitors' },
];
@@@ -1392,12 +1392,12 @@ function renderHeroStats(stats) {
// ── Community ────────────────────────────────────────────────
groups.push({
kicker: 'Community', icon: 'fa-people-group', rows: [
-- { key: 'streamers', icon: 'fa-satellite-dish', num: stats.streamers, label: 'Streamers', metric: 'streamers', title: 'People who have gone live' },
++ { key: 'streamers', recent: R.streamers, icon: 'fa-satellite-dish', num: stats.streamers, label: 'Streamers', metric: 'streamers', title: 'People who have gone live' },
{ key: 'users', icon: 'fa-users', num: stats.users, label: 'Users', metric: 'users', title: 'Registered users', recent: R.users },
{ key: 'anons', icon: 'fa-user-secret', num: stats.anons, label: 'Anons', metric: 'anons', title: 'Anonymous chatters ever seen', recent: R.anons },
{ icon: 'fa-heart', num: stats.follows, label: 'Follows', metric: 'follows', title: 'Channel follows', recent: R.follows },
{ key: 'chatMessages', icon: 'fa-comments', num: stats.chatMessages, label: 'Messages', metric: 'messages', title: 'Chat messages sent', recent: R.messages },
-- { key: 'hoursWatched', icon: 'fa-couch', num: stats.hoursWatched, label: 'Hrs Watched', metric: 'hoursWatched', title: 'Hours the community has spent watching streams', unit: 'h' },
++ { key: 'hoursWatched', recent: R.hours, unit: 'h', icon: 'fa-couch', num: stats.hoursWatched, label: 'Hrs Watched', metric: 'hoursWatched', title: 'Hours the community has spent watching streams', unit: 'h' },
],
});
@@@ -1405,20 +1405,20 @@ function renderHeroStats(stats) {
groups.push({
kicker: 'Economy', icon: 'fa-coins', rows: [
{ icon: 'fa-hand-holding-dollar', num: stats.vibesTipped, label: 'Vibes Tipped', metric: 'vibes', title: 'Vibes donated between people (100 Vibes = $1)', recent: R.vibes },
-- { icon: 'fa-hand-holding-heart', num: stats.supporters, label: 'Supporters', metric: 'supporters', title: 'People who have tipped Vibes to a streamer' },
++ { recent: R.supporters, icon: 'fa-hand-holding-heart', num: stats.supporters, label: 'Supporters', metric: 'supporters', title: 'People who have tipped Vibes to a streamer' },
{ icon: 'fa-cart-shopping', num: stats.vibesBought, label: 'Vibes Bought', metric: 'vibesBought', title: 'Vibes purchased with real money (PowerChat, card, PayPal, crypto)', recent: R.vibesBought },
{ icon: 'fa-star', num: stats.activeSubs, label: 'Subs', metric: 'subs', title: 'Active channel subscriptions', recent: R.subs },
// OpenCoins are network-wide; channel points below are per channel. Shown only when
// the wallet service answered — four zeroed chips would read as "nobody has any".
...(stats.coinsEarned != null ? [
-- { icon: 'fa-circle-dollar-to-slot', num: stats.coinsEarned, label: 'Coins Earned', title: 'OpenCoins earned across the whole network — the site-wide currency you get for watching, chatting and using the tools' },
-- { icon: 'fa-basket-shopping', num: stats.coinsSpent, label: 'Coins Spent', title: 'OpenCoins spent on emotes, themes, cosmetics and sounds' },
++ { recent: R.coinsEarned, icon: 'fa-circle-dollar-to-slot', num: stats.coinsEarned, label: 'Coins Earned', title: 'OpenCoins earned across the whole network — the site-wide currency you get for watching, chatting and using the tools' },
++ { recent: R.coinsSpent, icon: 'fa-basket-shopping', num: stats.coinsSpent, label: 'Coins Spent', title: 'OpenCoins spent on emotes, themes, cosmetics and sounds' },
{ icon: 'fa-vault', num: stats.coinsCirculating, label: 'Coins Held', title: 'OpenCoins sitting in wallets right now, across every OpenVibe site' },
-- { icon: 'fa-wallet', num: stats.coinHolders, label: 'Wallets', title: 'People holding OpenCoins' },
++ { recent: R.coinHolders, icon: 'fa-wallet', num: stats.coinHolders, label: 'Wallets', title: 'People holding OpenCoins' },
] : []),
{ icon: 'fa-coins', num: stats.pointsEarned, label: 'Points Earned', metric: 'points', title: 'Channel points earned by viewers (watching, chatting, following) — per channel, unlike OpenCoins', recent: R.points },
{ icon: 'fa-gift', num: stats.pointsSpent, label: 'Points Spent', metric: 'pointsSpent', title: `Channel points spent on rewards · ${_fmtCount(stats.redemptions || 0)} rewards redeemed`, recent: R.pointsSpent, sub: stats.redemptions ? `${_fmtCount(stats.redemptions)} rewards` : '' },
-- { icon: 'fa-bullseye', num: stats.goalsActive, label: 'Goals', title: `Donation goals running now · ${stats.goalsReached || 0} reached so far`, sub: stats.goalsReached ? `${_fmtCount(stats.goalsReached)} reached` : '' },
++ { recent: R.goals, icon: 'fa-bullseye', num: stats.goalsActive, label: 'Goals', title: `Donation goals running now · ${stats.goalsReached || 0} reached so far`, sub: stats.goalsReached ? `${_fmtCount(stats.goalsReached)} reached` : '' },
],
});
@@@ -1430,7 +1430,7 @@ function renderHeroStats(stats) {
{ icon: 'fa-scissors', num: stats.clips, label: 'Clips', metric: 'clips', title: 'Clips created', recent: R.clips },
{ icon: 'fa-clock', num: stats.streamHours, label: 'Hours', metric: 'hours', title: 'Hours of video archived', recent: R.hours, unit: 'h' },
{ icon: 'fa-brain', num: stats.aiMemories, label: 'AI Moments', metric: 'aiMoments', title: 'Moments the AI remembers across every stream', recent: R.aiMoments },
-- { icon: 'fa-face-grin-squint', num: stats.emotes, label: 'Emotes', metric: 'emotes', title: 'Custom channel emotes uploaded' },
++ { recent: R.emotes, icon: 'fa-face-grin-squint', num: stats.emotes, label: 'Emotes', metric: 'emotes', title: 'Custom channel emotes uploaded' },
{ icon: 'fa-paste', num: stats.pastes, label: 'Pastes', title: `${stats.pasteText || 0} text · ${stats.pasteImages || 0} image pastes`, sub: (stats.pasteText != null && stats.pasteImages != null) ? `${_fmtCount(stats.pasteText)} txt · ${_fmtCount(stats.pasteImages)} img` : '' },
],
});
ddiff --git a/server/db/database.js b/server/db/database.js
iindex c95e6ef1..53c182c6 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -4186,6 +4186,13 @@ function _computeHomeStats() {
messages: winCount('chat_messages', 'timestamp', 'COALESCE(is_deleted, 0) = 0'),
hours: { d: hoursSince('-1 day'), w: hoursSince('-7 days'), m: hoursSince('-30 days'), pw: hoursPrevWeek() },
streamers: streamersWin(),
++ emotes: winCount('emotes', 'created_at'),
++ goals: winCount('donation_goals', 'created_at'),
++ // Distinct people who tipped in each window — a headcount, like streamers.
++ supporters: (() => {
++ const q = (a, b) => c(`SELECT COUNT(DISTINCT from_user_id) AS count FROM transactions WHERE type = 'donation' AND from_user_id IS NOT NULL AND created_at >= datetime('now', ?)${b ? " AND created_at < datetime('now', ?)" : ''}`, b ? [a, b] : [a]);
++ return { d: q('-1 day'), w: q('-7 days'), m: q('-30 days'), pw: q('-14 days', '-7 days') };
++ })(),
vibes: winSum('transactions', 'amount', 'created_at', `type = 'donation' AND created_at >= '${vibesSince}'`),
points: winSum('coin_transactions', 'amount', 'created_at', 'amount > 0'),
pointsSpent: winSum('coin_transactions', '-amount', 'created_at', 'amount < 0'),
ddiff --git a/server/home/routes.js b/server/home/routes.js
iindex 71523848..2d49bbd2 100644
---- a/server/home/routes.js
++++ b/server/home/routes.js
@@@ -200,6 +200,12 @@ async function heroStats() {
stats.coinsSpent = c.spent;
stats.coinsCirculating = c.circulating;
stats.coinHolders = c.holders;
++ if (c.recent) {
++ stats.recent = stats.recent || {};
++ stats.recent.coinsEarned = c.recent.earned;
++ stats.recent.coinsSpent = c.recent.spent;
++ stats.recent.coinHolders = c.recent.holders;
++ }
}
} catch { /* the board is better off missing four chips than failing */ }
try { stats.concurrency = db.getConcurrencyBaseline(); } catch { /* */ }