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
Changes
@@@ -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"@@@ -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",@@@ -221,7 +221,7 @@</div><!-- Newest shipped commit — built in the open, receipts included. Sits under thephilosophy 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>@@@ -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-linertry {-- 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)} · ${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>';}}@@@ -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 placecontainer.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)} · ${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 () => {@@@ -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 loglegalBase: location.origin,variant: variantForPage(),links: LINKS,@@@ -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) {@@@ -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.nnew file mode 100644@@@ -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');
@@ -27,7 +27,7 @@
@@ -27,7 +27,7 @@
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.35.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-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",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
"sharp": "^0.33.0",
"sharp": "^0.33.0",
@@ -2839,9 +2839,9 @@
@@ -2839,9 +2839,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -45,7 +45,7 @@
@@ -45,7 +45,7 @@
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.35.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-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",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
"sharp": "^0.33.0",
"sharp": "^0.33.0",
@@ -221,7 +221,7 @@
@@ -221,7 +221,7 @@
</div>
</div>
<!-- Newest shipped commit — built in the open, receipts included. Sits under the
<!-- Newest shipped commit — built in the open, receipts included. Sits under the
philosophy row so the proof is next to the promise. -->
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 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>
</div>
</div>
@@ -725,7 +725,7 @@
@@ -725,7 +725,7 @@
<div class="content-container">
<div class="content-container">
<div class="page-header">
<div class="page-header">
<h1><i class="fa-solid fa-code-commit"></i> Updates</h1>
<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>
<div id="updates-list" class="updates-list">
<div id="updates-list" class="updates-list">
<div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>
<div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>
@@ -1249,12 +1249,8 @@ async function loadHomePulse() {
@@ -1249,12 +1249,8 @@ async function loadHomePulse() {
function _renderHomePulse(p) {
function _renderHomePulse(p) {
if (!p) return;
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 grid = document.getElementById('pulse-grid');
const section = document.getElementById('home-pulse-section');
const section = document.getElementById('home-pulse-section');
@@ -2013,48 +2009,23 @@ function renderHomePagination(containerId, rail) {
@@ -2013,48 +2009,23 @@ function renderHomePagination(containerId, rail) {
</button>`;
</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');
const container = document.getElementById('home-changelog');
if (!container) return;
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 {
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)} · ${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 {
} 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>';
}
}
}
}
@@ -2498,46 +2498,44 @@ async function doRegenerateKey() {
@@ -2498,46 +2498,44 @@ async function doRegenerateKey() {
let _userLoaded = false;
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() {
async function loadUpdatesPage() {
const container = document.getElementById('updates-list');
const container = document.getElementById('updates-list');
if (!container) return;
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>';
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)} · ${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 () => {
document.addEventListener('DOMContentLoaded', async () => {
@@ -56,7 +56,8 @@
@@ -56,7 +56,8 @@
OpenVibeFooter.init({
OpenVibeFooter.init({
service: 'live',
service: 'live',
brandName: 'OpenVibe.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,
legalBase: location.origin,
variant: variantForPage(),
variant: variantForPage(),
links: LINKS,
links: LINKS,
@@ -42,6 +42,7 @@ function _stripAudiencePrefix(s) {
@@ -42,6 +42,7 @@ function _stripAudiencePrefix(s) {
.trim();
.trim();
}
}
const _BAD_AUDIENCE = /\b(live\s*-?\s*)?stream(ing|s)?\b|\blivestream/i;
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) {
function _cleanList(arr, maxLen, max) {
if (!Array.isArray(arr)) return [];
if (!Array.isArray(arr)) return [];
const seen = new Set(); const out = [];
const seen = new Set(); const out = [];
@@ -123,7 +124,7 @@ async function tick() {
@@ -123,7 +124,7 @@ async function tick() {
} catch { /* */ }
} catch { /* */ }
const prompt =
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.
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.`;
@@ -155,13 +156,15 @@ Return ONLY the JSON object.`;
const parsed = _parseJson(text);
const parsed = _parseJson(text);
if (!parsed) return;
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 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
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.
// Fresh daily batch; top up from the previous batch only if the model returned few.
const old = _loadPool();
const old = _loadPool();
audiences = _topUp(audiences, old.audiences.map(_stripAudiencePrefix).filter(a => a && !_BAD_AUDIENCE.test(a)), TARGET);
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() }));
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)`);
console.log(`[Slogans] Fresh daily batch: ${audiences.length} words, ${quips.length} slogans (from full AI context)`);
} catch (e) {
} catch (e) {
@@ -39,7 +39,7 @@ const FALLBACK_QUIPS = [
@@ -39,7 +39,7 @@ const FALLBACK_QUIPS = [
'Powered by caffeine and spite.',
'Powered by caffeine and spite.',
'The internet hangout everyone forgot they wanted.',
'The internet hangout everyone forgot they wanted.',
'Clip it before it happens.',
'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.',
'Where the wifi is questionable and the community is not.',
'Stream from a van. We support that lifestyle.',
'Stream from a van. We support that lifestyle.',
'One account. All of OpenVibe.',
'One account. All of OpenVibe.',
@@ -134,14 +134,17 @@ function _cleanAudience(raw) {
@@ -134,14 +134,17 @@ function _cleanAudience(raw) {
.trim();
.trim();
}
}
+const NO_FREE = /\b(free|no[- ]cost)\b|\$\s?0\b/i;
+
function heroSlogans() {
function heroSlogans() {
let s = null;
let s = null;
try {
try {
s = db.getState('home_hero_slogans');
s = db.getState('home_hero_slogans');
if (typeof s === 'string') s = JSON.parse(s);
if (typeof s === 'string') s = JSON.parse(s);
} catch { s = null; }
} 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 hasAi = aiAud.length >= 4 || aiQuips.length >= 4;
const updatedAt = (s && Number(s.updated_at)) || 0;
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.
// The slogan job regenerates every 12h; next_at is when the current batch is due to refresh.
new file mode 100644
new file mode 100644
@@ -0,0 +1,37 @@
@@ -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');