openvibers/openvibe.network Commits
Every page of each Network host sends that host's own page list (#861)
Changes
@@@ -290,7 +290,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}function initNavbar(user) {if (typeof OpenVibeNavbar !== 'undefined') {-- OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: token || null, user: user || null, menu: { before: [{ id: 'ov-wallet', label: 'Wallet', short: 'Wallet', icon: 'fa-wallet', href: 'https://openvibe.network/wallet' }] }, links: [{ label: 'Link', href: 'https://openvibe.link', icon: 'fa-desktop', featured: true }, { label: 'My Account', href: '/my' }, { label: 'Wallet', href: '/wallet' }, { label: 'Themes', href: '/themes' }], networkLinks: 5 });++ OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: token || null, user: user || null, menu: { before: [{ id: 'ov-wallet', label: 'Wallet', short: 'Wallet', icon: 'fa-wallet', href: 'https://openvibe.network/wallet' }] }, links: [{"label":"Network","href":"/"},{"label":"Link","href":"https://openvibe.link/","icon":"fa-desktop","featured":true},{"label":"My Account","href":"https://openvibe.my/"},{"label":"Wallet","href":"/wallet"},{"label":"Themes","href":"/themes"}], networkLinks: 5 });}if (user) {document.getElementById('hero-guest-ctas').style.display = 'none';@@@ -4,6 +4,8 @@admin/core.js's helpers (api, esc, toast, API, token) and on thetabLoaders map in admin/boot.js (loaded last). No ES modules.═══════════════════════════════════════════════════════════════ */++// Network's own page list: the shared navbar's fallback (Home, My Account, Themes) is not this host's list.++const OV_PAGE_LINKS = [{"label":"Network","href":"/"},{"label":"Link","href":"https://openvibe.link/","icon":"fa-desktop","featured":true},{"label":"My Account","href":"https://openvibe.my/"},{"label":"Wallet","href":"/wallet"},{"label":"Themes","href":"/themes"}];const tabLoaders = {setup: loadSetup,glossary: loadGlossary,@@@ -72,7 +74,7 @@ async function _bootAdmin() {}// Init navbarif (window.OpenVibeNavbar) {-- OpenVibeNavbar.init({ service: 'network', token, user: currentUser });++ OpenVibeNavbar.init({ service: 'network', token, user: currentUser, links: OV_PAGE_LINKS });const bellMount = OpenVibeNavbar.getBellMount();if (bellMount && window.OpenVibeNotifications) {OpenVibeNotifications.init({ mountEl: bellMount, apiBase: API, token });@@@ -15,6 +15,15 @@ const OV_NAV_AUTH = MY_HOST ? {logoutUrl: '/auth/logout?next={path}',} : {};++// The page list this host's shared navbar shows. openvibe.my's own sections, openvibe.link's machines hub (this file++// also renders /machines there), and Network's own pages everywhere else. Never the shared fallback: that is++// Home, My Account, Themes on every host (the owner, 2026-11-12).++const OV_PAGE_LINKS = MY_HOST++ ? [{"label":"My Account","href":"/"},{"label":"Profile","href":"/profile"},{"label":"Themes","href":"/themes"},{"label":"Security","href":"/security"}]++ : location.hostname.replace(/^www\./, '') === 'openvibe.link'++ ? [{"label":"Link","href":"/","icon":"fa-desktop","featured":true},{"label":"Machines","href":"/machines","icon":"fa-laptop"},{"label":"Network","href":"https://openvibe.network/"},{"label":"My Account","href":"https://openvibe.my/"}]++ : [{"label":"Network","href":"/"},{"label":"Link","href":"https://openvibe.link/","icon":"fa-desktop","featured":true},{"label":"My Account","href":"https://openvibe.my/"},{"label":"Wallet","href":"/wallet"},{"label":"Themes","href":"/themes"}];++// /my#delete is the return target of the re-authentication path (and a deep link): it opens the accounts// section and puts the cursor in the delete confirmation field. Read before core.js rewrites the hash.const OV_DELETE_DEEP_LINK = window.location.hash === '#delete';@@@ -85,7 +94,7 @@ function showSignedOutHub() {card.append(heading, text, signIn, list, still);page.replaceChildren(card);try {-- if (window.OpenVibeNavbar) OpenVibeNavbar.init({ service: 'network', token: null, user: null, menu: OV_WALLET_MENU, ...OV_NAV_AUTH });++ if (window.OpenVibeNavbar) OpenVibeNavbar.init({ service: 'network', token: null, user: null, menu: OV_WALLET_MENU, links: OV_PAGE_LINKS, ...OV_NAV_AUTH });} catch { /* the page still offers the buttons */ }}@@@ -120,7 +129,7 @@ async function loadUser() {applyAnonModeUI(currentUser);loadAccounts();loadAnonInfo();-- OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: null, user: currentUser, menu: OV_WALLET_MENU, ...OV_NAV_AUTH });++ OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: null, user: currentUser, menu: OV_WALLET_MENU, links: OV_PAGE_LINKS, ...OV_NAV_AUTH });} else {const data = await apiFetch('/api/auth/me');currentUser = data.user;@@@ -145,7 +154,7 @@ async function loadUser() {loadRecentTools();// Init navbar-- OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: authToken, user: currentUser, menu: OV_WALLET_MENU, ...OV_NAV_AUTH });++ OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: authToken, user: currentUser, menu: OV_WALLET_MENU, links: OV_PAGE_LINKS, ...OV_NAV_AUTH });const bellMount = OpenVibeNavbar.getBellMount();if (bellMount && window.OpenVibeNotifications && authToken) {OpenVibeNotifications.init({ token: authToken, apiBase: API, swPath: '/openvibe-sw.js' });@@@ -96,7 +96,7 @@root.OpenVibeNavbar.init({service: 'network', notificationsRealtime: true, token: token(), user,menu: WALLET_MENU,-- links: [{ label: 'Link', href: 'https://openvibe.link', icon: 'fa-desktop', featured: true }, { label: 'Wallet', href: '/wallet' }, { label: 'My Account', href: 'https://openvibe.my/' }],++ links: [{"label":"Network","href":"/"},{"label":"Link","href":"https://openvibe.link/","icon":"fa-desktop","featured":true},{"label":"My Account","href":"https://openvibe.my/"},{"label":"Wallet","href":"/wallet"},{"label":"Themes","href":"/themes"}],});} catch { /* the page works without it */ }}@@@ -96,7 +96,7 @@ p{color:var(--text-secondary);font-size:14px;line-height:1.6}<script src="/js/account-safety.js" defer></script><script>(function () {-- if (window.OpenVibeNavbar) { try { OpenVibeNavbar.init({ service: 'network', apiBase: location.origin }); } catch (e) { /* */ } }++ if (window.OpenVibeNavbar) { try { OpenVibeNavbar.init({ service: 'network', apiBase: location.origin, links: [{"label":"Network","href":"/"},{"label":"Link","href":"https://openvibe.link/","icon":"fa-desktop","featured":true},{"label":"My Account","href":"https://openvibe.my/"},{"label":"Wallet","href":"/wallet"},{"label":"Themes","href":"/themes"}] }); } catch (e) { /* */ } }const card = document.getElementById('card'), ring = document.getElementById('ring'), ico = document.getElementById('ico');const title = document.getElementById('title'), msg = document.getElementById('msg'), actions = document.getElementById('actions'), hint = document.getElementById('hint');const burst = document.getElementById('burst');@@@ -1,4 +1,5 @@'use strict';++const pageLinks = require('../frame/page-links');// OpenVibeBot (ADR 0007 in OpenVibe.Sources): the page its user agent links to — what it is, what it reads, what it obeys,// and how a site blocks, slows or opts in. /bot.json says the same for machines. Static: no database, no outbound calls.@@@ -125,7 +126,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'<script src="/shared/navbar.js" defer></script><script src="/js/account-safety.js" defer></script>${require('../boost').SCRIPT}--<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}++<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}document.querySelectorAll('.bp-copy').forEach(function(b){b.addEventListener('click',function(){var el=document.getElementById(b.getAttribute('data-copy'));if(!el||!navigator.clipboard)return;navigator.clipboard.writeText(el.textContent).then(function(){b.textContent='Copied';b.classList.add('is-done');setTimeout(function(){b.textContent='Copy';b.classList.remove('is-done');},1600);},function(){});});});});</script></body></html>`;nnew file mode 100644@@@ -0,0 +1,52 @@++'use strict';++// The page list each host gives the shared navbar's "Pages on <site>" menu. Every page of a host sends the same++// list, and each row is a page of that host or a door named for where it goes (the owner, 2026-11-12:++// "Pages on OpenVibe.Link: Home, My Account, Themes" was Network's list on Link's menu, not Link's).++//++// Network serves four hosts. openvibe.network, openvibe.link and openvibe.my read these lists here; Computer has++// its own server. A page that passes no links gets the shared navbar's fallback (Home, My Account, Themes), which++// is why every Network page passes the host's list explicitly.++const NETWORK = Object.freeze([++ { label: 'Network', href: '/' },++ { label: 'Link', href: 'https://openvibe.link/', icon: 'fa-desktop', featured: true },++ { label: 'My Account', href: 'https://openvibe.my/' },++ { label: 'Wallet', href: '/wallet' },++ { label: 'Themes', href: '/themes' },++]);++++const LINK = Object.freeze([++ { label: 'Link', href: '/', icon: 'fa-desktop', featured: true },++ { label: 'Machines', href: '/machines', icon: 'fa-laptop' },++ { label: 'Network', href: 'https://openvibe.network/' },++ { label: 'My Account', href: 'https://openvibe.my/' },++]);++++const MY = Object.freeze([++ { label: 'My Account', href: '/' },++ { label: 'Profile', href: '/profile' },++ { label: 'Themes', href: '/themes' },++ { label: 'Security', href: '/security' },++]);++++const HOSTS = new Set(['openvibe.network', 'www.openvibe.network', 'openvibe.link', 'www.openvibe.link', 'openvibe.my', 'www.openvibe.my']);++++/** This request's host, lowercased, no port, no www. */++function hostOf(req) {++ return String((req && (req.hostname || (req.headers && req.headers.host))) || '').split(':')[0].toLowerCase().replace(/^www\./, '');++}++++/** The page list of the host Network is answering: Link and My have their own; everything else is Network's. */++function forHost(host) {++ const h = String(host || '').split(':')[0].toLowerCase().replace(/^www\./, '');++ if (h === 'openvibe.link') return LINK;++ if (h === 'openvibe.my') return MY;++ return NETWORK;++}++++/** A JS expression for a page that is cached across hosts: the same choice, made in the browser. */++function browserList() {++ const host = "location.hostname.replace(/^www\\./, '')";++ return `(${host} === 'openvibe.link' ? ${JSON.stringify(LINK)} : ${host} === 'openvibe.my' ? ${JSON.stringify(MY)} : ${JSON.stringify(NETWORK)})`;++}++++module.exports = { NETWORK, LINK, MY, HOSTS, hostOf, forHost, browserList };@@@ -27,6 +27,7 @@ const icon = (name, size) => `<span class="ov-icon" data-icon="${esc(name)}" dat// public domain serves the service itself (Network's exposure overlay), so the home page, the navbar and// "Opening next" always agree with what is actually running.const { LISTED, GROUPS, groupOf, fullName } = require('../frame/sites');++const pageLinks = require('../frame/page-links');const contracts = require('openvibe-contracts');const social = require('openvibe-shared/social');// OpenRestream is its own name, never "OpenVibe.OpenRestream" (the owner, 2026-10-28).@@@ -540,7 +541,7 @@ function render({ releaseId = '' } = {}) {})) + seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what })))) + seo.jsonLdTag(seo.jsonLd.faq(faqPairs()));const card = social.url('network', 'https://openvibe.network/shared') || 'https://openvibe.network/assets/logo-512.png';const summary = summaryOf(catalog);-- const html = withSameAs(lookBoot.apply(fs.readFileSync(SHELL, 'utf8'))).replace('<div id="navbar-mount"></div>', require('openvibe-shared/frame').serverNav({ name: 'OpenVibe.Network', service: 'network', home: '/', links: [{ label: 'Link', href: 'https://openvibe.link/' }, { label: 'My Account', href: '/my' }, { label: 'Wallet', href: '/wallet' }, { label: 'Themes', href: '/themes' }] }) + summary.body).replace('<h1>Go live, chat, play<br><span class="grad">and get things done.</span></h1>', () => kit.pageHead({ title: 'Go live, chat, play and get things done.', icon: 'home', lede: 'Your places, people and projects across OpenVibe.' })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog, act.popularity)).replace('<!--OV:CONSTELLATION-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(SITE_COUNT)).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n<link rel="stylesheet" href="${require('openvibe-shared/serve').url('kit.css')}"><script src="${require('openvibe-shared/serve').url('kit-web.js')}" defer></script>\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);++ const html = withSameAs(lookBoot.apply(fs.readFileSync(SHELL, 'utf8'))).replace('<div id="navbar-mount"></div>', require('openvibe-shared/frame').serverNav({ name: 'OpenVibe.Network', service: 'network', home: '/', links: pageLinks.NETWORK }) + summary.body).replace('<h1>Go live, chat, play<br><span class="grad">and get things done.</span></h1>', () => kit.pageHead({ title: 'Go live, chat, play and get things done.', icon: 'home', lede: 'Your places, people and projects across OpenVibe.' })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog, act.popularity)).replace('<!--OV:CONSTELLATION-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(SITE_COUNT)).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n<link rel="stylesheet" href="${require('openvibe-shared/serve').url('kit.css')}"><script src="${require('openvibe-shared/serve').url('kit-web.js')}" defer></script>\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);pageCache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };return pageCache;}@@@ -985,12 +985,17 @@ function requireAdmin(req, res, next) {// is added to the head here, and the small Twitter card they declare becomes the large one that image needs.{const legal = require('openvibe-shared/legal');++ const pageLinks = require('./frame/page-links');const card = require('openvibe-shared/social').url('network', 'https://openvibe.network/shared');const tags = `<meta property="og:image" content="${card}"><meta property="og:image:alt" content="OpenVibe.Network: one account for every OpenVibe site"><meta name="twitter:image" content="${card}">`;++ // Shared's legal template draws the navbar without links, so its fallback (Home, My Account, Themes) was the++ // Network list on /terms, /privacy and /dmca. Add this host's own list to the init call (Shared stays untouched).++ const navLinks = JSON.stringify(pageLinks.NETWORK);app.get(legal.PATHS, (req, res, next) => {const send = res.send.bind(res);res.send = (body) => send(String(body).replace('<meta name="twitter:card" content="summary">', '<meta name="twitter:card" content="summary_large_image">')++ .replace('try{OpenVibeNavbar.init({service:', `try{OpenVibeNavbar.init({links:${navLinks},service:`).replace('</head>', `${tags}</head>`));next();}, legal.handler({ id: 'network', service: 'network', host: 'openvibe.network', name: 'OpenVibe.Network', profile: 'account' }));@@@ -1,4 +1,5 @@'use strict';++const pageLinks = require('../frame/page-links');// openvibe.link: OpenVibe Link's own front door (the machine agent). What Link is (your machines, connected), what it// does today, how to install it on each OS, and what it never does. The chrome is Network's own; the page is Link's.// The version, the release date and the checksums come from Link's latest GitHub release (server/link/release.js).@@@ -242,7 +243,7 @@ ${require('../boost').SCRIPT}<script src="/shared/kit-web.js" defer></script><script type="module" src="/send.js"></script><script src="/egress-settings.js" defer></script>--<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',sessionUrl:'/auth/me',loginUrl:'/auth/login?next={path}',logoutUrl:'/auth/logout?next={url}',silentLogin:'/auth/login?silent=1&next={url}',links:[{label:'Link',href:'/',icon:'fa-desktop',featured:true,active:true},{label:'Machines',href:'/machines',icon:'fa-laptop'},{label:'Network',href:'https://openvibe.network/'},{label:'My Account',href:'https://openvibe.network/my'}]});}catch(e){}});</script>++<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',sessionUrl:'/auth/me',loginUrl:'/auth/login?next={path}',logoutUrl:'/auth/logout?next={url}',silentLogin:'/auth/login?silent=1&next={url}',links:${JSON.stringify(pageLinks.LINK)}});}catch(e){}});</script></body></html>`;}@@@ -1,4 +1,5 @@'use strict';++const pageLinks = require('./frame/page-links');// ═══════════════════════════════════════════════════════════════// Unknown paths answer 404 (roadmap §2.5 crawlability, D44).//@@@ -16,16 +17,17 @@ const ACCOUNT_HUB_PATHS = ['/my', '/my.html', '/themes', '/notifications', '/linconst JSON_PREFIXES = ['/api/', '/internal/', '/oauth/'];--let cached = null;--function page() {-- if (cached) return cached;++const cached = new Map();++function page(host = 'openvibe.network') {++ const key = pageLinks.hostOf({ hostname: host });++ if (cached.has(key)) return cached.get(key);let footer = '';let icons = '';let nav = '';try { footer = require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' }); } catch { /* optional */ }try { icons = require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' }); } catch { /* optional */ }try { nav = require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }] }); } catch { /* optional */ }-- cached = `<!DOCTYPE html>++ const html = `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">@@@ -64,10 +66,11 @@ ${nav}${footer}<script src="/shared/navbar.js" defer></script><script src="/js/account-safety.js" defer></script>--<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>++<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.forHost(key))}});}catch(e){}});</script></body></html>`;-- return cached;++ cached.set(key, html);++ return html;}/** Last handler of the app: every method, every path nothing else answered. */@@@ -76,7 +79,7 @@ function notFound(req, res) {res.set('Cache-Control', 'no-cache');if (JSON_PREFIXES.some(p => req.path.startsWith(p))) return res.json({ error: 'Not found' });res.set('X-Robots-Tag', 'noindex');-- return res.type('html').send(page());++ return res.type('html').send(page(pageLinks.hostOf(req)));}module.exports = { notFound, page, ACCOUNT_HUB_PATHS };@@@ -1,4 +1,5 @@'use strict';++const pageLinks = require('../frame/page-links');// ═══════════════════════════════════════════════════════════════// Public profiles (plan T21 step 3): one page per person on the Network, and the same data as JSON for every site.//@@@ -613,7 +614,7 @@ ${isPublic ? showcaseHtml(inv, name) + badgesHtml(inv) + gamesHtml(inv) + itemsH${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' })}<script src="/shared/navbar.js" defer></script><script src="/js/account-safety.js" defer></script>--<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>++<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}});</script></body></html>`;}@@@ -1,4 +1,5 @@'use strict';++const pageLinks = require('../frame/page-links');// /sites: the front door to the whole network, for people and for crawlers (SEO-9). Every open entry is listed under// the same eight groups the home page and the menu use (server/frame/sites.js GROUPS), with its own tagline, and// each site is anchored at its registry id (/sites#live) so it can be linked directly. One source: the registry the@@@ -82,7 +83,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'<script src="/shared/navbar.js" defer></script><script src="/js/account-safety.js" defer></script>${require('../boost').SCRIPT}--<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>++<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}});</script></body></html>`;}@@@ -1,4 +1,5 @@'use strict';++const pageLinks = require('../frame/page-links');/*** Open stats (owner, 2026-10-27: "on the network site, we should publish the stats for all of the different platforms* and publish all the full analytics … make it all public for everyone to see"): every OpenVibe site's numbers on one@@@ -479,7 +480,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'<script src="/shared/navbar.js" defer></script><script src="/js/account-safety.js" defer></script>${require('../boost').SCRIPT}--<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>++<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}});</script></body></html>`;}@@@ -1,4 +1,5 @@'use strict';++const pageLinks = require('../frame/page-links');// The network's share card (SEO-21): a status link pasted in chat shows it.const SHARE_CARD = require('openvibe-shared/social').url('network', 'https://openvibe.network/shared');@@@ -240,7 +241,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'<script src="/shared/navbar.js" defer></script><script src="/js/account-safety.js" defer></script>${boost.SCRIPT}--<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>++<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}});</script></body></html>`;}@@@ -1,4 +1,5 @@'use strict';++const pageLinks = require('../frame/page-links');// The network's share card (SEO-21): link previews of this page show it, as the home and legal pages do.const SHARE_CARD = require('openvibe-shared/social').url('network', 'https://openvibe.network/shared');@@@ -217,7 +218,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'<script src="/shared/shipped.js" defer></script>${boost.SCRIPT}<script>document.addEventListener('ov:boost:load',function(){if(window.OpenVibeShipped)OpenVibeShipped.scan(document.getElementById('main'));});</script>--<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>++<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}});</script></body></html>`;}@@@ -82,7 +82,7 @@ let { html } = home.render();// The first HTML carries the site's navigation for a crawler; the browser navbar replaces it on boot.const serverNav = /<div id="navbar-mount"><nav class="openvibe-navbar"[^>]*data-ov-server-nav="true"[\s\S]*?<\/nav><\/div>/.exec(html);assert.ok(serverNav, 'the first HTML has the server navigation');--for (const [label, href] of [['My Account', '/my'], ['Wallet', '/wallet'], ['Themes', '/themes']]) {++for (const [label, href] of [['Network', '/'], ['Link', 'https://openvibe.link/'], ['My Account', 'https://openvibe.my/'], ['Wallet', '/wallet'], ['Themes', '/themes']]) {assert.ok(serverNav[0].includes(`href="${href}"`) && serverNav[0].includes(`>${label}</a>`), `the server nav links ${label}`);}assert.ok(/<form class="intent-bar" action="https:\/\/openvibe.tools\/search" method="get"/.test(html), 'the search works without JavaScript (a tool search)');@@@ -151,7 +151,7 @@ const SHAPED = {assert.ok(html.includes('docs/install.md'), 'the install guide still links the Link docs');assert.ok(html.includes('github.com/OpenVibers/OpenVibe.Link'), 'the GitHub source keeps its URL');assert.ok(html.includes('/shared/navbar.js'), 'the shared navbar is on the page');-- assert.ok(html.includes("label:'Link'"), 'the navbar calls it Link');++ assert.ok(html.includes('"label":"Link"'), 'the navbar calls it Link');assert.ok(html.includes('<link rel="canonical" href="https://openvibe.link/">'), 'the canonical is openvibe.link');assert.ok(html.includes('<meta property="og:url" content="https://openvibe.link/">'), 'og:url is openvibe.link');assert.ok(html.includes('"name":"OpenVibe Link"'), 'the JSON-LD names OpenVibe Link');@@@ -276,10 +276,10 @@ const SHAPED = {// ── The navbar and the home card point at the new name ──const index = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');-- assert.ok(index.includes("label: 'Link', href: 'https://openvibe.link', icon: 'fa-desktop', featured: true"), 'the home page\u2019s navbar leads with Link');-- assert.ok(index.indexOf("label: 'Link'") < index.indexOf("label: 'My Account'"), 'and Link comes first');++ assert.ok(index.includes('"label":"Link","href":"https://openvibe.link/"'), 'the home page\u2019s navbar names Link');++ assert.ok(index.indexOf('"label":"Link"') < index.indexOf('"label":"My Account"'), 'and Link comes before My Account');const wallet = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'wallet', 'common.js'), 'utf8');-- assert.ok(wallet.includes("label: 'Link', href: 'https://openvibe.link'"), 'the wallet pages\u2019 navbar does too');++ assert.ok(wallet.includes('"label":"Link","href":"https://openvibe.link/"'), 'the wallet pages\u2019 navbar does too');const home = fs.readFileSync(path.join(__dirname, '..', 'server', 'home', 'render.js'), 'utf8');assert.ok(home.includes("'OpenVibe Link: your machines, connected', url: 'https://openvibe.link/'"), 'Network\u2019s home card names Link and links it');assert.ok(home.includes("'Install Link', url: 'https://openvibe.link/install'"), 'and its install card does too');nnew file mode 100644@@@ -0,0 +1,109 @@++'use strict';++// Every host Network serves gives the shared navbar one list, on every page (the owner, 2026-11-12: the phone++// screenshot of openvibe.link showed "Pages on OpenVibe.Link: Home, My Account, Themes" — Network's fallback list,++// not Link's pages). The home row is the site's own name; a door to another site is labelled with that site's name.++// node test/page-links.test.js++const assert = require('assert');++const fs = require('fs');++const http = require('http');++const path = require('path');++const { bootServer } = require('./helpers/boot-server');++const pageLinks = require('../server/frame/page-links');++const { ACCOUNT_HUB_PATHS } = require('../server/not-found');++++const ROOT = path.join(__dirname, '..');++const read = (f) => fs.readFileSync(path.join(ROOT, f), 'utf8');++++/** The links array in a page's OpenVibeNavbar.init(...) call, or null when the init lives in a client script. */++function initLinks(html) {++ const at = html.indexOf('OpenVibeNavbar.init');++ if (at < 0) return null;++ const brace = html.indexOf('{', at);++ if (brace < 0) return null;++ let depth = 0, q = null, esc = false, end = -1;++ for (let i = brace; i < html.length && end < 0; i += 1) {++ const c = html[i];++ if (q) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === q) q = null; continue; }++ if (c === '"' || c === "'" || c === '`') { q = c; continue; }++ if (c === '{') depth += 1; else if (c === '}') { depth -= 1; if (!depth) end = i; }++ }++ if (end < 0) return null;++ const args = html.slice(brace, end + 1);++ const m = /(['"]?)links\1\s*:\s*(\[)/.exec(args);++ if (!m) return null;++ const start = brace + m.index + m[0].lastIndexOf('[');++ depth = 0; q = null; esc = false;++ for (let i = start; i < html.length; i += 1) {++ const c = html[i];++ if (q) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === q) q = null; continue; }++ if (c === '"' || c === "'" || c === '`') { q = c; continue; }++ if (c === '[') depth += 1; else if (c === ']') { depth -= 1; if (!depth) return JSON.parse(html.slice(start, i + 1)); }++ }++ return null;++}++++(async () => {++ // ── The lists themselves: the home row names the site, and each row is unique. ──++ for (const [host, list, home] of [['openvibe.network', pageLinks.NETWORK, 'Network'], ['openvibe.link', pageLinks.LINK, 'Link'], ['openvibe.my', pageLinks.MY, 'My Account']]) {++ assert.strictEqual(pageLinks.forHost(host), list, `${host} has its own list`);++ assert.ok(list.length >= 3, `${host} lists its pages`);++ assert.strictEqual(list[0].label, home, `${host}'s home row is the site's name, never "Home"`);++ assert.strictEqual(list[0].href, '/', `${host}'s home row is its front door`);++ assert.strictEqual(new Set(list.map((l) => l.label)).size, list.length, `${host} has no repeated label`);++ assert.ok(!list.some((l) => l.label === 'Home'), `${host} never calls its own home "Home"`);++ }++ assert.strictEqual(pageLinks.forHost('www.openvibe.link'), pageLinks.LINK, 'www uses the same list');++ assert.strictEqual(pageLinks.forHost('openvibe.network'), pageLinks.NETWORK, 'every other host is Network\'s');++++ // The hub's client-routed sections are the router's paths, not links it would 404.++ for (const p of pageLinks.MY.filter((l) => l.href.startsWith('/')).map((l) => l.href)) {++ assert.ok(p === '/' || ACCOUNT_HUB_PATHS.includes(p), `openvibe.my knows ${p}`);++ }++++ // Pages whose navbar is started from a client script carry the same list, byte for byte.++ for (const [file, list] of [++ ['public/index.html', pageLinks.NETWORK], ['public/verify-email.html', pageLinks.NETWORK],++ ['public/js/admin/boot.js', pageLinks.NETWORK], ['public/js/wallet/common.js', pageLinks.NETWORK],++ ['public/js/my/profile.js', pageLinks.NETWORK], ['public/js/my/profile.js', pageLinks.LINK], ['public/js/my/profile.js', pageLinks.MY],++ ]) assert.ok(read(file).includes(JSON.stringify(list)), `${file} sends the ${list[0].label} list`);++++ const srv = await bootServer();++ const port = new URL(srv.base).port;++ const get = (p, host) => new Promise((resolve, reject) => {++ const req = http.request({ host: '127.0.0.1', port, path: p, method: 'GET', headers: { host } }, (res) => {++ let body = '';++ res.on('data', (c) => { body += c; });++ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));++ });++ req.on('error', reject);++ req.end();++ });++ try {++ // Every server-rendered page of a host sends that host's list, and every same-host row answers (not 404).++ const pages = {++ 'openvibe.network': ['/', '/stats', '/updates', '/sites', '/bot', '/terms', '/privacy', '/dmca', '/no-such-page'],++ 'openvibe.link': ['/', '/no-such-page'],++ 'openvibe.my': ['/no-such-page'],++ };++ for (const [host, paths] of Object.entries(pages)) {++ for (const p of paths) {++ const r = await get(p, host);++ assert.ok([200, 404].includes(r.status), `${host}${p} answered ${r.status}`);++ const links = initLinks(r.body);++ assert.deepStrictEqual(links, pageLinks.forHost(host), `${host}${p} sends ${pageLinks.forHost(host)[0].label}'s list`);++ }++ for (const l of pageLinks.forHost(host)) {++ if (!l.href.startsWith('/')) continue;++ const r = await get(l.href, host);++ assert.notStrictEqual(r.status, 404, `${host}${l.href} is a route of this host`);++ assert.ok(r.status < 500, `${host}${l.href} does not fail (${r.status})`);++ }++ }++ console.log('page links: Network, Link and My send their own list on every page');++ } catch (err) {++ console.error(srv.logs());++ throw err;++ } finally {++ await srv.stop();++ }++})().then(() => process.exit(0)).catch((err) => { console.error(err); process.exit(1); });@@@ -12,7 +12,7 @@ const { spawn } = require('child_process');const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {-- htmlRawKB: 218, // 218: measured 217 KB (2026-11-11: Directory joins the network list on contracts 0.406.0, and the popular block draws its five site marks as inline SVG, this page having no icon font); 210.5: measured 209.9 KB with the popular block designed (2026-11-11: ranks, marks, numbers and its CSS, +0.9 KB); 209.5: measured 209 KB with openvibe-shared 4.190.0 (2026-11-10: the site name's dropdown, the footer's online strip, the balances pill); 206: measured 205.8 KB with openvibe-shared 4.185.0 (the navbar's links in the first HTML since 4.184.0), 2026-11-10; 205.4: measured 205.4 KB with the front door's five most popular destinations (plan L43, 2026-11-09: Report's ranked list adds about 1.2 KB of plain list markup); 204.2: measured 204.2 KB with openvibe-contracts 0.373.0 (the Git service in the network list) on openvibe-shared 4.173.0, 2026-11-09; 204: measured 203.5 KB with the shared page head's kit CSS on the home (openvibe-shared 4.163.0 + page head 1369721), 2026-11-09; 198.8 with openvibe-contracts 0.348.0 (2026-11-08: Studio joins the menu; Studio and Locker get menu lines); 197.4 with Email open (contracts 0.330.0, 2026-11-07: the mail site in the network list); 196 with the Node's own item in the navbar (openvibe-shared 4.117.0, 2026-11-06: the Node link and the llms.txt section); 194.9 with the Organization naming every open site in sameAs (SEO-27, 2026-11-03); 192.4 with Tours and Shoes open (contracts 0.265.0, 2026-10-31); 189.9 with Casino open (contracts 0.264.0, 2026-10-31); 189 with the look-boot script inline above the stylesheets on every page (L42, 2026-10-31: +1 KB); 188 with the agent layer's rel=alternate/describedby link tags on every public page (SEO-1/SEO-2, 2026-10-29: +0.2 KB); 187 with Bench open (contracts 0.238.0, 2026-10-28); 186 with Cam open (contracts 0.237.0, 2026-10-28); 184.4 with Fun open (contracts 0.236.0, 2026-10-28); 182.7 with Vision and Exchange open (contracts 0.235.0, 2026-10-28); 179 with Meme open (contracts 0.234.0, 2026-10-28); 174.1 with Domains, Report and Design open (contracts 0.226-0.228, 2026-10-28); 168.1 the hero's orbit and rich cards for live streams, recordings and screenshots (2026-10-28: the owner asked for animated visuals and YouTube-style cards); 152.4 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 147.9 with the home's FAQ (six questions and their FAQPage JSON-LD), the numbers panel and the service map's group headings (2026-10-22); 141.2 with Tips and VIP open (contracts 0.200.0, 37 sites; the Opening next section is gone); 134 with Play (contracts 0.172.1); 128.3 with Events, AI and Search added (31 sites, contracts 0.135.0); 122.3 (shared 2.21.0: a real glyph for every product card, where ten drew the 'V'); was 110.6 with 33 live sites (contracts 0.119.0). The home page lists every site, so it grows with the network++ htmlRawKB: 218.5, // 218.5: measured 218.1 KB with Network's own page list in the navbar (2026-11-12: the Network home row, and Link / My Account named as doors); 218: measured 217 KB (2026-11-11: Directory joins the network list on contracts 0.406.0, and the popular block draws its five site marks as inline SVG, this page having no icon font); 210.5: measured 209.9 KB with the popular block designed (2026-11-11: ranks, marks, numbers and its CSS, +0.9 KB); 209.5: measured 209 KB with openvibe-shared 4.190.0 (2026-11-10: the site name's dropdown, the footer's online strip, the balances pill); 206: measured 205.8 KB with openvibe-shared 4.185.0 (the navbar's links in the first HTML since 4.184.0), 2026-11-10; 205.4: measured 205.4 KB with the front door's five most popular destinations (plan L43, 2026-11-09: Report's ranked list adds about 1.2 KB of plain list markup); 204.2: measured 204.2 KB with openvibe-contracts 0.373.0 (the Git service in the network list) on openvibe-shared 4.173.0, 2026-11-09; 204: measured 203.5 KB with the shared page head's kit CSS on the home (openvibe-shared 4.163.0 + page head 1369721), 2026-11-09; 198.8 with openvibe-contracts 0.348.0 (2026-11-08: Studio joins the menu; Studio and Locker get menu lines); 197.4 with Email open (contracts 0.330.0, 2026-11-07: the mail site in the network list); 196 with the Node's own item in the navbar (openvibe-shared 4.117.0, 2026-11-06: the Node link and the llms.txt section); 194.9 with the Organization naming every open site in sameAs (SEO-27, 2026-11-03); 192.4 with Tours and Shoes open (contracts 0.265.0, 2026-10-31); 189.9 with Casino open (contracts 0.264.0, 2026-10-31); 189 with the look-boot script inline above the stylesheets on every page (L42, 2026-10-31: +1 KB); 188 with the agent layer's rel=alternate/describedby link tags on every public page (SEO-1/SEO-2, 2026-10-29: +0.2 KB); 187 with Bench open (contracts 0.238.0, 2026-10-28); 186 with Cam open (contracts 0.237.0, 2026-10-28); 184.4 with Fun open (contracts 0.236.0, 2026-10-28); 182.7 with Vision and Exchange open (contracts 0.235.0, 2026-10-28); 179 with Meme open (contracts 0.234.0, 2026-10-28); 174.1 with Domains, Report and Design open (contracts 0.226-0.228, 2026-10-28); 168.1 the hero's orbit and rich cards for live streams, recordings and screenshots (2026-10-28: the owner asked for animated visuals and YouTube-style cards); 152.4 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 147.9 with the home's FAQ (six questions and their FAQPage JSON-LD), the numbers panel and the service map's group headings (2026-10-22); 141.2 with Tips and VIP open (contracts 0.200.0, 37 sites; the Opening next section is gone); 134 with Play (contracts 0.172.1); 128.3 with Events, AI and Search added (31 sites, contracts 0.135.0); 122.3 (shared 2.21.0: a real glyph for every product card, where ten drew the 'V'); was 110.6 with 33 live sites (contracts 0.119.0). The home page lists every site, so it grows with the networkhtmlBrotliKB: 39.5, // 39.5: measured 39.3 KB brotli with openvibe-shared 4.190.0 (2026-11-10: the site name's dropdown, the footer's online strip, the balances pill); 39: measured 38.6 KB brotli with the shared page head's kit CSS on the home (openvibe-shared 4.163.0 + page head 1369721), 2026-11-09; 38.1 with openvibe-contracts 0.348.0 (2026-11-08: Studio joins the menu; Studio and Locker get menu lines); 37.9 with openvibe-shared 4.129.0 (2026-11-07: the one menu (the burger opens the OpenVibe menu)); 37.8 with openvibe-shared 4.129.0 (2026-11-07: the one menu (the burger opens the OpenVibe menu)); 36.1 with the Node's own item in the navbar (openvibe-shared 4.117.0, 2026-11-06); 35.5 with Tours and Shoes open (2026-10-31); 35.1 with the look-boot script inline above the stylesheets on every page (L42, 2026-10-31: +0.6 KB); 34.2 with the lower half as one block of chips on phones (2026-10-30); 33.1 with Games granted its Bench match capabilities (BENCH-7, 2026-10-29: the home lists what each service may do); 32.0 with Fun, Vision and Exchange open (contracts 0.236.0, 2026-10-28); 31.2 with Domains, Report and Design open (2026-10-28); 30.1 with the Organization saying who OpenVibe is (description, other names, what it is not, 15 sites; 2026-10-28); 28.5 with the FAQ, numbers and groups (2026-10-22); 26.1 with Play; 25.1 (31 sites, contracts 0.135.0); 24.1 (shared 2.21.0, the same glyphs); 20.5 (contracts 0.116.0)jsFiles: 8, // 7 with boost.js, was 6 (2026-10-13, plan T11 step 3: one more script on every boosted page; it loads route-transition.js on the first move only)jsRawKB: 450.5, // 450.5 openvibe-shared v4.214.0 (2026-11-12: measured 450.2, raised by pin-bump from 449.5); 449.5 openvibe-shared v4.211.0 (2026-11-11: measured 449.4, raised by pin-bump from 448.5); 448.5 openvibe-shared v4.205.0 (2026-11-11: measured 448.4, raised by pin-bump from 445); 445 openvibe-shared v4.202.0 (2026-11-11: measured 444.6, raised by pin-bump from 444); 444 openvibe-shared v4.201.0 (2026-11-11: measured 443.7, raised by pin-bump from 440.5); 440.5 openvibe-shared v4.200.0 (2026-11-10: measured 440.2, raised by pin-bump from 440.0); 440.0: measured 439.6 KB raw / 106.7 KB brotli with openvibe-shared 4.197.0 (2026-11-10: the menu's row of places, the You tab's row, the Sign in mark, the tooltip); 438.5: measured 438 KB raw / 106.5 KB brotli with openvibe-shared 4.190.0 (2026-11-10: the site name's dropdown, the footer's online strip, the balances pill); 421: measured 420.9 KB raw / 102.2 KB brotli with openvibe-shared 4.186.0 (the mark's Working and Thinking states, the Dock and the menu as one sheet, bare controls), 2026-11-10; 420.5: measured 419.9 KB raw / 101.9 KB brotli with openvibe-shared 4.185.0 (Calm reduces, the solids and site marks, the You tab, the Dock out of the orb), 2026-11-10; 417: measured 416.4 KB raw / 100.9 KB brotli with openvibe-shared 4.179.0, 2026-11-09; 410: measured 409.2 KB raw / 99.4 KB brotli with openvibe-shared 4.173.0 (the wallet's cookie mode), 2026-11-09; 409: measured 408.1 KB raw / 99.0 KB brotli after merging main's Frame feed work (openvibe-shared 4.167.0), 2026-11-09; 406: measured 405.7 KB raw / 98.4 KB brotli with the shared page head's kit script on the home (openvibe-shared 4.163.0 + page head 1369721), 2026-11-09; 381.8: measured 375.8 KB raw / 91.1 KB brotli with openvibe-shared 4.159.0, plus room for 4.160.0–4.161.0 (the navbar's slots loader, the keyboard watcher, the pop motion), 2026-11-09; 373 with openvibe-shared 4.153.0 (2026-11-08: signed in to the network, signed in here, on phones too; a page can turn away a carried card); 372.3 with openvibe-shared 4.152.0 (2026-11-08: the menu marks the page you are on and opens without the keyboard on a phone, floating cards follow you across OpenVibe sites (overlay.js), and the navbar preconnects to the next OpenVibe site on intent); 369.6 with openvibe-shared 4.150.0 (2026-11-08: the bell opens the Dock on Activity, the Dock's phone header is in two rows, and the menu's orb is the close button with an open/close animation); 368.6 with openvibe-shared 4.149.0 (2026-11-08: the menu has tabs at the top; the phone's menu button is the orb of 4.148.0); 366.5 with openvibe-shared 4.118.0 (2026-11-06: the Locker re-auth link, the 44 px footer and TLD targets; main's 4.117.0 navbar item and the account-safety helper included); about 363.8 with both (2026-11-06: openvibe-shared 4.117.0's featured navbar item, +1.7 KB, and the account-safety helper, +8.5 KB; merged, to be confirmed by CI's measure); 353.6 with the account-safety helper on every page (2026-11-06: the one re-authentication path and the scheduled-deletion banner, +8.5 KB); 345.1 with openvibe-shared 4.114.0 (2026-11-06: the one Share sheet in the navbar (ov-share.js and ov-qr.js load on the first press); navbar.js gains its click handler); 343.9 with openvibe-shared 4.112.1 (2026-11-06: theme-loader.js (4.111.1) and the footer/appbar phone CSS (4.112.1)); 342.3 with openvibe-shared 4.109.2 (2026-11-03: OpenVibe's own select, the navbar loads ov-forms); 339.6 with openvibe-shared 4.107.0 (2026-11-03: open.<tld> sites are named for their domain; the brand knows its front door); 337.8 with openvibe-shared 4.103.0 (2026-11-02: the puzzle glyph, two-line sidebar rows, boost lets parsed pages go, eight solids) (2026-11-02); 334.1 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 333.2 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 330.1 with openvibe-shared 4.52.0 (from 4.47.2): the seasonal fall, rich embeds, the Dock and its Chats tab (2026-10-30); 323.2 with openvibe-shared 4.47.2 (the comfort settings in theme-loader 4.45, touch targets 4.46, the grouped updates log 4.47.2); 313.7 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 306.4 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 298.9 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 293.1 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 279.1 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 269.3 with shared 4.11.0's seasonal navbar (Pumpkin Night and the season badge, +2.6); 249.8 with boost.js (13.1); 228.3
@@ -290,7 +290,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
@@ -290,7 +290,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
function initNavbar(user) {
function initNavbar(user) {
if (typeof OpenVibeNavbar !== 'undefined') {
if (typeof OpenVibeNavbar !== 'undefined') {
- OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: token || null, user: user || null, menu: { before: [{ id: 'ov-wallet', label: 'Wallet', short: 'Wallet', icon: 'fa-wallet', href: 'https://openvibe.network/wallet' }] }, links: [{ label: 'Link', href: 'https://openvibe.link', icon: 'fa-desktop', featured: true }, { label: 'My Account', href: '/my' }, { label: 'Wallet', href: '/wallet' }, { label: 'Themes', href: '/themes' }], networkLinks: 5 });
+ OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: token || null, user: user || null, menu: { before: [{ id: 'ov-wallet', label: 'Wallet', short: 'Wallet', icon: 'fa-wallet', href: 'https://openvibe.network/wallet' }] }, links: [{"label":"Network","href":"/"},{"label":"Link","href":"https://openvibe.link/","icon":"fa-desktop","featured":true},{"label":"My Account","href":"https://openvibe.my/"},{"label":"Wallet","href":"/wallet"},{"label":"Themes","href":"/themes"}], networkLinks: 5 });
}
}
if (user) {
if (user) {
document.getElementById('hero-guest-ctas').style.display = 'none';
document.getElementById('hero-guest-ctas').style.display = 'none';
@@ -4,6 +4,8 @@
@@ -4,6 +4,8 @@
admin/core.js's helpers (api, esc, toast, API, token) and on the
admin/core.js's helpers (api, esc, toast, API, token) and on the
tabLoaders map in admin/boot.js (loaded last). No ES modules.
tabLoaders map in admin/boot.js (loaded last). No ES modules.
═══════════════════════════════════════════════════════════════ */
═══════════════════════════════════════════════════════════════ */
+// Network's own page list: the shared navbar's fallback (Home, My Account, Themes) is not this host's list.
+const OV_PAGE_LINKS = [{"label":"Network","href":"/"},{"label":"Link","href":"https://openvibe.link/","icon":"fa-desktop","featured":true},{"label":"My Account","href":"https://openvibe.my/"},{"label":"Wallet","href":"/wallet"},{"label":"Themes","href":"/themes"}];
const tabLoaders = {
const tabLoaders = {
setup: loadSetup,
setup: loadSetup,
glossary: loadGlossary,
glossary: loadGlossary,
@@ -72,7 +74,7 @@ async function _bootAdmin() {
@@ -72,7 +74,7 @@ async function _bootAdmin() {
}
}
// Init navbar
// Init navbar
if (window.OpenVibeNavbar) {
if (window.OpenVibeNavbar) {
- OpenVibeNavbar.init({ service: 'network', token, user: currentUser });
+ OpenVibeNavbar.init({ service: 'network', token, user: currentUser, links: OV_PAGE_LINKS });
const bellMount = OpenVibeNavbar.getBellMount();
const bellMount = OpenVibeNavbar.getBellMount();
if (bellMount && window.OpenVibeNotifications) {
if (bellMount && window.OpenVibeNotifications) {
OpenVibeNotifications.init({ mountEl: bellMount, apiBase: API, token });
OpenVibeNotifications.init({ mountEl: bellMount, apiBase: API, token });
@@ -15,6 +15,15 @@ const OV_NAV_AUTH = MY_HOST ? {
@@ -15,6 +15,15 @@ const OV_NAV_AUTH = MY_HOST ? {
logoutUrl: '/auth/logout?next={path}',
logoutUrl: '/auth/logout?next={path}',
} : {};
} : {};
+// The page list this host's shared navbar shows. openvibe.my's own sections, openvibe.link's machines hub (this file
+// also renders /machines there), and Network's own pages everywhere else. Never the shared fallback: that is
+// Home, My Account, Themes on every host (the owner, 2026-11-12).
+const OV_PAGE_LINKS = MY_HOST
+ ? [{"label":"My Account","href":"/"},{"label":"Profile","href":"/profile"},{"label":"Themes","href":"/themes"},{"label":"Security","href":"/security"}]
+ : location.hostname.replace(/^www\./, '') === 'openvibe.link'
+ ? [{"label":"Link","href":"/","icon":"fa-desktop","featured":true},{"label":"Machines","href":"/machines","icon":"fa-laptop"},{"label":"Network","href":"https://openvibe.network/"},{"label":"My Account","href":"https://openvibe.my/"}]
+ : [{"label":"Network","href":"/"},{"label":"Link","href":"https://openvibe.link/","icon":"fa-desktop","featured":true},{"label":"My Account","href":"https://openvibe.my/"},{"label":"Wallet","href":"/wallet"},{"label":"Themes","href":"/themes"}];
+
// /my#delete is the return target of the re-authentication path (and a deep link): it opens the accounts
// /my#delete is the return target of the re-authentication path (and a deep link): it opens the accounts
// section and puts the cursor in the delete confirmation field. Read before core.js rewrites the hash.
// section and puts the cursor in the delete confirmation field. Read before core.js rewrites the hash.
const OV_DELETE_DEEP_LINK = window.location.hash === '#delete';
const OV_DELETE_DEEP_LINK = window.location.hash === '#delete';
@@ -85,7 +94,7 @@ function showSignedOutHub() {
@@ -85,7 +94,7 @@ function showSignedOutHub() {
card.append(heading, text, signIn, list, still);
card.append(heading, text, signIn, list, still);
page.replaceChildren(card);
page.replaceChildren(card);
try {
try {
- if (window.OpenVibeNavbar) OpenVibeNavbar.init({ service: 'network', token: null, user: null, menu: OV_WALLET_MENU, ...OV_NAV_AUTH });
+ if (window.OpenVibeNavbar) OpenVibeNavbar.init({ service: 'network', token: null, user: null, menu: OV_WALLET_MENU, links: OV_PAGE_LINKS, ...OV_NAV_AUTH });
} catch { /* the page still offers the buttons */ }
} catch { /* the page still offers the buttons */ }
}
}
@@ -120,7 +129,7 @@ async function loadUser() {
@@ -120,7 +129,7 @@ async function loadUser() {
applyAnonModeUI(currentUser);
applyAnonModeUI(currentUser);
loadAccounts();
loadAccounts();
loadAnonInfo();
loadAnonInfo();
- OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: null, user: currentUser, menu: OV_WALLET_MENU, ...OV_NAV_AUTH });
+ OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: null, user: currentUser, menu: OV_WALLET_MENU, links: OV_PAGE_LINKS, ...OV_NAV_AUTH });
} else {
} else {
const data = await apiFetch('/api/auth/me');
const data = await apiFetch('/api/auth/me');
currentUser = data.user;
currentUser = data.user;
@@ -145,7 +154,7 @@ async function loadUser() {
@@ -145,7 +154,7 @@ async function loadUser() {
loadRecentTools();
loadRecentTools();
// Init navbar
// Init navbar
- OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: authToken, user: currentUser, menu: OV_WALLET_MENU, ...OV_NAV_AUTH });
+ OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: authToken, user: currentUser, menu: OV_WALLET_MENU, links: OV_PAGE_LINKS, ...OV_NAV_AUTH });
const bellMount = OpenVibeNavbar.getBellMount();
const bellMount = OpenVibeNavbar.getBellMount();
if (bellMount && window.OpenVibeNotifications && authToken) {
if (bellMount && window.OpenVibeNotifications && authToken) {
OpenVibeNotifications.init({ token: authToken, apiBase: API, swPath: '/openvibe-sw.js' });
OpenVibeNotifications.init({ token: authToken, apiBase: API, swPath: '/openvibe-sw.js' });
@@ -96,7 +96,7 @@
@@ -96,7 +96,7 @@
root.OpenVibeNavbar.init({
root.OpenVibeNavbar.init({
service: 'network', notificationsRealtime: true, token: token(), user,
service: 'network', notificationsRealtime: true, token: token(), user,
menu: WALLET_MENU,
menu: WALLET_MENU,
- links: [{ label: 'Link', href: 'https://openvibe.link', icon: 'fa-desktop', featured: true }, { label: 'Wallet', href: '/wallet' }, { label: 'My Account', href: 'https://openvibe.my/' }],
+ links: [{"label":"Network","href":"/"},{"label":"Link","href":"https://openvibe.link/","icon":"fa-desktop","featured":true},{"label":"My Account","href":"https://openvibe.my/"},{"label":"Wallet","href":"/wallet"},{"label":"Themes","href":"/themes"}],
});
});
} catch { /* the page works without it */ }
} catch { /* the page works without it */ }
}
}
@@ -96,7 +96,7 @@ p{color:var(--text-secondary);font-size:14px;line-height:1.6}
@@ -96,7 +96,7 @@ p{color:var(--text-secondary);font-size:14px;line-height:1.6}
<script src="/js/account-safety.js" defer></script>
<script src="/js/account-safety.js" defer></script>
<script>
<script>
(function () {
(function () {
- if (window.OpenVibeNavbar) { try { OpenVibeNavbar.init({ service: 'network', apiBase: location.origin }); } catch (e) { /* */ } }
+ if (window.OpenVibeNavbar) { try { OpenVibeNavbar.init({ service: 'network', apiBase: location.origin, links: [{"label":"Network","href":"/"},{"label":"Link","href":"https://openvibe.link/","icon":"fa-desktop","featured":true},{"label":"My Account","href":"https://openvibe.my/"},{"label":"Wallet","href":"/wallet"},{"label":"Themes","href":"/themes"}] }); } catch (e) { /* */ } }
const card = document.getElementById('card'), ring = document.getElementById('ring'), ico = document.getElementById('ico');
const card = document.getElementById('card'), ring = document.getElementById('ring'), ico = document.getElementById('ico');
const title = document.getElementById('title'), msg = document.getElementById('msg'), actions = document.getElementById('actions'), hint = document.getElementById('hint');
const title = document.getElementById('title'), msg = document.getElementById('msg'), actions = document.getElementById('actions'), hint = document.getElementById('hint');
const burst = document.getElementById('burst');
const burst = document.getElementById('burst');
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'use strict';
+const pageLinks = require('../frame/page-links');
// OpenVibeBot (ADR 0007 in OpenVibe.Sources): the page its user agent links to — what it is, what it reads, what it obeys,
// OpenVibeBot (ADR 0007 in OpenVibe.Sources): the page its user agent links to — what it is, what it reads, what it obeys,
// and how a site blocks, slows or opts in. /bot.json says the same for machines. Static: no database, no outbound calls.
// and how a site blocks, slows or opts in. /bot.json says the same for machines. Static: no database, no outbound calls.
@@ -125,7 +126,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
@@ -125,7 +126,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
<script src="/shared/navbar.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/js/account-safety.js" defer></script>
<script src="/js/account-safety.js" defer></script>
${require('../boost').SCRIPT}
${require('../boost').SCRIPT}
-<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}
+<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}
document.querySelectorAll('.bp-copy').forEach(function(b){b.addEventListener('click',function(){var el=document.getElementById(b.getAttribute('data-copy'));if(!el||!navigator.clipboard)return;navigator.clipboard.writeText(el.textContent).then(function(){b.textContent='Copied';b.classList.add('is-done');setTimeout(function(){b.textContent='Copy';b.classList.remove('is-done');},1600);},function(){});});});});</script>
document.querySelectorAll('.bp-copy').forEach(function(b){b.addEventListener('click',function(){var el=document.getElementById(b.getAttribute('data-copy'));if(!el||!navigator.clipboard)return;navigator.clipboard.writeText(el.textContent).then(function(){b.textContent='Copied';b.classList.add('is-done');setTimeout(function(){b.textContent='Copy';b.classList.remove('is-done');},1600);},function(){});});});});</script>
</body>
</body>
</html>`;
</html>`;
new file mode 100644
new file mode 100644
@@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
+'use strict';
+// The page list each host gives the shared navbar's "Pages on <site>" menu. Every page of a host sends the same
+// list, and each row is a page of that host or a door named for where it goes (the owner, 2026-11-12:
+// "Pages on OpenVibe.Link: Home, My Account, Themes" was Network's list on Link's menu, not Link's).
+//
+// Network serves four hosts. openvibe.network, openvibe.link and openvibe.my read these lists here; Computer has
+// its own server. A page that passes no links gets the shared navbar's fallback (Home, My Account, Themes), which
+// is why every Network page passes the host's list explicitly.
+const NETWORK = Object.freeze([
+ { label: 'Network', href: '/' },
+ { label: 'Link', href: 'https://openvibe.link/', icon: 'fa-desktop', featured: true },
+ { label: 'My Account', href: 'https://openvibe.my/' },
+ { label: 'Wallet', href: '/wallet' },
+ { label: 'Themes', href: '/themes' },
+]);
+
+const LINK = Object.freeze([
+ { label: 'Link', href: '/', icon: 'fa-desktop', featured: true },
+ { label: 'Machines', href: '/machines', icon: 'fa-laptop' },
+ { label: 'Network', href: 'https://openvibe.network/' },
+ { label: 'My Account', href: 'https://openvibe.my/' },
+]);
+
+const MY = Object.freeze([
+ { label: 'My Account', href: '/' },
+ { label: 'Profile', href: '/profile' },
+ { label: 'Themes', href: '/themes' },
+ { label: 'Security', href: '/security' },
+]);
+
+const HOSTS = new Set(['openvibe.network', 'www.openvibe.network', 'openvibe.link', 'www.openvibe.link', 'openvibe.my', 'www.openvibe.my']);
+
+/** This request's host, lowercased, no port, no www. */
+function hostOf(req) {
+ return String((req && (req.hostname || (req.headers && req.headers.host))) || '').split(':')[0].toLowerCase().replace(/^www\./, '');
+}
+
+/** The page list of the host Network is answering: Link and My have their own; everything else is Network's. */
+function forHost(host) {
+ const h = String(host || '').split(':')[0].toLowerCase().replace(/^www\./, '');
+ if (h === 'openvibe.link') return LINK;
+ if (h === 'openvibe.my') return MY;
+ return NETWORK;
+}
+
+/** A JS expression for a page that is cached across hosts: the same choice, made in the browser. */
+function browserList() {
+ const host = "location.hostname.replace(/^www\\./, '')";
+ return `(${host} === 'openvibe.link' ? ${JSON.stringify(LINK)} : ${host} === 'openvibe.my' ? ${JSON.stringify(MY)} : ${JSON.stringify(NETWORK)})`;
+}
+
+module.exports = { NETWORK, LINK, MY, HOSTS, hostOf, forHost, browserList };
@@ -27,6 +27,7 @@ const icon = (name, size) => `<span class="ov-icon" data-icon="${esc(name)}" dat
@@ -27,6 +27,7 @@ const icon = (name, size) => `<span class="ov-icon" data-icon="${esc(name)}" dat
// public domain serves the service itself (Network's exposure overlay), so the home page, the navbar and
// public domain serves the service itself (Network's exposure overlay), so the home page, the navbar and
// "Opening next" always agree with what is actually running.
// "Opening next" always agree with what is actually running.
const { LISTED, GROUPS, groupOf, fullName } = require('../frame/sites');
const { LISTED, GROUPS, groupOf, fullName } = require('../frame/sites');
+const pageLinks = require('../frame/page-links');
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const social = require('openvibe-shared/social');
const social = require('openvibe-shared/social');
// OpenRestream is its own name, never "OpenVibe.OpenRestream" (the owner, 2026-10-28).
// OpenRestream is its own name, never "OpenVibe.OpenRestream" (the owner, 2026-10-28).
@@ -540,7 +541,7 @@ function render({ releaseId = '' } = {}) {
@@ -540,7 +541,7 @@ function render({ releaseId = '' } = {}) {
})) + seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what })))) + seo.jsonLdTag(seo.jsonLd.faq(faqPairs()));
})) + seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what })))) + seo.jsonLdTag(seo.jsonLd.faq(faqPairs()));
const card = social.url('network', 'https://openvibe.network/shared') || 'https://openvibe.network/assets/logo-512.png';
const card = social.url('network', 'https://openvibe.network/shared') || 'https://openvibe.network/assets/logo-512.png';
const summary = summaryOf(catalog);
const summary = summaryOf(catalog);
- const html = withSameAs(lookBoot.apply(fs.readFileSync(SHELL, 'utf8'))).replace('<div id="navbar-mount"></div>', require('openvibe-shared/frame').serverNav({ name: 'OpenVibe.Network', service: 'network', home: '/', links: [{ label: 'Link', href: 'https://openvibe.link/' }, { label: 'My Account', href: '/my' }, { label: 'Wallet', href: '/wallet' }, { label: 'Themes', href: '/themes' }] }) + summary.body).replace('<h1>Go live, chat, play<br><span class="grad">and get things done.</span></h1>', () => kit.pageHead({ title: 'Go live, chat, play and get things done.', icon: 'home', lede: 'Your places, people and projects across OpenVibe.' })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog, act.popularity)).replace('<!--OV:CONSTELLATION-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(SITE_COUNT)).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n<link rel="stylesheet" href="${require('openvibe-shared/serve').url('kit.css')}"><script src="${require('openvibe-shared/serve').url('kit-web.js')}" defer></script>\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);
+ const html = withSameAs(lookBoot.apply(fs.readFileSync(SHELL, 'utf8'))).replace('<div id="navbar-mount"></div>', require('openvibe-shared/frame').serverNav({ name: 'OpenVibe.Network', service: 'network', home: '/', links: pageLinks.NETWORK }) + summary.body).replace('<h1>Go live, chat, play<br><span class="grad">and get things done.</span></h1>', () => kit.pageHead({ title: 'Go live, chat, play and get things done.', icon: 'home', lede: 'Your places, people and projects across OpenVibe.' })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog, act.popularity)).replace('<!--OV:CONSTELLATION-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(SITE_COUNT)).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n<link rel="stylesheet" href="${require('openvibe-shared/serve').url('kit.css')}"><script src="${require('openvibe-shared/serve').url('kit-web.js')}" defer></script>\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);
pageCache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };
pageCache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };
return pageCache;
return pageCache;
}
}
@@ -985,12 +985,17 @@ function requireAdmin(req, res, next) {
@@ -985,12 +985,17 @@ function requireAdmin(req, res, next) {
// is added to the head here, and the small Twitter card they declare becomes the large one that image needs.
// is added to the head here, and the small Twitter card they declare becomes the large one that image needs.
{
{
const legal = require('openvibe-shared/legal');
const legal = require('openvibe-shared/legal');
+ const pageLinks = require('./frame/page-links');
const card = require('openvibe-shared/social').url('network', 'https://openvibe.network/shared');
const card = require('openvibe-shared/social').url('network', 'https://openvibe.network/shared');
const tags = `<meta property="og:image" content="${card}"><meta property="og:image:alt" content="OpenVibe.Network: one account for every OpenVibe site"><meta name="twitter:image" content="${card}">`;
const tags = `<meta property="og:image" content="${card}"><meta property="og:image:alt" content="OpenVibe.Network: one account for every OpenVibe site"><meta name="twitter:image" content="${card}">`;
+ // Shared's legal template draws the navbar without links, so its fallback (Home, My Account, Themes) was the
+ // Network list on /terms, /privacy and /dmca. Add this host's own list to the init call (Shared stays untouched).
+ const navLinks = JSON.stringify(pageLinks.NETWORK);
app.get(legal.PATHS, (req, res, next) => {
app.get(legal.PATHS, (req, res, next) => {
const send = res.send.bind(res);
const send = res.send.bind(res);
res.send = (body) => send(String(body)
res.send = (body) => send(String(body)
.replace('<meta name="twitter:card" content="summary">', '<meta name="twitter:card" content="summary_large_image">')
.replace('<meta name="twitter:card" content="summary">', '<meta name="twitter:card" content="summary_large_image">')
+ .replace('try{OpenVibeNavbar.init({service:', `try{OpenVibeNavbar.init({links:${navLinks},service:`)
.replace('</head>', `${tags}</head>`));
.replace('</head>', `${tags}</head>`));
next();
next();
}, legal.handler({ id: 'network', service: 'network', host: 'openvibe.network', name: 'OpenVibe.Network', profile: 'account' }));
}, legal.handler({ id: 'network', service: 'network', host: 'openvibe.network', name: 'OpenVibe.Network', profile: 'account' }));
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'use strict';
+const pageLinks = require('../frame/page-links');
// openvibe.link: OpenVibe Link's own front door (the machine agent). What Link is (your machines, connected), what it
// openvibe.link: OpenVibe Link's own front door (the machine agent). What Link is (your machines, connected), what it
// does today, how to install it on each OS, and what it never does. The chrome is Network's own; the page is Link's.
// does today, how to install it on each OS, and what it never does. The chrome is Network's own; the page is Link's.
// The version, the release date and the checksums come from Link's latest GitHub release (server/link/release.js).
// The version, the release date and the checksums come from Link's latest GitHub release (server/link/release.js).
@@ -242,7 +243,7 @@ ${require('../boost').SCRIPT}
@@ -242,7 +243,7 @@ ${require('../boost').SCRIPT}
<script src="/shared/kit-web.js" defer></script>
<script src="/shared/kit-web.js" defer></script>
<script type="module" src="/send.js"></script>
<script type="module" src="/send.js"></script>
<script src="/egress-settings.js" defer></script>
<script src="/egress-settings.js" defer></script>
-<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',sessionUrl:'/auth/me',loginUrl:'/auth/login?next={path}',logoutUrl:'/auth/logout?next={url}',silentLogin:'/auth/login?silent=1&next={url}',links:[{label:'Link',href:'/',icon:'fa-desktop',featured:true,active:true},{label:'Machines',href:'/machines',icon:'fa-laptop'},{label:'Network',href:'https://openvibe.network/'},{label:'My Account',href:'https://openvibe.network/my'}]});}catch(e){}});</script>
+<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',sessionUrl:'/auth/me',loginUrl:'/auth/login?next={path}',logoutUrl:'/auth/logout?next={url}',silentLogin:'/auth/login?silent=1&next={url}',links:${JSON.stringify(pageLinks.LINK)}});}catch(e){}});</script>
</body>
</body>
</html>`;
</html>`;
}
}
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'use strict';
+const pageLinks = require('./frame/page-links');
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
// Unknown paths answer 404 (roadmap §2.5 crawlability, D44).
// Unknown paths answer 404 (roadmap §2.5 crawlability, D44).
//
//
@@ -16,16 +17,17 @@ const ACCOUNT_HUB_PATHS = ['/my', '/my.html', '/themes', '/notifications', '/lin
@@ -16,16 +17,17 @@ const ACCOUNT_HUB_PATHS = ['/my', '/my.html', '/themes', '/notifications', '/lin
const JSON_PREFIXES = ['/api/', '/internal/', '/oauth/'];
const JSON_PREFIXES = ['/api/', '/internal/', '/oauth/'];
-let cached = null;
-function page() {
- if (cached) return cached;
+const cached = new Map();
+function page(host = 'openvibe.network') {
+ const key = pageLinks.hostOf({ hostname: host });
+ if (cached.has(key)) return cached.get(key);
let footer = '';
let footer = '';
let icons = '';
let icons = '';
let nav = '';
let nav = '';
try { footer = require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' }); } catch { /* optional */ }
try { footer = require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' }); } catch { /* optional */ }
try { icons = require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' }); } catch { /* optional */ }
try { icons = require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' }); } catch { /* optional */ }
try { nav = require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }] }); } catch { /* optional */ }
try { nav = require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }] }); } catch { /* optional */ }
- cached = `<!DOCTYPE html>
+ const html = `<!DOCTYPE html>
<html lang="en">
<html lang="en">
<head>
<head>
<meta charset="utf-8">
<meta charset="utf-8">
@@ -64,10 +66,11 @@ ${nav}
@@ -64,10 +66,11 @@ ${nav}
${footer}
${footer}
<script src="/shared/navbar.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/js/account-safety.js" defer></script>
<script src="/js/account-safety.js" defer></script>
-<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>
+<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.forHost(key))}});}catch(e){}});</script>
</body>
</body>
</html>`;
</html>`;
- return cached;
+ cached.set(key, html);
+ return html;
}
}
/** Last handler of the app: every method, every path nothing else answered. */
/** Last handler of the app: every method, every path nothing else answered. */
@@ -76,7 +79,7 @@ function notFound(req, res) {
@@ -76,7 +79,7 @@ function notFound(req, res) {
res.set('Cache-Control', 'no-cache');
res.set('Cache-Control', 'no-cache');
if (JSON_PREFIXES.some(p => req.path.startsWith(p))) return res.json({ error: 'Not found' });
if (JSON_PREFIXES.some(p => req.path.startsWith(p))) return res.json({ error: 'Not found' });
res.set('X-Robots-Tag', 'noindex');
res.set('X-Robots-Tag', 'noindex');
- return res.type('html').send(page());
+ return res.type('html').send(page(pageLinks.hostOf(req)));
}
}
module.exports = { notFound, page, ACCOUNT_HUB_PATHS };
module.exports = { notFound, page, ACCOUNT_HUB_PATHS };
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'use strict';
+const pageLinks = require('../frame/page-links');
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
// Public profiles (plan T21 step 3): one page per person on the Network, and the same data as JSON for every site.
// Public profiles (plan T21 step 3): one page per person on the Network, and the same data as JSON for every site.
//
//
@@ -613,7 +614,7 @@ ${isPublic ? showcaseHtml(inv, name) + badgesHtml(inv) + gamesHtml(inv) + itemsH
@@ -613,7 +614,7 @@ ${isPublic ? showcaseHtml(inv, name) + badgesHtml(inv) + gamesHtml(inv) + itemsH
${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' })}
${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' })}
<script src="/shared/navbar.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/js/account-safety.js" defer></script>
<script src="/js/account-safety.js" defer></script>
-<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>
+<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}});</script>
</body>
</body>
</html>`;
</html>`;
}
}
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'use strict';
+const pageLinks = require('../frame/page-links');
// /sites: the front door to the whole network, for people and for crawlers (SEO-9). Every open entry is listed under
// /sites: the front door to the whole network, for people and for crawlers (SEO-9). Every open entry is listed under
// the same eight groups the home page and the menu use (server/frame/sites.js GROUPS), with its own tagline, and
// the same eight groups the home page and the menu use (server/frame/sites.js GROUPS), with its own tagline, and
// each site is anchored at its registry id (/sites#live) so it can be linked directly. One source: the registry the
// each site is anchored at its registry id (/sites#live) so it can be linked directly. One source: the registry the
@@ -82,7 +83,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
@@ -82,7 +83,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
<script src="/shared/navbar.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/js/account-safety.js" defer></script>
<script src="/js/account-safety.js" defer></script>
${require('../boost').SCRIPT}
${require('../boost').SCRIPT}
-<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>
+<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}});</script>
</body>
</body>
</html>`;
</html>`;
}
}
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'use strict';
+const pageLinks = require('../frame/page-links');
/**
/**
* Open stats (owner, 2026-10-27: "on the network site, we should publish the stats for all of the different platforms
* Open stats (owner, 2026-10-27: "on the network site, we should publish the stats for all of the different platforms
* and publish all the full analytics … make it all public for everyone to see"): every OpenVibe site's numbers on one
* and publish all the full analytics … make it all public for everyone to see"): every OpenVibe site's numbers on one
@@ -479,7 +480,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
@@ -479,7 +480,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
<script src="/shared/navbar.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/js/account-safety.js" defer></script>
<script src="/js/account-safety.js" defer></script>
${require('../boost').SCRIPT}
${require('../boost').SCRIPT}
-<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>
+<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}});</script>
</body>
</body>
</html>`;
</html>`;
}
}
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'use strict';
+const pageLinks = require('../frame/page-links');
// The network's share card (SEO-21): a status link pasted in chat shows it.
// The network's share card (SEO-21): a status link pasted in chat shows it.
const SHARE_CARD = require('openvibe-shared/social').url('network', 'https://openvibe.network/shared');
const SHARE_CARD = require('openvibe-shared/social').url('network', 'https://openvibe.network/shared');
@@ -240,7 +241,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
@@ -240,7 +241,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
<script src="/shared/navbar.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/js/account-safety.js" defer></script>
<script src="/js/account-safety.js" defer></script>
${boost.SCRIPT}
${boost.SCRIPT}
-<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>
+<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}});</script>
</body>
</body>
</html>`;
</html>`;
}
}
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'use strict';
+const pageLinks = require('../frame/page-links');
// The network's share card (SEO-21): link previews of this page show it, as the home and legal pages do.
// The network's share card (SEO-21): link previews of this page show it, as the home and legal pages do.
const SHARE_CARD = require('openvibe-shared/social').url('network', 'https://openvibe.network/shared');
const SHARE_CARD = require('openvibe-shared/social').url('network', 'https://openvibe.network/shared');
@@ -217,7 +218,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
@@ -217,7 +218,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
<script src="/shared/shipped.js" defer></script>
<script src="/shared/shipped.js" defer></script>
${boost.SCRIPT}
${boost.SCRIPT}
<script>document.addEventListener('ov:boost:load',function(){if(window.OpenVibeShipped)OpenVibeShipped.scan(document.getElementById('main'));});</script>
<script>document.addEventListener('ov:boost:load',function(){if(window.OpenVibeShipped)OpenVibeShipped.scan(document.getElementById('main'));});</script>
-<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>
+<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin,links:${JSON.stringify(pageLinks.NETWORK)}});}catch(e){}});</script>
</body>
</body>
</html>`;
</html>`;
}
}
@@ -82,7 +82,7 @@ let { html } = home.render();
@@ -82,7 +82,7 @@ let { html } = home.render();
// The first HTML carries the site's navigation for a crawler; the browser navbar replaces it on boot.
// The first HTML carries the site's navigation for a crawler; the browser navbar replaces it on boot.
const serverNav = /<div id="navbar-mount"><nav class="openvibe-navbar"[^>]*data-ov-server-nav="true"[\s\S]*?<\/nav><\/div>/.exec(html);
const serverNav = /<div id="navbar-mount"><nav class="openvibe-navbar"[^>]*data-ov-server-nav="true"[\s\S]*?<\/nav><\/div>/.exec(html);
assert.ok(serverNav, 'the first HTML has the server navigation');
assert.ok(serverNav, 'the first HTML has the server navigation');
-for (const [label, href] of [['My Account', '/my'], ['Wallet', '/wallet'], ['Themes', '/themes']]) {
+for (const [label, href] of [['Network', '/'], ['Link', 'https://openvibe.link/'], ['My Account', 'https://openvibe.my/'], ['Wallet', '/wallet'], ['Themes', '/themes']]) {
assert.ok(serverNav[0].includes(`href="${href}"`) && serverNav[0].includes(`>${label}</a>`), `the server nav links ${label}`);
assert.ok(serverNav[0].includes(`href="${href}"`) && serverNav[0].includes(`>${label}</a>`), `the server nav links ${label}`);
}
}
assert.ok(/<form class="intent-bar" action="https:\/\/openvibe.tools\/search" method="get"/.test(html), 'the search works without JavaScript (a tool search)');
assert.ok(/<form class="intent-bar" action="https:\/\/openvibe.tools\/search" method="get"/.test(html), 'the search works without JavaScript (a tool search)');
@@ -151,7 +151,7 @@ const SHAPED = {
@@ -151,7 +151,7 @@ const SHAPED = {
assert.ok(html.includes('docs/install.md'), 'the install guide still links the Link docs');
assert.ok(html.includes('docs/install.md'), 'the install guide still links the Link docs');
assert.ok(html.includes('github.com/OpenVibers/OpenVibe.Link'), 'the GitHub source keeps its URL');
assert.ok(html.includes('github.com/OpenVibers/OpenVibe.Link'), 'the GitHub source keeps its URL');
assert.ok(html.includes('/shared/navbar.js'), 'the shared navbar is on the page');
assert.ok(html.includes('/shared/navbar.js'), 'the shared navbar is on the page');
- assert.ok(html.includes("label:'Link'"), 'the navbar calls it Link');
+ assert.ok(html.includes('"label":"Link"'), 'the navbar calls it Link');
assert.ok(html.includes('<link rel="canonical" href="https://openvibe.link/">'), 'the canonical is openvibe.link');
assert.ok(html.includes('<link rel="canonical" href="https://openvibe.link/">'), 'the canonical is openvibe.link');
assert.ok(html.includes('<meta property="og:url" content="https://openvibe.link/">'), 'og:url is openvibe.link');
assert.ok(html.includes('<meta property="og:url" content="https://openvibe.link/">'), 'og:url is openvibe.link');
assert.ok(html.includes('"name":"OpenVibe Link"'), 'the JSON-LD names OpenVibe Link');
assert.ok(html.includes('"name":"OpenVibe Link"'), 'the JSON-LD names OpenVibe Link');
@@ -276,10 +276,10 @@ const SHAPED = {
@@ -276,10 +276,10 @@ const SHAPED = {
// ── The navbar and the home card point at the new name ──
// ── The navbar and the home card point at the new name ──
const index = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');
const index = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');
- assert.ok(index.includes("label: 'Link', href: 'https://openvibe.link', icon: 'fa-desktop', featured: true"), 'the home page\u2019s navbar leads with Link');
- assert.ok(index.indexOf("label: 'Link'") < index.indexOf("label: 'My Account'"), 'and Link comes first');
+ assert.ok(index.includes('"label":"Link","href":"https://openvibe.link/"'), 'the home page\u2019s navbar names Link');
+ assert.ok(index.indexOf('"label":"Link"') < index.indexOf('"label":"My Account"'), 'and Link comes before My Account');
const wallet = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'wallet', 'common.js'), 'utf8');
const wallet = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'wallet', 'common.js'), 'utf8');
- assert.ok(wallet.includes("label: 'Link', href: 'https://openvibe.link'"), 'the wallet pages\u2019 navbar does too');
+ assert.ok(wallet.includes('"label":"Link","href":"https://openvibe.link/"'), 'the wallet pages\u2019 navbar does too');
const home = fs.readFileSync(path.join(__dirname, '..', 'server', 'home', 'render.js'), 'utf8');
const home = fs.readFileSync(path.join(__dirname, '..', 'server', 'home', 'render.js'), 'utf8');
assert.ok(home.includes("'OpenVibe Link: your machines, connected', url: 'https://openvibe.link/'"), 'Network\u2019s home card names Link and links it');
assert.ok(home.includes("'OpenVibe Link: your machines, connected', url: 'https://openvibe.link/'"), 'Network\u2019s home card names Link and links it');
assert.ok(home.includes("'Install Link', url: 'https://openvibe.link/install'"), 'and its install card does too');
assert.ok(home.includes("'Install Link', url: 'https://openvibe.link/install'"), 'and its install card does too');
new file mode 100644
new file mode 100644
@@ -0,0 +1,109 @@
@@ -0,0 +1,109 @@
+'use strict';
+// Every host Network serves gives the shared navbar one list, on every page (the owner, 2026-11-12: the phone
+// screenshot of openvibe.link showed "Pages on OpenVibe.Link: Home, My Account, Themes" — Network's fallback list,
+// not Link's pages). The home row is the site's own name; a door to another site is labelled with that site's name.
+// node test/page-links.test.js
+const assert = require('assert');
+const fs = require('fs');
+const http = require('http');
+const path = require('path');
+const { bootServer } = require('./helpers/boot-server');
+const pageLinks = require('../server/frame/page-links');
+const { ACCOUNT_HUB_PATHS } = require('../server/not-found');
+
+const ROOT = path.join(__dirname, '..');
+const read = (f) => fs.readFileSync(path.join(ROOT, f), 'utf8');
+
+/** The links array in a page's OpenVibeNavbar.init(...) call, or null when the init lives in a client script. */
+function initLinks(html) {
+ const at = html.indexOf('OpenVibeNavbar.init');
+ if (at < 0) return null;
+ const brace = html.indexOf('{', at);
+ if (brace < 0) return null;
+ let depth = 0, q = null, esc = false, end = -1;
+ for (let i = brace; i < html.length && end < 0; i += 1) {
+ const c = html[i];
+ if (q) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === q) q = null; continue; }
+ if (c === '"' || c === "'" || c === '`') { q = c; continue; }
+ if (c === '{') depth += 1; else if (c === '}') { depth -= 1; if (!depth) end = i; }
+ }
+ if (end < 0) return null;
+ const args = html.slice(brace, end + 1);
+ const m = /(['"]?)links\1\s*:\s*(\[)/.exec(args);
+ if (!m) return null;
+ const start = brace + m.index + m[0].lastIndexOf('[');
+ depth = 0; q = null; esc = false;
+ for (let i = start; i < html.length; i += 1) {
+ const c = html[i];
+ if (q) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === q) q = null; continue; }
+ if (c === '"' || c === "'" || c === '`') { q = c; continue; }
+ if (c === '[') depth += 1; else if (c === ']') { depth -= 1; if (!depth) return JSON.parse(html.slice(start, i + 1)); }
+ }
+ return null;
+}
+
+(async () => {
+ // ── The lists themselves: the home row names the site, and each row is unique. ──
+ for (const [host, list, home] of [['openvibe.network', pageLinks.NETWORK, 'Network'], ['openvibe.link', pageLinks.LINK, 'Link'], ['openvibe.my', pageLinks.MY, 'My Account']]) {
+ assert.strictEqual(pageLinks.forHost(host), list, `${host} has its own list`);
+ assert.ok(list.length >= 3, `${host} lists its pages`);
+ assert.strictEqual(list[0].label, home, `${host}'s home row is the site's name, never "Home"`);
+ assert.strictEqual(list[0].href, '/', `${host}'s home row is its front door`);
+ assert.strictEqual(new Set(list.map((l) => l.label)).size, list.length, `${host} has no repeated label`);
+ assert.ok(!list.some((l) => l.label === 'Home'), `${host} never calls its own home "Home"`);
+ }
+ assert.strictEqual(pageLinks.forHost('www.openvibe.link'), pageLinks.LINK, 'www uses the same list');
+ assert.strictEqual(pageLinks.forHost('openvibe.network'), pageLinks.NETWORK, 'every other host is Network\'s');
+
+ // The hub's client-routed sections are the router's paths, not links it would 404.
+ for (const p of pageLinks.MY.filter((l) => l.href.startsWith('/')).map((l) => l.href)) {
+ assert.ok(p === '/' || ACCOUNT_HUB_PATHS.includes(p), `openvibe.my knows ${p}`);
+ }
+
+ // Pages whose navbar is started from a client script carry the same list, byte for byte.
+ for (const [file, list] of [
+ ['public/index.html', pageLinks.NETWORK], ['public/verify-email.html', pageLinks.NETWORK],
+ ['public/js/admin/boot.js', pageLinks.NETWORK], ['public/js/wallet/common.js', pageLinks.NETWORK],
+ ['public/js/my/profile.js', pageLinks.NETWORK], ['public/js/my/profile.js', pageLinks.LINK], ['public/js/my/profile.js', pageLinks.MY],
+ ]) assert.ok(read(file).includes(JSON.stringify(list)), `${file} sends the ${list[0].label} list`);
+
+ const srv = await bootServer();
+ const port = new URL(srv.base).port;
+ const get = (p, host) => new Promise((resolve, reject) => {
+ const req = http.request({ host: '127.0.0.1', port, path: p, method: 'GET', headers: { host } }, (res) => {
+ let body = '';
+ res.on('data', (c) => { body += c; });
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
+ });
+ req.on('error', reject);
+ req.end();
+ });
+ try {
+ // Every server-rendered page of a host sends that host's list, and every same-host row answers (not 404).
+ const pages = {
+ 'openvibe.network': ['/', '/stats', '/updates', '/sites', '/bot', '/terms', '/privacy', '/dmca', '/no-such-page'],
+ 'openvibe.link': ['/', '/no-such-page'],
+ 'openvibe.my': ['/no-such-page'],
+ };
+ for (const [host, paths] of Object.entries(pages)) {
+ for (const p of paths) {
+ const r = await get(p, host);
+ assert.ok([200, 404].includes(r.status), `${host}${p} answered ${r.status}`);
+ const links = initLinks(r.body);
+ assert.deepStrictEqual(links, pageLinks.forHost(host), `${host}${p} sends ${pageLinks.forHost(host)[0].label}'s list`);
+ }
+ for (const l of pageLinks.forHost(host)) {
+ if (!l.href.startsWith('/')) continue;
+ const r = await get(l.href, host);
+ assert.notStrictEqual(r.status, 404, `${host}${l.href} is a route of this host`);
+ assert.ok(r.status < 500, `${host}${l.href} does not fail (${r.status})`);
+ }
+ }
+ console.log('page links: Network, Link and My send their own list on every page');
+ } catch (err) {
+ console.error(srv.logs());
+ throw err;
+ } finally {
+ await srv.stop();
+ }
+})().then(() => process.exit(0)).catch((err) => { console.error(err); process.exit(1); });
@@ -12,7 +12,7 @@ const { spawn } = require('child_process');
@@ -12,7 +12,7 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
const BUDGETS = {
- htmlRawKB: 218, // 218: measured 217 KB (2026-11-11: Directory joins the network list on contracts 0.406.0, and the popular block draws its five site marks as inline SVG, this page having no icon font); 210.5: measured 209.9 KB with the popular block designed (2026-11-11: ranks, marks, numbers and its CSS, +0.9 KB); 209.5: measured 209 KB with openvibe-shared 4.190.0 (2026-11-10: the site name's dropdown, the footer's online strip, the balances pill); 206: measured 205.8 KB with openvibe-shared 4.185.0 (the navbar's links in the first HTML since 4.184.0), 2026-11-10; 205.4: measured 205.4 KB with the front door's five most popular destinations (plan L43, 2026-11-09: Report's ranked list adds about 1.2 KB of plain list markup); 204.2: measured 204.2 KB with openvibe-contracts 0.373.0 (the Git service in the network list) on openvibe-shared 4.173.0, 2026-11-09; 204: measured 203.5 KB with the shared page head's kit CSS on the home (openvibe-shared 4.163.0 + page head 1369721), 2026-11-09; 198.8 with openvibe-contracts 0.348.0 (2026-11-08: Studio joins the menu; Studio and Locker get menu lines); 197.4 with Email open (contracts 0.330.0, 2026-11-07: the mail site in the network list); 196 with the Node's own item in the navbar (openvibe-shared 4.117.0, 2026-11-06: the Node link and the llms.txt section); 194.9 with the Organization naming every open site in sameAs (SEO-27, 2026-11-03); 192.4 with Tours and Shoes open (contracts 0.265.0, 2026-10-31); 189.9 with Casino open (contracts 0.264.0, 2026-10-31); 189 with the look-boot script inline above the stylesheets on every page (L42, 2026-10-31: +1 KB); 188 with the agent layer's rel=alternate/describedby link tags on every public page (SEO-1/SEO-2, 2026-10-29: +0.2 KB); 187 with Bench open (contracts 0.238.0, 2026-10-28); 186 with Cam open (contracts 0.237.0, 2026-10-28); 184.4 with Fun open (contracts 0.236.0, 2026-10-28); 182.7 with Vision and Exchange open (contracts 0.235.0, 2026-10-28); 179 with Meme open (contracts 0.234.0, 2026-10-28); 174.1 with Domains, Report and Design open (contracts 0.226-0.228, 2026-10-28); 168.1 the hero's orbit and rich cards for live streams, recordings and screenshots (2026-10-28: the owner asked for animated visuals and YouTube-style cards); 152.4 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 147.9 with the home's FAQ (six questions and their FAQPage JSON-LD), the numbers panel and the service map's group headings (2026-10-22); 141.2 with Tips and VIP open (contracts 0.200.0, 37 sites; the Opening next section is gone); 134 with Play (contracts 0.172.1); 128.3 with Events, AI and Search added (31 sites, contracts 0.135.0); 122.3 (shared 2.21.0: a real glyph for every product card, where ten drew the 'V'); was 110.6 with 33 live sites (contracts 0.119.0). The home page lists every site, so it grows with the network
+ htmlRawKB: 218.5, // 218.5: measured 218.1 KB with Network's own page list in the navbar (2026-11-12: the Network home row, and Link / My Account named as doors); 218: measured 217 KB (2026-11-11: Directory joins the network list on contracts 0.406.0, and the popular block draws its five site marks as inline SVG, this page having no icon font); 210.5: measured 209.9 KB with the popular block designed (2026-11-11: ranks, marks, numbers and its CSS, +0.9 KB); 209.5: measured 209 KB with openvibe-shared 4.190.0 (2026-11-10: the site name's dropdown, the footer's online strip, the balances pill); 206: measured 205.8 KB with openvibe-shared 4.185.0 (the navbar's links in the first HTML since 4.184.0), 2026-11-10; 205.4: measured 205.4 KB with the front door's five most popular destinations (plan L43, 2026-11-09: Report's ranked list adds about 1.2 KB of plain list markup); 204.2: measured 204.2 KB with openvibe-contracts 0.373.0 (the Git service in the network list) on openvibe-shared 4.173.0, 2026-11-09; 204: measured 203.5 KB with the shared page head's kit CSS on the home (openvibe-shared 4.163.0 + page head 1369721), 2026-11-09; 198.8 with openvibe-contracts 0.348.0 (2026-11-08: Studio joins the menu; Studio and Locker get menu lines); 197.4 with Email open (contracts 0.330.0, 2026-11-07: the mail site in the network list); 196 with the Node's own item in the navbar (openvibe-shared 4.117.0, 2026-11-06: the Node link and the llms.txt section); 194.9 with the Organization naming every open site in sameAs (SEO-27, 2026-11-03); 192.4 with Tours and Shoes open (contracts 0.265.0, 2026-10-31); 189.9 with Casino open (contracts 0.264.0, 2026-10-31); 189 with the look-boot script inline above the stylesheets on every page (L42, 2026-10-31: +1 KB); 188 with the agent layer's rel=alternate/describedby link tags on every public page (SEO-1/SEO-2, 2026-10-29: +0.2 KB); 187 with Bench open (contracts 0.238.0, 2026-10-28); 186 with Cam open (contracts 0.237.0, 2026-10-28); 184.4 with Fun open (contracts 0.236.0, 2026-10-28); 182.7 with Vision and Exchange open (contracts 0.235.0, 2026-10-28); 179 with Meme open (contracts 0.234.0, 2026-10-28); 174.1 with Domains, Report and Design open (contracts 0.226-0.228, 2026-10-28); 168.1 the hero's orbit and rich cards for live streams, recordings and screenshots (2026-10-28: the owner asked for animated visuals and YouTube-style cards); 152.4 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 147.9 with the home's FAQ (six questions and their FAQPage JSON-LD), the numbers panel and the service map's group headings (2026-10-22); 141.2 with Tips and VIP open (contracts 0.200.0, 37 sites; the Opening next section is gone); 134 with Play (contracts 0.172.1); 128.3 with Events, AI and Search added (31 sites, contracts 0.135.0); 122.3 (shared 2.21.0: a real glyph for every product card, where ten drew the 'V'); was 110.6 with 33 live sites (contracts 0.119.0). The home page lists every site, so it grows with the network
htmlBrotliKB: 39.5, // 39.5: measured 39.3 KB brotli with openvibe-shared 4.190.0 (2026-11-10: the site name's dropdown, the footer's online strip, the balances pill); 39: measured 38.6 KB brotli with the shared page head's kit CSS on the home (openvibe-shared 4.163.0 + page head 1369721), 2026-11-09; 38.1 with openvibe-contracts 0.348.0 (2026-11-08: Studio joins the menu; Studio and Locker get menu lines); 37.9 with openvibe-shared 4.129.0 (2026-11-07: the one menu (the burger opens the OpenVibe menu)); 37.8 with openvibe-shared 4.129.0 (2026-11-07: the one menu (the burger opens the OpenVibe menu)); 36.1 with the Node's own item in the navbar (openvibe-shared 4.117.0, 2026-11-06); 35.5 with Tours and Shoes open (2026-10-31); 35.1 with the look-boot script inline above the stylesheets on every page (L42, 2026-10-31: +0.6 KB); 34.2 with the lower half as one block of chips on phones (2026-10-30); 33.1 with Games granted its Bench match capabilities (BENCH-7, 2026-10-29: the home lists what each service may do); 32.0 with Fun, Vision and Exchange open (contracts 0.236.0, 2026-10-28); 31.2 with Domains, Report and Design open (2026-10-28); 30.1 with the Organization saying who OpenVibe is (description, other names, what it is not, 15 sites; 2026-10-28); 28.5 with the FAQ, numbers and groups (2026-10-22); 26.1 with Play; 25.1 (31 sites, contracts 0.135.0); 24.1 (shared 2.21.0, the same glyphs); 20.5 (contracts 0.116.0)
htmlBrotliKB: 39.5, // 39.5: measured 39.3 KB brotli with openvibe-shared 4.190.0 (2026-11-10: the site name's dropdown, the footer's online strip, the balances pill); 39: measured 38.6 KB brotli with the shared page head's kit CSS on the home (openvibe-shared 4.163.0 + page head 1369721), 2026-11-09; 38.1 with openvibe-contracts 0.348.0 (2026-11-08: Studio joins the menu; Studio and Locker get menu lines); 37.9 with openvibe-shared 4.129.0 (2026-11-07: the one menu (the burger opens the OpenVibe menu)); 37.8 with openvibe-shared 4.129.0 (2026-11-07: the one menu (the burger opens the OpenVibe menu)); 36.1 with the Node's own item in the navbar (openvibe-shared 4.117.0, 2026-11-06); 35.5 with Tours and Shoes open (2026-10-31); 35.1 with the look-boot script inline above the stylesheets on every page (L42, 2026-10-31: +0.6 KB); 34.2 with the lower half as one block of chips on phones (2026-10-30); 33.1 with Games granted its Bench match capabilities (BENCH-7, 2026-10-29: the home lists what each service may do); 32.0 with Fun, Vision and Exchange open (contracts 0.236.0, 2026-10-28); 31.2 with Domains, Report and Design open (2026-10-28); 30.1 with the Organization saying who OpenVibe is (description, other names, what it is not, 15 sites; 2026-10-28); 28.5 with the FAQ, numbers and groups (2026-10-22); 26.1 with Play; 25.1 (31 sites, contracts 0.135.0); 24.1 (shared 2.21.0, the same glyphs); 20.5 (contracts 0.116.0)
jsFiles: 8, // 7 with boost.js, was 6 (2026-10-13, plan T11 step 3: one more script on every boosted page; it loads route-transition.js on the first move only)
jsFiles: 8, // 7 with boost.js, was 6 (2026-10-13, plan T11 step 3: one more script on every boosted page; it loads route-transition.js on the first move only)
jsRawKB: 450.5, // 450.5 openvibe-shared v4.214.0 (2026-11-12: measured 450.2, raised by pin-bump from 449.5); 449.5 openvibe-shared v4.211.0 (2026-11-11: measured 449.4, raised by pin-bump from 448.5); 448.5 openvibe-shared v4.205.0 (2026-11-11: measured 448.4, raised by pin-bump from 445); 445 openvibe-shared v4.202.0 (2026-11-11: measured 444.6, raised by pin-bump from 444); 444 openvibe-shared v4.201.0 (2026-11-11: measured 443.7, raised by pin-bump from 440.5); 440.5 openvibe-shared v4.200.0 (2026-11-10: measured 440.2, raised by pin-bump from 440.0); 440.0: measured 439.6 KB raw / 106.7 KB brotli with openvibe-shared 4.197.0 (2026-11-10: the menu's row of places, the You tab's row, the Sign in mark, the tooltip); 438.5: measured 438 KB raw / 106.5 KB brotli with openvibe-shared 4.190.0 (2026-11-10: the site name's dropdown, the footer's online strip, the balances pill); 421: measured 420.9 KB raw / 102.2 KB brotli with openvibe-shared 4.186.0 (the mark's Working and Thinking states, the Dock and the menu as one sheet, bare controls), 2026-11-10; 420.5: measured 419.9 KB raw / 101.9 KB brotli with openvibe-shared 4.185.0 (Calm reduces, the solids and site marks, the You tab, the Dock out of the orb), 2026-11-10; 417: measured 416.4 KB raw / 100.9 KB brotli with openvibe-shared 4.179.0, 2026-11-09; 410: measured 409.2 KB raw / 99.4 KB brotli with openvibe-shared 4.173.0 (the wallet's cookie mode), 2026-11-09; 409: measured 408.1 KB raw / 99.0 KB brotli after merging main's Frame feed work (openvibe-shared 4.167.0), 2026-11-09; 406: measured 405.7 KB raw / 98.4 KB brotli with the shared page head's kit script on the home (openvibe-shared 4.163.0 + page head 1369721), 2026-11-09; 381.8: measured 375.8 KB raw / 91.1 KB brotli with openvibe-shared 4.159.0, plus room for 4.160.0–4.161.0 (the navbar's slots loader, the keyboard watcher, the pop motion), 2026-11-09; 373 with openvibe-shared 4.153.0 (2026-11-08: signed in to the network, signed in here, on phones too; a page can turn away a carried card); 372.3 with openvibe-shared 4.152.0 (2026-11-08: the menu marks the page you are on and opens without the keyboard on a phone, floating cards follow you across OpenVibe sites (overlay.js), and the navbar preconnects to the next OpenVibe site on intent); 369.6 with openvibe-shared 4.150.0 (2026-11-08: the bell opens the Dock on Activity, the Dock's phone header is in two rows, and the menu's orb is the close button with an open/close animation); 368.6 with openvibe-shared 4.149.0 (2026-11-08: the menu has tabs at the top; the phone's menu button is the orb of 4.148.0); 366.5 with openvibe-shared 4.118.0 (2026-11-06: the Locker re-auth link, the 44 px footer and TLD targets; main's 4.117.0 navbar item and the account-safety helper included); about 363.8 with both (2026-11-06: openvibe-shared 4.117.0's featured navbar item, +1.7 KB, and the account-safety helper, +8.5 KB; merged, to be confirmed by CI's measure); 353.6 with the account-safety helper on every page (2026-11-06: the one re-authentication path and the scheduled-deletion banner, +8.5 KB); 345.1 with openvibe-shared 4.114.0 (2026-11-06: the one Share sheet in the navbar (ov-share.js and ov-qr.js load on the first press); navbar.js gains its click handler); 343.9 with openvibe-shared 4.112.1 (2026-11-06: theme-loader.js (4.111.1) and the footer/appbar phone CSS (4.112.1)); 342.3 with openvibe-shared 4.109.2 (2026-11-03: OpenVibe's own select, the navbar loads ov-forms); 339.6 with openvibe-shared 4.107.0 (2026-11-03: open.<tld> sites are named for their domain; the brand knows its front door); 337.8 with openvibe-shared 4.103.0 (2026-11-02: the puzzle glyph, two-line sidebar rows, boost lets parsed pages go, eight solids) (2026-11-02); 334.1 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 333.2 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 330.1 with openvibe-shared 4.52.0 (from 4.47.2): the seasonal fall, rich embeds, the Dock and its Chats tab (2026-10-30); 323.2 with openvibe-shared 4.47.2 (the comfort settings in theme-loader 4.45, touch targets 4.46, the grouped updates log 4.47.2); 313.7 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 306.4 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 298.9 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 293.1 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 279.1 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 269.3 with shared 4.11.0's seasonal navbar (Pumpkin Night and the season badge, +2.6); 249.8 with boost.js (13.1); 228.3
jsRawKB: 450.5, // 450.5 openvibe-shared v4.214.0 (2026-11-12: measured 450.2, raised by pin-bump from 449.5); 449.5 openvibe-shared v4.211.0 (2026-11-11: measured 449.4, raised by pin-bump from 448.5); 448.5 openvibe-shared v4.205.0 (2026-11-11: measured 448.4, raised by pin-bump from 445); 445 openvibe-shared v4.202.0 (2026-11-11: measured 444.6, raised by pin-bump from 444); 444 openvibe-shared v4.201.0 (2026-11-11: measured 443.7, raised by pin-bump from 440.5); 440.5 openvibe-shared v4.200.0 (2026-11-10: measured 440.2, raised by pin-bump from 440.0); 440.0: measured 439.6 KB raw / 106.7 KB brotli with openvibe-shared 4.197.0 (2026-11-10: the menu's row of places, the You tab's row, the Sign in mark, the tooltip); 438.5: measured 438 KB raw / 106.5 KB brotli with openvibe-shared 4.190.0 (2026-11-10: the site name's dropdown, the footer's online strip, the balances pill); 421: measured 420.9 KB raw / 102.2 KB brotli with openvibe-shared 4.186.0 (the mark's Working and Thinking states, the Dock and the menu as one sheet, bare controls), 2026-11-10; 420.5: measured 419.9 KB raw / 101.9 KB brotli with openvibe-shared 4.185.0 (Calm reduces, the solids and site marks, the You tab, the Dock out of the orb), 2026-11-10; 417: measured 416.4 KB raw / 100.9 KB brotli with openvibe-shared 4.179.0, 2026-11-09; 410: measured 409.2 KB raw / 99.4 KB brotli with openvibe-shared 4.173.0 (the wallet's cookie mode), 2026-11-09; 409: measured 408.1 KB raw / 99.0 KB brotli after merging main's Frame feed work (openvibe-shared 4.167.0), 2026-11-09; 406: measured 405.7 KB raw / 98.4 KB brotli with the shared page head's kit script on the home (openvibe-shared 4.163.0 + page head 1369721), 2026-11-09; 381.8: measured 375.8 KB raw / 91.1 KB brotli with openvibe-shared 4.159.0, plus room for 4.160.0–4.161.0 (the navbar's slots loader, the keyboard watcher, the pop motion), 2026-11-09; 373 with openvibe-shared 4.153.0 (2026-11-08: signed in to the network, signed in here, on phones too; a page can turn away a carried card); 372.3 with openvibe-shared 4.152.0 (2026-11-08: the menu marks the page you are on and opens without the keyboard on a phone, floating cards follow you across OpenVibe sites (overlay.js), and the navbar preconnects to the next OpenVibe site on intent); 369.6 with openvibe-shared 4.150.0 (2026-11-08: the bell opens the Dock on Activity, the Dock's phone header is in two rows, and the menu's orb is the close button with an open/close animation); 368.6 with openvibe-shared 4.149.0 (2026-11-08: the menu has tabs at the top; the phone's menu button is the orb of 4.148.0); 366.5 with openvibe-shared 4.118.0 (2026-11-06: the Locker re-auth link, the 44 px footer and TLD targets; main's 4.117.0 navbar item and the account-safety helper included); about 363.8 with both (2026-11-06: openvibe-shared 4.117.0's featured navbar item, +1.7 KB, and the account-safety helper, +8.5 KB; merged, to be confirmed by CI's measure); 353.6 with the account-safety helper on every page (2026-11-06: the one re-authentication path and the scheduled-deletion banner, +8.5 KB); 345.1 with openvibe-shared 4.114.0 (2026-11-06: the one Share sheet in the navbar (ov-share.js and ov-qr.js load on the first press); navbar.js gains its click handler); 343.9 with openvibe-shared 4.112.1 (2026-11-06: theme-loader.js (4.111.1) and the footer/appbar phone CSS (4.112.1)); 342.3 with openvibe-shared 4.109.2 (2026-11-03: OpenVibe's own select, the navbar loads ov-forms); 339.6 with openvibe-shared 4.107.0 (2026-11-03: open.<tld> sites are named for their domain; the brand knows its front door); 337.8 with openvibe-shared 4.103.0 (2026-11-02: the puzzle glyph, two-line sidebar rows, boost lets parsed pages go, eight solids) (2026-11-02); 334.1 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 333.2 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 330.1 with openvibe-shared 4.52.0 (from 4.47.2): the seasonal fall, rich embeds, the Dock and its Chats tab (2026-10-30); 323.2 with openvibe-shared 4.47.2 (the comfort settings in theme-loader 4.45, touch targets 4.46, the grouped updates log 4.47.2); 313.7 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 306.4 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 298.9 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 293.1 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 279.1 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 269.3 with shared 4.11.0's seasonal navbar (Pumpkin Night and the season badge, +2.6); 249.8 with boost.js (13.1); 228.3