openvibers/openvibe.live Commits

openvibe-shared v4.228.0 (was v4.225.0) (#865)

OP OpenVibers committed 14 h ago · c377b87f8b · 11 files, +78 −18

* openvibe-shared v4.228.0 (was v4.225.0)

Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v4.228.0 release.

* Home: below Live Now, a phone shows For you, Catch up and OpenVibe as tabs

* Tests: an unversioned asset revalidates after five minutes (openvibe-shared 4.227.0 cache-policy)

* Tests: live-notify.js keeps Live's own day of stale-while-revalidate (only Shared's unversioned assets moved to 300)

Changes

ddiff --git a/README.md b/README.md
iindex 7467bb5a..f4fcd211 100644
---- a/README.md
++++ b/README.md
@@@ -184,5 +184,5 @@ Minimum `.env`: `BASE_URL`, `JWT_SECRET`, `OV_NETWORK_URL`, `OV_NETWORK_INTERNAL
<!-- versions:start -->
- openvibe-contracts: v0.417.0
- openvibe-sdk: v0.161.0
--- openvibe-shared: v4.225.0
++- openvibe-shared: v4.228.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex d59cbf3f..0c7ef5d0 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,7 +7,7 @@
"domain": "openvibe.live (www, ingest, whip)",
"contracts": "openvibe-contracts v0.417.0",
"sdk": "openvibe-sdk v0.161.0",
-- "shared": "openvibe-shared v4.225.0",
++ "shared": "openvibe-shared v4.228.0",
"capabilities": [
"live.chat_context.read",
"live.chat_effects.write",
ddiff --git a/package-lock.json b/package-lock.json
iindex 0fcc704f..9737cbd7 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -26,7 +26,7 @@
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.417.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.225.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
@@@ -3417,9 +3417,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.225.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.225.0",
-- "integrity": "sha512-zuZIgYODwJJ7UpDBTVnT+czEI5l6YFWqt1nbfbPeEsW6/NlmDZHJWPSGpBv223jRpvhJYH7Duea6nuRr8KZ3sg==",
++ "version": "4.228.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0",
++ "integrity": "sha512-HR0Hac/K+qKmwMf2QZWCFxW+uVja36nyAtxVbNFgPGUDFdmWhmb6RMoP/CkU0+GSCPsC50k6aTS+oPx2czDgGg==",
"license": "MIT",
"dependencies": {
"clean-css": "^5.3.3",
ddiff --git a/package.json b/package.json
iindex b471f0f6..b9cb1055 100644
---- a/package.json
++++ b/package.json
@@@ -46,7 +46,7 @@
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.417.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.225.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
ddiff --git a/public/features.json b/public/features.json
iindex 3e286583..32027e21 100644
---- a/public/features.json
++++ b/public/features.json
@@@ -35,7 +35,7 @@
"css": [
"/css/features/home.css",
"/css/home-fx.css",
-- "/css/home-tour.css"
++ "/css/home-tour.css", "/shared/ov-parts.css"
],
"js": [
"/shared/kit-web.js",
ddiff --git a/public/fragments/home.html b/public/fragments/home.html
iindex 83cca4a9..d64fd424 100644
---- a/public/fragments/home.html
++++ b/public/fragments/home.html
@@@ -97,6 +97,16 @@
<div class="agent-cards" id="home-agents-grid"></div>
</div>
++ <!-- Below Live Now, a phone (600 px and under) shows one part at a time as tabs: For you, Catch up, OpenVibe (the
++ owner, 2026-11-11: "consolidate these into tabs, self-contained tabs, so the page isn't so freaking huge … at
++ least on mobile"). Every part stays in this HTML for crawlers and readers; a wider screen stacks them as before.
++ openvibe-shared ov-parts.css (showcase.parts()) draws it with radios and :has(), no script. -->
++ <div class="ov-parts ov-parts--front home-parts" data-parts="3">
++ <input type="radio" class="ov-parts-r" name="ovp-home" id="ovp-home-foryou" value="foryou" checked>
++ <input type="radio" class="ov-parts-r" name="ovp-home" id="ovp-home-catchup" value="catchup">
++ <input type="radio" class="ov-parts-r" name="ovp-home" id="ovp-home-openvibe" value="openvibe">
++ <div class="ov-parts-bar" role="group" aria-label="The rest of the home page"><label for="ovp-home-foryou">For you</label><label for="ovp-home-catchup">Catch up</label><label for="ovp-home-openvibe">OpenVibe</label></div>
++ <section class="ov-part" data-part="foryou" aria-label="For you">
<!-- Around OpenVibe: the digest, the star spotlight and the pulse, every one shown on every screen (the owner,
2026-10-31: "don't reduce size by removing features"). -->
<div class="home-around" id="home-around">
@@@ -131,6 +141,8 @@
<div class="pulse-grid" data-ovk-scroll id="pulse-grid"></div>
</div>
</div><!-- /home-around -->
++ </section>
++ <section class="ov-part" data-part="catchup" aria-label="Catch up">
<!-- What people made, above the introduction to the site (roadmap 32.7: the home leads with live
content and creators): channels that were just on, their recordings, the clips people took
@@@ -182,6 +194,8 @@
<div class="moments-row ov-rail-track" data-ovk-scroll id="home-moments-row"></div>
</div>
</div><!-- /home-catchup -->
++ </section>
++ <section class="ov-part" data-part="openvibe" aria-label="OpenVibe">
<!-- New here?: the restream tour and the About banner, one after the other on every screen (2026-11-07: the
tab row that showed one at a time on a phone is gone; the owner's rule is never to hide content behind
@@@ -360,6 +374,8 @@
<button type="button" class="ovnet-switch" data-ovnet-switcher><span class="ovnet-orb" aria-hidden="true"><svg class="ovmb" viewBox="0 0 24 24" width="22" height="22"><rect class="ovmb-bar" x="4" y="5.2" width="16" height="5.6" rx="2.8"/><circle class="ovmb-dot" cx="7.4" cy="8" r="1.15"/><rect class="ovmb-app ovmb-a0" x="4.2" y="14.6" width="4" height="4" rx="1.3"/><rect class="ovmb-app ovmb-a1" x="10" y="14.6" width="4" height="4" rx="1.3"/><rect class="ovmb-app ovmb-a2" x="15.8" y="14.6" width="4" height="4" rx="1.3"/></svg></span><span class="ovnet-switch-t"><b>Every site is in the menu</b><small>This button, at the top of every page</small></span><i class="ovnet-switch-go" aria-hidden="true"></i></button>
</footer>
<!--/ov:network--></div>
++ </section>
++ </div><!-- /ov-parts -->
</div>
ddiff --git a/public/js/app-home.js b/public/js/app-home.js
iindex 0a744952..7a5c9ecd 100644
---- a/public/js/app-home.js
++++ b/public/js/app-home.js
@@@ -1213,8 +1213,26 @@ function demoteHomeIntroSections() {
if (!last) return;
block.dataset.ovDemoted = '1';
block.classList.add('home-intro-demoted');
-- try { last.appendChild(block); } catch { /* */ }
--}
++ // On a phone the intro is in the OpenVibe tab (home-parts): it moves to the end of its own part, never out of it.
++ const part = block.closest('.ov-part');
++ try { (part || last).appendChild(block); } catch { /* */ }
++}
++
++/* A link to something inside a phone tab that is not open (#home-network, #ovnet-g-streams…) opens that tab first, so
++ * the jump lands on what it names. Wide screens stack the parts and need nothing. */
++function openHomePartFor(hash) {
++ let el = null;
++ try { el = hash && hash.length > 1 ? document.getElementById(decodeURIComponent(hash.slice(1))) : null; } catch { el = null; }
++ const part = el && el.closest('.home-parts > .ov-part');
++ if (!part) return;
++ const radio = document.getElementById(`ovp-home-${part.dataset.part}`);
++ if (radio && !radio.checked) { radio.checked = true; requestAnimationFrame(() => { try { el.scrollIntoView({ block: 'start' }); } catch { /* */ } }); }
++}
++window.addEventListener('hashchange', () => openHomePartFor(location.hash));
++document.addEventListener('click', (e) => {
++ const a = e.target && e.target.closest && e.target.closest('a[href^="#"]');
++ if (a) openHomePartFor(a.getAttribute('href'));
++}, true);
/* ── The OpenVibe network (2026-11-07 redesign) ──────────────────────────────────────────────────────────────
* The section's tiles are in the page already (scripts/build-network-section.js, from OpenVibe.Network's frame), so
@@@ -1350,6 +1368,7 @@ function initHomeNetwork() {
async function loadHome() {
initHomeNetwork(); // the OpenVibe network: every site, alive (no tabs: everything shows)
demoteHomeIntroSections();
++ openHomePartFor(location.hash);
void loadHomeChangelog();
_initHomeAbout();
updateNavHeroTransparency(); // transparent nav over the hero at the top
ddiff --git a/test/home-catchup.test.js b/test/home-catchup.test.js
iindex 98b819ec..e5da544d 100644
---- a/test/home-catchup.test.js
++++ b/test/home-catchup.test.js
@@@ -14,7 +14,8 @@ const path = require('path');
const assets = require('../server/web/assets');
try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */ }
const html = assets.renderRoute(assets.document('index.html').html, '/');
--const home = html.slice(html.indexOf('<section id="page-home"'), html.indexOf('</section>', html.indexOf('<section id="page-home"')));
++// The home page runs to the next page's shell (its phone tabs are <section>s of their own, so not to the first </section>).
++const home = html.slice(html.indexOf('<section id="page-home"'), html.indexOf('<section id="page-', html.indexOf('<section id="page-home"') + 10));
const css = fs.readFileSync(path.join(__dirname, '../public/css/features/home.css'), 'utf8');
const style = fs.readFileSync(path.join(__dirname, '../public/css/style.css'), 'utf8');
const js = fs.readFileSync(path.join(__dirname, '../public/js/app-home.js'), 'utf8');
ddiff --git a/test/home-order.test.js b/test/home-order.test.js
iindex 264d1879..f9975045 100644
---- a/test/home-order.test.js
++++ b/test/home-order.test.js
@@@ -12,7 +12,8 @@ const assert = require('assert');
const assets = require('../server/web/assets');
try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */ }
const html = assets.renderRoute(assets.document('index.html').html, '/');
--const home = html.slice(html.indexOf('<section id="page-home"'), html.indexOf('</section>', html.indexOf('<section id="page-home"')));
++// The home page runs to the next page's shell (its phone tabs are <section>s of their own, so not to the first </section>).
++const home = html.slice(html.indexOf('<section id="page-home"'), html.indexOf('<section id="page-', html.indexOf('<section id="page-home"') + 10));
const at = (needle) => {
const i = home.indexOf(needle);
assert.ok(i >= 0, `${needle} is on the home page`);
ddiff --git a/test/home-tabs.test.js b/test/home-tabs.test.js
iindex 925fc09c..5da90474 100644
---- a/test/home-tabs.test.js
++++ b/test/home-tabs.test.js
@@@ -26,7 +26,8 @@ const { parseHTML } = loadLinkedom();
const assets = require('../server/web/assets');
try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */ }
const html = assets.renderRoute(assets.document('index.html').html, '/');
--const home = html.slice(html.indexOf('<section id="page-home"'), html.indexOf('</section>', html.indexOf('<section id="page-home"')));
++// The home page runs to the next page's shell (its phone tabs are <section>s of their own, so not to the first </section>).
++const home = html.slice(html.indexOf('<section id="page-home"'), html.indexOf('<section id="page-', html.indexOf('<section id="page-home"') + 10));
const css = fs.readFileSync(path.join(__dirname, '../public/css/features/home.css'), 'utf8');
const js = fs.readFileSync(path.join(__dirname, '../public/js/app-home.js'), 'utf8');
@@@ -137,6 +138,27 @@ check('the OpenVibe network is its own section: every site a real link, by group
assert.ok(/data-ovnet-switcher><span class="ovnet-orb" aria-hidden="true"><svg class="ovmb"[^>]*><rect class="ovmb-bar"/.test(net) && net.includes('<b>Every site is in the menu</b>') && !/one tap away in the/.test(net), 'the line about the menu carries the menu button as the bar draws it now, and says where it is');
});
++check('on a phone the rest of the home is three parts as tabs (For you, Catch up, OpenVibe), every one in the HTML', () => {
++ // The owner, 2026-11-11: "consolidate these into tabs, self-contained tabs, so the page isn't so freaking huge … at
++ // least on mobile". openvibe-shared ov-parts.css draws it (radios and :has(), no script); wide screens stack them.
++ const start = home.indexOf('<div class="ov-parts ov-parts--front home-parts" data-parts="3">');
++ assert.ok(start > home.indexOf('id="stream-grid-live"'), 'below Live Now');
++ const parts = [...home.slice(start).matchAll(/<section class="ov-part" data-part="([a-z]+)"/g)].map((m) => m[1]);
++ assert.deepStrictEqual(parts, ['foryou', 'catchup', 'openvibe']);
++ for (const [id, label] of [['foryou', 'For you'], ['catchup', 'Catch up'], ['openvibe', 'OpenVibe']]) {
++ assert.ok(home.includes(`<input type="radio" class="ov-parts-r" name="ovp-home" id="ovp-home-${id}" value="${id}"`), `${id}: its radio`);
++ assert.ok(home.includes(`<label for="ovp-home-${id}">${label}</label>`), `${id}: its tab`);
++ }
++ const at = (needle) => home.indexOf(needle);
++ const sec = (id) => at(`data-part="${id}"`);
++ assert.ok(sec('foryou') < at('id="home-around"') && at('id="home-around"') < sec('catchup'), 'For you holds Around OpenVibe');
++ assert.ok(sec('catchup') < at('id="home-catchup"') && at('id="home-catchup"') < sec('openvibe'), 'Catch up holds the rails');
++ assert.ok(sec('openvibe') < at('id="home-intro"') && at('id="home-intro"') < at('id="home-network"'), 'OpenVibe holds New here? and the network');
++ const features = JSON.parse(fs.readFileSync(path.join(__dirname, '../public/features.json'), 'utf8'));
++ assert.ok(JSON.stringify(features).includes('/shared/ov-parts.css'), 'the home links the tabs\' stylesheet (blocking, so nothing jumps)');
++ assert.ok(/function openHomePartFor\(hash\)/.test(js) && /closest\('\.ov-part'\)/.test(js), 'a link into a closed tab opens it; a returning reader\'s New here? stays inside its tab');
++});
++
check('the network section is built by its script, between its markers, and nothing in it is a tab', () => {
const frag = fs.readFileSync(path.join(__dirname, '../public/fragments/home.html'), 'utf8');
assert.strictEqual(frag.split('<!--ov:network-->').length, 2, 'one start marker');
@@@ -198,16 +220,17 @@ check('the tour still loads when its mount nears the viewport', () => {
assert.notStrictEqual(block.parentElement, p.el('home-changelog-wrapper').parentElement, 'and before the changelog');
});
-- await check('after enough visits the whole block moves to the end of the page, and still works there', async () => {
++ await check('after enough visits the whole block moves to the end of its OpenVibe tab, and still works there', async () => {
const p = openHome({ views: 18 });
p.run('initHomeNetwork(); demoteHomeIntroSections();');
await tick();
const block = p.el('home-intro');
assert.strictEqual(block.dataset.ovDemoted, '1', 'it was demoted');
assert.ok(block.classList.contains('home-intro-demoted'), 'and marked as such');
-- const container = p.el('home-changelog-wrapper').parentElement;
-- assert.strictEqual(block.parentElement, container, 'it moved into the changelog\'s container');
-- assert.strictEqual(container.lastElementChild, block, 'and to the end of it');
++ const part = block.parentElement;
++ assert.strictEqual(part && part.getAttribute('data-part'), 'openvibe', 'it stays inside its phone tab, never out of it');
++ assert.strictEqual(part.lastElementChild, block, 'at the end of it, after the network');
++ assert.ok(part.contains(p.el('home-network')), 'the network stays in the same tab');
for (const id of ['home-tour-mount', 'home-cta-banner']) {
assert.ok(block.contains(p.el(id)), `${id} travelled with it`);
}
ddiff --git a/test/seo-discovery.test.js b/test/seo-discovery.test.js
iindex 278c1e9c..0df88756 100644
---- a/test/seo-discovery.test.js
++++ b/test/seo-discovery.test.js
@@@ -83,7 +83,7 @@ async function check(name, fn) {
assert.strictEqual(current.headers['cdn-cache-control'], 'public, max-age=31536000, immutable');
const wrong = await get(`/shared/shipped.js?v=${v === '0123456789ab' ? 'ba9876543210' : '0123456789ab'}`);
assert.strictEqual(wrong.status, 200);
-- assert.strictEqual(wrong.headers['cache-control'], 'public, max-age=300, stale-while-revalidate=86400');
++ assert.strictEqual(wrong.headers['cache-control'], 'public, max-age=300, stale-while-revalidate=300');
assert.strictEqual(wrong.headers['cdn-cache-control'], undefined);
});
code = failures ? 1 : 0;