openvibers/openvibe.live Commits

The home's money four say 'None yet' as one chip, not four zeros (#566)

OP OpenVibers committed 11 days ago · bb3a110911 · 6 files, +82 −5

* The home's money four say 'None yet' as one chip, not four zeros (open item 135)

Vibes tipped, Supporters, Vibes bought and Subs count from the Vibes relaunch (stats_vibes_reset_at), and all four
have read 0 since 10-17, which reads as a dead site beside 734k coins earned. While all four are zero they are one
chip, 'None yet · Tips and subs · since 17 Oct', whose tip names each of the four; the first tip brings the four
back. Billing not answering (null) is not 'none yet', so the chips stay as they were then.

- getHomeStats returns economySince (the reset day, or null)
- app-home.js: the fresh chip; its word has its own class so the count-up leaves it alone
- test/home-economy-fresh.test.js renders the real board in a stand-in page

* Budget: home JS 1278 KB (the 'None yet' chip, 1277.1 measured)

Changes

ddiff --git a/public/css/features/home.css b/public/css/features/home.css
iindex fd8bc2cf..03a008d8 100644
---- a/public/css/features/home.css
++++ b/public/css/features/home.css
@@@ -688,6 +688,7 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
}
.hero-stat > i { grid-row: 1 / 2; align-self: center; justify-self: center; }
.hero-stat-num { white-space: nowrap; line-height: 1.12; }
++.hero-stat-word { font-size: 1.05rem; font-weight: 800; color: var(--text-primary); white-space: nowrap; line-height: 1.12; letter-spacing: -0.01em; }
.hero-stat-label { white-space: normal; overflow-wrap: anywhere; line-height: 1.15; }
.hero-stat-delta { grid-column: 1 / -1; justify-self: start; margin-top: 1px; }
.hero-stat-strip { grid-auto-rows: 1fr; align-items: stretch; }
ddiff --git a/public/js/app-home.js b/public/js/app-home.js
iindex c8b5400f..e030c5ca 100644
---- a/public/js/app-home.js
++++ b/public/js/app-home.js
@@@ -342,12 +342,25 @@ function _renderHeroStats(stats) {
});
// ── Economy ──────────────────────────────────────────────────
++ // The money four (tips, supporters, Vibes bought, subs) count from the Vibes relaunch. While all four are still
++ // zero they are one chip that says so ("none yet since 17 Oct"), not four bare zeros that read as a dead site; the
++ // first tip brings the four back. Every one of them is still said: nothing is hidden.
++ const money = [
++ { icon: 'fa-hand-holding-dollar', num: stats.vibesTipped, label: 'Vibes tipped', metric: 'vibes', title: 'Vibes tipped to streamers (100 Vibes = $1), on Live, PowerChat and OpenVibe.Tips', recent: R.vibes },
++ { recent: R.supporters, icon: 'fa-hand-holding-heart', num: stats.supporters, label: 'Supporters', metric: 'supporters', title: 'People who have tipped a streamer' },
++ { icon: 'fa-cart-shopping', num: stats.vibesBought, label: 'Vibes bought', metric: 'vibesBought', title: 'Vibes bought 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 },
++ ];
++ const moneyFresh = money.every(r => r.num === 0);
++ const since = stats.economySince && !isNaN(Date.parse(stats.economySince))
++ ? new Date(stats.economySince + 'T12:00:00Z').toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) : '';
++ const freshTip = { label: 'Tips and subs', title: `No tips, supporters, Vibes bought or subscriptions yet${since ? ` since the Vibes relaunch on ${since}` : ''}. Tip a streamer from their channel: the first one brings these four numbers back.`, desc: '', recent: null, metric: null };
++ const fresh = {
++ html: `<div class="hero-stat hero-stat--fresh" data-tip="${esc(JSON.stringify(freshTip))}"><i class="fa-solid fa-hand-holding-heart"></i><div class="hero-stat-meta"><span class="hero-stat-word">None yet</span><span class="hero-stat-label">Tips and subs${since ? ` · since ${esc(since)}` : ''}</span></div></div>`,
++ };
groups.push({
kicker: 'Economy', icon: 'fa-coins', rows: [
-- { icon: 'fa-hand-holding-dollar', num: stats.vibesTipped, label: 'Vibes tipped', metric: 'vibes', title: 'Vibes tipped to streamers (100 Vibes = $1), on Live, PowerChat and OpenVibe.Tips', recent: R.vibes },
-- { recent: R.supporters, icon: 'fa-hand-holding-heart', num: stats.supporters, label: 'Supporters', metric: 'supporters', title: 'People who have tipped a streamer' },
-- { icon: 'fa-cart-shopping', num: stats.vibesBought, label: 'Vibes bought', metric: 'vibesBought', title: 'Vibes bought 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 },
++ ...(moneyFresh ? [fresh] : money),
// OpenCoins are network-wide; channel points below are per channel. Shown only when the wallet service
// answered: zeroed chips would read as "nobody has any". (Coins held was coins earned less 50 spent: gone.)
...(stats.coinsEarned != null ? [
ddiff --git a/scripts/perf/check-budgets.js b/scripts/perf/check-budgets.js
iindex 81ded14c..929c0c5b 100644
---- a/scripts/perf/check-budgets.js
++++ b/scripts/perf/check-budgets.js
@@@ -22,7 +22,7 @@ const BUDGETS = {
htmlRawKB: 128, // 119.3 with the icons drawn on the server (2026-11-01, plan L54 K4: the OpenVibe glyphs in the HTML, 20.8 KB brotli); measured 94.9 before
htmlBrotliKB: 30,
jsFiles: 26, // measured 23 (incl. route scripts for "/")
-- jsRawKB: 1276, // 1275.2 (2026-11-02: route placeholders in ov-skeleton.js, L52 step 6: a loading route shows its shape, not an empty box); 1268.4 (2026-11-02: openvibe-shared 4.103.0, the puzzle glyph, two-line sidebar rows, boost's fix and eight solids; the 404 offers the channel for a bare /name); 1266.3 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 1262.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 1259.8 (2026-11-02: content labels on cards and the player, L1); 1257.5 (2026-11-01: the Catch up rails load by themselves as you swipe, no Load more on a phone); 1254.2 (2026-11-01: openvibe-shared 4.87.0), about 1255 with #534 (feed and card avatars at their size); 1253.8 (2026-11-01: avatars ask Media for their drawn width, ovImgW); 1252.3 (2026-11-01: Following in the topics row, the picked topic remembered); 1250.2 (2026-11-01: topics live now, readable chat names on light themes); 1247.7 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 1244.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 1243.5 with openvibe-shared 4.64.0: the look travels between sites, the Look panel in the menu (2026-10-31); 1242.7 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face, and the ring on page moves (2026-10-31); 1238.8 with the home's featured stream once and the intro closed on phones (2026-10-31); 1237.4 with the home's two more tab blocks on phones (2026-10-30); 1233.9 with the home's Catch up tabs on phones (2026-10-30); 1230.8 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 1227.8 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 1223.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 1215.4 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); measured 1203.9 (2026-10-28: openvibe-shared 4.30.0, the bell center: tabs with counts, the Unread filter, thumbnails, swipe, the phone sheet; 4.31.0's ✦ adds about 3 KB); 1187.2 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0: opaque themed toasts held while a menu is open, grouping, priority, System cards and the rich bell panel); 1158.8 (2026-10-28: openvibe-shared 4.25.0, the menu's orb, messages, the phone sheet and auto-hide in the navbar; the messenger opens from the bar)
++ jsRawKB: 1278, // 1277.1 (2026-11-02: the home's money four as one 'None yet' chip while all four are zero, open item 135); 1275.2 (2026-11-02: route placeholders in ov-skeleton.js, L52 step 6: a loading route shows its shape, not an empty box); 1268.4 (2026-11-02: openvibe-shared 4.103.0, the puzzle glyph, two-line sidebar rows, boost's fix and eight solids; the 404 offers the channel for a bare /name); 1266.3 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 1262.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 1259.8 (2026-11-02: content labels on cards and the player, L1); 1257.5 (2026-11-01: the Catch up rails load by themselves as you swipe, no Load more on a phone); 1254.2 (2026-11-01: openvibe-shared 4.87.0), about 1255 with #534 (feed and card avatars at their size); 1253.8 (2026-11-01: avatars ask Media for their drawn width, ovImgW); 1252.3 (2026-11-01: Following in the topics row, the picked topic remembered); 1250.2 (2026-11-01: topics live now, readable chat names on light themes); 1247.7 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 1244.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 1243.5 with openvibe-shared 4.64.0: the look travels between sites, the Look panel in the menu (2026-10-31); 1242.7 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face, and the ring on page moves (2026-10-31); 1238.8 with the home's featured stream once and the intro closed on phones (2026-10-31); 1237.4 with the home's two more tab blocks on phones (2026-10-30); 1233.9 with the home's Catch up tabs on phones (2026-10-30); 1230.8 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 1227.8 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 1223.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 1215.4 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); measured 1203.9 (2026-10-28: openvibe-shared 4.30.0, the bell center: tabs with counts, the Unread filter, thumbnails, swipe, the phone sheet; 4.31.0's ✦ adds about 3 KB); 1187.2 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0: opaque themed toasts held while a menu is open, grouping, priority, System cards and the rich bell panel); 1158.8 (2026-10-28: openvibe-shared 4.25.0, the menu's orb, messages, the phone sheet and auto-hide in the navbar; the messenger opens from the bar)
jsBrotliKB: 299, // 298.4 (2026-11-02: route placeholders, L52 step 6); 296.7 (2026-11-02: openvibe-shared 4.103.0 and the 404's channel offer); 296.2 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 295.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 294.6 (2026-11-02: content labels on cards and the player, L1); 294.1 (2026-11-01: the Catch up rails load by themselves as you swipe); 292.6 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 291.1 with openvibe-shared 4.57.1 (the Dock's async token and scope-aware controls, 4.57.0; the tool frame's facts, 4.56.0) (2026-10-31); 290.6 with the home's two more tab blocks on phones (2026-10-30); 289.8 with the home's Catch up tabs on phones (2026-10-30); 288.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 287.4 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); measured 283.2 (2026-10-28: openvibe-shared 4.30.0, the bell center as above); 279.6 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0 as above); 273.1 (2026-10-28: openvibe-shared 4.25.0 as above); 270.5 (2026-10-28: openvibe-shared 4.22.0, the navbar's search magnifier with / and Ctrl+K and the More menu; search.js and loading.js load only when used); 269.6 (2026-10-28: openvibe-shared 4.19.0, the account menu's gear and people-group icons inline; 268 on 2026-10-27: openvibe-shared 4.16.0, the folded balance chip, Across OpenVibe with site marks and the icon preload in the navbar); 265.3 (2026-10-27: openvibe-shared 4.15.0, the compact footer opens into the whole one, and the pulse line); 263.3 on 10-23 (openvibe-shared 4.9.0, status text twins in every theme); 262.5 on 10-17 (openvibe-shared 4.4.0, every site in the launcher)
cssRawKB: 270, // measured ~243 since the first-paint subset (style.core.css) replaced style.css (~717 before)
cssBrotliKB: 55, // measured ~47.5 (was ~130)
ddiff --git a/server/db/database.js b/server/db/database.js
iindex 849f3751..14b75d0b 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -1709,6 +1709,9 @@ async function _computeHomeStats() {
// Distinct people who have tipped someone.
supporters: await c(`SELECT COUNT(DISTINCT ${TIP_SUPPORTER}) AS count FROM channel_tips WHERE created_at >= ?`, [vibesSince]),
vibesBought: null,
++ // The day the Vibes numbers count from (the 10-17 relaunch reset, site setting stats_vibes_reset_at), so the
++ // home can say "none yet since 17 Oct" instead of four bare zeros. Null when there was no reset.
++ economySince: vibesSince.startsWith('1970') ? null : vibesSince.slice(0, 10),
// Donation goals: currently running + ever reached.
goalsActive: await c(`SELECT COUNT(*) AS count FROM donation_goals WHERE is_active = 1`),
goalsReached: await c(`SELECT COUNT(*) AS count FROM donation_goals WHERE reached_at IS NOT NULL OR current_amount >= target_amount`),
ddiff --git a/test/home-economy-fresh.test.js b/test/home-economy-fresh.test.js
nnew file mode 100644
iindex 00000000..93ab6a45
---- /dev/null
++++ b/test/home-economy-fresh.test.js
@@@ -0,0 +1,58 @@
++/**
++ * The home's Economy row (public/js/app-home.js): the money four (Vibes tipped, Supporters, Vibes bought, Subs) count
++ * from the Vibes relaunch. While all four are zero they are one chip, "None yet · Tips and subs · since 17 Oct", not
++ * four bare zeros that read as a dead site; any one of them above zero brings all four back. Nothing is hidden: the
++ * chip's tip names each of the four.
++ *
++ * node test/home-economy-fresh.test.js
++ */
++'use strict';
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const vm = require('vm');
++
++const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app-home.js'), 'utf8');
++
++/** Render the stat board for `stats` with a stand-in page; returns the board's markup. */
++function board(stats) {
++ const el = () => ({
++ innerHTML: '', hidden: true, style: {}, dataset: {}, textContent: '', offsetHeight: 0,
++ classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } },
++ querySelector: () => el(), querySelectorAll: () => [], addEventListener() {}, setAttribute() {}, getAttribute() { return null; },
++ });
++ const wrap = el();
++ const document = { getElementById: (id) => (id === 'hero-stats' ? wrap : null), querySelector: () => null, querySelectorAll: () => [], addEventListener() {}, documentElement: el(), body: el(), readyState: 'complete' };
++ const window = { matchMedia: () => ({ matches: true, addEventListener() {} }), addEventListener() {}, location: { pathname: '/' } };
++ const ctx = vm.createContext({ document, window, console, requestAnimationFrame: () => {}, setTimeout: () => 0, clearTimeout() {}, setInterval: () => 0, clearInterval() {}, localStorage: { getItem: () => null, setItem() {} }, navigator: {},
++ // app.js's globals the board uses.
++ esc: (v) => String(v == null ? '' : v).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
++ _fmtCount: (n) => String(Number(n) || 0) });
++ vm.runInContext(src, ctx, { filename: 'app-home.js' });
++ vm.runInContext('_renderHeroStats', ctx)(stats);
++ return wrap.innerHTML;
++}
++
++const FOUR = ['Vibes tipped', 'Supporters', 'Vibes bought', 'Subs'];
++const base = { recent: {}, concurrency: {}, liveNow: 0, viewersNow: 0, coinsEarned: null, pointsEarned: 5, pointsSpent: 1, goalsActive: 0, economySince: '2026-10-17' };
++
++const zero = board({ ...base, vibesTipped: 0, supporters: 0, vibesBought: 0, activeSubs: 0 });
++assert.ok(zero.includes('hero-stat--fresh'), 'all four at zero: one chip');
++assert.ok(zero.includes('<span class="hero-stat-word">None yet</span>'));
++assert.ok(zero.includes('Tips and subs · since 17 Oct'), 'it says since when');
++FOUR.forEach((word) => assert.ok(!zero.includes(`<span class="hero-stat-label">${word}`), `${word}: not a bare zero of its own`));
++assert.match(zero, /No tips, supporters, Vibes bought or subscriptions yet since the Vibes relaunch on 17 Oct/, 'the tip names all four');
++
++const one = board({ ...base, vibesTipped: 500, supporters: 1, vibesBought: 0, activeSubs: 0 });
++assert.ok(!one.includes('hero-stat--fresh'), 'a first tip brings the four back');
++FOUR.forEach((word) => assert.ok(one.includes(`<span class="hero-stat-label">${word}`), word));
++
++// Billing not answering (null) is not "none yet": the chips stay as they were.
++const unknown = board({ ...base, vibesTipped: 0, supporters: 0, vibesBought: null, activeSubs: null });
++assert.ok(!unknown.includes('hero-stat--fresh'));
++
++// No reset date: the chip leaves the date out.
++const noDate = board({ ...base, economySince: null, vibesTipped: 0, supporters: 0, vibesBought: 0, activeSubs: 0 });
++assert.ok(noDate.includes('<span class="hero-stat-label">Tips and subs</span>'));
++
++console.log('home economy fresh: all checks passed');
ddiff --git a/test/home-stats-series.test.js b/test/home-stats-series.test.js
iindex 1d667d6b..764ef71c 100644
---- a/test/home-stats-series.test.js
++++ b/test/home-stats-series.test.js
@@@ -65,6 +65,7 @@ await db.run("INSERT INTO channel_tips (from_user_id, to_user_id, amount, source
let stats = await db.getHomeStats();
assert.strictEqual(stats.vibesTipped, 750, 'no reset → everything counts');
++assert.strictEqual(stats.economySince, null, 'no reset → no "since" date for the home to show');
assert.strictEqual(stats.supporters, 2);
assert.strictEqual((await db.getHomeStatSeries('vibes', 30)).total, 750);
@@@ -75,6 +76,7 @@ if (!stats) { // fall back: the cache TTL keeps the old value; verify via the se
assert.ok(await db.vibesStatsSince() > '2000-01-01', 'cutoff parsed');
} else {
assert.strictEqual(stats.vibesTipped, 50, 'only vibes after the reset count');
++ assert.match(stats.economySince, /^\d{4}-\d{2}-\d{2}$/, 'the day the Vibes count from, for "none yet since …"');
assert.strictEqual(stats.supporters, 1);
assert.strictEqual(stats.recent.vibes.d, 50);
assert.strictEqual(stats.recent.vibes.m, 50, 'the 30-day window is also cut at the reset');