openvibers/openvibe.live Commits

What shipped on Live comes from the network's shared system (openvibe-shared 1.11.1 shipped.js, fed by the network changelog of deployed commits): the hero one-liner, the home Recent Changes (same day groups, same expand button) and /updates (this site or all of OpenVibe, Load more, Patch notes); the footer links this site's log. No 'free'/'$0' copy: the footer tagline and a fallback slogan said 'Free', the slogan prompt asked for it, and generated or stored slogans with it are now dropped. test/shipped-live.test.js

OP OpenVibers committed 24 Sept 2026 · a49efd5efd · 9 files, +106 −93


Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex c1d7f418..84dbc1a7 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -27,7 +27,7 @@
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.35.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.7.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.6.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.11.1",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sharp": "^0.33.0",
@@@ -2839,9 +2839,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "1.6.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.6.0",
-- "integrity": "sha512-kd9DdLQg2iK6m/VzwBBhKBNsg9riMzdI4jJFvfgIgW3vTsIz1SflAtZdMjRGbDPx3Hdvba2oOT2085n+npmzNQ==",
++ "version": "1.11.1",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.11.1",
++ "integrity": "sha512-hBaZlZRVZjkGfsPQE16rQIou2lUuusADea3Evx1tVjB84FTHGHS7ZALfcXaNOk7dR53HOVTJrQ0njmScdreUiw==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex 11042a2e..30860f60 100644
---- a/package.json
++++ b/package.json
@@@ -45,7 +45,7 @@
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.35.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.7.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.6.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.11.1",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sharp": "^0.33.0",
ddiff --git a/public/index.html b/public/index.html
iindex 63c6b9e3..5a7b9c3e 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -221,7 +221,7 @@
</div>
<!-- Newest shipped commit — built in the open, receipts included. Sits under the
philosophy row so the proof is next to the promise. -->
-- <a class="hero-latest" id="hero-latest" href="/updates" onclick="return handleLinkClick(event, '/updates')" style="display:none"></a>
++ <a class="hero-latest" id="hero-latest" data-ov-shipped="latest" data-service="live" href="/updates" onclick="return handleLinkClick(event, '/updates')" hidden></a>
<div class="hero-stats" id="hero-stats" aria-label="Site stats"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div></div></div>
</div>
</div>
@@@ -725,7 +725,7 @@
<div class="content-container">
<div class="page-header">
<h1><i class="fa-solid fa-code-commit"></i> Updates</h1>
-- <p class="page-subtitle">Recent changes and patch notes from the <a href="https://github.com/OpenVibers/OpenVibe.Live" target="_blank">GitHub repository</a>.</p>
++ <p class="page-subtitle">Every change deployed to OpenVibe.Live, newest first: each line is a commit from the <a href="https://github.com/OpenVibers/OpenVibe.Live" target="_blank" rel="noopener">OpenVibers repositories</a>, and batches become <a href="https://openvibe.blog/@openvibe" rel="noopener">Patch notes on openvibe.blog</a>. See <a href="https://openvibe.network/updates">the whole network</a>.</p>
</div>
<div id="updates-list" class="updates-list">
<div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>
ddiff --git a/public/js/app-home.js b/public/js/app-home.js
iindex 37ec23af..ffd60ce3 100644
---- a/public/js/app-home.js
++++ b/public/js/app-home.js
@@@ -1249,12 +1249,8 @@ async function loadHomePulse() {
function _renderHomePulse(p) {
if (!p) return;
-- // Hero one-liner: the newest shipped commit.
-- const latest = document.getElementById('hero-latest');
-- if (latest && p.latestUpdate && p.latestUpdate.subject) {
-- latest.innerHTML = `<i class="fa-solid fa-rocket"></i> shipped ${esc(timeAgo(p.latestUpdate.date))}: <b>${esc(p.latestUpdate.subject)}</b>`;
-- latest.style.display = '';
-- }
++ // The hero one-liner ("shipped 5m ago: …") is the network's shared view (openvibe-shared
++ // shipped.js, data-ov-shipped="latest" on #hero-latest), mounted by loadShipped().
const grid = document.getElementById('pulse-grid');
const section = document.getElementById('home-pulse-section');
@@@ -2013,48 +2009,23 @@ function renderHomePagination(containerId, rail) {
</button>`;
}
--async function loadHomeChangelog(attempt = 0) {
++/** Home "Recent Changes": Live's deployed changes grouped by day, drawn by the shared view. */
++async function loadHomeChangelog() {
const container = document.getElementById('home-changelog');
if (!container) return;
--
++ const shipped = await loadShipped();
++ if (!shipped) { container.innerHTML = '<p style="opacity:0.5;text-align:center;padding:16px 0;">Recent changes are not available right now.</p>'; return; }
++ shipped.scan(document); // the hero one-liner
try {
-- const data = await api('/updates?limit=15');
-- if (!data.commits || data.commits.length === 0) {
-- container.innerHTML = '<p style="opacity:0.5;text-align:center;padding:16px 0;">No recent changes.</p>';
-- return;
-- }
--
-- // Group commits by date (same pattern as updates page)
-- const groups = {};
-- for (const c of data.commits) {
-- const day = new Date(c.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
-- if (!groups[day]) groups[day] = [];
-- groups[day].push(c);
-- }
--
-- let html = '';
-- for (const [day, commits] of Object.entries(groups)) {
-- html += `<div class="updates-day">
-- <h3 class="updates-day-header">${esc(day)}</h3>
-- <div class="updates-day-commits">`;
-- for (const c of commits) {
-- const time = new Date(c.date).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
-- html += `<div class="update-entry">
-- <a class="update-hash" href="https://github.com/OpenVibers/OpenVibe.Live/commit/${c.hash}" target="_blank" title="View on GitHub">${esc(c.short)}</a>
-- <span class="update-subject">${esc(c.subject)}</span>
-- <span class="update-meta">${esc(c.author)} &middot; ${esc(time)}</span>
-- </div>`;
-- }
-- html += '</div></div>';
-- }
-- container.innerHTML = html;
++ const res = await fetch(`${shipped.API}?service=live&limit=15`, { headers: { Accept: 'application/json' }, credentials: 'omit' });
++ const data = res.ok ? await res.json() : null;
++ const entries = data && Array.isArray(data.entries) ? data.entries : [];
++ container.textContent = '';
++ if (!entries.length) { container.innerHTML = '<p style="opacity:0.5;text-align:center;padding:16px 0;">No recent changes.</p>'; return; }
++ container.classList.add('ov-shipped-log');
++ shipped.appendDays(container, entries, false);
} catch {
-- // Retry up to 2 times with increasing delay (handles Cloudflare challenge timing)
-- if (attempt < 2) {
-- setTimeout(() => loadHomeChangelog(attempt + 1), (attempt + 1) * 3000);
-- } else {
-- container.innerHTML = '<p style="opacity:0.5;text-align:center;padding:16px 0;">Failed to load changelog.</p>';
-- }
++ container.innerHTML = '<p style="opacity:0.5;text-align:center;padding:16px 0;">Recent changes are not available right now.</p>';
}
}
ddiff --git a/public/js/app.js b/public/js/app.js
iindex a528cda9..34bb7808 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -2498,46 +2498,44 @@ async function doRegenerateKey() {
let _userLoaded = false;
--/* ── Updates / Changelog Page ─────────────────────────────────── */
++/**
++ * The network's shared "shipped" views (openvibe-shared shipped.js, served from this site's own
++ * /shared/ pin): what OpenVibe.Live deployed, from the network changelog (openvibe.network
++ * /api/v1/changelog). Loaded once, on demand, with the same marker the shared footer uses, so the
++ * two never load it twice. Resolves the OpenVibeShipped global, or null when it cannot load.
++ */
++let _shippedLoading = null;
++function loadShipped() {
++ if (window.OpenVibeShipped) return Promise.resolve(window.OpenVibeShipped);
++ if (_shippedLoading) return _shippedLoading;
++ _shippedLoading = new Promise((resolve) => {
++ const existing = document.querySelector('script[data-ov-shipped-src]');
++ const done = () => resolve(window.OpenVibeShipped || null);
++ if (existing) { existing.addEventListener('load', done); existing.addEventListener('error', done); setTimeout(done, 8000); return; }
++ const sc = document.createElement('script');
++ sc.src = '/shared/shipped.js';
++ sc.defer = true;
++ sc.setAttribute('data-ov-shipped-src', '');
++ sc.onload = done;
++ sc.onerror = done;
++ document.head.appendChild(sc);
++ });
++ return _shippedLoading;
++}
++
++/* ── Updates page: the network's shared update log (openvibe-shared shipped.js) ───────── */
async function loadUpdatesPage() {
const container = document.getElementById('updates-list');
if (!container) return;
++ if (container.dataset.ovShippedMounted) return; // already showing; "Load more" keeps its place
container.innerHTML = '<div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>';
--
-- try {
-- const data = await api('/updates?limit=50');
-- if (!data.commits || data.commits.length === 0) {
-- container.innerHTML = '<p style="opacity:0.6;text-align:center;padding:32px 0;">No updates found.</p>';
-- return;
-- }
--
-- // Group commits by date
-- const groups = {};
-- for (const c of data.commits) {
-- const day = new Date(c.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
-- if (!groups[day]) groups[day] = [];
-- groups[day].push(c);
-- }
--
-- let html = '';
-- for (const [day, commits] of Object.entries(groups)) {
-- html += `<div class="updates-day">
-- <h3 class="updates-day-header">${esc(day)}</h3>
-- <div class="updates-day-commits">`;
-- for (const c of commits) {
-- const time = new Date(c.date).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
-- html += `<div class="update-entry">
-- <a class="update-hash" href="https://github.com/OpenVibers/OpenVibe.Live/commit/${c.hash}" target="_blank" title="View on GitHub">${esc(c.short)}</a>
-- <span class="update-subject">${esc(c.subject)}</span>
-- <span class="update-meta">${esc(c.author)} &middot; ${esc(time)}</span>
-- </div>`;
-- }
-- html += '</div></div>';
-- }
-- container.innerHTML = html;
-- } catch (err) {
-- container.innerHTML = `<p style="color:var(--error);text-align:center;padding:32px 0;">Failed to load updates.</p>`;
++ const shipped = await loadShipped();
++ if (!shipped) {
++ container.innerHTML = '<p style="opacity:0.6;text-align:center;padding:32px 0;">The update log could not be loaded. <a href="https://openvibe.network/updates?site=live">See it on openvibe.network</a>.</p>';
++ return;
}
++ container.dataset.ovShippedMounted = '1';
++ await shipped.log(container, { service: 'live', limit: 50, syncUrl: false });
}
document.addEventListener('DOMContentLoaded', async () => {
ddiff --git a/public/js/footer-live.js b/public/js/footer-live.js
iindex c4aa4eb0..8f0f6fdd 100644
---- a/public/js/footer-live.js
++++ b/public/js/footer-live.js
@@@ -56,7 +56,8 @@
OpenVibeFooter.init({
service: 'live',
brandName: 'OpenVibe.Live',
-- tagline: 'Free, open-source live streaming. Go live from a browser, OBS or a robot, restream everywhere, and keep your chat, emotes and clips in one place.',
++ tagline: 'Open-source live streaming. Go live from a browser, OBS or a robot, restream everywhere, and keep your chat, emotes and clips in one place.',
++ updates: '/updates', // the footer's "shipped X ago" line and Updates link open this site's log
legalBase: location.origin,
variant: variantForPage(),
links: LINKS,
ddiff --git a/server/ai/slogan-job.js b/server/ai/slogan-job.js
iindex ca8a3036..1258996f 100644
---- a/server/ai/slogan-job.js
++++ b/server/ai/slogan-job.js
@@@ -42,6 +42,7 @@ function _stripAudiencePrefix(s) {
.trim();
}
const _BAD_AUDIENCE = /\b(live\s*-?\s*)?stream(ing|s)?\b|\blivestream/i;
++const _NO_FREE = /\b(free|no[- ]cost)\b|\$\s?0\b/i;
function _cleanList(arr, maxLen, max) {
if (!Array.isArray(arr)) return [];
const seen = new Set(); const out = [];
@@@ -123,7 +124,7 @@ async function tick() {
} catch { /* */ }
const prompt =
--`You write the rotating hero copy for OpenVibe.Live — a scrappy, open-source, community-run live-streaming site at the heart of the OpenVibe network ("Free & Open Live Streaming" — good vibes, no suits). Voice: witty, warm, self-aware, anti-corporate, meme-literate, a little unhinged — but ALWAYS kind, never punching down.
++`You write the rotating hero copy for OpenVibe.Live — a scrappy, open-source, community-run live-streaming site at the heart of the OpenVibe network ("Open Live Streaming" — good vibes, no suits). Never call anything "free", "$0" or "no cost". Voice: witty, warm, self-aware, anti-corporate, meme-literate, a little unhinged — but ALWAYS kind, never punching down.
Everything below is REAL data about THIS community right now. Lean into it hard: reference the actual people, running jokes, recurring topics, and memes so the copy feels like an inside joke the community is in on. Reference usernames by name in good fun (no @), and NEVER mock or embarrass anyone.
@@@ -155,13 +156,15 @@ Return ONLY the JSON object.`;
const parsed = _parseJson(text);
if (!parsed) return;
let audiences = _cleanList((parsed.audiences || []).map(_stripAudiencePrefix).filter(a => a && !_BAD_AUDIENCE.test(a) && !/^for\b/i.test(a)), 60, TARGET);
-- let quips = _cleanList(parsed.quips || [], 110, TARGET);
++ // The owner's rule: no "free"/"$0" copy anywhere, whatever the model writes.
++ audiences = audiences.filter(a => !_NO_FREE.test(a));
++ let quips = _cleanList((parsed.quips || []).filter(q => !_NO_FREE.test(String(q))), 110, TARGET);
if (audiences.length < 6 && quips.length < 6) return; // bad batch — keep yesterday's
// Fresh daily batch; top up from the previous batch only if the model returned few.
const old = _loadPool();
audiences = _topUp(audiences, old.audiences.map(_stripAudiencePrefix).filter(a => a && !_BAD_AUDIENCE.test(a)), TARGET);
-- quips = _topUp(quips, old.quips, TARGET);
++ quips = _topUp(quips, old.quips.filter(q => !_NO_FREE.test(String(q))), TARGET);
db.setState('home_hero_slogans', JSON.stringify({ v: SLOGAN_FORMAT, audiences, quips, updated_at: Date.now() }));
console.log(`[Slogans] Fresh daily batch: ${audiences.length} words, ${quips.length} slogans (from full AI context)`);
} catch (e) {
ddiff --git a/server/home/routes.js b/server/home/routes.js
iindex 5af6d415..4b42dbac 100644
---- a/server/home/routes.js
++++ b/server/home/routes.js
@@@ -39,7 +39,7 @@ const FALLBACK_QUIPS = [
'Powered by caffeine and spite.',
'The internet hangout everyone forgot they wanted.',
'Clip it before it happens.',
-- 'Free to start, because gatekeeping is boring.',
++ 'Open to everyone, because gatekeeping is boring.',
'Where the wifi is questionable and the community is not.',
'Stream from a van. We support that lifestyle.',
'One account. All of OpenVibe.',
@@@ -134,14 +134,17 @@ function _cleanAudience(raw) {
.trim();
}
++const NO_FREE = /\b(free|no[- ]cost)\b|\$\s?0\b/i;
++
function heroSlogans() {
let s = null;
try {
s = db.getState('home_hero_slogans');
if (typeof s === 'string') s = JSON.parse(s);
} catch { s = null; }
-- const aiAud = (s && Array.isArray(s.audiences)) ? s.audiences.map(_cleanAudience).filter(a => a && a.length <= 60) : [];
-- const aiQuips = (s && Array.isArray(s.quips)) ? s.quips.filter(q => q && String(q).length <= 120) : [];
++ // No "free"/"$0" copy (the owner's rule), even in a batch stored before the slogan job filtered it.
++ const aiAud = (s && Array.isArray(s.audiences)) ? s.audiences.map(_cleanAudience).filter(a => a && a.length <= 60 && !NO_FREE.test(a)) : [];
++ const aiQuips = (s && Array.isArray(s.quips)) ? s.quips.filter(q => q && String(q).length <= 120 && !NO_FREE.test(String(q))) : [];
const hasAi = aiAud.length >= 4 || aiQuips.length >= 4;
const updatedAt = (s && Number(s.updated_at)) || 0;
// The slogan job regenerates every 12h; next_at is when the current batch is due to refresh.
ddiff --git a/test/shipped-live.test.js b/test/shipped-live.test.js
nnew file mode 100644
iindex 00000000..0e5915e2
---- /dev/null
++++ b/test/shipped-live.test.js
@@@ -0,0 +1,37 @@
++'use strict';
++/**
++ * Live shows what shipped through the network's shared system (openvibe-shared shipped.js, fed by
++ * the network changelog), not its own git log: the hero one-liner, the home "Recent Changes" and
++ * /updates. And the owner's copy rule: nothing Live writes calls itself "free" or "$0".
++ */
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const ROOT = path.join(__dirname, '..');
++const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
++
++// The shared views
++const index = read('public/index.html');
++assert.ok(/id="hero-latest" data-ov-shipped="latest" data-service="live" href="\/updates"/.test(index), 'the hero one-liner is the shared view');
++const app = read('public/js/app.js');
++assert.ok(app.includes("sc.src = '/shared/shipped.js'") && app.includes("sc.setAttribute('data-ov-shipped-src', '')"), 'shipped.js from this site\'s own /shared/ pin, with the footer\'s marker');
++assert.ok(/shipped\.log\(container, \{ service: 'live'/.test(app), '/updates is the shared log');
++const home = read('public/js/app-home.js');
++assert.ok(home.includes('shipped.appendDays(container, entries, false)') && !home.includes("api('/updates"), 'home Recent Changes uses the shared renderer, not the git log');
++assert.ok(!/latest\.innerHTML/.test(home), 'the old hero renderer is gone');
++assert.ok(read('public/js/footer-live.js').includes("updates: '/updates'"), 'the footer links this site\'s log');
++assert.ok(require('openvibe-shared/files').isBrowserFile('shipped.js'), 'Live serves /shared/shipped.js');
++
++// No "free"/"$0" copy
++const NO_FREE = /\b(free|no[- ]cost)\b|\$\s?0\b/i;
++const routes = read('server/home/routes.js');
++for (const list of ['FALLBACK_QUIPS', 'FALLBACK_AUDIENCES']) {
++ const m = new RegExp(`const ${list} = \\[([\\s\\S]*?)\\];`).exec(routes);
++ assert.ok(m, `${list} found`);
++ for (const line of m[1].split('\n')) assert.ok(!NO_FREE.test(line), `${list}: ${line.trim()}`);
++}
++assert.ok(!NO_FREE.test(/tagline: '([^']*)'/.exec(read('public/js/footer-live.js'))[1]), 'footer tagline');
++const slogan = read('server/ai/slogan-job.js');
++assert.ok(!slogan.includes('Free & Open'), 'the slogan prompt does not ask for "free"');
++assert.ok(/filter\(q => !_NO_FREE\.test/.test(slogan) && routes.includes('!NO_FREE.test(String(q))'), 'generated and stored slogans are filtered');
++console.log('shipped-live: all checks passed');