openvibers/openvibe.live Commits
openvibe-shared v4.228.0 (was v4.225.0) (#865)
Changes
@@@ -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 -->@@@ -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",@@@ -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",@@@ -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",@@@ -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",@@@ -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 livecontent 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: thetab 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>@@@ -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@@@ -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');@@@ -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`);@@@ -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 groupassert.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`);}@@@ -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;
@@ -184,5 +184,5 @@ Minimum `.env`: `BASE_URL`, `JWT_SECRET`, `OV_NETWORK_URL`, `OV_NETWORK_INTERNAL
@@ -184,5 +184,5 @@ Minimum `.env`: `BASE_URL`, `JWT_SECRET`, `OV_NETWORK_URL`, `OV_NETWORK_INTERNAL
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.417.0
- openvibe-contracts: v0.417.0
- openvibe-sdk: v0.161.0
- openvibe-sdk: v0.161.0
-- openvibe-shared: v4.225.0
+- openvibe-shared: v4.228.0
<!-- versions:end -->
<!-- versions:end -->
@@ -7,7 +7,7 @@
@@ -7,7 +7,7 @@
"domain": "openvibe.live (www, ingest, whip)",
"domain": "openvibe.live (www, ingest, whip)",
"contracts": "openvibe-contracts v0.417.0",
"contracts": "openvibe-contracts v0.417.0",
"sdk": "openvibe-sdk v0.161.0",
"sdk": "openvibe-sdk v0.161.0",
- "shared": "openvibe-shared v4.225.0",
+ "shared": "openvibe-shared v4.228.0",
"capabilities": [
"capabilities": [
"live.chat_context.read",
"live.chat_context.read",
"live.chat_effects.write",
"live.chat_effects.write",
@@ -26,7 +26,7 @@
@@ -26,7 +26,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.417.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-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",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
@@ -3417,9 +3417,9 @@
@@ -3417,9 +3417,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"clean-css": "^5.3.3",
"clean-css": "^5.3.3",
@@ -46,7 +46,7 @@
@@ -46,7 +46,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.417.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-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",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
@@ -35,7 +35,7 @@
@@ -35,7 +35,7 @@
"css": [
"css": [
"/css/features/home.css",
"/css/features/home.css",
"/css/home-fx.css",
"/css/home-fx.css",
- "/css/home-tour.css"
+ "/css/home-tour.css", "/shared/ov-parts.css"
],
],
"js": [
"js": [
"/shared/kit-web.js",
"/shared/kit-web.js",
@@ -97,6 +97,16 @@
@@ -97,6 +97,16 @@
<div class="agent-cards" id="home-agents-grid"></div>
<div class="agent-cards" id="home-agents-grid"></div>
</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,
<!-- 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"). -->
2026-10-31: "don't reduce size by removing features"). -->
<div class="home-around" id="home-around">
<div class="home-around" id="home-around">
@@ -131,6 +141,8 @@
@@ -131,6 +141,8 @@
<div class="pulse-grid" data-ovk-scroll id="pulse-grid"></div>
<div class="pulse-grid" data-ovk-scroll id="pulse-grid"></div>
</div>
</div>
</div><!-- /home-around -->
</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
<!-- 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
content and creators): channels that were just on, their recordings, the clips people took
@@ -182,6 +194,8 @@
@@ -182,6 +194,8 @@
<div class="moments-row ov-rail-track" data-ovk-scroll id="home-moments-row"></div>
<div class="moments-row ov-rail-track" data-ovk-scroll id="home-moments-row"></div>
</div>
</div>
</div><!-- /home-catchup -->
</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
<!-- 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
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 @@
@@ -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>
<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>
</footer>
<!--/ov:network--></div>
<!--/ov:network--></div>
+ </section>
+ </div><!-- /ov-parts -->
</div>
</div>
@@ -1213,8 +1213,26 @@ function demoteHomeIntroSections() {
@@ -1213,8 +1213,26 @@ function demoteHomeIntroSections() {
if (!last) return;
if (!last) return;
block.dataset.ovDemoted = '1';
block.dataset.ovDemoted = '1';
block.classList.add('home-intro-demoted');
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 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
* 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() {
@@ -1350,6 +1368,7 @@ function initHomeNetwork() {
async function loadHome() {
async function loadHome() {
initHomeNetwork(); // the OpenVibe network: every site, alive (no tabs: everything shows)
initHomeNetwork(); // the OpenVibe network: every site, alive (no tabs: everything shows)
demoteHomeIntroSections();
demoteHomeIntroSections();
+ openHomePartFor(location.hash);
void loadHomeChangelog();
void loadHomeChangelog();
_initHomeAbout();
_initHomeAbout();
updateNavHeroTransparency(); // transparent nav over the hero at the top
updateNavHeroTransparency(); // transparent nav over the hero at the top
@@ -14,7 +14,8 @@ const path = require('path');
@@ -14,7 +14,8 @@ const path = require('path');
const assets = require('../server/web/assets');
const assets = require('../server/web/assets');
try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */ }
try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */ }
const html = assets.renderRoute(assets.document('index.html').html, '/');
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 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 style = fs.readFileSync(path.join(__dirname, '../public/css/style.css'), 'utf8');
const js = fs.readFileSync(path.join(__dirname, '../public/js/app-home.js'), 'utf8');
const js = fs.readFileSync(path.join(__dirname, '../public/js/app-home.js'), 'utf8');
@@ -12,7 +12,8 @@ const assert = require('assert');
@@ -12,7 +12,8 @@ const assert = require('assert');
const assets = require('../server/web/assets');
const assets = require('../server/web/assets');
try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */ }
try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */ }
const html = assets.renderRoute(assets.document('index.html').html, '/');
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 at = (needle) => {
const i = home.indexOf(needle);
const i = home.indexOf(needle);
assert.ok(i >= 0, `${needle} is on the home page`);
assert.ok(i >= 0, `${needle} is on the home page`);
@@ -26,7 +26,8 @@ const { parseHTML } = loadLinkedom();
@@ -26,7 +26,8 @@ const { parseHTML } = loadLinkedom();
const assets = require('../server/web/assets');
const assets = require('../server/web/assets');
try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */ }
try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */ }
const html = assets.renderRoute(assets.document('index.html').html, '/');
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 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');
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
@@ -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');
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', () => {
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');
const frag = fs.readFileSync(path.join(__dirname, '../public/fragments/home.html'), 'utf8');
assert.strictEqual(frag.split('<!--ov:network-->').length, 2, 'one start marker');
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', () => {
@@ -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');
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 });
const p = openHome({ views: 18 });
p.run('initHomeNetwork(); demoteHomeIntroSections();');
p.run('initHomeNetwork(); demoteHomeIntroSections();');
await tick();
await tick();
const block = p.el('home-intro');
const block = p.el('home-intro');
assert.strictEqual(block.dataset.ovDemoted, '1', 'it was demoted');
assert.strictEqual(block.dataset.ovDemoted, '1', 'it was demoted');
assert.ok(block.classList.contains('home-intro-demoted'), 'and marked as such');
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']) {
for (const id of ['home-tour-mount', 'home-cta-banner']) {
assert.ok(block.contains(p.el(id)), `${id} travelled with it`);
assert.ok(block.contains(p.el(id)), `${id} travelled with it`);
}
}
@@ -83,7 +83,7 @@ async function check(name, fn) {
@@ -83,7 +83,7 @@ async function check(name, fn) {
assert.strictEqual(current.headers['cdn-cache-control'], 'public, max-age=31536000, immutable');
assert.strictEqual(current.headers['cdn-cache-control'], 'public, max-age=31536000, immutable');
const wrong = await get(`/shared/shipped.js?v=${v === '0123456789ab' ? 'ba9876543210' : '0123456789ab'}`);
const wrong = await get(`/shared/shipped.js?v=${v === '0123456789ab' ? 'ba9876543210' : '0123456789ab'}`);
assert.strictEqual(wrong.status, 200);
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);
assert.strictEqual(wrong.headers['cdn-cache-control'], undefined);
});
});
code = failures ? 1 : 0;
code = failures ? 1 : 0;