openvibers/openvibe.network Commits
Paint every page's look before the first frame (L42) (#461)
Changes
@@@ -9,12 +9,22 @@<meta name="color-scheme" content="dark light"><link rel="apple-touch-icon" href="/assets/logo-192.png"><link rel="manifest" href="/manifest.webmanifest">--<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.-- Paints the cached theme synchronously so the default palette never flashes. Reads-- localStorage only: no network, nothing to block on. theme-loader.js still runs after-- and reconciles with the account. -->++<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts++ openvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one a++ link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).++ theme-loader.js loads with defer, so without it the default palette (or an old season) showed first. -->++<!--ov:look-boot--><script>--(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();++// A browser from before look-boot has only its theme (ov_theme), not yet a painted look (ov_paint): paint that once.++(function () {++ try {++ if (localStorage.getItem('ov_paint')) return;++ var t = JSON.parse(localStorage.getItem('ov_theme') || 'null'), v = t && t.variables, el = document.documentElement;++ if (!v) return;++ for (var k in v) if (k.charAt(0) === '-' && /^[#0-9a-z(),.%\s-]{1,80}$/i.test(String(v[k])) && !/url|expression/i.test(String(v[k]))) el.style.setProperty(k, v[k]);++ if (t.id && /^[a-z0-9-]{1,40}$/.test(t.id)) el.setAttribute('data-theme', t.id);++ } catch (_) { /* nothing kept: theme-loader.js paints shortly */ }++})();</script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/admin.css">@@@ -28,12 +28,22 @@<link rel="canonical" href="https://openvibe.network/"><script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://openvibe.network/#org","name":"OpenVibe","alternateName":["OpenVibe Network","OpenVibers"],"url":"https://openvibe.network/","logo":"https://openvibe.network/assets/logo-512.png","description":"OpenVibe is an open-source, community-run network of websites that share one free account: live streaming from the browser (OpenVibe.Live), more than 180 online tools (OpenVibe.Tools), chat and the Vibe assistant (OpenVibe.Chat), games, a wiki, news, reviews, deals and an open developer platform.","disambiguatingDescription":"The open-source network of websites at openvibe.network, made by OpenVibers; not the Openvibe mobile social-media aggregator app.","sameAs":["https://github.com/OpenVibers","https://discord.gg/M6MuRUaeJj","https://openvibe.live","https://openvibe.tools","https://openvibe.chat","https://openvibe.games","https://openvibe.media","https://openvibe.video","https://openvibe.community","https://openvibe.space","https://openvibe.wiki","https://openvibe.blog","https://openvibe.news","https://openvibe.services","https://openre.stream"]},{"@type":"WebSite","@id":"https://openvibe.network/#site","url":"https://openvibe.network/","name":"OpenVibe.Network","description":"One free account for every OpenVibe site: go live from your browser, online tools, chat, games, videos, jobs and an open platform to build on.","publisher":{"@id":"https://openvibe.network/#org"}}]}</script><link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">--<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.-- Paints the cached theme synchronously so the default palette never flashes. Reads-- localStorage only: no network, nothing to block on. theme-loader.js still runs after-- and reconciles with the account. -->++<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts++ openvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one a++ link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).++ theme-loader.js loads with defer, so without it the default palette (or an old season) showed first. -->++<!--ov:look-boot--><script>--(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();++// A browser from before look-boot has only its theme (ov_theme), not yet a painted look (ov_paint): paint that once.++(function () {++ try {++ if (localStorage.getItem('ov_paint')) return;++ var t = JSON.parse(localStorage.getItem('ov_theme') || 'null'), v = t && t.variables, el = document.documentElement;++ if (!v) return;++ for (var k in v) if (k.charAt(0) === '-' && /^[#0-9a-z(),.%\s-]{1,80}$/i.test(String(v[k])) && !/url|expression/i.test(String(v[k]))) el.style.setProperty(k, v[k]);++ if (t.id && /^[a-z0-9-]{1,40}$/.test(t.id)) el.setAttribute('data-theme', t.id);++ } catch (_) { /* nothing kept: theme-loader.js paints shortly */ }++})();</script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/shared/showcase.css">@@@ -22,12 +22,22 @@<meta name="apple-mobile-web-app-capable" content="yes"><meta name="mobile-web-app-capable" content="yes"><link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">--<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.-- Paints the cached theme synchronously so the default palette never flashes. Reads-- localStorage only: no network, nothing to block on. theme-loader.js still runs after-- and reconciles with the account. -->++<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts++ openvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one a++ link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).++ theme-loader.js loads with defer, so without it the default palette (or an old season) showed first. -->++<!--ov:look-boot--><script>--(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();++// A browser from before look-boot has only its theme (ov_theme), not yet a painted look (ov_paint): paint that once.++(function () {++ try {++ if (localStorage.getItem('ov_paint')) return;++ var t = JSON.parse(localStorage.getItem('ov_theme') || 'null'), v = t && t.variables, el = document.documentElement;++ if (!v) return;++ for (var k in v) if (k.charAt(0) === '-' && /^[#0-9a-z(),.%\s-]{1,80}$/i.test(String(v[k])) && !/url|expression/i.test(String(v[k]))) el.style.setProperty(k, v[k]);++ if (t.id && /^[a-z0-9-]{1,40}$/.test(t.id)) el.setAttribute('data-theme', t.id);++ } catch (_) { /* nothing kept: theme-loader.js paints shortly */ }++})();</script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/my.css">@@@ -10,8 +10,22 @@<link rel="apple-touch-icon" href="/assets/logo-192.png"><link rel="manifest" href="/manifest.webmanifest"><meta name="robots" content="noindex, nofollow">++<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts++ openvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one a++ link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).++ theme-loader.js loads with defer, so without it the default palette (or an old season) showed first. -->++<!--ov:look-boot--><script>--(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();++// A browser from before look-boot has only its theme (ov_theme), not yet a painted look (ov_paint): paint that once.++(function () {++ try {++ if (localStorage.getItem('ov_paint')) return;++ var t = JSON.parse(localStorage.getItem('ov_theme') || 'null'), v = t && t.variables, el = document.documentElement;++ if (!v) return;++ for (var k in v) if (k.charAt(0) === '-' && /^[#0-9a-z(),.%\s-]{1,80}$/i.test(String(v[k])) && !/url|expression/i.test(String(v[k]))) el.style.setProperty(k, v[k]);++ if (t.id && /^[a-z0-9-]{1,40}$/.test(t.id)) el.setAttribute('data-theme', t.id);++ } catch (_) { /* nothing kept: theme-loader.js paints shortly */ }++})();</script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><style>@@@ -11,8 +11,22 @@<link rel="manifest" href="/manifest.webmanifest"><meta name="description" content="Confirming the email address on your OpenVibe account."><meta name="robots" content="noindex, nofollow">++<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts++ openvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one a++ link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).++ theme-loader.js loads with defer, so without it the default palette (or an old season) showed first. -->++<!--ov:look-boot--><script>--(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();++// A browser from before look-boot has only its theme (ov_theme), not yet a painted look (ov_paint): paint that once.++(function () {++ try {++ if (localStorage.getItem('ov_paint')) return;++ var t = JSON.parse(localStorage.getItem('ov_theme') || 'null'), v = t && t.variables, el = document.documentElement;++ if (!v) return;++ for (var k in v) if (k.charAt(0) === '-' && /^[#0-9a-z(),.%\s-]{1,80}$/i.test(String(v[k])) && !/url|expression/i.test(String(v[k]))) el.style.setProperty(k, v[k]);++ if (t.id && /^[a-z0-9-]{1,40}$/.test(t.id)) el.setAttribute('data-theme', t.id);++ } catch (_) { /* nothing kept: theme-loader.js paints shortly */ }++})();</script><script src="/shared/theme-loader.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">@@@ -12,6 +12,9 @@ const showcase = require('openvibe-shared/showcase');const toolsCatalog = require('../domains/catalog');const activity = require('./activity');const boost = require('../boost');++// The look before first paint (openvibe-shared look-boot, plan L42): the shell carries the marker and this++// puts the script there, above the stylesheets, in the page the server renders.++const lookBoot = require('../look-boot');const SHELL = path.join(__dirname, '..', '..', 'public', 'index.html');const esc = seo.esc;@@@ -418,7 +421,7 @@ function render({ releaseId = '' } = {}) {})) + seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what })))) + seo.jsonLdTag(seo.jsonLd.faq(faqPairs()));const card = social.url('network', 'https://openvibe.network/shared') || 'https://openvibe.network/assets/logo-512.png';const summary = summaryOf(catalog);-- const html = 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-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(OPEN.length + 1)).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>`);++ const html = lookBoot.apply(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-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(OPEN.length + 1)).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;}@@@ -20,6 +20,9 @@ const fs = require('fs');const bcrypt = require('bcryptjs');const config = require('./config');++// The look before first paint (openvibe-shared look-boot, plan L42): the public pages carry a marker and this++// puts openvibe-shared's script there, so a page paints the kept or carried look before its first frame.++const lookBoot = require('./look-boot');const { initDb } = require('./db/database');const urlRegistry = require('./url-registry');const { BRAND } = require('openvibe-shared/brand');@@@ -81,11 +84,11 @@ function redirectWithoutHtml(req, res, targetPath) {}function sendMyAccountApp(res) {-- return res.sendFile(path.join(__dirname, '..', 'public', 'my.html'));++ return lookBoot.send(res, 'my.html');}function sendLandingPage(res) {-- return res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));++ return lookBoot.send(res, 'index.html');}// ── Security ─────────────────────────────────────────────────@@@ -844,7 +847,7 @@ app.get(require('./not-found').ACCOUNT_HUB_PATHS, (req, res) => {app.get('/verify-email', (req, res) => {res.setHeader('Cache-Control', 'no-store');res.setHeader('X-Robots-Tag', 'noindex');-- res.sendFile(path.join(__dirname, '..', 'public', 'verify-email.html'));++ lookBoot.send(res, 'verify-email.html');});// FedCM identity provider (browser-native sign-in for the other OpenVibe sites) — server/auth/fedcm.js@@@ -864,9 +867,12 @@ app.get('/sso/check', require('./auth/sso-check').createSsoCheckRoute(() => ({ dapp.get(['/sso/fanout', '/sso/fanout.html'], (req, res) => {res.setHeader('Cache-Control', 'no-store');res.setHeader('X-Robots-Tag', 'noindex');-- res.sendFile(path.join(__dirname, '..', 'public', 'sso-fanout.html'));++ lookBoot.send(res, 'sso-fanout.html');});++// The public pages carry the look before first paint; the ones the static file server would answer by their own++// file name (public/*.html) get it here, ahead of express.static below (plan L42).++app.use(lookBoot.middleware());app.use(express.static(path.join(__dirname, '..', 'public'), { setHeaders: require('./static-headers').publicStaticHeaders }));// openvibe-shared: the OpenVibe.Shared release package.json pins, from node_modules.@@@ -940,7 +946,7 @@ app.get(['/login', '/forgot-password', '/reset-password'], (req, res) => {// Admin panel — serve the SPA for /admin and any client-routed sub-path// (e.g. /admin/settings, /admin/analytics/overview). Auth is checked client-side.app.get(['/admin', '/admin/*'], (req, res) => {-- res.sendFile(path.join(__dirname, '..', 'public', 'admin.html'));++ lookBoot.send(res, 'admin.html');});// Anything no route above answered is a 404: JSON under /api, /internal and /oauth, otherwise a smallnnew file mode 100644@@@ -0,0 +1,64 @@++'use strict';++/**++ * The look before first paint (openvibe-shared 4.64.0 look-boot, plan L42).++ *++ * Every OpenVibe site keeps a record of the look it painted (localStorage 'ov_paint'), and a link from++ * another OpenVibe page carries that look in '?ov_look='. openvibe-shared's look-boot paints it before++ * the first frame: the colours, the season, Calm, the text size and color-scheme. Network's own bootstrap++ * painted only the old 'ov_theme' record, so a page could show the wrong season, or flash, until++ * theme-loader.js (deferred) ran.++ *++ * The pages carry a marker (<!--ov:look-boot-->) where the script belongs; this module puts it there:++ * at serve time for the page the server renders (the home page), and for the pages served raw, once, at boot.++ * The marker never reaches a page.++ */++const fs = require('fs');++const path = require('path');++const { BOOT, tag } = require('openvibe-shared/look-boot');++++const MARK = '<!--ov:look-boot-->';++const PUBLIC_DIR = path.join(__dirname, '..', 'public');++++/** The marker replaced by the inline script. A page without the marker is left alone. */++function apply(html) {++ if (typeof html !== 'string' || !html.includes(MARK)) return html;++ return html.replace(MARK, () => tag());++}++++// The public pages that carry the marker, read once at boot: their look-boot is in the bytes from here on.++const files = new Map();++for (const name of fs.readdirSync(PUBLIC_DIR)) {++ if (!name.endsWith('.html')) continue;++ const html = fs.readFileSync(path.join(PUBLIC_DIR, name), 'utf8');++ if (!html.includes(MARK)) continue;++ files.set(name, apply(html));++}++++/** The page with look-boot in it, or null when the file is not one of ours. */++function page(name) {++ return files.get(name) || null;++}++++/** Send a public page with look-boot in it. The headers a caller set (no-store, a redirect) are kept. */++function send(res, name) {++ const html = page(name);++ if (!html) return false;++ res.set('Content-Type', 'text/html; charset=utf-8');++ if (!res.get('Cache-Control')) res.set('Cache-Control', 'public, max-age=0');++ res.send(html);++ return true;++}++++/**++ * The pages the static file server would answer raw (public/*.html reached by their own file name):++ * they get look-boot too, so no path serves a page without it.++ */++function middleware() {++ return (req, res, next) => {++ if (!req.path.endsWith('.html')) return next();++ if (!page(path.basename(req.path))) return next();++ send(res, path.basename(req.path));++ };++}++++module.exports = { MARK, BOOT, apply, page, send, middleware, files };nnew file mode 100644@@@ -0,0 +1,53 @@++'use strict';++// The look before first paint (openvibe-shared 4.64.0 look-boot, plan L42; the owner, 10-31: seasonal turned off++// on Network was still on, and orange, until the deferred theme-loader.js caught up). Every page that carried the++// old ov_theme bootstrap now paints the look this browser last painted here (ov_paint), or the one a link from++// another OpenVibe page carried (?ov_look=), before its first stylesheet: once, and the marker never reaches a page.++// A browser from before look-boot (its theme only) is painted once from that theme, colours only.++// node test/look-boot.test.js++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { bootServer } = require('./helpers/boot-server');++const { BOOT } = require('openvibe-shared/look-boot');++const { MARK } = require('../server/look-boot');++++const PAGES = [++ ['/', 'the home page'],++ ['/my', 'the account hub'],++ ['/admin', 'the admin panel'],++ ['/verify-email', 'the email verification page'],++ ['/sso/fanout', 'the sign-out fanout'],++];++++(async () => {++ const srv = await bootServer();++ try {++ for (const [path, name] of PAGES) {++ const r = await fetch(srv.base + path);++ assert.strictEqual(r.status, 200, `${path} (${name}) answers 200`);++ const html = await r.text();++ assert.ok(!html.includes(MARK), `${path}: the marker never reaches the page`);++ assert.strictEqual(html.split(BOOT).length, 2, `${path}: look-boot once`);++ const at = html.indexOf(BOOT);++ const firstSheet = html.search(/<link[^>]+rel="stylesheet"/);++ assert.ok(at > 0 && firstSheet > 0 && at < firstSheet, `${path}: it paints before any stylesheet`);++ // The one-time fallback for a browser from before look-boot: its theme only, colours only.++ const legacy = html.slice(at, firstSheet);++ assert.match(legacy, /if \(localStorage\.getItem\('ov_paint'\)\) return;/, `${path}: the fallback waits for a painted look`);++ assert.match(legacy, /!\/url\|expression\/i\.test/, `${path}: the fallback takes colours only`);++ }++ // No public page keeps the old bootstrap (it painted the theme only, so the season stayed on until the loader ran).++ const pub = path.join(__dirname, '..', 'public');++ for (const name of fs.readdirSync(pub).filter((n) => n.endsWith('.html'))) {++ const html = fs.readFileSync(path.join(pub, name), 'utf8');++ assert.ok(!html.includes("var raw=localStorage.getItem('ov_theme')"), `${name}: the old theme-only bootstrap is gone`);++ }++ console.log('look boot: every page paints the kept or carried look before its first stylesheet');++ } catch (err) {++ console.error(srv.logs());++ throw err;++ } finally {++ await srv.stop();++ }++})().then(() => process.exit(0)).catch((err) => { console.error(err); process.exit(1); });@@@ -12,8 +12,8 @@ const { spawn } = require('child_process');const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {-- htmlRawKB: 188, // 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: 34.5, // 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: 189, // 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: 35.5, // 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: 334, // 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.3jsBrotliKB: 84, // 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
@@ -9,12 +9,22 @@
@@ -9,12 +9,22 @@
<meta name="color-scheme" content="dark light">
<meta name="color-scheme" content="dark light">
<link rel="apple-touch-icon" href="/assets/logo-192.png">
<link rel="apple-touch-icon" href="/assets/logo-192.png">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="manifest" href="/manifest.webmanifest">
-<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
- Paints the cached theme synchronously so the default palette never flashes. Reads
- localStorage only: no network, nothing to block on. theme-loader.js still runs after
- and reconciles with the account. -->
+<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts
+ openvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one a
+ link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).
+ theme-loader.js loads with defer, so without it the default palette (or an old season) showed first. -->
+<!--ov:look-boot-->
<script>
<script>
-(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
+// A browser from before look-boot has only its theme (ov_theme), not yet a painted look (ov_paint): paint that once.
+(function () {
+ try {
+ if (localStorage.getItem('ov_paint')) return;
+ var t = JSON.parse(localStorage.getItem('ov_theme') || 'null'), v = t && t.variables, el = document.documentElement;
+ if (!v) return;
+ for (var k in v) if (k.charAt(0) === '-' && /^[#0-9a-z(),.%\s-]{1,80}$/i.test(String(v[k])) && !/url|expression/i.test(String(v[k]))) el.style.setProperty(k, v[k]);
+ if (t.id && /^[a-z0-9-]{1,40}$/.test(t.id)) el.setAttribute('data-theme', t.id);
+ } catch (_) { /* nothing kept: theme-loader.js paints shortly */ }
+})();
</script>
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="stylesheet" href="/css/admin.css">
@@ -28,12 +28,22 @@
@@ -28,12 +28,22 @@
<link rel="canonical" href="https://openvibe.network/">
<link rel="canonical" href="https://openvibe.network/">
<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://openvibe.network/#org","name":"OpenVibe","alternateName":["OpenVibe Network","OpenVibers"],"url":"https://openvibe.network/","logo":"https://openvibe.network/assets/logo-512.png","description":"OpenVibe is an open-source, community-run network of websites that share one free account: live streaming from the browser (OpenVibe.Live), more than 180 online tools (OpenVibe.Tools), chat and the Vibe assistant (OpenVibe.Chat), games, a wiki, news, reviews, deals and an open developer platform.","disambiguatingDescription":"The open-source network of websites at openvibe.network, made by OpenVibers; not the Openvibe mobile social-media aggregator app.","sameAs":["https://github.com/OpenVibers","https://discord.gg/M6MuRUaeJj","https://openvibe.live","https://openvibe.tools","https://openvibe.chat","https://openvibe.games","https://openvibe.media","https://openvibe.video","https://openvibe.community","https://openvibe.space","https://openvibe.wiki","https://openvibe.blog","https://openvibe.news","https://openvibe.services","https://openre.stream"]},{"@type":"WebSite","@id":"https://openvibe.network/#site","url":"https://openvibe.network/","name":"OpenVibe.Network","description":"One free account for every OpenVibe site: go live from your browser, online tools, chat, games, videos, jobs and an open platform to build on.","publisher":{"@id":"https://openvibe.network/#org"}}]}</script>
<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://openvibe.network/#org","name":"OpenVibe","alternateName":["OpenVibe Network","OpenVibers"],"url":"https://openvibe.network/","logo":"https://openvibe.network/assets/logo-512.png","description":"OpenVibe is an open-source, community-run network of websites that share one free account: live streaming from the browser (OpenVibe.Live), more than 180 online tools (OpenVibe.Tools), chat and the Vibe assistant (OpenVibe.Chat), games, a wiki, news, reviews, deals and an open developer platform.","disambiguatingDescription":"The open-source network of websites at openvibe.network, made by OpenVibers; not the Openvibe mobile social-media aggregator app.","sameAs":["https://github.com/OpenVibers","https://discord.gg/M6MuRUaeJj","https://openvibe.live","https://openvibe.tools","https://openvibe.chat","https://openvibe.games","https://openvibe.media","https://openvibe.video","https://openvibe.community","https://openvibe.space","https://openvibe.wiki","https://openvibe.blog","https://openvibe.news","https://openvibe.services","https://openre.stream"]},{"@type":"WebSite","@id":"https://openvibe.network/#site","url":"https://openvibe.network/","name":"OpenVibe.Network","description":"One free account for every OpenVibe site: go live from your browser, online tools, chat, games, videos, jobs and an open platform to build on.","publisher":{"@id":"https://openvibe.network/#org"}}]}</script>
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
-<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
- Paints the cached theme synchronously so the default palette never flashes. Reads
- localStorage only: no network, nothing to block on. theme-loader.js still runs after
- and reconciles with the account. -->
+<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts
+ openvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one a
+ link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).
+ theme-loader.js loads with defer, so without it the default palette (or an old season) showed first. -->
+<!--ov:look-boot-->
<script>
<script>
-(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
+// A browser from before look-boot has only its theme (ov_theme), not yet a painted look (ov_paint): paint that once.
+(function () {
+ try {
+ if (localStorage.getItem('ov_paint')) return;
+ var t = JSON.parse(localStorage.getItem('ov_theme') || 'null'), v = t && t.variables, el = document.documentElement;
+ if (!v) return;
+ for (var k in v) if (k.charAt(0) === '-' && /^[#0-9a-z(),.%\s-]{1,80}$/i.test(String(v[k])) && !/url|expression/i.test(String(v[k]))) el.style.setProperty(k, v[k]);
+ if (t.id && /^[a-z0-9-]{1,40}$/.test(t.id)) el.setAttribute('data-theme', t.id);
+ } catch (_) { /* nothing kept: theme-loader.js paints shortly */ }
+})();
</script>
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/shared/showcase.css">
<link rel="stylesheet" href="/shared/showcase.css">
@@ -22,12 +22,22 @@
@@ -22,12 +22,22 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
-<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
- Paints the cached theme synchronously so the default palette never flashes. Reads
- localStorage only: no network, nothing to block on. theme-loader.js still runs after
- and reconciles with the account. -->
+<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts
+ openvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one a
+ link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).
+ theme-loader.js loads with defer, so without it the default palette (or an old season) showed first. -->
+<!--ov:look-boot-->
<script>
<script>
-(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
+// A browser from before look-boot has only its theme (ov_theme), not yet a painted look (ov_paint): paint that once.
+(function () {
+ try {
+ if (localStorage.getItem('ov_paint')) return;
+ var t = JSON.parse(localStorage.getItem('ov_theme') || 'null'), v = t && t.variables, el = document.documentElement;
+ if (!v) return;
+ for (var k in v) if (k.charAt(0) === '-' && /^[#0-9a-z(),.%\s-]{1,80}$/i.test(String(v[k])) && !/url|expression/i.test(String(v[k]))) el.style.setProperty(k, v[k]);
+ if (t.id && /^[a-z0-9-]{1,40}$/.test(t.id)) el.setAttribute('data-theme', t.id);
+ } catch (_) { /* nothing kept: theme-loader.js paints shortly */ }
+})();
</script>
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/my.css">
<link rel="stylesheet" href="/css/my.css">
@@ -10,8 +10,22 @@
@@ -10,8 +10,22 @@
<link rel="apple-touch-icon" href="/assets/logo-192.png">
<link rel="apple-touch-icon" href="/assets/logo-192.png">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="manifest" href="/manifest.webmanifest">
<meta name="robots" content="noindex, nofollow">
<meta name="robots" content="noindex, nofollow">
+<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts
+ openvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one a
+ link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).
+ theme-loader.js loads with defer, so without it the default palette (or an old season) showed first. -->
+<!--ov:look-boot-->
<script>
<script>
-(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
+// A browser from before look-boot has only its theme (ov_theme), not yet a painted look (ov_paint): paint that once.
+(function () {
+ try {
+ if (localStorage.getItem('ov_paint')) return;
+ var t = JSON.parse(localStorage.getItem('ov_theme') || 'null'), v = t && t.variables, el = document.documentElement;
+ if (!v) return;
+ for (var k in v) if (k.charAt(0) === '-' && /^[#0-9a-z(),.%\s-]{1,80}$/i.test(String(v[k])) && !/url|expression/i.test(String(v[k]))) el.style.setProperty(k, v[k]);
+ if (t.id && /^[a-z0-9-]{1,40}$/.test(t.id)) el.setAttribute('data-theme', t.id);
+ } catch (_) { /* nothing kept: theme-loader.js paints shortly */ }
+})();
</script>
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
<style>
@@ -11,8 +11,22 @@
@@ -11,8 +11,22 @@
<link rel="manifest" href="/manifest.webmanifest">
<link rel="manifest" href="/manifest.webmanifest">
<meta name="description" content="Confirming the email address on your OpenVibe account.">
<meta name="description" content="Confirming the email address on your OpenVibe account.">
<meta name="robots" content="noindex, nofollow">
<meta name="robots" content="noindex, nofollow">
+<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts
+ openvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one a
+ link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).
+ theme-loader.js loads with defer, so without it the default palette (or an old season) showed first. -->
+<!--ov:look-boot-->
<script>
<script>
-(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
+// A browser from before look-boot has only its theme (ov_theme), not yet a painted look (ov_paint): paint that once.
+(function () {
+ try {
+ if (localStorage.getItem('ov_paint')) return;
+ var t = JSON.parse(localStorage.getItem('ov_theme') || 'null'), v = t && t.variables, el = document.documentElement;
+ if (!v) return;
+ for (var k in v) if (k.charAt(0) === '-' && /^[#0-9a-z(),.%\s-]{1,80}$/i.test(String(v[k])) && !/url|expression/i.test(String(v[k]))) el.style.setProperty(k, v[k]);
+ if (t.id && /^[a-z0-9-]{1,40}$/.test(t.id)) el.setAttribute('data-theme', t.id);
+ } catch (_) { /* nothing kept: theme-loader.js paints shortly */ }
+})();
</script>
</script>
<script src="/shared/theme-loader.js"></script>
<script src="/shared/theme-loader.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
@@ -12,6 +12,9 @@ const showcase = require('openvibe-shared/showcase');
@@ -12,6 +12,9 @@ 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 boost = require('../boost');
+// The look before first paint (openvibe-shared look-boot, plan L42): the shell carries the marker and this
+// puts the script there, above the stylesheets, in the page the server renders.
+const lookBoot = require('../look-boot');
const SHELL = path.join(__dirname, '..', '..', 'public', 'index.html');
const SHELL = path.join(__dirname, '..', '..', 'public', 'index.html');
const esc = seo.esc;
const esc = seo.esc;
@@ -418,7 +421,7 @@ function render({ releaseId = '' } = {}) {
@@ -418,7 +421,7 @@ function render({ releaseId = '' } = {}) {
})) + seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what })))) + seo.jsonLdTag(seo.jsonLd.faq(faqPairs()));
})) + seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what })))) + seo.jsonLdTag(seo.jsonLd.faq(faqPairs()));
const card = social.url('network', 'https://openvibe.network/shared') || 'https://openvibe.network/assets/logo-512.png';
const card = social.url('network', 'https://openvibe.network/shared') || 'https://openvibe.network/assets/logo-512.png';
const summary = summaryOf(catalog);
const summary = summaryOf(catalog);
- const html = 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-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(OPEN.length + 1)).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>`);
+ const html = lookBoot.apply(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-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(OPEN.length + 1)).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;
}
}
@@ -20,6 +20,9 @@ const fs = require('fs');
@@ -20,6 +20,9 @@ const fs = require('fs');
const bcrypt = require('bcryptjs');
const bcrypt = require('bcryptjs');
const config = require('./config');
const config = require('./config');
+// The look before first paint (openvibe-shared look-boot, plan L42): the public pages carry a marker and this
+// puts openvibe-shared's script there, so a page paints the kept or carried look before its first frame.
+const lookBoot = require('./look-boot');
const { initDb } = require('./db/database');
const { initDb } = require('./db/database');
const urlRegistry = require('./url-registry');
const urlRegistry = require('./url-registry');
const { BRAND } = require('openvibe-shared/brand');
const { BRAND } = require('openvibe-shared/brand');
@@ -81,11 +84,11 @@ function redirectWithoutHtml(req, res, targetPath) {
@@ -81,11 +84,11 @@ function redirectWithoutHtml(req, res, targetPath) {
}
}
function sendMyAccountApp(res) {
function sendMyAccountApp(res) {
- return res.sendFile(path.join(__dirname, '..', 'public', 'my.html'));
+ return lookBoot.send(res, 'my.html');
}
}
function sendLandingPage(res) {
function sendLandingPage(res) {
- return res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
+ return lookBoot.send(res, 'index.html');
}
}
// ── Security ─────────────────────────────────────────────────
// ── Security ─────────────────────────────────────────────────
@@ -844,7 +847,7 @@ app.get(require('./not-found').ACCOUNT_HUB_PATHS, (req, res) => {
@@ -844,7 +847,7 @@ app.get(require('./not-found').ACCOUNT_HUB_PATHS, (req, res) => {
app.get('/verify-email', (req, res) => {
app.get('/verify-email', (req, res) => {
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Cache-Control', 'no-store');
res.setHeader('X-Robots-Tag', 'noindex');
res.setHeader('X-Robots-Tag', 'noindex');
- res.sendFile(path.join(__dirname, '..', 'public', 'verify-email.html'));
+ lookBoot.send(res, 'verify-email.html');
});
});
// FedCM identity provider (browser-native sign-in for the other OpenVibe sites) — server/auth/fedcm.js
// FedCM identity provider (browser-native sign-in for the other OpenVibe sites) — server/auth/fedcm.js
@@ -864,9 +867,12 @@ app.get('/sso/check', require('./auth/sso-check').createSsoCheckRoute(() => ({ d
@@ -864,9 +867,12 @@ app.get('/sso/check', require('./auth/sso-check').createSsoCheckRoute(() => ({ d
app.get(['/sso/fanout', '/sso/fanout.html'], (req, res) => {
app.get(['/sso/fanout', '/sso/fanout.html'], (req, res) => {
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Cache-Control', 'no-store');
res.setHeader('X-Robots-Tag', 'noindex');
res.setHeader('X-Robots-Tag', 'noindex');
- res.sendFile(path.join(__dirname, '..', 'public', 'sso-fanout.html'));
+ lookBoot.send(res, 'sso-fanout.html');
});
});
+// The public pages carry the look before first paint; the ones the static file server would answer by their own
+// file name (public/*.html) get it here, ahead of express.static below (plan L42).
+app.use(lookBoot.middleware());
app.use(express.static(path.join(__dirname, '..', 'public'), { setHeaders: require('./static-headers').publicStaticHeaders }));
app.use(express.static(path.join(__dirname, '..', 'public'), { setHeaders: require('./static-headers').publicStaticHeaders }));
// openvibe-shared: the OpenVibe.Shared release package.json pins, from node_modules.
// openvibe-shared: the OpenVibe.Shared release package.json pins, from node_modules.
@@ -940,7 +946,7 @@ app.get(['/login', '/forgot-password', '/reset-password'], (req, res) => {
@@ -940,7 +946,7 @@ app.get(['/login', '/forgot-password', '/reset-password'], (req, res) => {
// Admin panel — serve the SPA for /admin and any client-routed sub-path
// Admin panel — serve the SPA for /admin and any client-routed sub-path
// (e.g. /admin/settings, /admin/analytics/overview). Auth is checked client-side.
// (e.g. /admin/settings, /admin/analytics/overview). Auth is checked client-side.
app.get(['/admin', '/admin/*'], (req, res) => {
app.get(['/admin', '/admin/*'], (req, res) => {
- res.sendFile(path.join(__dirname, '..', 'public', 'admin.html'));
+ lookBoot.send(res, 'admin.html');
});
});
// Anything no route above answered is a 404: JSON under /api, /internal and /oauth, otherwise a small
// Anything no route above answered is a 404: JSON under /api, /internal and /oauth, otherwise a small
new file mode 100644
new file mode 100644
@@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
+'use strict';
+/**
+ * The look before first paint (openvibe-shared 4.64.0 look-boot, plan L42).
+ *
+ * Every OpenVibe site keeps a record of the look it painted (localStorage 'ov_paint'), and a link from
+ * another OpenVibe page carries that look in '?ov_look='. openvibe-shared's look-boot paints it before
+ * the first frame: the colours, the season, Calm, the text size and color-scheme. Network's own bootstrap
+ * painted only the old 'ov_theme' record, so a page could show the wrong season, or flash, until
+ * theme-loader.js (deferred) ran.
+ *
+ * The pages carry a marker (<!--ov:look-boot-->) where the script belongs; this module puts it there:
+ * at serve time for the page the server renders (the home page), and for the pages served raw, once, at boot.
+ * The marker never reaches a page.
+ */
+const fs = require('fs');
+const path = require('path');
+const { BOOT, tag } = require('openvibe-shared/look-boot');
+
+const MARK = '<!--ov:look-boot-->';
+const PUBLIC_DIR = path.join(__dirname, '..', 'public');
+
+/** The marker replaced by the inline script. A page without the marker is left alone. */
+function apply(html) {
+ if (typeof html !== 'string' || !html.includes(MARK)) return html;
+ return html.replace(MARK, () => tag());
+}
+
+// The public pages that carry the marker, read once at boot: their look-boot is in the bytes from here on.
+const files = new Map();
+for (const name of fs.readdirSync(PUBLIC_DIR)) {
+ if (!name.endsWith('.html')) continue;
+ const html = fs.readFileSync(path.join(PUBLIC_DIR, name), 'utf8');
+ if (!html.includes(MARK)) continue;
+ files.set(name, apply(html));
+}
+
+/** The page with look-boot in it, or null when the file is not one of ours. */
+function page(name) {
+ return files.get(name) || null;
+}
+
+/** Send a public page with look-boot in it. The headers a caller set (no-store, a redirect) are kept. */
+function send(res, name) {
+ const html = page(name);
+ if (!html) return false;
+ res.set('Content-Type', 'text/html; charset=utf-8');
+ if (!res.get('Cache-Control')) res.set('Cache-Control', 'public, max-age=0');
+ res.send(html);
+ return true;
+}
+
+/**
+ * The pages the static file server would answer raw (public/*.html reached by their own file name):
+ * they get look-boot too, so no path serves a page without it.
+ */
+function middleware() {
+ return (req, res, next) => {
+ if (!req.path.endsWith('.html')) return next();
+ if (!page(path.basename(req.path))) return next();
+ send(res, path.basename(req.path));
+ };
+}
+
+module.exports = { MARK, BOOT, apply, page, send, middleware, files };
new file mode 100644
new file mode 100644
@@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
+'use strict';
+// The look before first paint (openvibe-shared 4.64.0 look-boot, plan L42; the owner, 10-31: seasonal turned off
+// on Network was still on, and orange, until the deferred theme-loader.js caught up). Every page that carried the
+// old ov_theme bootstrap now paints the look this browser last painted here (ov_paint), or the one a link from
+// another OpenVibe page carried (?ov_look=), before its first stylesheet: once, and the marker never reaches a page.
+// A browser from before look-boot (its theme only) is painted once from that theme, colours only.
+// node test/look-boot.test.js
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { bootServer } = require('./helpers/boot-server');
+const { BOOT } = require('openvibe-shared/look-boot');
+const { MARK } = require('../server/look-boot');
+
+const PAGES = [
+ ['/', 'the home page'],
+ ['/my', 'the account hub'],
+ ['/admin', 'the admin panel'],
+ ['/verify-email', 'the email verification page'],
+ ['/sso/fanout', 'the sign-out fanout'],
+];
+
+(async () => {
+ const srv = await bootServer();
+ try {
+ for (const [path, name] of PAGES) {
+ const r = await fetch(srv.base + path);
+ assert.strictEqual(r.status, 200, `${path} (${name}) answers 200`);
+ const html = await r.text();
+ assert.ok(!html.includes(MARK), `${path}: the marker never reaches the page`);
+ assert.strictEqual(html.split(BOOT).length, 2, `${path}: look-boot once`);
+ const at = html.indexOf(BOOT);
+ const firstSheet = html.search(/<link[^>]+rel="stylesheet"/);
+ assert.ok(at > 0 && firstSheet > 0 && at < firstSheet, `${path}: it paints before any stylesheet`);
+ // The one-time fallback for a browser from before look-boot: its theme only, colours only.
+ const legacy = html.slice(at, firstSheet);
+ assert.match(legacy, /if \(localStorage\.getItem\('ov_paint'\)\) return;/, `${path}: the fallback waits for a painted look`);
+ assert.match(legacy, /!\/url\|expression\/i\.test/, `${path}: the fallback takes colours only`);
+ }
+ // No public page keeps the old bootstrap (it painted the theme only, so the season stayed on until the loader ran).
+ const pub = path.join(__dirname, '..', 'public');
+ for (const name of fs.readdirSync(pub).filter((n) => n.endsWith('.html'))) {
+ const html = fs.readFileSync(path.join(pub, name), 'utf8');
+ assert.ok(!html.includes("var raw=localStorage.getItem('ov_theme')"), `${name}: the old theme-only bootstrap is gone`);
+ }
+ console.log('look boot: every page paints the kept or carried look before its first stylesheet');
+ } catch (err) {
+ console.error(srv.logs());
+ throw err;
+ } finally {
+ await srv.stop();
+ }
+})().then(() => process.exit(0)).catch((err) => { console.error(err); process.exit(1); });
@@ -12,8 +12,8 @@ const { spawn } = require('child_process');
@@ -12,8 +12,8 @@ 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: 188, // 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: 34.5, // 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: 189, // 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: 35.5, // 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: 334, // 333.2 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 330.1 with openvibe-shared 4.52.0 (from 4.47.2): the seasonal fall, rich embeds, the Dock and its Chats tab (2026-10-30); 323.2 with openvibe-shared 4.47.2 (the comfort settings in theme-loader 4.45, touch targets 4.46, the grouped updates log 4.47.2); 313.7 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 306.4 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 298.9 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 293.1 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 279.1 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 269.3 with shared 4.11.0's seasonal navbar (Pumpkin Night and the season badge, +2.6); 249.8 with boost.js (13.1); 228.3
jsRawKB: 334, // 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: 84, // 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
jsBrotliKB: 84, // 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