openvibers/openvibe.network Commits
Serve the front door, status and updates pages on boost.js (#173)
Changes
@@@ -631,6 +631,8 @@ Include them in any service page:<script src="https://openvibe.network/shared/user-card.js"></script>```++**Boost (plan T11).** `/`, `/status` and `/updates` load `/shared/boost.js` (`data-main="#main"`): a same-site link between two pages with the same external scripts swaps only `<main id="main">` instead of loading the page. Today that is the `/updates?site=` filter links between `/updates` views; `/`, `/status` and `/updates` each load other scripts (the footer, the mark, the shipped log), so a move between them is a normal page load until their script sets match. Each carries `<meta name="ov-boost" content="network@<release>">`, the release id `/release.json` serves (`server/boost.js`), so a page from another deploy is a normal page load too. On `/` the static hero, intent picker and constellation stay outside `#main`, and the release id is part of the page cache key, so a deploy changes the `/` ETag. Pages without the marker (`/my`, `/login`, `/admin/*`, `/verify-email`, `/@:username`, the legal pages) are always a normal page load, by design. Page script that lives outside `#main` and wires something inside it re-wires on `ov:boost:load` (the shipped log on `/updates`, the shipped strip on `/`).++---## Admin Panel@@@ -213,6 +213,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}</div></section>++ <main id="main"><!--OV:HOME--><!-- What shipped across the network (the changelog on openvibe.blog; openvibe-shared shipped.js) -->@@@ -229,6 +230,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}<p>openvibe.network is the identity behind it all: sign in once and every OpenVibe site signs you in too, your themes and notifications follow you, your history across the network lives in one place, and the OpenCoins wallet works everywhere.</p></div></section>++ </main></div>@@@ -238,7 +240,8 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}<script src="/shared/navbar.js"></script><script src="/shared/footer.js"></script><script src="/shared/shipped.js" defer></script>--<script>document.addEventListener('DOMContentLoaded', function () { if (window.OpenVibeShipped) OpenVibeShipped.mount('#ov-shipped', { limit: 8, title: 'Recently shipped across OpenVibe', more: '/updates' }); });</script>++<script src="/shared/boost.js" data-main="#main" defer></script>++<script>(function () { function shipped() { if (window.OpenVibeShipped && document.getElementById('ov-shipped')) OpenVibeShipped.mount('#ov-shipped', { limit: 8, title: 'Recently shipped across OpenVibe', more: '/updates' }); } document.addEventListener('DOMContentLoaded', shipped); document.addEventListener('ov:boost:load', shipped); })();</script><script>(function () {if (window.OpenVibeFooter) { try { OpenVibeFooter.init({ service: 'network', variant: 'full', links: [{ heading: 'OpenVibe.Network', items: [{ name: 'My account', url: '/my' }, { name: 'History', url: '/my#history' }, { name: 'Themes', url: '/themes' }, { name: 'Linked services', url: '/my#linked' }, { name: 'Sign in', url: '/login' }] }] }); } catch (e) { /* */ } }nnew file mode 100644@@@ -0,0 +1,13 @@++'use strict';++// openvibe-shared/boost.js on Network's server-rendered pages (plan T11 step 3): a page carries the release marker and++// the script, and wraps what changes in <main id="main">. The marker is the served release id (release.release, the id++// /release.json and the ov-release meta carry), so a page from another deploy is a normal page load. Pages without++// the marker (/my, /login, /admin/*, /verify-email, /@:username, the legal pages) are always a normal load.++// boost.js also needs the same external scripts on both pages: only the /updates filter views share theirs, so a++// move between /, /status and /updates is a normal load until their script sets match.++const esc = require('openvibe-shared/seo').esc;++++const meta = (releaseId) => `<meta name="ov-boost" content="network@${esc(releaseId || 'dev')}">`;++const SCRIPT = '<script src="/shared/boost.js" data-main="#main" defer></script>';++++module.exports = { meta, SCRIPT };@@@ -11,6 +11,7 @@ const icons = require('openvibe-shared/icons');const showcase = require('openvibe-shared/showcase');const toolsCatalog = require('../domains/catalog');const activity = require('./activity');++const boost = require('../boost');const SHELL = path.join(__dirname, '..', '..', 'public', 'index.html');const esc = seo.esc;@@@ -196,22 +197,23 @@ function summaryOf(catalog) {}let pageCache = { key: '', html: '', etag: '' };--function render() {++// releaseId: the served release (server/index.js release.release), the boost marker; it keys the cache so a deploy changes the ETag.++function render({ releaseId = '' } = {}) {const { catalog } = toolsCatalog.peek();const act = activity.peek();const stat = fs.statSync(SHELL);-- const key = `${stat.mtimeMs}:${catalog.updated}:${catalog.tools.length}:${act.version}`;++ const key = `${stat.mtimeMs}:${catalog.updated}:${catalog.tools.length}:${act.version}:${releaseId}`;if (pageCache.key === key) return pageCache;const ld = seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what }))));const summary = summaryOf(catalog);-- const html = fs.readFileSync(SHELL, 'utf8').replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + summary.body + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog)).replace('<!--OV:CONSTELLATION-->', constellation()).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', `${ld}\n${summary.head}\n</head>`);++ const html = fs.readFileSync(SHELL, 'utf8').replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + summary.body + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog)).replace('<!--OV:CONSTELLATION-->', constellation()).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);pageCache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };return pageCache;}--function sendHome(req, res) {++function sendHome(req, res, { releaseId = '' } = {}) {toolsCatalog.getCatalog().catch(() => {}); // refresh in the background; this response uses what is known now-- const page = render();++ const page = render({ releaseId });res.set('Content-Type', 'text/html; charset=utf-8');res.set('Cache-Control', cache.htmlHeaders({ maxAge: 120 }));res.set('ETag', page.etag);@@@ -577,9 +577,9 @@ libraryTags.start();app.use('/api/v1/creators', rateLimit({ windowMs: 60_000, max: 300 }), require('./analytics/creators').router({ fullGuard: require('./identity/principals').guard('network.analytics.creator.read') }));// Incidents and maintenance (WS-N task 12): public list; staff admins or network.status.incident (ovhost) write.app.use('/api/v1/status/incidents', rateLimit({ windowMs: 60_000, max: 120 }), require('./status/incidents').router({ requireAuth, incidentGuard: require('./identity/principals').guard('network.status.incident') }));--app.use(require('./status/routes').createStatusRoutes({ ecosystem }));++app.use(require('./status/routes').createStatusRoutes({ ecosystem, release }));// What shipped network-wide: the changelog proxy every site's widget reads, and /updates.--app.use(require('./updates/routes').createUpdatesRoutes({ blogUrl: process.env.OV_BLOG_INTERNAL_URL || 'http://127.0.0.1:4810' }).router);++app.use(require('./updates/routes').createUpdatesRoutes({ blogUrl: process.env.OV_BLOG_INTERNAL_URL || 'http://127.0.0.1:4810', release }).router);app.get('/api/.well-known/jwks', (_req, res) => {// Without key files the ephemeral HS256 "key pair" is one shared secret: publishing it would let@@@ -828,7 +828,7 @@ app.get(['/login.html', '/admin.html'], (req, res) => {// Landing page at the apex rootapp.get(['/', '/index.html'], (req, res) => {if (req.path === '/index.html') return redirectWithoutHtml(req, res, '/');-- return require('./home/render').sendHome(req, res);++ return require('./home/render').sendHome(req, res, { releaseId: release.release });});app.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(require('./home/render').llmsTxt()));app.get('/llms-full.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(require('./home/render').llmsFullTxt({ baseUrl: config.baseUrl })));@@@ -26,6 +26,7 @@ const express = require('express');const contracts = require('openvibe-contracts');const seo = require('openvibe-shared/seo');const exposure = require('../registry/exposure');++const boost = require('../boost');const SLO_FILE = path.join(__dirname, '..', '..', 'docs', 'slo.json');// The daily developer path against production (OpenVibe.Host openvibe-devpath.timer, WS-N task 1): step@@@ -170,7 +171,7 @@ function incidentsSection(incidents) {}).join('')}<p><a href="/api/v1/status/incidents">History (JSON)</a></p></section>`;}--function renderPage(list, slo, generatedAt, devPath = null, toolsJob = null, healthSince = null, incidents = null) {++function renderPage(list, slo, generatedAt, devPath = null, toolsJob = null, healthSince = null, incidents = null, releaseId = '') {const counts = summary(list);const tr = list.map((r) => `<tr id="svc-${esc(r.id)}"><td><b>${esc(r.name)}</b><small>${esc(r.id)} · manifest: ${esc(r.manifest_status)}</small></td>@@@ -188,6 +189,7 @@ function renderPage(list, slo, generatedAt, devPath = null, toolsJob = null, hea<title>Status · OpenVibe.Network</title><meta name="description" content="Observed status of each OpenVibe service: readiness, release and when it was last checked."><meta name="robots" content="noindex, nofollow">++${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>@@@ -232,12 +234,13 @@ ${sloRows}</main>${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' })}<script src="/shared/navbar.js" defer></script>++${boost.SCRIPT}<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script></body></html>`;}--function createStatusRoutes({ ecosystem, now = () => new Date() }) {++function createStatusRoutes({ ecosystem, release = null, now = () => new Date() }) {const r = express.Router();r.get('/api/v1/status', (_req, res) => {const list = rows(ecosystem);@@@ -264,7 +267,7 @@ function createStatusRoutes({ ecosystem, now = () => new Date() }) {res.set('Content-Type', 'text/html; charset=utf-8').set('Cache-Control', 'no-cache, max-age=0').set('X-Robots-Tag', 'noindex, nofollow');let incidents = null;try { incidents = await require('./incidents').list(_req.app.locals.db); } catch { incidents = null; }-- res.send(renderPage(rows(ecosystem), slo, now().toISOString(), loadDevPath(), loadDevPath(TOOLSJOB_FILE), typeof ecosystem.releaseHealthSince === 'function' ? ecosystem.releaseHealthSince() : null, incidents));++ res.send(renderPage(rows(ecosystem), slo, now().toISOString(), loadDevPath(), loadDevPath(TOOLSJOB_FILE), typeof ecosystem.releaseHealthSince === 'function' ? ecosystem.releaseHealthSince() : null, incidents, release && release.release));});return r;}@@@ -11,6 +11,7 @@ const cache = require('openvibe-shared/cache-policy');* "Load more", a filter per site, the Patch notes posts).*/const express = require('express');++const boost = require('../boost');const TTL_MS = 60 * 1000;const MAX_KEYS = 300;@@@ -18,7 +19,7 @@ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&':const SERVICE_RE = /^[a-z][a-z0-9-]{1,39}$/;const CURSOR_RE = /^[A-Za-z0-9_-]{1,120}$/;--function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = globalThis.fetch, now = () => Date.now(), log = console } = {}) {++function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', release = null, fetchImpl = globalThis.fetch, now = () => Date.now(), log = console } = {}) {const base = String(blogUrl).replace(/\/+$/, '');const feedCache = new Map(); // key -> { at, body }@@@ -64,7 +65,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = glres.set('Content-Type', 'text/html; charset=utf-8').set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));// The unfiltered log is the page; a per-site view is a filter of it (followed, not indexed).if (asked) res.set('X-Robots-Tag', 'noindex, follow');-- res.send(renderPage({ site, filtered: Boolean(asked), body }));++ res.send(renderPage({ site, filtered: Boolean(asked), body, releaseId: release && release.release }));});return { router: r, feed, queryOf };}@@@ -85,7 +86,7 @@ function serviceFor(v) {return m && SITES.includes(m[1]) ? m[1] : null;}--function renderPage({ site, filtered, body }) {++function renderPage({ site, filtered, body, releaseId = '' }) {const entries = (body && Array.isArray(body.entries)) ? body.entries : [];const posts = (body && Array.isArray(body.posts)) ? body.posts.filter((p) => /^https:\/\//.test(p.url || '')) : [];const title = site ? `What shipped on OpenVibe.${nameOf(site)}` : 'What shipped across OpenVibe';@@@ -110,6 +111,7 @@ function renderPage({ site, filtered, body }) {<link rel="canonical" href="https://openvibe.network/updates${filtered && site ? `?site=${esc(site)}` : ''}">${filtered ? '<meta name="robots" content="noindex, follow">' : ''}<link rel="alternate" type="application/json" href="/api/v1/changelog${site ? `?service=${esc(site)}` : ''}">++${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>@@@ -143,6 +145,8 @@ ${dayHtml || '<p class="ov-shipped-empty">The update history could not be loaded${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact', updates: '/updates' })}<script src="/shared/navbar.js" defer></script><script src="/shared/shipped.js" defer></script>++${boost.SCRIPT}++<script>document.addEventListener('ov:boost:load',function(){if(window.OpenVibeShipped)OpenVibeShipped.scan(document.getElementById('main'));});</script><script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script></body></html>`;nnew file mode 100644@@@ -0,0 +1,107 @@++'use strict';++// openvibe-shared/boost.js on Network's pages (plan T11 step 3): /, /status and /updates carry the release marker++// (network@<served release id>), wrap what changes in <main id="main"> and load boost.js; the front door's static hero++// stays outside #main, a new release changes the / ETag, and the hero's intent picker still works. boost.js swaps++// only between pages with the same external scripts: the /updates views share theirs; /, /status and /updates differ.++// node test/boost.test.js++const assert = require('assert');++const http = require('http');++const vm = require('vm');++const express = require('express');++const activity = require('../server/home/activity');++const home = require('../server/home/render');++const { createStatusRoutes } = require('../server/status/routes');++const { createUpdatesRoutes } = require('../server/updates/routes');++++activity.setEnabled(false);++activity._set({ live: [], pastes: [], posts: [] });++++const SCRIPT = '<script src="/shared/boost.js" data-main="#main" defer></script>';++const count = (s, needle) => s.split(needle).length - 1;++function boosted(html, page, releaseId) {++ assert.ok(html.includes(`<meta name="ov-boost" content="network@${releaseId}">`), `${page}: the release marker`);++ assert.strictEqual(count(html, 'name="ov-boost"'), 1, `${page}: one marker`);++ assert.ok(html.indexOf('name="ov-boost"') < html.indexOf('</head>'), `${page}: the marker is in <head>`);++ assert.strictEqual(count(html, 'id="main"'), 1, `${page}: one #main`);++ assert.strictEqual(count(html, SCRIPT), 1, `${page}: boost.js once`);++ assert.ok(html.indexOf(SCRIPT) > html.indexOf('</main>') && html.indexOf(SCRIPT) < html.indexOf('</body>'), `${page}: boost.js after #main, before </body>`);++}++++(async () => {++ // ── / : in-process render ───────────────────────────────────────────────────++ const a = home.render({ releaseId: 'aaaaaaaaaaaa' });++ const html = a.html;++ boosted(html, '/', 'aaaaaaaaaaaa');++ assert.strictEqual(count(html, '</head>'), 1, 'exactly one </head>');++ assert.strictEqual((html.match(/<h1[\s>]/g) || []).length, 1, 'exactly one <h1>');++ const open = html.indexOf('<main id="main">'), close = html.indexOf('</main>');++ for (const [what, at] of [['the hero', html.indexOf('<section class="hero">')], ['the intents', html.indexOf('<ul class="intents"')], ['the constellation', html.indexOf('<div class="hero-sky">')], ['the <h1>', html.indexOf('<h1>')]]) {++ assert.ok(at > 0 && at < open, `${what} stays outside #main`);++ }++ for (const [what, at] of [['the home body', html.indexOf('aria-labelledby="h-now"')], ['the sites', html.indexOf('id="network"')], ['the shipped strip', html.indexOf('id="ov-shipped"')]]) {++ assert.ok(at > open && at < close, `${what} is inside #main`);++ }++ assert.ok(close < html.indexOf('<footer'), '#main closes before the footer');++++ // A deploy changes the marker and so the ETag; the same release is the same cached page.++ const b = home.render({ releaseId: 'bbbbbbbbbbbb' });++ assert.notStrictEqual(b.etag, a.etag, 'a new release id changes the / ETag');++ assert.ok(b.html.includes('content="network@bbbbbbbbbbbb"'));++ assert.strictEqual(home.render({ releaseId: 'bbbbbbbbbbbb' }).etag, b.etag, 'the same release keeps its ETag');++++ // ── the hero's intent picker (its inline script, over the rendered intents) ─++ const intents = [...html.matchAll(/<a class="intent" href="([^"]+)" data-id="([^"]+)" data-k="([^"]*)"/g)].map(([, href, id, k]) => {++ const classes = new Set();++ return { href, id, classes, getAttribute: (n) => ({ 'data-id': id, 'data-k': k }[n]), classList: { toggle: (c, on) => (on ? classes.add(c) : classes.delete(c)) } };++ });++ assert.ok(intents.length > 5, 'the intents render');++ const script = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map((m) => m[1]).find((s) => s.includes("'.intent-bar'"));++ assert.ok(script, 'the intent picker script is on the page');++ const on = {};++ const target = (name) => ({ addEventListener: (type, fn) => { on[`${name}:${type}`] = fn; } });++ const form = target('form'), q = { ...target('q'), value: '' };++ const location = { href: '/' };++ vm.runInNewContext(script, { location, document: { querySelector: (s) => (s === '.intent-bar' ? form : null), getElementById: (id) => (id === 'intent-q' ? q : null), querySelectorAll: (s) => (s === '.intent' ? intents : []) } });++ const byId = (id) => intents.find((i) => i.id === id);++ q.value = 'go live'; on['q:input']();++ assert.ok(byId('live').classes.has('match'), '"go live" lights the live intent');++ assert.ok(byId('chat').classes.has('dim') && !byId('live').classes.has('dim'), 'the others dim');++ let prevented = false;++ on['form:submit']({ preventDefault: () => { prevented = true; } });++ assert.ok(prevented && location.href === byId('live').href, 'Enter opens the intent it names');++ q.value = ''; on['q:input']();++ assert.ok(intents.every((i) => !i.classes.has('dim') && !i.classes.has('match')), 'an empty search clears the picker');++ q.value = 'merge pdf'; prevented = false; on['form:submit']({ preventDefault: () => { prevented = true; } });++ assert.ok(!prevented, 'a tool search stays the form\'s own openvibe.tools search');++++ // ── /status and /updates: the route factories get the release ──────────────++ const release = { release: 'cccccccccccc' };++ const ecosystem = { current: () => [], lastPollAt: () => null, pollMs: 60000, releaseHealthSince: () => null };++ const feed = { entries: [{ service: 'wiki', sha: 'a'.repeat(40), short: 'aaaaaaa', subject: 'x', deployed_at: '2026-10-13T07:00:00.000Z', url: 'https://github.com/OpenVibers/OpenVibe.Wiki/commit/aaa' }], posts: [] };++ const app = express();++ app.use(createStatusRoutes({ ecosystem, release }));++ app.use(createUpdatesRoutes({ release, fetchImpl: async () => ({ ok: true, json: async () => feed }), log: null }).router);++ const srv = http.createServer(app);++ await new Promise((r) => srv.listen(0, '127.0.0.1', r));++ const base = `http://127.0.0.1:${srv.address().port}`;++ try {++ const status = await (await fetch(`${base}/status`)).text();++ boosted(status, '/status', 'cccccccccccc');++ assert.ok(status.indexOf('<h1>Service status</h1>') > status.indexOf('id="main"'), '/status: its body is inside #main');++ const scriptSet = (html) => [...html.matchAll(/<script[^>]*\ssrc="([^"]+)"/g)].map((m) => m[1]).sort().join(' ');++ const sets = {};++ for (const url of ['/updates', '/updates?site=wiki']) {++ const up = await (await fetch(base + url)).text();++ sets[url] = scriptSet(up);++ boosted(up, url, 'cccccccccccc');++ const m = up.indexOf('id="main"'), shipped = up.indexOf('data-ov-shipped="log"');++ assert.ok(shipped > m && shipped < up.indexOf('</main>'), `${url}: the log (and its loading line) is inside #main`);++ assert.ok(/addEventListener\('ov:boost:load',function\(\)\{if\(window\.OpenVibeShipped\)OpenVibeShipped\.scan\(document\.getElementById\('main'\)\)/.test(up), `${url}: the log re-mounts after a swap`);++ }++ assert.strictEqual(sets['/updates?site=wiki'], sets['/updates'], 'the /updates filter links swap: same external scripts');++ assert.notStrictEqual(scriptSet(status), sets['/updates'], '/status <-> /updates is a normal page load: other external scripts');++ } finally {++ srv.close();++ }++ console.log('boost: / /status /updates carry the marker, #main and boost.js');++})().catch((err) => { console.error(err); process.exitCode = 1; });@@@ -14,9 +14,9 @@ const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {htmlRawKB: 132, // measured 128.3 with Events, AI and Search added (31 sites, contracts 0.135.0); 122.3 (shared 2.21.0: a real glyph for every product card, where ten drew the 'V'); was 110.6 with 33 live sites (contracts 0.119.0). The home page lists every site, so it grows with the networkhtmlBrotliKB: 26, // 25.1 (31 sites, contracts 0.135.0); 24.1 (shared 2.21.0, the same glyphs); 20.5 (contracts 0.116.0)-- jsFiles: 7, // 6-- jsRawKB: 260, // 228.3-- jsBrotliKB: 62, // 53.9++ 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: 266, // 249.8 with boost.js (13.1); 228.3++ jsBrotliKB: 64, // 60.3 with boost.js (3.8); 53.9cssFiles: 2, // 1 (Font Awesome, cross-origin)externalFiles: 3, // 1 locally; production adds Cloudflare's beacon};
@@ -631,6 +631,8 @@ Include them in any service page:
@@ -631,6 +631,8 @@ Include them in any service page:
<script src="https://openvibe.network/shared/user-card.js"></script>
<script src="https://openvibe.network/shared/user-card.js"></script>
```
```
+**Boost (plan T11).** `/`, `/status` and `/updates` load `/shared/boost.js` (`data-main="#main"`): a same-site link between two pages with the same external scripts swaps only `<main id="main">` instead of loading the page. Today that is the `/updates?site=` filter links between `/updates` views; `/`, `/status` and `/updates` each load other scripts (the footer, the mark, the shipped log), so a move between them is a normal page load until their script sets match. Each carries `<meta name="ov-boost" content="network@<release>">`, the release id `/release.json` serves (`server/boost.js`), so a page from another deploy is a normal page load too. On `/` the static hero, intent picker and constellation stay outside `#main`, and the release id is part of the page cache key, so a deploy changes the `/` ETag. Pages without the marker (`/my`, `/login`, `/admin/*`, `/verify-email`, `/@:username`, the legal pages) are always a normal page load, by design. Page script that lives outside `#main` and wires something inside it re-wires on `ov:boost:load` (the shipped log on `/updates`, the shipped strip on `/`).
+
---
---
## Admin Panel
## Admin Panel
@@ -213,6 +213,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
@@ -213,6 +213,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
</div>
</div>
</section>
</section>
+ <main id="main">
<!--OV:HOME-->
<!--OV:HOME-->
<!-- What shipped across the network (the changelog on openvibe.blog; openvibe-shared shipped.js) -->
<!-- What shipped across the network (the changelog on openvibe.blog; openvibe-shared shipped.js) -->
@@ -229,6 +230,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
@@ -229,6 +230,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
<p>openvibe.network is the identity behind it all: sign in once and every OpenVibe site signs you in too, your themes and notifications follow you, your history across the network lives in one place, and the OpenCoins wallet works everywhere.</p>
<p>openvibe.network is the identity behind it all: sign in once and every OpenVibe site signs you in too, your themes and notifications follow you, your history across the network lives in one place, and the OpenCoins wallet works everywhere.</p>
</div>
</div>
</section>
</section>
+ </main>
</div>
</div>
@@ -238,7 +240,8 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
@@ -238,7 +240,8 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
<script src="/shared/navbar.js"></script>
<script src="/shared/navbar.js"></script>
<script src="/shared/footer.js"></script>
<script src="/shared/footer.js"></script>
<script src="/shared/shipped.js" defer></script>
<script src="/shared/shipped.js" defer></script>
-<script>document.addEventListener('DOMContentLoaded', function () { if (window.OpenVibeShipped) OpenVibeShipped.mount('#ov-shipped', { limit: 8, title: 'Recently shipped across OpenVibe', more: '/updates' }); });</script>
+<script src="/shared/boost.js" data-main="#main" defer></script>
+<script>(function () { function shipped() { if (window.OpenVibeShipped && document.getElementById('ov-shipped')) OpenVibeShipped.mount('#ov-shipped', { limit: 8, title: 'Recently shipped across OpenVibe', more: '/updates' }); } document.addEventListener('DOMContentLoaded', shipped); document.addEventListener('ov:boost:load', shipped); })();</script>
<script>
<script>
(function () {
(function () {
if (window.OpenVibeFooter) { try { OpenVibeFooter.init({ service: 'network', variant: 'full', links: [{ heading: 'OpenVibe.Network', items: [{ name: 'My account', url: '/my' }, { name: 'History', url: '/my#history' }, { name: 'Themes', url: '/themes' }, { name: 'Linked services', url: '/my#linked' }, { name: 'Sign in', url: '/login' }] }] }); } catch (e) { /* */ } }
if (window.OpenVibeFooter) { try { OpenVibeFooter.init({ service: 'network', variant: 'full', links: [{ heading: 'OpenVibe.Network', items: [{ name: 'My account', url: '/my' }, { name: 'History', url: '/my#history' }, { name: 'Themes', url: '/themes' }, { name: 'Linked services', url: '/my#linked' }, { name: 'Sign in', url: '/login' }] }] }); } catch (e) { /* */ } }
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+'use strict';
+// openvibe-shared/boost.js on Network's server-rendered pages (plan T11 step 3): a page carries the release marker and
+// the script, and wraps what changes in <main id="main">. The marker is the served release id (release.release, the id
+// /release.json and the ov-release meta carry), so a page from another deploy is a normal page load. Pages without
+// the marker (/my, /login, /admin/*, /verify-email, /@:username, the legal pages) are always a normal load.
+// boost.js also needs the same external scripts on both pages: only the /updates filter views share theirs, so a
+// move between /, /status and /updates is a normal load until their script sets match.
+const esc = require('openvibe-shared/seo').esc;
+
+const meta = (releaseId) => `<meta name="ov-boost" content="network@${esc(releaseId || 'dev')}">`;
+const SCRIPT = '<script src="/shared/boost.js" data-main="#main" defer></script>';
+
+module.exports = { meta, SCRIPT };
@@ -11,6 +11,7 @@ const icons = require('openvibe-shared/icons');
@@ -11,6 +11,7 @@ const icons = require('openvibe-shared/icons');
const showcase = require('openvibe-shared/showcase');
const showcase = require('openvibe-shared/showcase');
const toolsCatalog = require('../domains/catalog');
const toolsCatalog = require('../domains/catalog');
const activity = require('./activity');
const activity = require('./activity');
+const boost = require('../boost');
const SHELL = path.join(__dirname, '..', '..', 'public', 'index.html');
const SHELL = path.join(__dirname, '..', '..', 'public', 'index.html');
const esc = seo.esc;
const esc = seo.esc;
@@ -196,22 +197,23 @@ function summaryOf(catalog) {
@@ -196,22 +197,23 @@ function summaryOf(catalog) {
}
}
let pageCache = { key: '', html: '', etag: '' };
let pageCache = { key: '', html: '', etag: '' };
-function render() {
+// releaseId: the served release (server/index.js release.release), the boost marker; it keys the cache so a deploy changes the ETag.
+function render({ releaseId = '' } = {}) {
const { catalog } = toolsCatalog.peek();
const { catalog } = toolsCatalog.peek();
const act = activity.peek();
const act = activity.peek();
const stat = fs.statSync(SHELL);
const stat = fs.statSync(SHELL);
- const key = `${stat.mtimeMs}:${catalog.updated}:${catalog.tools.length}:${act.version}`;
+ const key = `${stat.mtimeMs}:${catalog.updated}:${catalog.tools.length}:${act.version}:${releaseId}`;
if (pageCache.key === key) return pageCache;
if (pageCache.key === key) return pageCache;
const ld = seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what }))));
const ld = seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what }))));
const summary = summaryOf(catalog);
const summary = summaryOf(catalog);
- const html = fs.readFileSync(SHELL, 'utf8').replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + summary.body + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog)).replace('<!--OV:CONSTELLATION-->', constellation()).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', `${ld}\n${summary.head}\n</head>`);
+ const html = fs.readFileSync(SHELL, 'utf8').replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + summary.body + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog)).replace('<!--OV:CONSTELLATION-->', constellation()).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);
pageCache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };
pageCache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };
return pageCache;
return pageCache;
}
}
-function sendHome(req, res) {
+function sendHome(req, res, { releaseId = '' } = {}) {
toolsCatalog.getCatalog().catch(() => {}); // refresh in the background; this response uses what is known now
toolsCatalog.getCatalog().catch(() => {}); // refresh in the background; this response uses what is known now
- const page = render();
+ const page = render({ releaseId });
res.set('Content-Type', 'text/html; charset=utf-8');
res.set('Content-Type', 'text/html; charset=utf-8');
res.set('Cache-Control', cache.htmlHeaders({ maxAge: 120 }));
res.set('Cache-Control', cache.htmlHeaders({ maxAge: 120 }));
res.set('ETag', page.etag);
res.set('ETag', page.etag);
@@ -577,9 +577,9 @@ libraryTags.start();
@@ -577,9 +577,9 @@ libraryTags.start();
app.use('/api/v1/creators', rateLimit({ windowMs: 60_000, max: 300 }), require('./analytics/creators').router({ fullGuard: require('./identity/principals').guard('network.analytics.creator.read') }));
app.use('/api/v1/creators', rateLimit({ windowMs: 60_000, max: 300 }), require('./analytics/creators').router({ fullGuard: require('./identity/principals').guard('network.analytics.creator.read') }));
// Incidents and maintenance (WS-N task 12): public list; staff admins or network.status.incident (ovhost) write.
// Incidents and maintenance (WS-N task 12): public list; staff admins or network.status.incident (ovhost) write.
app.use('/api/v1/status/incidents', rateLimit({ windowMs: 60_000, max: 120 }), require('./status/incidents').router({ requireAuth, incidentGuard: require('./identity/principals').guard('network.status.incident') }));
app.use('/api/v1/status/incidents', rateLimit({ windowMs: 60_000, max: 120 }), require('./status/incidents').router({ requireAuth, incidentGuard: require('./identity/principals').guard('network.status.incident') }));
-app.use(require('./status/routes').createStatusRoutes({ ecosystem }));
+app.use(require('./status/routes').createStatusRoutes({ ecosystem, release }));
// What shipped network-wide: the changelog proxy every site's widget reads, and /updates.
// What shipped network-wide: the changelog proxy every site's widget reads, and /updates.
-app.use(require('./updates/routes').createUpdatesRoutes({ blogUrl: process.env.OV_BLOG_INTERNAL_URL || 'http://127.0.0.1:4810' }).router);
+app.use(require('./updates/routes').createUpdatesRoutes({ blogUrl: process.env.OV_BLOG_INTERNAL_URL || 'http://127.0.0.1:4810', release }).router);
app.get('/api/.well-known/jwks', (_req, res) => {
app.get('/api/.well-known/jwks', (_req, res) => {
// Without key files the ephemeral HS256 "key pair" is one shared secret: publishing it would let
// Without key files the ephemeral HS256 "key pair" is one shared secret: publishing it would let
@@ -828,7 +828,7 @@ app.get(['/login.html', '/admin.html'], (req, res) => {
@@ -828,7 +828,7 @@ app.get(['/login.html', '/admin.html'], (req, res) => {
// Landing page at the apex root
// Landing page at the apex root
app.get(['/', '/index.html'], (req, res) => {
app.get(['/', '/index.html'], (req, res) => {
if (req.path === '/index.html') return redirectWithoutHtml(req, res, '/');
if (req.path === '/index.html') return redirectWithoutHtml(req, res, '/');
- return require('./home/render').sendHome(req, res);
+ return require('./home/render').sendHome(req, res, { releaseId: release.release });
});
});
app.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(require('./home/render').llmsTxt()));
app.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(require('./home/render').llmsTxt()));
app.get('/llms-full.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(require('./home/render').llmsFullTxt({ baseUrl: config.baseUrl })));
app.get('/llms-full.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(require('./home/render').llmsFullTxt({ baseUrl: config.baseUrl })));
@@ -26,6 +26,7 @@ const express = require('express');
@@ -26,6 +26,7 @@ const express = require('express');
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const seo = require('openvibe-shared/seo');
const seo = require('openvibe-shared/seo');
const exposure = require('../registry/exposure');
const exposure = require('../registry/exposure');
+const boost = require('../boost');
const SLO_FILE = path.join(__dirname, '..', '..', 'docs', 'slo.json');
const SLO_FILE = path.join(__dirname, '..', '..', 'docs', 'slo.json');
// The daily developer path against production (OpenVibe.Host openvibe-devpath.timer, WS-N task 1): step
// The daily developer path against production (OpenVibe.Host openvibe-devpath.timer, WS-N task 1): step
@@ -170,7 +171,7 @@ function incidentsSection(incidents) {
@@ -170,7 +171,7 @@ function incidentsSection(incidents) {
}).join('')}<p><a href="/api/v1/status/incidents">History (JSON)</a></p></section>`;
}).join('')}<p><a href="/api/v1/status/incidents">History (JSON)</a></p></section>`;
}
}
-function renderPage(list, slo, generatedAt, devPath = null, toolsJob = null, healthSince = null, incidents = null) {
+function renderPage(list, slo, generatedAt, devPath = null, toolsJob = null, healthSince = null, incidents = null, releaseId = '') {
const counts = summary(list);
const counts = summary(list);
const tr = list.map((r) => `<tr id="svc-${esc(r.id)}">
const tr = list.map((r) => `<tr id="svc-${esc(r.id)}">
<td><b>${esc(r.name)}</b><small>${esc(r.id)} · manifest: ${esc(r.manifest_status)}</small></td>
<td><b>${esc(r.name)}</b><small>${esc(r.id)} · manifest: ${esc(r.manifest_status)}</small></td>
@@ -188,6 +189,7 @@ function renderPage(list, slo, generatedAt, devPath = null, toolsJob = null, hea
@@ -188,6 +189,7 @@ function renderPage(list, slo, generatedAt, devPath = null, toolsJob = null, hea
<title>Status · OpenVibe.Network</title>
<title>Status · OpenVibe.Network</title>
<meta name="description" content="Observed status of each OpenVibe service: readiness, release and when it was last checked.">
<meta name="description" content="Observed status of each OpenVibe service: readiness, release and when it was last checked.">
<meta name="robots" content="noindex, nofollow">
<meta name="robots" content="noindex, nofollow">
+${boost.meta(releaseId)}
${require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' })}
${require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' })}
<meta name="color-scheme" content="dark light">
<meta name="color-scheme" content="dark light">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
@@ -232,12 +234,13 @@ ${sloRows}
@@ -232,12 +234,13 @@ ${sloRows}
</main>
</main>
${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' })}
${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact' })}
<script src="/shared/navbar.js" defer></script>
<script src="/shared/navbar.js" defer></script>
+${boost.SCRIPT}
<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>
<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>
</body>
</body>
</html>`;
</html>`;
}
}
-function createStatusRoutes({ ecosystem, now = () => new Date() }) {
+function createStatusRoutes({ ecosystem, release = null, now = () => new Date() }) {
const r = express.Router();
const r = express.Router();
r.get('/api/v1/status', (_req, res) => {
r.get('/api/v1/status', (_req, res) => {
const list = rows(ecosystem);
const list = rows(ecosystem);
@@ -264,7 +267,7 @@ function createStatusRoutes({ ecosystem, now = () => new Date() }) {
@@ -264,7 +267,7 @@ function createStatusRoutes({ ecosystem, now = () => new Date() }) {
res.set('Content-Type', 'text/html; charset=utf-8').set('Cache-Control', 'no-cache, max-age=0').set('X-Robots-Tag', 'noindex, nofollow');
res.set('Content-Type', 'text/html; charset=utf-8').set('Cache-Control', 'no-cache, max-age=0').set('X-Robots-Tag', 'noindex, nofollow');
let incidents = null;
let incidents = null;
try { incidents = await require('./incidents').list(_req.app.locals.db); } catch { incidents = null; }
try { incidents = await require('./incidents').list(_req.app.locals.db); } catch { incidents = null; }
- res.send(renderPage(rows(ecosystem), slo, now().toISOString(), loadDevPath(), loadDevPath(TOOLSJOB_FILE), typeof ecosystem.releaseHealthSince === 'function' ? ecosystem.releaseHealthSince() : null, incidents));
+ res.send(renderPage(rows(ecosystem), slo, now().toISOString(), loadDevPath(), loadDevPath(TOOLSJOB_FILE), typeof ecosystem.releaseHealthSince === 'function' ? ecosystem.releaseHealthSince() : null, incidents, release && release.release));
});
});
return r;
return r;
}
}
@@ -11,6 +11,7 @@ const cache = require('openvibe-shared/cache-policy');
@@ -11,6 +11,7 @@ const cache = require('openvibe-shared/cache-policy');
* "Load more", a filter per site, the Patch notes posts).
* "Load more", a filter per site, the Patch notes posts).
*/
*/
const express = require('express');
const express = require('express');
+const boost = require('../boost');
const TTL_MS = 60 * 1000;
const TTL_MS = 60 * 1000;
const MAX_KEYS = 300;
const MAX_KEYS = 300;
@@ -18,7 +19,7 @@ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&':
@@ -18,7 +19,7 @@ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&':
const SERVICE_RE = /^[a-z][a-z0-9-]{1,39}$/;
const SERVICE_RE = /^[a-z][a-z0-9-]{1,39}$/;
const CURSOR_RE = /^[A-Za-z0-9_-]{1,120}$/;
const CURSOR_RE = /^[A-Za-z0-9_-]{1,120}$/;
-function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = globalThis.fetch, now = () => Date.now(), log = console } = {}) {
+function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', release = null, fetchImpl = globalThis.fetch, now = () => Date.now(), log = console } = {}) {
const base = String(blogUrl).replace(/\/+$/, '');
const base = String(blogUrl).replace(/\/+$/, '');
const feedCache = new Map(); // key -> { at, body }
const feedCache = new Map(); // key -> { at, body }
@@ -64,7 +65,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = gl
@@ -64,7 +65,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = gl
res.set('Content-Type', 'text/html; charset=utf-8').set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));
res.set('Content-Type', 'text/html; charset=utf-8').set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));
// The unfiltered log is the page; a per-site view is a filter of it (followed, not indexed).
// The unfiltered log is the page; a per-site view is a filter of it (followed, not indexed).
if (asked) res.set('X-Robots-Tag', 'noindex, follow');
if (asked) res.set('X-Robots-Tag', 'noindex, follow');
- res.send(renderPage({ site, filtered: Boolean(asked), body }));
+ res.send(renderPage({ site, filtered: Boolean(asked), body, releaseId: release && release.release }));
});
});
return { router: r, feed, queryOf };
return { router: r, feed, queryOf };
}
}
@@ -85,7 +86,7 @@ function serviceFor(v) {
@@ -85,7 +86,7 @@ function serviceFor(v) {
return m && SITES.includes(m[1]) ? m[1] : null;
return m && SITES.includes(m[1]) ? m[1] : null;
}
}
-function renderPage({ site, filtered, body }) {
+function renderPage({ site, filtered, body, releaseId = '' }) {
const entries = (body && Array.isArray(body.entries)) ? body.entries : [];
const entries = (body && Array.isArray(body.entries)) ? body.entries : [];
const posts = (body && Array.isArray(body.posts)) ? body.posts.filter((p) => /^https:\/\//.test(p.url || '')) : [];
const posts = (body && Array.isArray(body.posts)) ? body.posts.filter((p) => /^https:\/\//.test(p.url || '')) : [];
const title = site ? `What shipped on OpenVibe.${nameOf(site)}` : 'What shipped across OpenVibe';
const title = site ? `What shipped on OpenVibe.${nameOf(site)}` : 'What shipped across OpenVibe';
@@ -110,6 +111,7 @@ function renderPage({ site, filtered, body }) {
@@ -110,6 +111,7 @@ function renderPage({ site, filtered, body }) {
<link rel="canonical" href="https://openvibe.network/updates${filtered && site ? `?site=${esc(site)}` : ''}">
<link rel="canonical" href="https://openvibe.network/updates${filtered && site ? `?site=${esc(site)}` : ''}">
${filtered ? '<meta name="robots" content="noindex, follow">' : ''}
${filtered ? '<meta name="robots" content="noindex, follow">' : ''}
<link rel="alternate" type="application/json" href="/api/v1/changelog${site ? `?service=${esc(site)}` : ''}">
<link rel="alternate" type="application/json" href="/api/v1/changelog${site ? `?service=${esc(site)}` : ''}">
+${boost.meta(releaseId)}
${require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' })}
${require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' })}
<meta name="color-scheme" content="dark light">
<meta name="color-scheme" content="dark light">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
@@ -143,6 +145,8 @@ ${dayHtml || '<p class="ov-shipped-empty">The update history could not be loaded
@@ -143,6 +145,8 @@ ${dayHtml || '<p class="ov-shipped-empty">The update history could not be loaded
${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact', updates: '/updates' })}
${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact', updates: '/updates' })}
<script src="/shared/navbar.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/shared/shipped.js" defer></script>
<script src="/shared/shipped.js" defer></script>
+${boost.SCRIPT}
+<script>document.addEventListener('ov:boost:load',function(){if(window.OpenVibeShipped)OpenVibeShipped.scan(document.getElementById('main'));});</script>
<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>
<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:location.origin});}catch(e){}});</script>
</body>
</body>
</html>`;
</html>`;
new file mode 100644
new file mode 100644
@@ -0,0 +1,107 @@
@@ -0,0 +1,107 @@
+'use strict';
+// openvibe-shared/boost.js on Network's pages (plan T11 step 3): /, /status and /updates carry the release marker
+// (network@<served release id>), wrap what changes in <main id="main"> and load boost.js; the front door's static hero
+// stays outside #main, a new release changes the / ETag, and the hero's intent picker still works. boost.js swaps
+// only between pages with the same external scripts: the /updates views share theirs; /, /status and /updates differ.
+// node test/boost.test.js
+const assert = require('assert');
+const http = require('http');
+const vm = require('vm');
+const express = require('express');
+const activity = require('../server/home/activity');
+const home = require('../server/home/render');
+const { createStatusRoutes } = require('../server/status/routes');
+const { createUpdatesRoutes } = require('../server/updates/routes');
+
+activity.setEnabled(false);
+activity._set({ live: [], pastes: [], posts: [] });
+
+const SCRIPT = '<script src="/shared/boost.js" data-main="#main" defer></script>';
+const count = (s, needle) => s.split(needle).length - 1;
+function boosted(html, page, releaseId) {
+ assert.ok(html.includes(`<meta name="ov-boost" content="network@${releaseId}">`), `${page}: the release marker`);
+ assert.strictEqual(count(html, 'name="ov-boost"'), 1, `${page}: one marker`);
+ assert.ok(html.indexOf('name="ov-boost"') < html.indexOf('</head>'), `${page}: the marker is in <head>`);
+ assert.strictEqual(count(html, 'id="main"'), 1, `${page}: one #main`);
+ assert.strictEqual(count(html, SCRIPT), 1, `${page}: boost.js once`);
+ assert.ok(html.indexOf(SCRIPT) > html.indexOf('</main>') && html.indexOf(SCRIPT) < html.indexOf('</body>'), `${page}: boost.js after #main, before </body>`);
+}
+
+(async () => {
+ // ── / : in-process render ───────────────────────────────────────────────────
+ const a = home.render({ releaseId: 'aaaaaaaaaaaa' });
+ const html = a.html;
+ boosted(html, '/', 'aaaaaaaaaaaa');
+ assert.strictEqual(count(html, '</head>'), 1, 'exactly one </head>');
+ assert.strictEqual((html.match(/<h1[\s>]/g) || []).length, 1, 'exactly one <h1>');
+ const open = html.indexOf('<main id="main">'), close = html.indexOf('</main>');
+ for (const [what, at] of [['the hero', html.indexOf('<section class="hero">')], ['the intents', html.indexOf('<ul class="intents"')], ['the constellation', html.indexOf('<div class="hero-sky">')], ['the <h1>', html.indexOf('<h1>')]]) {
+ assert.ok(at > 0 && at < open, `${what} stays outside #main`);
+ }
+ for (const [what, at] of [['the home body', html.indexOf('aria-labelledby="h-now"')], ['the sites', html.indexOf('id="network"')], ['the shipped strip', html.indexOf('id="ov-shipped"')]]) {
+ assert.ok(at > open && at < close, `${what} is inside #main`);
+ }
+ assert.ok(close < html.indexOf('<footer'), '#main closes before the footer');
+
+ // A deploy changes the marker and so the ETag; the same release is the same cached page.
+ const b = home.render({ releaseId: 'bbbbbbbbbbbb' });
+ assert.notStrictEqual(b.etag, a.etag, 'a new release id changes the / ETag');
+ assert.ok(b.html.includes('content="network@bbbbbbbbbbbb"'));
+ assert.strictEqual(home.render({ releaseId: 'bbbbbbbbbbbb' }).etag, b.etag, 'the same release keeps its ETag');
+
+ // ── the hero's intent picker (its inline script, over the rendered intents) ─
+ const intents = [...html.matchAll(/<a class="intent" href="([^"]+)" data-id="([^"]+)" data-k="([^"]*)"/g)].map(([, href, id, k]) => {
+ const classes = new Set();
+ return { href, id, classes, getAttribute: (n) => ({ 'data-id': id, 'data-k': k }[n]), classList: { toggle: (c, on) => (on ? classes.add(c) : classes.delete(c)) } };
+ });
+ assert.ok(intents.length > 5, 'the intents render');
+ const script = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map((m) => m[1]).find((s) => s.includes("'.intent-bar'"));
+ assert.ok(script, 'the intent picker script is on the page');
+ const on = {};
+ const target = (name) => ({ addEventListener: (type, fn) => { on[`${name}:${type}`] = fn; } });
+ const form = target('form'), q = { ...target('q'), value: '' };
+ const location = { href: '/' };
+ vm.runInNewContext(script, { location, document: { querySelector: (s) => (s === '.intent-bar' ? form : null), getElementById: (id) => (id === 'intent-q' ? q : null), querySelectorAll: (s) => (s === '.intent' ? intents : []) } });
+ const byId = (id) => intents.find((i) => i.id === id);
+ q.value = 'go live'; on['q:input']();
+ assert.ok(byId('live').classes.has('match'), '"go live" lights the live intent');
+ assert.ok(byId('chat').classes.has('dim') && !byId('live').classes.has('dim'), 'the others dim');
+ let prevented = false;
+ on['form:submit']({ preventDefault: () => { prevented = true; } });
+ assert.ok(prevented && location.href === byId('live').href, 'Enter opens the intent it names');
+ q.value = ''; on['q:input']();
+ assert.ok(intents.every((i) => !i.classes.has('dim') && !i.classes.has('match')), 'an empty search clears the picker');
+ q.value = 'merge pdf'; prevented = false; on['form:submit']({ preventDefault: () => { prevented = true; } });
+ assert.ok(!prevented, 'a tool search stays the form\'s own openvibe.tools search');
+
+ // ── /status and /updates: the route factories get the release ──────────────
+ const release = { release: 'cccccccccccc' };
+ const ecosystem = { current: () => [], lastPollAt: () => null, pollMs: 60000, releaseHealthSince: () => null };
+ const feed = { entries: [{ service: 'wiki', sha: 'a'.repeat(40), short: 'aaaaaaa', subject: 'x', deployed_at: '2026-10-13T07:00:00.000Z', url: 'https://github.com/OpenVibers/OpenVibe.Wiki/commit/aaa' }], posts: [] };
+ const app = express();
+ app.use(createStatusRoutes({ ecosystem, release }));
+ app.use(createUpdatesRoutes({ release, fetchImpl: async () => ({ ok: true, json: async () => feed }), log: null }).router);
+ const srv = http.createServer(app);
+ await new Promise((r) => srv.listen(0, '127.0.0.1', r));
+ const base = `http://127.0.0.1:${srv.address().port}`;
+ try {
+ const status = await (await fetch(`${base}/status`)).text();
+ boosted(status, '/status', 'cccccccccccc');
+ assert.ok(status.indexOf('<h1>Service status</h1>') > status.indexOf('id="main"'), '/status: its body is inside #main');
+ const scriptSet = (html) => [...html.matchAll(/<script[^>]*\ssrc="([^"]+)"/g)].map((m) => m[1]).sort().join(' ');
+ const sets = {};
+ for (const url of ['/updates', '/updates?site=wiki']) {
+ const up = await (await fetch(base + url)).text();
+ sets[url] = scriptSet(up);
+ boosted(up, url, 'cccccccccccc');
+ const m = up.indexOf('id="main"'), shipped = up.indexOf('data-ov-shipped="log"');
+ assert.ok(shipped > m && shipped < up.indexOf('</main>'), `${url}: the log (and its loading line) is inside #main`);
+ assert.ok(/addEventListener\('ov:boost:load',function\(\)\{if\(window\.OpenVibeShipped\)OpenVibeShipped\.scan\(document\.getElementById\('main'\)\)/.test(up), `${url}: the log re-mounts after a swap`);
+ }
+ assert.strictEqual(sets['/updates?site=wiki'], sets['/updates'], 'the /updates filter links swap: same external scripts');
+ assert.notStrictEqual(scriptSet(status), sets['/updates'], '/status <-> /updates is a normal page load: other external scripts');
+ } finally {
+ srv.close();
+ }
+ console.log('boost: / /status /updates carry the marker, #main and boost.js');
+})().catch((err) => { console.error(err); process.exitCode = 1; });
@@ -14,9 +14,9 @@ const { measure, check, format } = require('openvibe-shared/perf-budget');
@@ -14,9 +14,9 @@ const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
const BUDGETS = {
htmlRawKB: 132, // measured 128.3 with Events, AI and Search added (31 sites, contracts 0.135.0); 122.3 (shared 2.21.0: a real glyph for every product card, where ten drew the 'V'); was 110.6 with 33 live sites (contracts 0.119.0). The home page lists every site, so it grows with the network
htmlRawKB: 132, // measured 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: 26, // 25.1 (31 sites, contracts 0.135.0); 24.1 (shared 2.21.0, the same glyphs); 20.5 (contracts 0.116.0)
htmlBrotliKB: 26, // 25.1 (31 sites, contracts 0.135.0); 24.1 (shared 2.21.0, the same glyphs); 20.5 (contracts 0.116.0)
- jsFiles: 7, // 6
- jsRawKB: 260, // 228.3
- jsBrotliKB: 62, // 53.9
+ 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: 266, // 249.8 with boost.js (13.1); 228.3
+ jsBrotliKB: 64, // 60.3 with boost.js (3.8); 53.9
cssFiles: 2, // 1 (Font Awesome, cross-origin)
cssFiles: 2, // 1 (Font Awesome, cross-origin)
externalFiles: 3, // 1 locally; production adds Cloudflare's beacon
externalFiles: 3, // 1 locally; production adds Cloudflare's beacon
};
};