openvibers/openvibe.mediahub Commits
Font Awesome out, the OpenVibe glyphs in (plan L54 K4) (#226)
Changes
@@@ -120,8 +120,8 @@ async function createApp(opts = {}) {defaultSrc: ["'self'"],// The OpenVibe Frame (theme-loader, navbar, footer) comes from the Network; the inline init is ours.scriptSrc: ["'self'", "'unsafe-inline'", 'https://openvibe.network', 'https://static.cloudflareinsights.com'],-- styleSrc: ["'self'", "'unsafe-inline'", 'https://openvibe.network', 'https://fonts.googleapis.com', 'https://cdnjs.cloudflare.com'],-- fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com', 'https://cdnjs.cloudflare.com'],++ styleSrc: ["'self'", "'unsafe-inline'", 'https://openvibe.network', 'https://fonts.googleapis.com'],++ fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],// The Video home's live-now cards show OpenVibe.Live's own stream thumbnails (its /api/thumbnails).imgSrc: ["'self'", 'data:', 'https://openvibe.network', 'https://openvibe.media', liveOrigin],// The video player loads its media file from OpenVibe.Media (https://openvibe.media/v|/c), which may@@@ -97,6 +97,8 @@ ${appIcon.headTags({ site: 'media-hub' })}<link rel="stylesheet" href="${asset('css/app.css')}">${(o.styles || []).map((name) => `<link rel="stylesheet" href="${esc(ovServe.url(name))}">`).join('\n')}${render(scripts)}++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->++<script id="ov-glyphs-loader" src="${ovServe.url('ov-glyphs.js')}" defer></script><meta name="ov-boost" content="media-hub@${esc(RELEASE)}"><script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,body: `<a class="skip" href="#main">Skip to content</a>@@@ -255,7 +255,7 @@ function downloadMenu({ kind, id, item, has }) {{ href: `${at}/chapters.json`, ext: 'json', purpose: 'for programs' }] }));if (item.thumbnail_url) rows.push(row({ icon: 'fa-regular fa-image', label: 'Cover picture', about: null, links: [{ href: `${at}/thumbnail`, ext: 'jpg', text: 'Image' }] }));rows.push(row({ icon: 'fa-solid fa-circle-info', label: 'Details', about: 'Title, channel, dates, summary', links: [{ href: `${at}/info.json`, ext: 'json' }] }));-- const everything = html`<li class="ovv-dl-row ovv-dl-row--all"><i class="fa-solid fa-file-zipper" aria-hidden="true"></i><span class="ovv-dl-what"><span class="ovv-dl-label">Everything</span><span class="ovv-dl-about">All of the above but the video, in one .zip</span></span><span class="ovv-dl-formats"><a class="ovv-dl-format ovv-dl-format--main" href="${at}/all.zip" download rel="nofollow" title="${FORMAT_HELP.zip}" aria-label="Everything: ZIP (${FORMAT_HELP.zip})">ZIP</a></span></li>`;++ const everything = html`<li class="ovv-dl-row ovv-dl-row--all"><i class="fa-solid fa-box-archive" aria-hidden="true"></i><span class="ovv-dl-what"><span class="ovv-dl-label">Everything</span><span class="ovv-dl-about">All of the above but the video, in one .zip</span></span><span class="ovv-dl-formats"><a class="ovv-dl-format ovv-dl-format--main" href="${at}/all.zip" download rel="nofollow" title="${FORMAT_HELP.zip}" aria-label="Everything: ZIP (${FORMAT_HELP.zip})">ZIP</a></span></li>`;return html`<details class="ovv-dl" data-ovv-dl><summary class="ovv-btn"><i class="fa-solid fa-download" aria-hidden="true"></i> Download</summary><div class="ovv-dl-menu" role="group" aria-label="Download this ${kind === 'clip' ? 'clip' : 'recording'}"><div class="ovv-dl-head"><span>Download this ${kind === 'clip' ? 'clip' : 'recording'}</span><button type="button" class="ovv-dl-close" data-ovv-dl-close aria-label="Close"><i class="fa-solid fa-xmark" aria-hidden="true"></i></button></div>@@@ -64,7 +64,8 @@ function renderPage(o) {${appIcon.headTags({ site: 'media-hub' })}<link rel="stylesheet" href="${asset('css/app.css')}"><link rel="stylesheet" href="${asset('css/video.css')}">--<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->++<script id="ov-glyphs-loader" src="${ovServe.url('ov-glyphs.js')}" defer></script>${o.player ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>\n` : ''}${(o.scripts || []).map((rel) => `<script src="${asset(rel)}" defer></script>`).join('\n')}`;const account = signedIn? html`Signed in as <strong>${viewer.displayName || viewer.username || 'you'}</strong> · <a href="/auth/logout?next=${encodeURIComponent(pathNow)}">Sign out</a>`nnew file mode 100644@@@ -0,0 +1,79 @@++'use strict';++// No Font Awesome (plan L54 K4; the owner, 2026-11-01: "get rid of Font Awesome completely and just use our own++// openvibe.design system"). OpenVibe.MediaHub's icons are the OpenVibe glyphs (openvibe-shared/glyphs, drawn in the browser by++// /shared/ov-glyphs.js, which the Shared navbar loads on a page that names an icon) and the brand logos from++// openvibe-shared/brands. This holds it there:++// - no page or stylesheet links Font Awesome, and nothing draws with Font Awesome's font;++// - every page that names an icon loads the navbar or the glyph drop-in;++// - every fa- name OpenVibe.MediaHub uses is one the glyphs or the brands draw (a name nobody draws would show nothing).++// node test/no-font-awesome.test.js++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const glyphs = require('openvibe-shared/glyphs');++const brands = require('openvibe-shared/brands');++++const ROOT = path.join(__dirname, '..');++const walk = (dir, ok, out = []) => {++ if (!fs.existsSync(dir)) return out;++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {++ if (e.name === 'node_modules' || e.name === 'vendor' || e.name.startsWith('.')) continue;++ const p = path.join(dir, e.name);++ if (e.isDirectory()) walk(p, ok, out); else if (ok.test(e.name)) out.push(p);++ }++ return out;++};++const rel = (p) => path.relative(ROOT, p);++++// The pages (whole HTML files) and the code that draws them.++const PAGE_ROOTS = [];++const CODE_ROOTS = ["public","server"];++const LAYOUTS = ["server/video/layout.js","server/render/layout.js"]; // the server-rendered pages: their layout brings the navbar or the glyphs++++// A Font Awesome stylesheet, wherever it is written.++const FA_CSS = /fontawesome[^"'\s]*\.css|font-awesome[^"'\s]*\.css|cdnjs\.cloudflare\.com\/ajax\/libs\/font-awesome/i;++++const pages = [];++for (const r of PAGE_ROOTS) pages.push(...walk(path.join(ROOT, r), /\.html$/));++for (const p of pages) {++ const html = fs.readFileSync(p, 'utf8');++ assert.ok(!/fontawesome|font-awesome/i.test(html), `${rel(p)} links Font Awesome`);++ // A whole page (with a <head>; fragments are parts of a page) that names an icon loads what draws it.++ if (/<head[\s>]/i.test(html) && /\bfa-[a-z]/.test(html) && !/\/shared\/navbar|ov-glyphs\.js/.test(html)) {++ assert.fail(`${rel(p)} has fa- icons but loads neither the navbar nor /shared/ov-glyphs.js`);++ }++}++++// Nothing draws with Font Awesome's font.++for (const r of CODE_ROOTS) {++ for (const p of walk(path.join(ROOT, r), /\.css$/)) {++ assert.ok(!/font-family:\s*["']?Font ?Awesome/i.test(fs.readFileSync(p, 'utf8')), `${rel(p)} draws with Font Awesome's font`);++ }++ for (const p of walk(path.join(ROOT, r), /\.(html|js|mjs|cjs)$/)) {++ const t = fs.readFileSync(p, 'utf8');++ assert.ok(!/['"]\d+px FontAwesome|Font Awesome 6 Free/.test(t), `${rel(p)} draws with Font Awesome's font`);++ assert.ok(!FA_CSS.test(t), `${rel(p)} links Font Awesome`);++ }++}++++// The server-rendered pages get the glyphs from the Shared navbar (shell.page) or from the drop-in.++for (const f of LAYOUTS) {++ const t = fs.readFileSync(path.join(ROOT, f), 'utf8');++ assert.ok(/shell\.page|ov-glyphs\.js|\/shared\/navbar/.test(t), `${f} renders pages without the navbar or /shared/ov-glyphs.js`);++}++++// Every fa- name in the pages, the scripts and the server views is drawn by a glyph or a brand.++const MOD = /^(solid|regular|light|thin|duotone|brands|sharp|classic|fw|lg|xl|xs|sm|2xs|2xl|\d+x|li|ul|border|inverse|pull-left|pull-right|rotate-\d+|rotate-by|flip-horizontal|flip-vertical|flip-both|width-auto|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|swap-opacity|stack-text)$/;++const files = [];++for (const r of CODE_ROOTS) files.push(...walk(path.join(ROOT, r), /\.(html|js)$/));++const missing = new Map();++for (const f of files) {++ for (const m of fs.readFileSync(f, 'utf8').matchAll(/\bfa-([a-z0-9]+(?:-[a-z0-9]+)*)\b/g)) {++ const n = m[1];++ if (MOD.test(n) || glyphs.fromFa(n) || brands.fromFa(n)) continue;++ if (!missing.has(n)) missing.set(n, rel(f));++ }++}++assert.deepStrictEqual([...missing].map(([n, f]) => `fa-${n} (${f})`), [], 'every icon this site names is drawn by the OpenVibe glyphs or brands');++++console.log(`no font awesome: ${pages.length} pages, ${files.length} files, every fa- name drawn`);@@@ -17,9 +17,9 @@ const BUDGETS = {htmlRawKB: 51, // 50.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 49.4 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 44.2 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 39.4 on 10-22: the home gained the backup command, the limits table, six questions (and their// FAQPage JSON-LD) and next steps on the network; was 29.7 at 27.5htmlBrotliKB: 12, // 10.6 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 9.4 on 10-22 (the same sections); was 7.7 at 7.1-- jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell)-- jsRawKB: 331, // 328.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 324.8 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 323.2 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 321.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 318.1 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 315.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 307.8 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 300.5 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; 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 with Shared 4.15.0 (2026-10-27: the compact footer opens into the whole one, the pulse line); 264.3 with Shared 4.13.0; 239.8-- jsBrotliKB: 80.8, // 80.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 79.5 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 78.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 77.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 76.1 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 74.2 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; 70.5 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 67.9 Shared 4.15.0; 65.3 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 56.5++ jsFiles: 6, // 6: /shared/ov-glyphs.js in place of Font Awesome's stylesheet (2026-11-01, plan L54 K4); was: 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell)++ jsRawKB: 393, // 393: /shared/ov-glyphs.js in place of Font Awesome (2026-11-01, plan L54 K4; Font Awesome's CSS and font were external, uncounted, and heavier); 328.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 324.8 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 323.2 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 321.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 318.1 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 315.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 307.8 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 300.5 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; 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 with Shared 4.15.0 (2026-10-27: the compact footer opens into the whole one, the pulse line); 264.3 with Shared 4.13.0; 239.8++ jsBrotliKB: 96, // 96: /shared/ov-glyphs.js in place of Font Awesome (2026-11-01, plan L54 K4); 80.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 79.5 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 78.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 77.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 76.1 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 74.2 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; 70.5 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 67.9 Shared 4.15.0; 65.3 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 56.5cssFiles: 2, // 2 (app.css + the cached /shared/showcase.css)cssRawKB: 34, // 33.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 29 openvibe-shared 4.26.1 (hero visual); 23.6 with Shared 4.13.0 (2026-10-26: account links); measured 16.6 (shared 4.1.8: four steps two by two); was 16.5 at 15.0cssBrotliKB: 7.4, // 7.2 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 5.4 with Shared 4.13.0 (2026-10-26: account links); 3.7; 4.2 with the drive previews (10-15)
@@ -120,8 +120,8 @@ async function createApp(opts = {}) {
@@ -120,8 +120,8 @@ async function createApp(opts = {}) {
defaultSrc: ["'self'"],
defaultSrc: ["'self'"],
// The OpenVibe Frame (theme-loader, navbar, footer) comes from the Network; the inline init is ours.
// The OpenVibe Frame (theme-loader, navbar, footer) comes from the Network; the inline init is ours.
scriptSrc: ["'self'", "'unsafe-inline'", 'https://openvibe.network', 'https://static.cloudflareinsights.com'],
scriptSrc: ["'self'", "'unsafe-inline'", 'https://openvibe.network', 'https://static.cloudflareinsights.com'],
- styleSrc: ["'self'", "'unsafe-inline'", 'https://openvibe.network', 'https://fonts.googleapis.com', 'https://cdnjs.cloudflare.com'],
- fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com', 'https://cdnjs.cloudflare.com'],
+ styleSrc: ["'self'", "'unsafe-inline'", 'https://openvibe.network', 'https://fonts.googleapis.com'],
+ fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
// The Video home's live-now cards show OpenVibe.Live's own stream thumbnails (its /api/thumbnails).
// The Video home's live-now cards show OpenVibe.Live's own stream thumbnails (its /api/thumbnails).
imgSrc: ["'self'", 'data:', 'https://openvibe.network', 'https://openvibe.media', liveOrigin],
imgSrc: ["'self'", 'data:', 'https://openvibe.network', 'https://openvibe.media', liveOrigin],
// The video player loads its media file from OpenVibe.Media (https://openvibe.media/v|/c), which may
// The video player loads its media file from OpenVibe.Media (https://openvibe.media/v|/c), which may
@@ -97,6 +97,8 @@ ${appIcon.headTags({ site: 'media-hub' })}
@@ -97,6 +97,8 @@ ${appIcon.headTags({ site: 'media-hub' })}
<link rel="stylesheet" href="${asset('css/app.css')}">
<link rel="stylesheet" href="${asset('css/app.css')}">
${(o.styles || []).map((name) => `<link rel="stylesheet" href="${esc(ovServe.url(name))}">`).join('\n')}
${(o.styles || []).map((name) => `<link rel="stylesheet" href="${esc(ovServe.url(name))}">`).join('\n')}
${render(scripts)}
${render(scripts)}
+<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
+<script id="ov-glyphs-loader" src="${ovServe.url('ov-glyphs.js')}" defer></script>
<meta name="ov-boost" content="media-hub@${esc(RELEASE)}">
<meta name="ov-boost" content="media-hub@${esc(RELEASE)}">
<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,
<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,
body: `<a class="skip" href="#main">Skip to content</a>
body: `<a class="skip" href="#main">Skip to content</a>
@@ -255,7 +255,7 @@ function downloadMenu({ kind, id, item, has }) {
@@ -255,7 +255,7 @@ function downloadMenu({ kind, id, item, has }) {
{ href: `${at}/chapters.json`, ext: 'json', purpose: 'for programs' }] }));
{ href: `${at}/chapters.json`, ext: 'json', purpose: 'for programs' }] }));
if (item.thumbnail_url) rows.push(row({ icon: 'fa-regular fa-image', label: 'Cover picture', about: null, links: [{ href: `${at}/thumbnail`, ext: 'jpg', text: 'Image' }] }));
if (item.thumbnail_url) rows.push(row({ icon: 'fa-regular fa-image', label: 'Cover picture', about: null, links: [{ href: `${at}/thumbnail`, ext: 'jpg', text: 'Image' }] }));
rows.push(row({ icon: 'fa-solid fa-circle-info', label: 'Details', about: 'Title, channel, dates, summary', links: [{ href: `${at}/info.json`, ext: 'json' }] }));
rows.push(row({ icon: 'fa-solid fa-circle-info', label: 'Details', about: 'Title, channel, dates, summary', links: [{ href: `${at}/info.json`, ext: 'json' }] }));
- const everything = html`<li class="ovv-dl-row ovv-dl-row--all"><i class="fa-solid fa-file-zipper" aria-hidden="true"></i><span class="ovv-dl-what"><span class="ovv-dl-label">Everything</span><span class="ovv-dl-about">All of the above but the video, in one .zip</span></span><span class="ovv-dl-formats"><a class="ovv-dl-format ovv-dl-format--main" href="${at}/all.zip" download rel="nofollow" title="${FORMAT_HELP.zip}" aria-label="Everything: ZIP (${FORMAT_HELP.zip})">ZIP</a></span></li>`;
+ const everything = html`<li class="ovv-dl-row ovv-dl-row--all"><i class="fa-solid fa-box-archive" aria-hidden="true"></i><span class="ovv-dl-what"><span class="ovv-dl-label">Everything</span><span class="ovv-dl-about">All of the above but the video, in one .zip</span></span><span class="ovv-dl-formats"><a class="ovv-dl-format ovv-dl-format--main" href="${at}/all.zip" download rel="nofollow" title="${FORMAT_HELP.zip}" aria-label="Everything: ZIP (${FORMAT_HELP.zip})">ZIP</a></span></li>`;
return html`<details class="ovv-dl" data-ovv-dl><summary class="ovv-btn"><i class="fa-solid fa-download" aria-hidden="true"></i> Download</summary>
return html`<details class="ovv-dl" data-ovv-dl><summary class="ovv-btn"><i class="fa-solid fa-download" aria-hidden="true"></i> Download</summary>
<div class="ovv-dl-menu" role="group" aria-label="Download this ${kind === 'clip' ? 'clip' : 'recording'}">
<div class="ovv-dl-menu" role="group" aria-label="Download this ${kind === 'clip' ? 'clip' : 'recording'}">
<div class="ovv-dl-head"><span>Download this ${kind === 'clip' ? 'clip' : 'recording'}</span><button type="button" class="ovv-dl-close" data-ovv-dl-close aria-label="Close"><i class="fa-solid fa-xmark" aria-hidden="true"></i></button></div>
<div class="ovv-dl-head"><span>Download this ${kind === 'clip' ? 'clip' : 'recording'}</span><button type="button" class="ovv-dl-close" data-ovv-dl-close aria-label="Close"><i class="fa-solid fa-xmark" aria-hidden="true"></i></button></div>
@@ -64,7 +64,8 @@ function renderPage(o) {
@@ -64,7 +64,8 @@ function renderPage(o) {
${appIcon.headTags({ site: 'media-hub' })}
${appIcon.headTags({ site: 'media-hub' })}
<link rel="stylesheet" href="${asset('css/app.css')}">
<link rel="stylesheet" href="${asset('css/app.css')}">
<link rel="stylesheet" href="${asset('css/video.css')}">
<link rel="stylesheet" href="${asset('css/video.css')}">
-<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
+<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
+<script id="ov-glyphs-loader" src="${ovServe.url('ov-glyphs.js')}" defer></script>
${o.player ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>\n` : ''}${(o.scripts || []).map((rel) => `<script src="${asset(rel)}" defer></script>`).join('\n')}`;
${o.player ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>\n` : ''}${(o.scripts || []).map((rel) => `<script src="${asset(rel)}" defer></script>`).join('\n')}`;
const account = signedIn
const account = signedIn
? html`Signed in as <strong>${viewer.displayName || viewer.username || 'you'}</strong> · <a href="/auth/logout?next=${encodeURIComponent(pathNow)}">Sign out</a>`
? html`Signed in as <strong>${viewer.displayName || viewer.username || 'you'}</strong> · <a href="/auth/logout?next=${encodeURIComponent(pathNow)}">Sign out</a>`
new file mode 100644
new file mode 100644
@@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
+'use strict';
+// No Font Awesome (plan L54 K4; the owner, 2026-11-01: "get rid of Font Awesome completely and just use our own
+// openvibe.design system"). OpenVibe.MediaHub's icons are the OpenVibe glyphs (openvibe-shared/glyphs, drawn in the browser by
+// /shared/ov-glyphs.js, which the Shared navbar loads on a page that names an icon) and the brand logos from
+// openvibe-shared/brands. This holds it there:
+// - no page or stylesheet links Font Awesome, and nothing draws with Font Awesome's font;
+// - every page that names an icon loads the navbar or the glyph drop-in;
+// - every fa- name OpenVibe.MediaHub uses is one the glyphs or the brands draw (a name nobody draws would show nothing).
+// node test/no-font-awesome.test.js
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const glyphs = require('openvibe-shared/glyphs');
+const brands = require('openvibe-shared/brands');
+
+const ROOT = path.join(__dirname, '..');
+const walk = (dir, ok, out = []) => {
+ if (!fs.existsSync(dir)) return out;
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
+ if (e.name === 'node_modules' || e.name === 'vendor' || e.name.startsWith('.')) continue;
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) walk(p, ok, out); else if (ok.test(e.name)) out.push(p);
+ }
+ return out;
+};
+const rel = (p) => path.relative(ROOT, p);
+
+// The pages (whole HTML files) and the code that draws them.
+const PAGE_ROOTS = [];
+const CODE_ROOTS = ["public","server"];
+const LAYOUTS = ["server/video/layout.js","server/render/layout.js"]; // the server-rendered pages: their layout brings the navbar or the glyphs
+
+// A Font Awesome stylesheet, wherever it is written.
+const FA_CSS = /fontawesome[^"'\s]*\.css|font-awesome[^"'\s]*\.css|cdnjs\.cloudflare\.com\/ajax\/libs\/font-awesome/i;
+
+const pages = [];
+for (const r of PAGE_ROOTS) pages.push(...walk(path.join(ROOT, r), /\.html$/));
+for (const p of pages) {
+ const html = fs.readFileSync(p, 'utf8');
+ assert.ok(!/fontawesome|font-awesome/i.test(html), `${rel(p)} links Font Awesome`);
+ // A whole page (with a <head>; fragments are parts of a page) that names an icon loads what draws it.
+ if (/<head[\s>]/i.test(html) && /\bfa-[a-z]/.test(html) && !/\/shared\/navbar|ov-glyphs\.js/.test(html)) {
+ assert.fail(`${rel(p)} has fa- icons but loads neither the navbar nor /shared/ov-glyphs.js`);
+ }
+}
+
+// Nothing draws with Font Awesome's font.
+for (const r of CODE_ROOTS) {
+ for (const p of walk(path.join(ROOT, r), /\.css$/)) {
+ assert.ok(!/font-family:\s*["']?Font ?Awesome/i.test(fs.readFileSync(p, 'utf8')), `${rel(p)} draws with Font Awesome's font`);
+ }
+ for (const p of walk(path.join(ROOT, r), /\.(html|js|mjs|cjs)$/)) {
+ const t = fs.readFileSync(p, 'utf8');
+ assert.ok(!/['"]\d+px FontAwesome|Font Awesome 6 Free/.test(t), `${rel(p)} draws with Font Awesome's font`);
+ assert.ok(!FA_CSS.test(t), `${rel(p)} links Font Awesome`);
+ }
+}
+
+// The server-rendered pages get the glyphs from the Shared navbar (shell.page) or from the drop-in.
+for (const f of LAYOUTS) {
+ const t = fs.readFileSync(path.join(ROOT, f), 'utf8');
+ assert.ok(/shell\.page|ov-glyphs\.js|\/shared\/navbar/.test(t), `${f} renders pages without the navbar or /shared/ov-glyphs.js`);
+}
+
+// Every fa- name in the pages, the scripts and the server views is drawn by a glyph or a brand.
+const MOD = /^(solid|regular|light|thin|duotone|brands|sharp|classic|fw|lg|xl|xs|sm|2xs|2xl|\d+x|li|ul|border|inverse|pull-left|pull-right|rotate-\d+|rotate-by|flip-horizontal|flip-vertical|flip-both|width-auto|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|swap-opacity|stack-text)$/;
+const files = [];
+for (const r of CODE_ROOTS) files.push(...walk(path.join(ROOT, r), /\.(html|js)$/));
+const missing = new Map();
+for (const f of files) {
+ for (const m of fs.readFileSync(f, 'utf8').matchAll(/\bfa-([a-z0-9]+(?:-[a-z0-9]+)*)\b/g)) {
+ const n = m[1];
+ if (MOD.test(n) || glyphs.fromFa(n) || brands.fromFa(n)) continue;
+ if (!missing.has(n)) missing.set(n, rel(f));
+ }
+}
+assert.deepStrictEqual([...missing].map(([n, f]) => `fa-${n} (${f})`), [], 'every icon this site names is drawn by the OpenVibe glyphs or brands');
+
+console.log(`no font awesome: ${pages.length} pages, ${files.length} files, every fa- name drawn`);
@@ -17,9 +17,9 @@ const BUDGETS = {
@@ -17,9 +17,9 @@ const BUDGETS = {
htmlRawKB: 51, // 50.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 49.4 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 44.2 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 39.4 on 10-22: the home gained the backup command, the limits table, six questions (and their
htmlRawKB: 51, // 50.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 49.4 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 44.2 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 39.4 on 10-22: the home gained the backup command, the limits table, six questions (and their
// FAQPage JSON-LD) and next steps on the network; was 29.7 at 27.5
// FAQPage JSON-LD) and next steps on the network; was 29.7 at 27.5
htmlBrotliKB: 12, // 10.6 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 9.4 on 10-22 (the same sections); was 7.7 at 7.1
htmlBrotliKB: 12, // 10.6 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 9.4 on 10-22 (the same sections); was 7.7 at 7.1
- jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell)
- jsRawKB: 331, // 328.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 324.8 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 323.2 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 321.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 318.1 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 315.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 307.8 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 300.5 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; 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 with Shared 4.15.0 (2026-10-27: the compact footer opens into the whole one, the pulse line); 264.3 with Shared 4.13.0; 239.8
- jsBrotliKB: 80.8, // 80.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 79.5 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 78.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 77.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 76.1 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 74.2 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; 70.5 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 67.9 Shared 4.15.0; 65.3 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 56.5
+ jsFiles: 6, // 6: /shared/ov-glyphs.js in place of Font Awesome's stylesheet (2026-11-01, plan L54 K4); was: 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell)
+ jsRawKB: 393, // 393: /shared/ov-glyphs.js in place of Font Awesome (2026-11-01, plan L54 K4; Font Awesome's CSS and font were external, uncounted, and heavier); 328.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 324.8 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 323.2 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 321.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 318.1 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 315.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 307.8 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 300.5 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; 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 with Shared 4.15.0 (2026-10-27: the compact footer opens into the whole one, the pulse line); 264.3 with Shared 4.13.0; 239.8
+ jsBrotliKB: 96, // 96: /shared/ov-glyphs.js in place of Font Awesome (2026-11-01, plan L54 K4); 80.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 79.5 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 78.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 77.6 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 76.1 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 74.2 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; 70.5 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 67.9 Shared 4.15.0; 65.3 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 56.5
cssFiles: 2, // 2 (app.css + the cached /shared/showcase.css)
cssFiles: 2, // 2 (app.css + the cached /shared/showcase.css)
cssRawKB: 34, // 33.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 29 openvibe-shared 4.26.1 (hero visual); 23.6 with Shared 4.13.0 (2026-10-26: account links); measured 16.6 (shared 4.1.8: four steps two by two); was 16.5 at 15.0
cssRawKB: 34, // 33.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 29 openvibe-shared 4.26.1 (hero visual); 23.6 with Shared 4.13.0 (2026-10-26: account links); measured 16.6 (shared 4.1.8: four steps two by two); was 16.5 at 15.0
cssBrotliKB: 7.4, // 7.2 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 5.4 with Shared 4.13.0 (2026-10-26: account links); 3.7; 4.2 with the drive previews (10-15)
cssBrotliKB: 7.4, // 7.2 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 5.4 with Shared 4.13.0 (2026-10-26: account links); 3.7; 4.2 with the drive previews (10-15)