openvibers/openvibe.network Commits
The Node's own front door at node.openvibe.network, and the navbar's one accent (#593)
Changes
@@@ -22,7 +22,7 @@"multer": "^1.4.5-lts.1","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.298.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.115.1",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.117.0","pg": "^8.23.0","qrcode": "1.5.4","uuid": "^11.1.0",@@@ -1766,9 +1766,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.115.1",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.115.1",-- "integrity": "sha512-xmdNHGlZSJdGoeruXoz6ZtDicY5472mhwo7uuv0Ct1HwyZdyhv9Qn1GLPgz0heues+0w6Q759dXMwHZ52zuDbg==",++ "version": "4.117.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.117.0",++ "integrity": "sha512-Rdphz8Wyb5bCbGtaULsPD5dODFBCNYYzIgeZMTPM43gF7IR7/j/CD6HddZf3jzieT4dndwcaqdWcpLCStKJNVQ==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -26,7 +26,7 @@"multer": "^1.4.5-lts.1","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.298.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.115.1",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.117.0","pg": "^8.23.0","qrcode": "1.5.4","uuid": "^11.1.0",@@@ -293,7 +293,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: '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: 'Node', href: 'https://node.openvibe.network', icon: 'fa-desktop', featured: true }, { label: 'My Account', href: '/my' }, { label: 'Wallet', href: '/wallet' }, { label: 'Themes', href: '/themes' }], networkLinks: 5 });}if (user) {document.getElementById('hero-guest-ctas').style.display = 'none';@@@ -96,7 +96,7 @@root.OpenVibeNavbar.init({service: 'network', notificationsRealtime: true, token: token(), user,menu: WALLET_MENU,-- links: [{ label: 'Wallet', href: '/wallet' }, { label: 'My Account', href: '/my' }],++ links: [{ label: 'Node', href: 'https://node.openvibe.network', icon: 'fa-desktop', featured: true }, { label: 'Wallet', href: '/wallet' }, { label: 'My Account', href: '/my' }],});} catch { /* the page works without it */ }}@@@ -487,6 +487,13 @@ function llmsTxt() {{ title: 'Sites', links: OPEN.map((site) => ({ title: siteName(site), url: `https://${site.host}/`, note: site.what })) },// A person's account ID: the endpoint an agent needs when it works for someone.{ title: 'Your account ID', links: [ACCOUNT_ID_LINK] },++ // The Node, on its own subdomain: the piece the network is built around, with its own map.++ { title: 'The Node', links: [++ { title: 'OpenVibe Node: your computer on the network', url: 'https://node.openvibe.network/',++ note: 'one program on your computer, server, phone or robot: run your agents and code on it, offer your models or GPU to the network and earn credits, drive robots, and keep your memory read-only' },++ { title: 'Install the Node', url: 'https://node.openvibe.network/install',++ note: 'the installer of the latest release, as a script to run: curl -fsSL https://node.openvibe.network/install | sh' },++ ] },// The JSON an agent came for is a main section, not something to skip (AGT-5 review).{ title: 'Machine-readable', links: [{ title: 'Open stats (JSON): every site\'s numbers', url: 'https://openvibe.network/api/v1/stats' },@@@ -224,6 +224,9 @@ function buildAllowedOriginsSet() {// Hard-coded OpenVibe defaults as baseline (survive registry reset/failure)for (const o of ['https://openvibe.network',++ // The Node's own front door (the owner, 2026-11-06): node.openvibe.network is served by Network, and its++ // page asks the network's own API (the account's look, the navbar's frame, the shared font) from that origin.++ 'https://node.openvibe.network','https://openvibe.live', 'https://www.openvibe.live','https://openvibe.tools','https://openvibe.games', 'https://www.openvibe.games', 'https://play.openvibe.games',@@@ -638,6 +641,9 @@ const openStats = require('./stats/open').createOpenStats({db, selfToken, release, statusRows: () => require('./status/routes').rows(ecosystem),prometheusUrl: config.prometheusUrl || process.env.PROMETHEUS_URL, liveUrl: process.env.OV_LIVE_INTERNAL_URL,});++// The Node's own front door on its own subdomain (node.openvibe.network): the Host header decides. It mounts++// before the agent layer, so the Node's page is not answered by Network's home renderer.++app.use(require('./node/routes').createNodeRoutes({ release: release.release }));// The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json, and the public pages as Markdown twins. It// answers their HTML as well, with the discovery tags in the head, so it mounts before the routes for those pages.require('openvibe-shared/agent-layer').mount(app, {nnew file mode 100644@@@ -0,0 +1,176 @@++'use strict';++// node.openvibe.network: the Node's own front door. What the Node is (your computer on the network), 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 the Node's.++// The version, the release date and the checksums come from the Node's latest GitHub release (server/node/release.js).++++const kit = require('openvibe-shared/kit');++++const HOST = 'node.openvibe.network';++const SITE = `https://${HOST}`;++const DOCS = 'https://github.com/OpenVibers/OpenVibe.Node/blob/main/docs';++const INSTALL_LINE = 'curl -fsSL https://node.openvibe.network/install | sh';++++// What the Node does today, each with where it happens. A page that promises tomorrow is a page that lies today.++const DOES = [++ { icon: 'code', title: 'Run your agents and code on your machine', body: 'Codes starts a coding task from any browser and this machine runs it, as an unprivileged user in a sandbox.', href: 'https://openvibe.codes/run', cta: 'OpenVibe.Codes' },++ { icon: 'spark', title: 'Offer your models or your GPU and earn credits', body: 'Exchange lists what your machine serves; you choose the models, the hours, the price and who may use it.', href: 'https://openvibe.exchange/sell', cta: 'OpenVibe.Exchange' },++ { icon: 'game', title: 'Drive robots', body: 'Bot drives what you approve, at the level you set, with a kill switch on the machine and a deadline on every command.', href: 'https://openvibe.bot', cta: 'OpenVibe.Bot' },++ { icon: 'live', title: 'Stream an agent\u2019s desk', body: 'Live carries your machine\u2019s screen to a channel, private until you choose: under a second behind, with a curtain that hides secret screens.', href: 'https://openvibe.live/docs/agent-streams', cta: 'Agent streams on OpenVibe.Live' },++ { icon: 'lock', title: 'Keep your memory, read-only', body: 'The collectors read what your coding tools remember: local, redacted, and never uploaded unless you say so.', href: `${DOCS}/memory.md`, cta: 'The memory docs' },++];++++// What the Node never does. The two sentences people ask for most.++const NEVER = [++ 'It cannot reach your computer without your pairing: nobody drives it, reads it or runs a command on it until you approved the machine.',++ 'Nothing runs that you did not turn on: no task, no camera, no driver and no upload starts on its own.',++ 'It opens no port: it connects out, so it works behind any home router.',++ 'It keeps a local kill switch: one command stops everything it drives, and it stays stopped.',++];++++const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));++++/** The install block per OS. The one-liner is for Linux and macOS; Windows gets the release asset and the pair step. */++function installBlock(release) {++ const version = esc(release.version || '');++ const exe = `https://github.com/OpenVibers/OpenVibe.Node/releases/download/${release.version || 'latest'}/openvibe-node-windows-amd64.exe`;++ return `<div class="np-installs">++<div class="np-install">++<h3>Linux and macOS</h3>++<p class="np-fine">One command. It finds your system, downloads the release, checks it against SHA256SUMS, sets up the service and shows a code to approve.</p>++${kit.snippet({ code: INSTALL_LINE, id: 'np-install-sh', label: 'Copy the install command' })}++<p class="np-fine">On a Raspberry Pi or another robot, the same command finds the kit and its driver.</p>++</div>++<div class="np-install">++<h3>Windows</h3>++<p class="np-fine">Download the binary, then in an Administrator prompt:</p>++${kit.snippet({ code: 'openvibe-node pair <CODE>', id: 'np-install-win', label: 'Copy the pair command' })}++<p class="np-fine"><a href="${esc(exe)}">openvibe-node-windows-amd64.exe</a> from the ${version ? esc(version) : 'latest'} release.</p>++</div>++</div>`;++}++++/** The release facts: the version, when it came out, the checksums and the release page. */++function releaseBlock(release) {++ const { versionLabel, dateLabel } = require('./release');++ const version = versionLabel(release);++ const date = dateLabel(release);++ const rows = [++ kit.row({ title: 'Latest version', sub: release.version || '', meta: date || '', metaIso: date || undefined }),++ kit.row({ href: release.checksumsUrl || '#', title: 'SHA256SUMS', sub: 'the checksums of every file in the release, to check the download against' }),++ kit.row({ href: release.url || '#', title: 'The release on GitHub', sub: 'every asset, and the notes of that release' }),++ ];++ return `<section class="np-sec" aria-labelledby="h-release"><h2 id="h-release">The latest release</h2>++<p class="np-fine">${version ? `Version <strong>${esc(version)}</strong>` : 'The latest version'}${date ? `, published ${esc(date)}` : ''}. The install command below always installs that release.</p>++${kit.list(rows, { flush: true })}++${release.stale ? '<p class="np-fine np-warn">GitHub could not be reached just now, so this is the last release we read.</p>' : ''}++</section>`;++}++++/** The page. `release` is what server/node/release.js answered. */++function render({ release = {}, releaseId } = {}) {++ const title = 'OpenVibe Node: your computer on the network';++ const desc = 'OpenVibe Node runs on your computer, server, phone or robot: it runs your agents and code, offers your models or GPU to the network, drives robots, and keeps your memory read-only.';++ const ld = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'OpenVibe Node', applicationCategory: 'DeveloperApplication',++ operatingSystem: 'Linux, macOS, Windows', description: desc, url: SITE,++ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },++ publisher: { '@type': 'Organization', '@id': 'https://openvibe.network/#org', name: 'OpenVibe' } };++ const does = DOES.map((d) => `<article class="np-does">++${kit.icon(d.icon)}++<h3>${esc(d.title)}${d.soon ? ' <span class="np-soon">coming with S1</span>' : ''}</h3>++<p>${esc(d.body)}</p>++<p class="np-fine"><a href="${esc(d.href)}">${esc(d.cta)}</a></p>++</article>`).join('');++++ return `<!DOCTYPE html>++<html lang="en">++<head>++<meta charset="utf-8">++<meta name="viewport" content="width=device-width, initial-scale=1">++<title>${esc(title)}</title>++<meta name="description" content="${esc(desc)}">++<link rel="canonical" href="${SITE}/">++${require('../boost').meta(releaseId)}++${require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' })}++<meta name="color-scheme" content="dark light">++<script src="/shared/theme-loader.js" defer></script>++<script type="application/ld+json">${JSON.stringify(ld).replace(/</g, '\\u003c')}</script>++<link rel="stylesheet" href="/shared/kit.css">++<style>++html,body{background:var(--bg-primary);color:var(--text-primary);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;margin:0}++${CSS}++</style>++</head>++<body>++<div id="navbar-mount"></div>++<main class="np" id="main">++<header class="np-head">++<p class="np-kicker">OpenVibe Node</p>++<h1>Your computer on the network</h1>++<p class="np-lede">One program, <code>openvibe-node</code>, on your computer, your server, your phone or your robot. It connects out to OpenVibe, needs no open ports, and keeps a local kill switch. You decide what it may do; nothing runs that you did not turn on.</p>++<p class="np-cta">${kit.button({ href: '/install', label: 'Install the Node', kind: 'primary' })} ${kit.button({ href: `${DOCS}/install.md`, label: 'The install guide', kind: 'quiet' })}</p>++</header>++++<section class="np-sec" aria-labelledby="h-does"><h2 id="h-does">What it does today</h2>++<div class="np-grid">${does}</div>++</section>++++<section class="np-sec" aria-labelledby="h-install"><h2 id="h-install">Install it</h2>++${installBlock(release)}++</section>++++${releaseBlock(release)}++++<section class="np-sec" aria-labelledby="h-never"><h2 id="h-never">What it never does</h2>++<ul class="np-never">${NEVER.map((n) => `<li>${esc(n)}</li>`).join('')}</ul>++</section>++++<section class="np-sec" aria-labelledby="h-docs"><h2 id="h-docs">The docs</h2>++<ul class="np-docs">++<li><a href="${DOCS}/install.md">Install</a> — every OS, every option, and what the installer does.</li>++<li><a href="${DOCS}/computer.md">Your computer for your agents</a> — the computer plugin, its levels and the live screen.</li>++<li><a href="${DOCS}/worker.md">A machine that runs your jobs</a> — the sandbox the job runner uses.</li>++<li><a href="${DOCS}/protocol.md">The protocol</a> — the wire format between the Node and OpenVibe.Bot.</li>++<li><a href="${DOCS}/memory.md">Your memory</a> — what the collectors read and how it is uploaded.</li>++</ul>++<p class="np-fine">The source is on <a href="https://github.com/OpenVibers/OpenVibe.Node">GitHub</a>. The installer also lives at <a href="https://openvibe.bot/install">openvibe.bot/install</a>, which keeps working.</p>++</section>++</main>++${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' })}++<script src="/shared/navbar.js" defer></script>++${require('../boost').SCRIPT}++<script src="/shared/kit-web.js" defer></script>++<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',links:[{label:'Node',href:'/',icon:'fa-desktop',featured:true,active:true},{label:'Network',href:'https://openvibe.network/'},{label:'My Account',href:'https://openvibe.network/my'}]});}catch(e){}});</script>++</body>++</html>`;++}++++const CSS = `++.np{max-width:1000px;margin:0 auto;padding:32px 20px 72px}++.np-kicker{margin:0 0 6px;font-size:.78rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--accent,#3b82f6)}++.np h1{margin:0 0 12px;font-size:clamp(1.7rem,5vw,2.6rem);letter-spacing:-.02em;line-height:1.12}++.np-lede{margin:0 0 18px;max-width:760px;color:var(--text-secondary,#96a7c2);line-height:1.65;font-size:1.03rem}++.np-lede code,.np code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em}++.np-cta{display:flex;flex-wrap:wrap;gap:10px;margin:0}++.np-sec{margin-top:44px}++.np-sec h2{margin:0 0 14px;font-size:1.3rem;letter-spacing:-.01em}++.np-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px}++.np-does{padding:18px;border-radius:16px;background:var(--bg-card,#0f1729);border:1px solid var(--border,#1f2d47);display:flex;flex-direction:column;gap:6px}++.np-does svg{width:22px;height:22px;color:var(--accent,#3b82f6)}++.np-does h3{margin:2px 0 0;font-size:1rem;line-height:1.35}++.np-does p{margin:0;color:var(--text-secondary,#96a7c2);font-size:.92rem;line-height:1.55}++.np-soon{display:inline-block;margin-left:6px;padding:2px 8px;border-radius:999px;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted,#7386a3);border:1px solid var(--border,#1f2d47);vertical-align:middle}++.np-installs{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px}++.np-install{padding:18px;border-radius:16px;background:var(--bg-card,#0f1729);border:1px solid var(--border,#1f2d47)}++.np-install h3{margin:0 0 8px;font-size:1rem}++.np-install .ovk-snip{margin:12px 0}++.np-fine{margin:10px 0 0;font-size:.88rem;color:var(--text-muted,#7386a3);line-height:1.55}++.np-warn{color:#f0b429}++.np-never,.np-docs{list-style:none;margin:0;padding:0;display:grid;gap:8px;max-width:820px}++.np-never li{position:relative;padding:11px 14px 11px 42px;border-radius:12px;background:var(--bg-card,#0f1729);border:1px solid var(--border,#1f2d47);color:var(--text-secondary,#96a7c2);font-size:.93rem;line-height:1.55}++.np-never li::before{content:"";position:absolute;left:15px;top:16px;width:13px;height:13px;border-radius:50%;background:color-mix(in srgb,var(--accent,#3b82f6) 22%,transparent);box-shadow:inset 0 0 0 2px var(--accent,#3b82f6)}++.np-docs li{padding:11px 14px;border-radius:12px;background:var(--bg-card,#0f1729);border:1px solid var(--border,#1f2d47);font-size:.93rem;line-height:1.55}++.np a:not(.ovk-btn){color:var(--accent,#3b82f6)}++@media (max-width:640px){.np{padding:22px 16px 52px}}++`;++++module.exports = { render, HOST, SITE, DOCS, INSTALL_LINE, DOES, NEVER };nnew file mode 100644@@@ -0,0 +1,88 @@++'use strict';++// The Node's latest release, read from GitHub (OpenVibers/OpenVibe.Node). The page shows the version and the++// checksums; /install answers with the same release's installer. The answer is cached for ten minutes, so a++// visitor never waits on GitHub, and when GitHub cannot be reached the last good answer is used; a cold start++// without GitHub falls back to the release links that always resolve (the "latest" ones).++++const REPO = 'OpenVibers/OpenVibe.Node';++const API = `https://api.github.com/repos/${REPO}/releases/latest`;++const TTL_MS = 10 * 60 * 1000; // ten minutes++const TIMEOUT_MS = 8000;++const LATEST = `https://github.com/${REPO}/releases/latest`;++++// What the page says when GitHub cannot be reached and nothing was cached: no version (the page says "the latest++// release" instead of naming one that may be old), and the links that always resolve to the newest release.++const FALLBACK = {++ version: '',++ publishedAt: '',++ url: LATEST,++ installUrl: `https://github.com/${REPO}/releases/latest/download/install.sh`,++ checksumsUrl: `https://github.com/${REPO}/releases/latest/download/SHA256SUMS`,++};++++let cached = null; // { at, data }++let inflight = null; // one fetch at a time++++/** The release fields the page and /install use. */++function shape(json) {++ const tag = String(json.tag_name || '').trim();++ const assets = Array.isArray(json.assets) ? json.assets : [];++ const asset = (name) => assets.find((a) => a && a.name === name);++ const install = asset('install.sh');++ const sums = asset('SHA256SUMS');++ return {++ version: tag,++ publishedAt: String(json.published_at || ''),++ url: String(json.html_url || (tag ? `https://github.com/${REPO}/releases/tag/${tag}` : LATEST)),++ installUrl: String((install && install.browser_download_url) || (tag ? `https://github.com/${REPO}/releases/download/${tag}/install.sh` : FALLBACK.installUrl)),++ checksumsUrl: String((sums && sums.browser_download_url) || (tag ? `https://github.com/${REPO}/releases/download/${tag}/SHA256SUMS` : FALLBACK.checksumsUrl)),++ };++}++++/** Fetch the latest release. Returns the cached answer while it is fresh, and never throws. */++async function latest({ fetchImpl = globalThis.fetch, now = Date.now } = {}) {++ if (cached && now() - cached.at < TTL_MS) return { ...cached.data, cached: true };++ if (inflight) return inflight;++ inflight = (async () => {++ try {++ const controller = typeof AbortController === 'function' ? new AbortController() : null;++ const timer = controller ? setTimeout(() => controller.abort(), TIMEOUT_MS) : null;++ let res;++ try {++ res = await fetchImpl(API, {++ signal: controller ? controller.signal : undefined,++ headers: { accept: 'application/vnd.github+json', 'user-agent': 'openvibe-network' },++ });++ } finally {++ if (timer) clearTimeout(timer);++ }++ if (!res || !res.ok) throw new Error(`GitHub releases API answered ${res && res.status}`);++ const data = shape(await res.json());++ if (!data.version) throw new Error('GitHub releases API answered without a tag');++ cached = { at: now(), data };++ return { ...data, cached: false };++ } catch {++ // GitHub is down or slow: the last good answer, or the written-down one. The page still works.++ return { ...(cached ? cached.data : FALLBACK), cached: true, stale: true };++ } finally {++ inflight = null;++ }++ })();++ return inflight;++}++++/** The version without the leading v, for a sentence. */++function versionLabel(release) {++ return String((release && release.version) || '').replace(/^v/, '');++}++++/** The release date, the day it was published. */++function dateLabel(release) {++ const iso = String((release && release.publishedAt) || '');++ if (!iso) return '';++ const d = new Date(iso);++ if (Number.isNaN(d.getTime())) return '';++ return d.toISOString().slice(0, 10);++}++++module.exports = { latest, versionLabel, dateLabel, REPO, API, TTL_MS, FALLBACK, LATEST };nnew file mode 100644@@@ -0,0 +1,110 @@++'use strict';++// node.openvibe.network, served by Network (the owner, 2026-11-06: the Node gets its own subdomain, "because it++// looks nicer"). The Host header decides: on this host '/' is the Node's page, '/install' answers with the++// installer of the latest release, and the page has its Markdown twin and its llms.txt, like Network's other pages.++//++// The route is mounted before the agent layer, so the Node's page is not answered by Network's home renderer.++++const { markdownTwin, linkHeaderTags } = require('openvibe-shared/agent-layer');++const page = require('./page');++const release = require('./release');++++const HOST = page.HOST;++const SITE = page.SITE;++++/** The request's host, lowercased, without the port. */++function hostOf(req) {++ return String((req.headers && req.headers.host) || '').split(':')[0].toLowerCase();++}++++/** The Node's llms.txt: what the page is, how to install it, and where the docs are. */++function llmsTxt() {++ return [++ '# OpenVibe Node',++ '',++ '> OpenVibe Node is one program (openvibe-node) that runs on your computer, server, phone or robot. It connects out to OpenVibe, needs no open ports, keeps a local kill switch, and does nothing you did not turn on.',++ '',++ '## The page',++ '',++ `- [OpenVibe Node: your computer on the network](${SITE}/): what it is, what it does today (run your agents and code, offer your models or GPU and earn credits, drive robots, stream an agent's desk, keep your memory read-only), how to install it on each OS, and what it never does`,++ '',++ '## Install',++ '',++ `- [Install the Node](${SITE}/install): the latest release's installer, as a script to run`,++ '',++ '```sh',++ page.INSTALL_LINE,++ '```',++ '',++ '## The docs',++ '',++ `- [Install](${page.DOCS}/install.md): every OS, every option, and what the installer does`,++ `- [Your computer for your agents](${page.DOCS}/computer.md): the computer plugin, its levels and the live screen`,++ `- [A machine that runs your jobs](${page.DOCS}/worker.md): the sandbox the job runner uses`,++ `- [The protocol](${page.DOCS}/protocol.md): the wire format between the Node and OpenVibe.Bot`,++ `- [Your memory](${page.DOCS}/memory.md): what the collectors read and how it is uploaded`,++ '- [The source on GitHub](https://github.com/OpenVibers/OpenVibe.Node)',++ '',++ ].join('\n');++}++++/**++ * The Node's routes. Returns null for a request that is not for this host, so the router can be mounted early++ * and every other host falls through to Network's own pages.++ */++function createNodeRoutes({ releaseId } = {}) {++ const router = require('express').Router();++++ router.use(async (req, res, next) => {++ if (hostOf(req) !== HOST) return next();++ const url = new URL(req.url, SITE);++ const path = url.pathname;++ const isTwin = path === '/index.md' || path.endsWith('.md') || url.searchParams.get('format') === 'markdown'++ || (req.headers.accept && req.accepts(['html', 'text/markdown']) === 'text/markdown');++++ // The installer: the latest release's own asset, so `curl -fsSL https://node.openvibe.network/install | sh`++ // installs that release. The same cached answer the page shows.++ if (path === '/install') {++ let rel;++ try { rel = await release.latest(); } catch { rel = release.FALLBACK; }++ const target = rel && rel.installUrl ? rel.installUrl : release.FALLBACK.installUrl;++ return res.set('Cache-Control', 'public, max-age=600').redirect(302, target);++ }++++ if (path === '/llms.txt') {++ return res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(llmsTxt());++ }++++ // The crawl files of this host, so a crawler on node.openvibe.network is told about this page and not++ // about Network's (the apex serves its own).++ if (path === '/robots.txt') {++ const robots = require('openvibe-shared/seo').robotsTxt({ sitemaps: [`${SITE}/sitemap.xml`] });++ return res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(robots);++ }++ if (path === '/sitemap.xml') {++ const xml = require('openvibe-shared/seo').sitemapXml([{ loc: `${SITE}/`, changefreq: 'weekly', priority: 1.0 }]);++ return res.type('application/xml').set('Cache-Control', 'public, max-age=3600').send(xml);++ }++++ if (path !== '/' && path !== '/index.md' && path !== '/index.html') return next();++++ let rel;++ try { rel = await release.latest(); } catch { rel = release.FALLBACK; }++ const html = page.render({ release: rel, releaseId });++ const canonical = `${SITE}/`;++ const links = linkHeaderTags(canonical, `${SITE}/llms.txt`);++ res.setHeader('Link', [res.getHeader('Link'), links.header].filter(Boolean).join(', '));++ res.setHeader('Vary', [res.getHeader('Vary'), 'Accept'].filter(Boolean).join(', '));++ res.setHeader('Cache-Control', 'public, max-age=600');++ if (isTwin) {++ res.setHeader('Content-Type', 'text/markdown; charset=utf-8');++ return res.end(req.method === 'HEAD' ? undefined : markdownTwin({ html, title: 'OpenVibe Node: your computer on the network', canonical }));++ }++ res.setHeader('Content-Type', 'text/html; charset=utf-8');++ return res.end(req.method === 'HEAD' ? undefined : html);++ });++++ return router;++}++++module.exports = { createNodeRoutes, llmsTxt, hostOf, HOST, SITE };nnew file mode 100644@@@ -0,0 +1,143 @@++'use strict';++// node.openvibe.network: the Node's own front door, served by Network. The Host header decides: on that host '/'++// is the Node's page, '/install' answers with the installer of the latest release, and the page has its Markdown++// twin and its llms.txt, like Network's other pages.++// node test/node-home.test.js++const assert = require('assert');++const http = require('http');++const express = require('express');++++const page = require('../server/node/page');++const release = require('../server/node/release');++const { createNodeRoutes, llmsTxt } = require('../server/node/routes');++++// ── The release answer: cached for ten minutes, with a fallback when GitHub cannot be reached ──++const FIXTURE = {++ tag_name: 'v9.9.9',++ published_at: '2026-11-06T12:00:00Z',++ html_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/tag/v9.9.9',++ assets: [++ { name: 'install.sh', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh' },++ { name: 'SHA256SUMS', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS' },++ ],++};++++(async () => {++ // The cache: one call to GitHub, then the same answer for ten minutes.++ let calls = 0;++ const fetchImpl = async () => { calls += 1; return { ok: true, status: 200, json: async () => FIXTURE }; };++ let now = 1000;++ const clock = () => now;++ const first = await release.latest({ fetchImpl, now: clock });++ assert.strictEqual(first.version, 'v9.9.9', 'the release is read from GitHub');++ assert.strictEqual(first.installUrl, 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh');++ assert.strictEqual(first.checksumsUrl, 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS');++ const second = await release.latest({ fetchImpl, now: clock });++ assert.strictEqual(calls, 1, 'the second call is answered from the cache');++ assert.strictEqual(second.version, 'v9.9.9');++++ // Past ten minutes, GitHub is asked again.++ now += release.TTL_MS + 1;++ await release.latest({ fetchImpl, now: clock });++ assert.strictEqual(calls, 2, 'after ten minutes GitHub is asked again');++++ // GitHub unreachable: the page still works, and says so. (Past the ten minutes, so the cache cannot answer.)++ now += release.TTL_MS + 1;++ const broken = await release.latest({ fetchImpl: async () => { throw new Error('no GitHub'); }, now: clock });++ assert.ok(broken.version, 'a release is still named');++ assert.strictEqual(broken.stale, true, 'and it is marked as the last one read');++ assert.ok(broken.installUrl.startsWith('https://github.com/OpenVibers/OpenVibe.Node/releases/download/'), 'the installer link still works');++++ // ── The page ──++ const html = page.render({ release: first, releaseId: 'rel_test' });++ assert.ok(html.includes('Your computer on the network'), 'the page says what the Node is');++ assert.ok(html.includes('curl -fsSL https://node.openvibe.network/install | sh'), 'the one-line installer is on the page');++ assert.ok(html.includes('v9.9.9'), 'the version is on the page');++ assert.ok(html.includes('2026-11-06'), 'the release date is on the page');++ assert.ok(html.includes('releases/download/v9.9.9/SHA256SUMS'), 'the checksums are on the page');++ assert.ok(html.includes('releases/tag/v9.9.9'), 'the release page is on the page');++ assert.ok(html.includes('openvibe-node-windows-amd64.exe'), 'Windows has its own way in');++ assert.ok(html.includes('openvibe.bot/install'), 'the installer also lives at openvibe.bot/install');++ for (const [what, needle] of [++ ['run your agents and code', 'openvibe.codes/run'],++ ['offer your models and earn credits', 'openvibe.exchange/sell'],++ ['drive robots', 'openvibe.bot'],++ ['stream an agent\u2019s desk', 'openvibe.live'],++ ['your memory, read-only', 'memory.md'],++ ]) assert.ok(html.includes(needle), `the page says it ${what}`);++ // What it never does.++ assert.ok(html.includes('without your pairing'), 'the page says the Node needs your pairing');++ assert.ok(html.includes('Nothing runs that you did not turn on'), 'and that nothing runs on its own');++ // The docs.++ assert.ok(html.includes('docs/install.md'), 'the install guide is linked');++ // Network's chrome.++ assert.ok(html.includes('/shared/navbar.js'), 'the shared navbar is on the page');++ assert.ok(html.includes('https://node.openvibe.network'), 'the page names its own address');++ assert.ok(html.includes('<link rel="canonical" href="https://node.openvibe.network/">'), 'and claims it as its own');++++ // The llms.txt says the same thing in plain lines.++ const txt = llmsTxt();++ assert.ok(txt.startsWith('# OpenVibe Node'), 'the llms.txt names the site');++ assert.ok(txt.includes('https://node.openvibe.network/'), 'and links the page');++ assert.ok(txt.includes('curl -fsSL https://node.openvibe.network/install | sh'), 'and the install command');++++ // ── The routes: the Host header decides ──++ const app = express();++ app.use(createNodeRoutes({ releaseId: 'rel_test' }));++ app.get('/', (_req, res) => res.type('html').send('<p>network home</p>'));++ const server = http.createServer(app).listen(0);++ await new Promise((r) => server.once('listening', r));++ const port = server.address().port;++ const get = (path, host, headers = {}) => new Promise((resolve) => {++ const req = http.request({ host: '127.0.0.1', port, path, method: 'GET', headers: { host, ...headers } }, (res) => {++ let body = '';++ res.on('data', (c) => { body += c; });++ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));++ });++ req.end();++ });++++ try {++ const home = await get('/', 'openvibe.network');++ assert.ok(home.body.includes('network home'), 'on the apex, Network\u2019s own home answers');++++ const node = await get('/', 'node.openvibe.network');++ assert.strictEqual(node.status, 200);++ assert.ok(node.body.includes('Your computer on the network'), 'on the Node\u2019s host, the Node\u2019s page answers');++ assert.ok(!node.body.includes('network home'), 'and not Network\u2019s home');++ assert.ok(String(node.headers.link || '').includes('text/markdown'), 'the page offers its Markdown twin');++++ const twin = await get('/index.md', 'node.openvibe.network');++ assert.strictEqual(twin.status, 200);++ assert.ok(String(twin.headers['content-type'] || '').includes('text/markdown'), 'the twin is Markdown');++ assert.ok(twin.body.includes('# OpenVibe Node'), 'and it says what the Node is');++ assert.ok(twin.body.includes('openvibe.network/install'), 'with the install command');++++ const install = await get('/install', 'node.openvibe.network');++ assert.strictEqual(install.status, 302, '/install redirects');++ assert.ok(String(install.headers.location || '').includes('releases/download/'), 'to the release\u2019s own installer');++ assert.ok(String(install.headers.location || '').endsWith('/install.sh'), 'the install script');++++ const llms = await get('/llms.txt', 'node.openvibe.network');++ assert.strictEqual(llms.status, 200);++ assert.ok(llms.body.startsWith('# OpenVibe Node'), 'the Node\u2019s own llms.txt answers');++++ // Network\u2019s own paths are untouched on the apex.++ const apex = await get('/llms.txt', 'openvibe.network');++ assert.notStrictEqual(apex.status, 200, 'the apex keeps its own llms.txt');++ } finally {++ server.close();++ }++++ // ── The navbar: the Node is the piece the Network is built around ──++ const index = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'index.html'), 'utf8');++ assert.ok(/links: \[\{ label: 'Node', href: 'https:\/\/node\.openvibe\.network', icon: 'fa-desktop', featured: true \}/.test(index), 'the home page\u2019s navbar leads with the Node, as the accent');++ assert.ok(index.indexOf("label: 'Node'") < index.indexOf("label: 'My Account'"), 'and the Node comes first');++++ // The Node's own origin is allowed to ask the network's API (the account's look, the navbar's frame, the)++ // shared font): without it, the Node\u2019s page loses its own chrome in the browser.++ const serverSrc = require('fs').readFileSync(require('path').join(__dirname, '..', 'server', 'index.js'), 'utf8');++ assert.ok(serverSrc.includes("'https://node.openvibe.network',"), 'the CORS allow-list knows the Node\u2019s own origin');++++ console.log('node-home: the Node\u2019s page, its installer, its twin and the navbar item ok');++})().catch((e) => { console.error(e); process.exit(1); });@@@ -12,11 +12,11 @@ const { spawn } = require('child_process');const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {-- htmlRawKB: 196, // 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: 36.5, // 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)++ htmlRawKB: 197, // 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: 36.8, // 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: 354, // 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-- jsBrotliKB: 86.5, // 86.2 with the account-safety helper on every page (2026-11-06: +2.2 KB brotli, the same feature); 83.4 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 82.6 with openvibe-shared 4.52.0 (from 4.47.2): the seasonal fall, rich embeds, the Dock and its Chats tab (2026-10-30); 81.4 with openvibe-shared 4.47.2 (comfort settings, touch targets, the grouped updates log); 78.9 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 77 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; 73.3 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 70.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 68.0 with shared 4.12.0 (light/dark switch, seasonal mark, theme pairs); 64.5 with shared 4.11.0's navbar; 60.3 with boost.js (3.8); 53.9++ jsRawKB: 365, // 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++ jsBrotliKB: 89.5, // about 88.8 with both (2026-11-06: 4.117.0's featured item and the account-safety helper); 86.6 with openvibe-shared 4.117.0 (2026-11-06: the featured navbar item's CSS and the phone-menu order); 83.4 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 82.6 with openvibe-shared 4.52.0 (from 4.47.2): the seasonal fall, rich embeds, the Dock and its Chats tab (2026-10-30); 81.4 with openvibe-shared 4.47.2 (comfort settings, touch targets, the grouped updates log); 78.9 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 77 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; 73.3 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 70.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 68.0 with shared 4.12.0 (light/dark switch, seasonal mark, theme pairs); 64.5 with shared 4.11.0's navbar; 60.3 with boost.js (3.8); 53.9cssFiles: 2, // 1 (the site's own; Font Awesome's stylesheet is gone since 2026-11-01)externalFiles: 3, // none locally (Font Awesome was the one); production adds Cloudflare's beacon};@@@ -136,6 +136,7 @@ const REVIEWED = {'server/registry/deploy-drift.js fetch': [1, 'fixed host api.github.com'],'server/registry/ecosystem.js fetch': [2, 'service URLs from openvibe-contracts manifests and operator env overrides'],'server/registry/library-tags.js fetch': [1, 'fixed host api.github.com'],++ 'server/node/release.js fetch': [1, 'the Node\'s own release page: a fixed GitHub API path (https://api.github.com/repos/OpenVibers/OpenVibe.Node/releases/latest), 8 s timeout, held 10 minutes; no request value ever names a host and it only ever reads the public release list'],'server/registry/machines.js fetch': [1, 'the machines hub calls Bot: the internal URL from operator config (OV_BOT_INTERNAL_URL, loopback by default), the paths are fixed, the machine id is a checked dev_ id (percent-encoded) and the person is named in a header, never in the URL'],'server/updates/routes.js fetch': [1, 'OV_BLOG_INTERNAL_URL (operator config)'],};
@@ -22,7 +22,7 @@
@@ -22,7 +22,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.298.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.298.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.115.1",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.117.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"qrcode": "1.5.4",
"qrcode": "1.5.4",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
@@ -1766,9 +1766,9 @@
@@ -1766,9 +1766,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.115.1",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.115.1",
- "integrity": "sha512-xmdNHGlZSJdGoeruXoz6ZtDicY5472mhwo7uuv0Ct1HwyZdyhv9Qn1GLPgz0heues+0w6Q759dXMwHZ52zuDbg==",
+ "version": "4.117.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.117.0",
+ "integrity": "sha512-Rdphz8Wyb5bCbGtaULsPD5dODFBCNYYzIgeZMTPM43gF7IR7/j/CD6HddZf3jzieT4dndwcaqdWcpLCStKJNVQ==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -26,7 +26,7 @@
@@ -26,7 +26,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.298.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.298.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.115.1",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.117.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"qrcode": "1.5.4",
"qrcode": "1.5.4",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
@@ -293,7 +293,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
@@ -293,7 +293,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: '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: 'Node', href: 'https://node.openvibe.network', icon: 'fa-desktop', featured: true }, { label: 'My Account', href: '/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';
@@ -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: 'Wallet', href: '/wallet' }, { label: 'My Account', href: '/my' }],
+ links: [{ label: 'Node', href: 'https://node.openvibe.network', icon: 'fa-desktop', featured: true }, { label: 'Wallet', href: '/wallet' }, { label: 'My Account', href: '/my' }],
});
});
} catch { /* the page works without it */ }
} catch { /* the page works without it */ }
}
}
@@ -487,6 +487,13 @@ function llmsTxt() {
@@ -487,6 +487,13 @@ function llmsTxt() {
{ title: 'Sites', links: OPEN.map((site) => ({ title: siteName(site), url: `https://${site.host}/`, note: site.what })) },
{ title: 'Sites', links: OPEN.map((site) => ({ title: siteName(site), url: `https://${site.host}/`, note: site.what })) },
// A person's account ID: the endpoint an agent needs when it works for someone.
// A person's account ID: the endpoint an agent needs when it works for someone.
{ title: 'Your account ID', links: [ACCOUNT_ID_LINK] },
{ title: 'Your account ID', links: [ACCOUNT_ID_LINK] },
+ // The Node, on its own subdomain: the piece the network is built around, with its own map.
+ { title: 'The Node', links: [
+ { title: 'OpenVibe Node: your computer on the network', url: 'https://node.openvibe.network/',
+ note: 'one program on your computer, server, phone or robot: run your agents and code on it, offer your models or GPU to the network and earn credits, drive robots, and keep your memory read-only' },
+ { title: 'Install the Node', url: 'https://node.openvibe.network/install',
+ note: 'the installer of the latest release, as a script to run: curl -fsSL https://node.openvibe.network/install | sh' },
+ ] },
// The JSON an agent came for is a main section, not something to skip (AGT-5 review).
// The JSON an agent came for is a main section, not something to skip (AGT-5 review).
{ title: 'Machine-readable', links: [
{ title: 'Machine-readable', links: [
{ title: 'Open stats (JSON): every site\'s numbers', url: 'https://openvibe.network/api/v1/stats' },
{ title: 'Open stats (JSON): every site\'s numbers', url: 'https://openvibe.network/api/v1/stats' },
@@ -224,6 +224,9 @@ function buildAllowedOriginsSet() {
@@ -224,6 +224,9 @@ function buildAllowedOriginsSet() {
// Hard-coded OpenVibe defaults as baseline (survive registry reset/failure)
// Hard-coded OpenVibe defaults as baseline (survive registry reset/failure)
for (const o of [
for (const o of [
'https://openvibe.network',
'https://openvibe.network',
+ // The Node's own front door (the owner, 2026-11-06): node.openvibe.network is served by Network, and its
+ // page asks the network's own API (the account's look, the navbar's frame, the shared font) from that origin.
+ 'https://node.openvibe.network',
'https://openvibe.live', 'https://www.openvibe.live',
'https://openvibe.live', 'https://www.openvibe.live',
'https://openvibe.tools',
'https://openvibe.tools',
'https://openvibe.games', 'https://www.openvibe.games', 'https://play.openvibe.games',
'https://openvibe.games', 'https://www.openvibe.games', 'https://play.openvibe.games',
@@ -638,6 +641,9 @@ const openStats = require('./stats/open').createOpenStats({
@@ -638,6 +641,9 @@ const openStats = require('./stats/open').createOpenStats({
db, selfToken, release, statusRows: () => require('./status/routes').rows(ecosystem),
db, selfToken, release, statusRows: () => require('./status/routes').rows(ecosystem),
prometheusUrl: config.prometheusUrl || process.env.PROMETHEUS_URL, liveUrl: process.env.OV_LIVE_INTERNAL_URL,
prometheusUrl: config.prometheusUrl || process.env.PROMETHEUS_URL, liveUrl: process.env.OV_LIVE_INTERNAL_URL,
});
});
+// The Node's own front door on its own subdomain (node.openvibe.network): the Host header decides. It mounts
+// before the agent layer, so the Node's page is not answered by Network's home renderer.
+app.use(require('./node/routes').createNodeRoutes({ release: release.release }));
// The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json, and the public pages as Markdown twins. It
// The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json, and the public pages as Markdown twins. It
// answers their HTML as well, with the discovery tags in the head, so it mounts before the routes for those pages.
// answers their HTML as well, with the discovery tags in the head, so it mounts before the routes for those pages.
require('openvibe-shared/agent-layer').mount(app, {
require('openvibe-shared/agent-layer').mount(app, {
new file mode 100644
new file mode 100644
@@ -0,0 +1,176 @@
@@ -0,0 +1,176 @@
+'use strict';
+// node.openvibe.network: the Node's own front door. What the Node is (your computer on the network), 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 the Node's.
+// The version, the release date and the checksums come from the Node's latest GitHub release (server/node/release.js).
+
+const kit = require('openvibe-shared/kit');
+
+const HOST = 'node.openvibe.network';
+const SITE = `https://${HOST}`;
+const DOCS = 'https://github.com/OpenVibers/OpenVibe.Node/blob/main/docs';
+const INSTALL_LINE = 'curl -fsSL https://node.openvibe.network/install | sh';
+
+// What the Node does today, each with where it happens. A page that promises tomorrow is a page that lies today.
+const DOES = [
+ { icon: 'code', title: 'Run your agents and code on your machine', body: 'Codes starts a coding task from any browser and this machine runs it, as an unprivileged user in a sandbox.', href: 'https://openvibe.codes/run', cta: 'OpenVibe.Codes' },
+ { icon: 'spark', title: 'Offer your models or your GPU and earn credits', body: 'Exchange lists what your machine serves; you choose the models, the hours, the price and who may use it.', href: 'https://openvibe.exchange/sell', cta: 'OpenVibe.Exchange' },
+ { icon: 'game', title: 'Drive robots', body: 'Bot drives what you approve, at the level you set, with a kill switch on the machine and a deadline on every command.', href: 'https://openvibe.bot', cta: 'OpenVibe.Bot' },
+ { icon: 'live', title: 'Stream an agent\u2019s desk', body: 'Live carries your machine\u2019s screen to a channel, private until you choose: under a second behind, with a curtain that hides secret screens.', href: 'https://openvibe.live/docs/agent-streams', cta: 'Agent streams on OpenVibe.Live' },
+ { icon: 'lock', title: 'Keep your memory, read-only', body: 'The collectors read what your coding tools remember: local, redacted, and never uploaded unless you say so.', href: `${DOCS}/memory.md`, cta: 'The memory docs' },
+];
+
+// What the Node never does. The two sentences people ask for most.
+const NEVER = [
+ 'It cannot reach your computer without your pairing: nobody drives it, reads it or runs a command on it until you approved the machine.',
+ 'Nothing runs that you did not turn on: no task, no camera, no driver and no upload starts on its own.',
+ 'It opens no port: it connects out, so it works behind any home router.',
+ 'It keeps a local kill switch: one command stops everything it drives, and it stays stopped.',
+];
+
+const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+
+/** The install block per OS. The one-liner is for Linux and macOS; Windows gets the release asset and the pair step. */
+function installBlock(release) {
+ const version = esc(release.version || '');
+ const exe = `https://github.com/OpenVibers/OpenVibe.Node/releases/download/${release.version || 'latest'}/openvibe-node-windows-amd64.exe`;
+ return `<div class="np-installs">
+<div class="np-install">
+<h3>Linux and macOS</h3>
+<p class="np-fine">One command. It finds your system, downloads the release, checks it against SHA256SUMS, sets up the service and shows a code to approve.</p>
+${kit.snippet({ code: INSTALL_LINE, id: 'np-install-sh', label: 'Copy the install command' })}
+<p class="np-fine">On a Raspberry Pi or another robot, the same command finds the kit and its driver.</p>
+</div>
+<div class="np-install">
+<h3>Windows</h3>
+<p class="np-fine">Download the binary, then in an Administrator prompt:</p>
+${kit.snippet({ code: 'openvibe-node pair <CODE>', id: 'np-install-win', label: 'Copy the pair command' })}
+<p class="np-fine"><a href="${esc(exe)}">openvibe-node-windows-amd64.exe</a> from the ${version ? esc(version) : 'latest'} release.</p>
+</div>
+</div>`;
+}
+
+/** The release facts: the version, when it came out, the checksums and the release page. */
+function releaseBlock(release) {
+ const { versionLabel, dateLabel } = require('./release');
+ const version = versionLabel(release);
+ const date = dateLabel(release);
+ const rows = [
+ kit.row({ title: 'Latest version', sub: release.version || '', meta: date || '', metaIso: date || undefined }),
+ kit.row({ href: release.checksumsUrl || '#', title: 'SHA256SUMS', sub: 'the checksums of every file in the release, to check the download against' }),
+ kit.row({ href: release.url || '#', title: 'The release on GitHub', sub: 'every asset, and the notes of that release' }),
+ ];
+ return `<section class="np-sec" aria-labelledby="h-release"><h2 id="h-release">The latest release</h2>
+<p class="np-fine">${version ? `Version <strong>${esc(version)}</strong>` : 'The latest version'}${date ? `, published ${esc(date)}` : ''}. The install command below always installs that release.</p>
+${kit.list(rows, { flush: true })}
+${release.stale ? '<p class="np-fine np-warn">GitHub could not be reached just now, so this is the last release we read.</p>' : ''}
+</section>`;
+}
+
+/** The page. `release` is what server/node/release.js answered. */
+function render({ release = {}, releaseId } = {}) {
+ const title = 'OpenVibe Node: your computer on the network';
+ const desc = 'OpenVibe Node runs on your computer, server, phone or robot: it runs your agents and code, offers your models or GPU to the network, drives robots, and keeps your memory read-only.';
+ const ld = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'OpenVibe Node', applicationCategory: 'DeveloperApplication',
+ operatingSystem: 'Linux, macOS, Windows', description: desc, url: SITE,
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
+ publisher: { '@type': 'Organization', '@id': 'https://openvibe.network/#org', name: 'OpenVibe' } };
+ const does = DOES.map((d) => `<article class="np-does">
+${kit.icon(d.icon)}
+<h3>${esc(d.title)}${d.soon ? ' <span class="np-soon">coming with S1</span>' : ''}</h3>
+<p>${esc(d.body)}</p>
+<p class="np-fine"><a href="${esc(d.href)}">${esc(d.cta)}</a></p>
+</article>`).join('');
+
+ return `<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>${esc(title)}</title>
+<meta name="description" content="${esc(desc)}">
+<link rel="canonical" href="${SITE}/">
+${require('../boost').meta(releaseId)}
+${require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' })}
+<meta name="color-scheme" content="dark light">
+<script src="/shared/theme-loader.js" defer></script>
+<script type="application/ld+json">${JSON.stringify(ld).replace(/</g, '\\u003c')}</script>
+<link rel="stylesheet" href="/shared/kit.css">
+<style>
+html,body{background:var(--bg-primary);color:var(--text-primary);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;margin:0}
+${CSS}
+</style>
+</head>
+<body>
+<div id="navbar-mount"></div>
+<main class="np" id="main">
+<header class="np-head">
+<p class="np-kicker">OpenVibe Node</p>
+<h1>Your computer on the network</h1>
+<p class="np-lede">One program, <code>openvibe-node</code>, on your computer, your server, your phone or your robot. It connects out to OpenVibe, needs no open ports, and keeps a local kill switch. You decide what it may do; nothing runs that you did not turn on.</p>
+<p class="np-cta">${kit.button({ href: '/install', label: 'Install the Node', kind: 'primary' })} ${kit.button({ href: `${DOCS}/install.md`, label: 'The install guide', kind: 'quiet' })}</p>
+</header>
+
+<section class="np-sec" aria-labelledby="h-does"><h2 id="h-does">What it does today</h2>
+<div class="np-grid">${does}</div>
+</section>
+
+<section class="np-sec" aria-labelledby="h-install"><h2 id="h-install">Install it</h2>
+${installBlock(release)}
+</section>
+
+${releaseBlock(release)}
+
+<section class="np-sec" aria-labelledby="h-never"><h2 id="h-never">What it never does</h2>
+<ul class="np-never">${NEVER.map((n) => `<li>${esc(n)}</li>`).join('')}</ul>
+</section>
+
+<section class="np-sec" aria-labelledby="h-docs"><h2 id="h-docs">The docs</h2>
+<ul class="np-docs">
+<li><a href="${DOCS}/install.md">Install</a> — every OS, every option, and what the installer does.</li>
+<li><a href="${DOCS}/computer.md">Your computer for your agents</a> — the computer plugin, its levels and the live screen.</li>
+<li><a href="${DOCS}/worker.md">A machine that runs your jobs</a> — the sandbox the job runner uses.</li>
+<li><a href="${DOCS}/protocol.md">The protocol</a> — the wire format between the Node and OpenVibe.Bot.</li>
+<li><a href="${DOCS}/memory.md">Your memory</a> — what the collectors read and how it is uploaded.</li>
+</ul>
+<p class="np-fine">The source is on <a href="https://github.com/OpenVibers/OpenVibe.Node">GitHub</a>. The installer also lives at <a href="https://openvibe.bot/install">openvibe.bot/install</a>, which keeps working.</p>
+</section>
+</main>
+${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' })}
+<script src="/shared/navbar.js" defer></script>
+${require('../boost').SCRIPT}
+<script src="/shared/kit-web.js" defer></script>
+<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',links:[{label:'Node',href:'/',icon:'fa-desktop',featured:true,active:true},{label:'Network',href:'https://openvibe.network/'},{label:'My Account',href:'https://openvibe.network/my'}]});}catch(e){}});</script>
+</body>
+</html>`;
+}
+
+const CSS = `
+.np{max-width:1000px;margin:0 auto;padding:32px 20px 72px}
+.np-kicker{margin:0 0 6px;font-size:.78rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--accent,#3b82f6)}
+.np h1{margin:0 0 12px;font-size:clamp(1.7rem,5vw,2.6rem);letter-spacing:-.02em;line-height:1.12}
+.np-lede{margin:0 0 18px;max-width:760px;color:var(--text-secondary,#96a7c2);line-height:1.65;font-size:1.03rem}
+.np-lede code,.np code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em}
+.np-cta{display:flex;flex-wrap:wrap;gap:10px;margin:0}
+.np-sec{margin-top:44px}
+.np-sec h2{margin:0 0 14px;font-size:1.3rem;letter-spacing:-.01em}
+.np-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px}
+.np-does{padding:18px;border-radius:16px;background:var(--bg-card,#0f1729);border:1px solid var(--border,#1f2d47);display:flex;flex-direction:column;gap:6px}
+.np-does svg{width:22px;height:22px;color:var(--accent,#3b82f6)}
+.np-does h3{margin:2px 0 0;font-size:1rem;line-height:1.35}
+.np-does p{margin:0;color:var(--text-secondary,#96a7c2);font-size:.92rem;line-height:1.55}
+.np-soon{display:inline-block;margin-left:6px;padding:2px 8px;border-radius:999px;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted,#7386a3);border:1px solid var(--border,#1f2d47);vertical-align:middle}
+.np-installs{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px}
+.np-install{padding:18px;border-radius:16px;background:var(--bg-card,#0f1729);border:1px solid var(--border,#1f2d47)}
+.np-install h3{margin:0 0 8px;font-size:1rem}
+.np-install .ovk-snip{margin:12px 0}
+.np-fine{margin:10px 0 0;font-size:.88rem;color:var(--text-muted,#7386a3);line-height:1.55}
+.np-warn{color:#f0b429}
+.np-never,.np-docs{list-style:none;margin:0;padding:0;display:grid;gap:8px;max-width:820px}
+.np-never li{position:relative;padding:11px 14px 11px 42px;border-radius:12px;background:var(--bg-card,#0f1729);border:1px solid var(--border,#1f2d47);color:var(--text-secondary,#96a7c2);font-size:.93rem;line-height:1.55}
+.np-never li::before{content:"";position:absolute;left:15px;top:16px;width:13px;height:13px;border-radius:50%;background:color-mix(in srgb,var(--accent,#3b82f6) 22%,transparent);box-shadow:inset 0 0 0 2px var(--accent,#3b82f6)}
+.np-docs li{padding:11px 14px;border-radius:12px;background:var(--bg-card,#0f1729);border:1px solid var(--border,#1f2d47);font-size:.93rem;line-height:1.55}
+.np a:not(.ovk-btn){color:var(--accent,#3b82f6)}
+@media (max-width:640px){.np{padding:22px 16px 52px}}
+`;
+
+module.exports = { render, HOST, SITE, DOCS, INSTALL_LINE, DOES, NEVER };
new file mode 100644
new file mode 100644
@@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
+'use strict';
+// The Node's latest release, read from GitHub (OpenVibers/OpenVibe.Node). The page shows the version and the
+// checksums; /install answers with the same release's installer. The answer is cached for ten minutes, so a
+// visitor never waits on GitHub, and when GitHub cannot be reached the last good answer is used; a cold start
+// without GitHub falls back to the release links that always resolve (the "latest" ones).
+
+const REPO = 'OpenVibers/OpenVibe.Node';
+const API = `https://api.github.com/repos/${REPO}/releases/latest`;
+const TTL_MS = 10 * 60 * 1000; // ten minutes
+const TIMEOUT_MS = 8000;
+const LATEST = `https://github.com/${REPO}/releases/latest`;
+
+// What the page says when GitHub cannot be reached and nothing was cached: no version (the page says "the latest
+// release" instead of naming one that may be old), and the links that always resolve to the newest release.
+const FALLBACK = {
+ version: '',
+ publishedAt: '',
+ url: LATEST,
+ installUrl: `https://github.com/${REPO}/releases/latest/download/install.sh`,
+ checksumsUrl: `https://github.com/${REPO}/releases/latest/download/SHA256SUMS`,
+};
+
+let cached = null; // { at, data }
+let inflight = null; // one fetch at a time
+
+/** The release fields the page and /install use. */
+function shape(json) {
+ const tag = String(json.tag_name || '').trim();
+ const assets = Array.isArray(json.assets) ? json.assets : [];
+ const asset = (name) => assets.find((a) => a && a.name === name);
+ const install = asset('install.sh');
+ const sums = asset('SHA256SUMS');
+ return {
+ version: tag,
+ publishedAt: String(json.published_at || ''),
+ url: String(json.html_url || (tag ? `https://github.com/${REPO}/releases/tag/${tag}` : LATEST)),
+ installUrl: String((install && install.browser_download_url) || (tag ? `https://github.com/${REPO}/releases/download/${tag}/install.sh` : FALLBACK.installUrl)),
+ checksumsUrl: String((sums && sums.browser_download_url) || (tag ? `https://github.com/${REPO}/releases/download/${tag}/SHA256SUMS` : FALLBACK.checksumsUrl)),
+ };
+}
+
+/** Fetch the latest release. Returns the cached answer while it is fresh, and never throws. */
+async function latest({ fetchImpl = globalThis.fetch, now = Date.now } = {}) {
+ if (cached && now() - cached.at < TTL_MS) return { ...cached.data, cached: true };
+ if (inflight) return inflight;
+ inflight = (async () => {
+ try {
+ const controller = typeof AbortController === 'function' ? new AbortController() : null;
+ const timer = controller ? setTimeout(() => controller.abort(), TIMEOUT_MS) : null;
+ let res;
+ try {
+ res = await fetchImpl(API, {
+ signal: controller ? controller.signal : undefined,
+ headers: { accept: 'application/vnd.github+json', 'user-agent': 'openvibe-network' },
+ });
+ } finally {
+ if (timer) clearTimeout(timer);
+ }
+ if (!res || !res.ok) throw new Error(`GitHub releases API answered ${res && res.status}`);
+ const data = shape(await res.json());
+ if (!data.version) throw new Error('GitHub releases API answered without a tag');
+ cached = { at: now(), data };
+ return { ...data, cached: false };
+ } catch {
+ // GitHub is down or slow: the last good answer, or the written-down one. The page still works.
+ return { ...(cached ? cached.data : FALLBACK), cached: true, stale: true };
+ } finally {
+ inflight = null;
+ }
+ })();
+ return inflight;
+}
+
+/** The version without the leading v, for a sentence. */
+function versionLabel(release) {
+ return String((release && release.version) || '').replace(/^v/, '');
+}
+
+/** The release date, the day it was published. */
+function dateLabel(release) {
+ const iso = String((release && release.publishedAt) || '');
+ if (!iso) return '';
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return '';
+ return d.toISOString().slice(0, 10);
+}
+
+module.exports = { latest, versionLabel, dateLabel, REPO, API, TTL_MS, FALLBACK, LATEST };
new file mode 100644
new file mode 100644
@@ -0,0 +1,110 @@
@@ -0,0 +1,110 @@
+'use strict';
+// node.openvibe.network, served by Network (the owner, 2026-11-06: the Node gets its own subdomain, "because it
+// looks nicer"). The Host header decides: on this host '/' is the Node's page, '/install' answers with the
+// installer of the latest release, and the page has its Markdown twin and its llms.txt, like Network's other pages.
+//
+// The route is mounted before the agent layer, so the Node's page is not answered by Network's home renderer.
+
+const { markdownTwin, linkHeaderTags } = require('openvibe-shared/agent-layer');
+const page = require('./page');
+const release = require('./release');
+
+const HOST = page.HOST;
+const SITE = page.SITE;
+
+/** The request's host, lowercased, without the port. */
+function hostOf(req) {
+ return String((req.headers && req.headers.host) || '').split(':')[0].toLowerCase();
+}
+
+/** The Node's llms.txt: what the page is, how to install it, and where the docs are. */
+function llmsTxt() {
+ return [
+ '# OpenVibe Node',
+ '',
+ '> OpenVibe Node is one program (openvibe-node) that runs on your computer, server, phone or robot. It connects out to OpenVibe, needs no open ports, keeps a local kill switch, and does nothing you did not turn on.',
+ '',
+ '## The page',
+ '',
+ `- [OpenVibe Node: your computer on the network](${SITE}/): what it is, what it does today (run your agents and code, offer your models or GPU and earn credits, drive robots, stream an agent's desk, keep your memory read-only), how to install it on each OS, and what it never does`,
+ '',
+ '## Install',
+ '',
+ `- [Install the Node](${SITE}/install): the latest release's installer, as a script to run`,
+ '',
+ '```sh',
+ page.INSTALL_LINE,
+ '```',
+ '',
+ '## The docs',
+ '',
+ `- [Install](${page.DOCS}/install.md): every OS, every option, and what the installer does`,
+ `- [Your computer for your agents](${page.DOCS}/computer.md): the computer plugin, its levels and the live screen`,
+ `- [A machine that runs your jobs](${page.DOCS}/worker.md): the sandbox the job runner uses`,
+ `- [The protocol](${page.DOCS}/protocol.md): the wire format between the Node and OpenVibe.Bot`,
+ `- [Your memory](${page.DOCS}/memory.md): what the collectors read and how it is uploaded`,
+ '- [The source on GitHub](https://github.com/OpenVibers/OpenVibe.Node)',
+ '',
+ ].join('\n');
+}
+
+/**
+ * The Node's routes. Returns null for a request that is not for this host, so the router can be mounted early
+ * and every other host falls through to Network's own pages.
+ */
+function createNodeRoutes({ releaseId } = {}) {
+ const router = require('express').Router();
+
+ router.use(async (req, res, next) => {
+ if (hostOf(req) !== HOST) return next();
+ const url = new URL(req.url, SITE);
+ const path = url.pathname;
+ const isTwin = path === '/index.md' || path.endsWith('.md') || url.searchParams.get('format') === 'markdown'
+ || (req.headers.accept && req.accepts(['html', 'text/markdown']) === 'text/markdown');
+
+ // The installer: the latest release's own asset, so `curl -fsSL https://node.openvibe.network/install | sh`
+ // installs that release. The same cached answer the page shows.
+ if (path === '/install') {
+ let rel;
+ try { rel = await release.latest(); } catch { rel = release.FALLBACK; }
+ const target = rel && rel.installUrl ? rel.installUrl : release.FALLBACK.installUrl;
+ return res.set('Cache-Control', 'public, max-age=600').redirect(302, target);
+ }
+
+ if (path === '/llms.txt') {
+ return res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(llmsTxt());
+ }
+
+ // The crawl files of this host, so a crawler on node.openvibe.network is told about this page and not
+ // about Network's (the apex serves its own).
+ if (path === '/robots.txt') {
+ const robots = require('openvibe-shared/seo').robotsTxt({ sitemaps: [`${SITE}/sitemap.xml`] });
+ return res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(robots);
+ }
+ if (path === '/sitemap.xml') {
+ const xml = require('openvibe-shared/seo').sitemapXml([{ loc: `${SITE}/`, changefreq: 'weekly', priority: 1.0 }]);
+ return res.type('application/xml').set('Cache-Control', 'public, max-age=3600').send(xml);
+ }
+
+ if (path !== '/' && path !== '/index.md' && path !== '/index.html') return next();
+
+ let rel;
+ try { rel = await release.latest(); } catch { rel = release.FALLBACK; }
+ const html = page.render({ release: rel, releaseId });
+ const canonical = `${SITE}/`;
+ const links = linkHeaderTags(canonical, `${SITE}/llms.txt`);
+ res.setHeader('Link', [res.getHeader('Link'), links.header].filter(Boolean).join(', '));
+ res.setHeader('Vary', [res.getHeader('Vary'), 'Accept'].filter(Boolean).join(', '));
+ res.setHeader('Cache-Control', 'public, max-age=600');
+ if (isTwin) {
+ res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
+ return res.end(req.method === 'HEAD' ? undefined : markdownTwin({ html, title: 'OpenVibe Node: your computer on the network', canonical }));
+ }
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
+ return res.end(req.method === 'HEAD' ? undefined : html);
+ });
+
+ return router;
+}
+
+module.exports = { createNodeRoutes, llmsTxt, hostOf, HOST, SITE };
new file mode 100644
new file mode 100644
@@ -0,0 +1,143 @@
@@ -0,0 +1,143 @@
+'use strict';
+// node.openvibe.network: the Node's own front door, served by Network. The Host header decides: on that host '/'
+// is the Node's page, '/install' answers with the installer of the latest release, and the page has its Markdown
+// twin and its llms.txt, like Network's other pages.
+// node test/node-home.test.js
+const assert = require('assert');
+const http = require('http');
+const express = require('express');
+
+const page = require('../server/node/page');
+const release = require('../server/node/release');
+const { createNodeRoutes, llmsTxt } = require('../server/node/routes');
+
+// ── The release answer: cached for ten minutes, with a fallback when GitHub cannot be reached ──
+const FIXTURE = {
+ tag_name: 'v9.9.9',
+ published_at: '2026-11-06T12:00:00Z',
+ html_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/tag/v9.9.9',
+ assets: [
+ { name: 'install.sh', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh' },
+ { name: 'SHA256SUMS', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS' },
+ ],
+};
+
+(async () => {
+ // The cache: one call to GitHub, then the same answer for ten minutes.
+ let calls = 0;
+ const fetchImpl = async () => { calls += 1; return { ok: true, status: 200, json: async () => FIXTURE }; };
+ let now = 1000;
+ const clock = () => now;
+ const first = await release.latest({ fetchImpl, now: clock });
+ assert.strictEqual(first.version, 'v9.9.9', 'the release is read from GitHub');
+ assert.strictEqual(first.installUrl, 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh');
+ assert.strictEqual(first.checksumsUrl, 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS');
+ const second = await release.latest({ fetchImpl, now: clock });
+ assert.strictEqual(calls, 1, 'the second call is answered from the cache');
+ assert.strictEqual(second.version, 'v9.9.9');
+
+ // Past ten minutes, GitHub is asked again.
+ now += release.TTL_MS + 1;
+ await release.latest({ fetchImpl, now: clock });
+ assert.strictEqual(calls, 2, 'after ten minutes GitHub is asked again');
+
+ // GitHub unreachable: the page still works, and says so. (Past the ten minutes, so the cache cannot answer.)
+ now += release.TTL_MS + 1;
+ const broken = await release.latest({ fetchImpl: async () => { throw new Error('no GitHub'); }, now: clock });
+ assert.ok(broken.version, 'a release is still named');
+ assert.strictEqual(broken.stale, true, 'and it is marked as the last one read');
+ assert.ok(broken.installUrl.startsWith('https://github.com/OpenVibers/OpenVibe.Node/releases/download/'), 'the installer link still works');
+
+ // ── The page ──
+ const html = page.render({ release: first, releaseId: 'rel_test' });
+ assert.ok(html.includes('Your computer on the network'), 'the page says what the Node is');
+ assert.ok(html.includes('curl -fsSL https://node.openvibe.network/install | sh'), 'the one-line installer is on the page');
+ assert.ok(html.includes('v9.9.9'), 'the version is on the page');
+ assert.ok(html.includes('2026-11-06'), 'the release date is on the page');
+ assert.ok(html.includes('releases/download/v9.9.9/SHA256SUMS'), 'the checksums are on the page');
+ assert.ok(html.includes('releases/tag/v9.9.9'), 'the release page is on the page');
+ assert.ok(html.includes('openvibe-node-windows-amd64.exe'), 'Windows has its own way in');
+ assert.ok(html.includes('openvibe.bot/install'), 'the installer also lives at openvibe.bot/install');
+ for (const [what, needle] of [
+ ['run your agents and code', 'openvibe.codes/run'],
+ ['offer your models and earn credits', 'openvibe.exchange/sell'],
+ ['drive robots', 'openvibe.bot'],
+ ['stream an agent\u2019s desk', 'openvibe.live'],
+ ['your memory, read-only', 'memory.md'],
+ ]) assert.ok(html.includes(needle), `the page says it ${what}`);
+ // What it never does.
+ assert.ok(html.includes('without your pairing'), 'the page says the Node needs your pairing');
+ assert.ok(html.includes('Nothing runs that you did not turn on'), 'and that nothing runs on its own');
+ // The docs.
+ assert.ok(html.includes('docs/install.md'), 'the install guide is linked');
+ // Network's chrome.
+ assert.ok(html.includes('/shared/navbar.js'), 'the shared navbar is on the page');
+ assert.ok(html.includes('https://node.openvibe.network'), 'the page names its own address');
+ assert.ok(html.includes('<link rel="canonical" href="https://node.openvibe.network/">'), 'and claims it as its own');
+
+ // The llms.txt says the same thing in plain lines.
+ const txt = llmsTxt();
+ assert.ok(txt.startsWith('# OpenVibe Node'), 'the llms.txt names the site');
+ assert.ok(txt.includes('https://node.openvibe.network/'), 'and links the page');
+ assert.ok(txt.includes('curl -fsSL https://node.openvibe.network/install | sh'), 'and the install command');
+
+ // ── The routes: the Host header decides ──
+ const app = express();
+ app.use(createNodeRoutes({ releaseId: 'rel_test' }));
+ app.get('/', (_req, res) => res.type('html').send('<p>network home</p>'));
+ const server = http.createServer(app).listen(0);
+ await new Promise((r) => server.once('listening', r));
+ const port = server.address().port;
+ const get = (path, host, headers = {}) => new Promise((resolve) => {
+ const req = http.request({ host: '127.0.0.1', port, path, method: 'GET', headers: { host, ...headers } }, (res) => {
+ let body = '';
+ res.on('data', (c) => { body += c; });
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
+ });
+ req.end();
+ });
+
+ try {
+ const home = await get('/', 'openvibe.network');
+ assert.ok(home.body.includes('network home'), 'on the apex, Network\u2019s own home answers');
+
+ const node = await get('/', 'node.openvibe.network');
+ assert.strictEqual(node.status, 200);
+ assert.ok(node.body.includes('Your computer on the network'), 'on the Node\u2019s host, the Node\u2019s page answers');
+ assert.ok(!node.body.includes('network home'), 'and not Network\u2019s home');
+ assert.ok(String(node.headers.link || '').includes('text/markdown'), 'the page offers its Markdown twin');
+
+ const twin = await get('/index.md', 'node.openvibe.network');
+ assert.strictEqual(twin.status, 200);
+ assert.ok(String(twin.headers['content-type'] || '').includes('text/markdown'), 'the twin is Markdown');
+ assert.ok(twin.body.includes('# OpenVibe Node'), 'and it says what the Node is');
+ assert.ok(twin.body.includes('openvibe.network/install'), 'with the install command');
+
+ const install = await get('/install', 'node.openvibe.network');
+ assert.strictEqual(install.status, 302, '/install redirects');
+ assert.ok(String(install.headers.location || '').includes('releases/download/'), 'to the release\u2019s own installer');
+ assert.ok(String(install.headers.location || '').endsWith('/install.sh'), 'the install script');
+
+ const llms = await get('/llms.txt', 'node.openvibe.network');
+ assert.strictEqual(llms.status, 200);
+ assert.ok(llms.body.startsWith('# OpenVibe Node'), 'the Node\u2019s own llms.txt answers');
+
+ // Network\u2019s own paths are untouched on the apex.
+ const apex = await get('/llms.txt', 'openvibe.network');
+ assert.notStrictEqual(apex.status, 200, 'the apex keeps its own llms.txt');
+ } finally {
+ server.close();
+ }
+
+ // ── The navbar: the Node is the piece the Network is built around ──
+ const index = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'index.html'), 'utf8');
+ assert.ok(/links: \[\{ label: 'Node', href: 'https:\/\/node\.openvibe\.network', icon: 'fa-desktop', featured: true \}/.test(index), 'the home page\u2019s navbar leads with the Node, as the accent');
+ assert.ok(index.indexOf("label: 'Node'") < index.indexOf("label: 'My Account'"), 'and the Node comes first');
+
+ // The Node's own origin is allowed to ask the network's API (the account's look, the navbar's frame, the)
+ // shared font): without it, the Node\u2019s page loses its own chrome in the browser.
+ const serverSrc = require('fs').readFileSync(require('path').join(__dirname, '..', 'server', 'index.js'), 'utf8');
+ assert.ok(serverSrc.includes("'https://node.openvibe.network',"), 'the CORS allow-list knows the Node\u2019s own origin');
+
+ console.log('node-home: the Node\u2019s page, its installer, its twin and the navbar item ok');
+})().catch((e) => { console.error(e); process.exit(1); });
@@ -12,11 +12,11 @@ const { spawn } = require('child_process');
@@ -12,11 +12,11 @@ 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: 196, // 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: 36.5, // 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)
+ htmlRawKB: 197, // 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: 36.8, // 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: 354, // 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
- jsBrotliKB: 86.5, // 86.2 with the account-safety helper on every page (2026-11-06: +2.2 KB brotli, the same feature); 83.4 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 82.6 with openvibe-shared 4.52.0 (from 4.47.2): the seasonal fall, rich embeds, the Dock and its Chats tab (2026-10-30); 81.4 with openvibe-shared 4.47.2 (comfort settings, touch targets, the grouped updates log); 78.9 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 77 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; 73.3 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 70.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 68.0 with shared 4.12.0 (light/dark switch, seasonal mark, theme pairs); 64.5 with shared 4.11.0's navbar; 60.3 with boost.js (3.8); 53.9
+ jsRawKB: 365, // 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
+ jsBrotliKB: 89.5, // about 88.8 with both (2026-11-06: 4.117.0's featured item and the account-safety helper); 86.6 with openvibe-shared 4.117.0 (2026-11-06: the featured navbar item's CSS and the phone-menu order); 83.4 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 82.6 with openvibe-shared 4.52.0 (from 4.47.2): the seasonal fall, rich embeds, the Dock and its Chats tab (2026-10-30); 81.4 with openvibe-shared 4.47.2 (comfort settings, touch targets, the grouped updates log); 78.9 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 77 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; 73.3 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 70.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 68.0 with shared 4.12.0 (light/dark switch, seasonal mark, theme pairs); 64.5 with shared 4.11.0's navbar; 60.3 with boost.js (3.8); 53.9
cssFiles: 2, // 1 (the site's own; Font Awesome's stylesheet is gone since 2026-11-01)
cssFiles: 2, // 1 (the site's own; Font Awesome's stylesheet is gone since 2026-11-01)
externalFiles: 3, // none locally (Font Awesome was the one); production adds Cloudflare's beacon
externalFiles: 3, // none locally (Font Awesome was the one); production adds Cloudflare's beacon
};
};
@@ -136,6 +136,7 @@ const REVIEWED = {
@@ -136,6 +136,7 @@ const REVIEWED = {
'server/registry/deploy-drift.js fetch': [1, 'fixed host api.github.com'],
'server/registry/deploy-drift.js fetch': [1, 'fixed host api.github.com'],
'server/registry/ecosystem.js fetch': [2, 'service URLs from openvibe-contracts manifests and operator env overrides'],
'server/registry/ecosystem.js fetch': [2, 'service URLs from openvibe-contracts manifests and operator env overrides'],
'server/registry/library-tags.js fetch': [1, 'fixed host api.github.com'],
'server/registry/library-tags.js fetch': [1, 'fixed host api.github.com'],
+ 'server/node/release.js fetch': [1, 'the Node\'s own release page: a fixed GitHub API path (https://api.github.com/repos/OpenVibers/OpenVibe.Node/releases/latest), 8 s timeout, held 10 minutes; no request value ever names a host and it only ever reads the public release list'],
'server/registry/machines.js fetch': [1, 'the machines hub calls Bot: the internal URL from operator config (OV_BOT_INTERNAL_URL, loopback by default), the paths are fixed, the machine id is a checked dev_ id (percent-encoded) and the person is named in a header, never in the URL'],
'server/registry/machines.js fetch': [1, 'the machines hub calls Bot: the internal URL from operator config (OV_BOT_INTERNAL_URL, loopback by default), the paths are fixed, the machine id is a checked dev_ id (percent-encoded) and the person is named in a header, never in the URL'],
'server/updates/routes.js fetch': [1, 'OV_BLOG_INTERNAL_URL (operator config)'],
'server/updates/routes.js fetch': [1, 'OV_BLOG_INTERNAL_URL (operator config)'],
};
};