openvibers/openvibe.network Commits

Font Awesome out, the OpenVibe glyphs in (plan L54 K4) (#484)

OP OpenVibers committed 12 days ago · 41a5076d5e · 19 files, +122 −46

* Font Awesome out, the OpenVibe glyphs in (plan L54 K4)

The owner, 2026-11-01: "get rid of Font Awesome completely and just use our own
openvibe.design system". The six pages that linked cdnjs' all.min.css no longer
do; the icons come from the OpenVibe glyphs (openvibe-shared/glyphs) and the brand
logos (openvibe-shared/brands). The Shared navbar draws them where the page has
one; login.html and sso-fanout.html, which have no navbar, load /shared/ov-glyphs.js.
cdnjs leaves the CSP: nothing else on the site used it.

Every fa- name Network named that no glyph draws is now one that is drawn:
chart-pie -> chart-column, laptop-code -> laptop, id-badge -> id-card,
toggle-on -> circle-check, toggle-off -> circle-xmark, puzzle-piece -> plug,
snail -> gauge, cog -> gear, stethoscope -> kit-medical, aws -> cloud,
file-invoice-dollar -> money-bill, vial -> flask-vial, bell-slash -> bell,
certificate -> ranking-star, arrows-split-up-and-left -> share-nodes,
lock-open -> unlock, image-portrait -> user-circle, file-export -> download,
file-import -> upload, code-merge -> code-fork, file-zipper -> box-archive,
user-xmark -> user-slash, envelope-circle-check -> envelope, computer -> desktop,
medal -> ranking-star.

test/no-font-awesome.test.js holds it there: no page links Font Awesome, nothing
draws with its font, and every fa- name is drawn.

* look-boot test: an inline <style> is a stylesheet too

/verify-email's only <link rel=stylesheet> was Font Awesome's; without it the page has only its
own inline style, and the boot must still come before that.

Changes

ddiff --git a/public/admin.html b/public/admin.html
iindex cb9229b0..2d6b1dd2 100644
---- a/public/admin.html
++++ b/public/admin.html
@@@ -26,7 +26,6 @@
} 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">
</head>
<body>
@@@ -69,7 +68,7 @@
<button class="admin-tab-btn" onclick="showTab('themes',this)"><i class="fa-solid fa-palette"></i> Theme review</button>
<button class="admin-tab-btn" onclick="showTab('checklist',this)"><i class="fa-solid fa-list-check"></i> Operator checklist</button>
<button class="admin-tab-btn owner-only" onclick="showTab('eventsops',this)"><i class="fa-solid fa-route"></i> Event deliveries</button>
-- <button class="admin-tab-btn" onclick="showTab('analytics',this)"><i class="fa-solid fa-chart-pie"></i> Analytics</button>
++ <button class="admin-tab-btn" onclick="showTab('analytics',this)"><i class="fa-solid fa-chart-column"></i> Analytics</button>
<button class="admin-tab-btn owner-only" onclick="showTab('domains',this)"><i class="fa-solid fa-globe"></i> Domains</button>
<button class="admin-tab-btn owner-only" onclick="showTab('deploy',this)"><i class="fa-solid fa-server"></i> Deploy</button>
<button class="admin-tab-btn" onclick="showTab('ssh',this)"><i class="fa-solid fa-terminal"></i> SSH</button>
@@@ -374,11 +373,11 @@
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px">
<div class="card">
-- <h3><i class="fa-solid fa-chart-pie"></i> Per-Service Traffic Share</h3>
++ <h3><i class="fa-solid fa-chart-column"></i> Per-Service Traffic Share</h3>
<canvas id="chart-service-share" height="260"></canvas>
</div>
<div class="card">
-- <h3><i class="fa-solid fa-chart-pie"></i> Per-Service Visitors</h3>
++ <h3><i class="fa-solid fa-chart-column"></i> Per-Service Visitors</h3>
<canvas id="chart-service-visitors" height="260"></canvas>
</div>
</div>
@@@ -469,7 +468,7 @@
<div id="svc-top-referers"></div>
</div>
<div class="card">
-- <h3><i class="fa-solid fa-snail"></i> Slowest Endpoints</h3>
++ <h3><i class="fa-solid fa-gauge"></i> Slowest Endpoints</h3>
<div id="svc-slowest"></div>
</div>
</div>
@@@ -582,7 +581,7 @@
<!-- Deploy: Config -->
<div id="deploy-config-panel" style="display:none">
<div class="card">
-- <h3><i class="fa-solid fa-cog"></i> Deploy Configuration</h3>
++ <h3><i class="fa-solid fa-gear"></i> Deploy Configuration</h3>
<div class="form-group" style="margin:12px 0">
<label>ACME / Let's Encrypt Email</label>
<input type="email" id="deploy-acme-email" placeholder="[email protected]" style="width:350px">
ddiff --git a/public/index.html b/public/index.html
iindex b48bb159..a08be4d1 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -27,7 +27,6 @@
<meta property="og:site_name" content="OpenVibe">
<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">
<!-- 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).
@@@ -45,7 +44,6 @@
} 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">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
ddiff --git a/public/js/admin/ai.js b/public/js/admin/ai.js
iindex 28c8bdb2..e95cff11 100644
---- a/public/js/admin/ai.js
++++ b/public/js/admin/ai.js
@@@ -289,7 +289,7 @@ async function loadStreamerMediaTools() {
style="width:100%;background:var(--bg-input);color:var(--text);border:1px solid var(--border);padding:10px;border-radius:6px;font-family:monospace;font-size:12px;resize:vertical"></textarea>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px">
<button type="button" class="btn btn-primary" onclick="saveStreamerMediaCookies()"><i class="fa-solid fa-floppy-disk"></i> Save &amp; Test</button>
-- <button type="button" class="btn" onclick="checkStreamerMediaCookies()" ${status.cookies_configured ? '' : 'disabled'}><i class="fa-solid fa-stethoscope"></i> Test Current Cookies</button>
++ <button type="button" class="btn" onclick="checkStreamerMediaCookies()" ${status.cookies_configured ? '' : 'disabled'}><i class="fa-solid fa-kit-medical"></i> Test Current Cookies</button>
<button type="button" class="btn" onclick="deleteStreamerMediaCookies()" ${status.cookies_configured ? '' : 'disabled'}><i class="fa-solid fa-trash"></i> Remove Cookies</button>
</div>
<div id="streamer-media-cookie-verdict"></div>
@@@ -485,7 +485,7 @@ async function loadTTS() {
</div>
<div class="card">
-- <h3><i class="fa-brands fa-aws" aria-hidden="true"></i> Amazon Polly</h3>
++ <h3><i class="fa-solid fa-cloud" aria-hidden="true"></i> Amazon Polly</h3>
${ownerNote}
${secret('tts-aws-key', 'AWS Access Key ID', s.awsAccessKeyId)}
${secret('tts-aws-secret', 'AWS Secret Access Key', s.awsSecretAccessKey)}
ddiff --git a/public/js/admin/analytics.js b/public/js/admin/analytics.js
iindex cc93890b..150afdee 100644
---- a/public/js/admin/analytics.js
++++ b/public/js/admin/analytics.js
@@@ -164,7 +164,7 @@ async function loadAnalyticsOverview() {
{ icon: 'fa-eye', label: 'Page Views', value: fmtNum(t.total_pageviews) },
{ icon: 'fa-code', label: 'API Calls', value: fmtNum(t.total_api_calls) },
{ icon: 'fa-users', label: 'Unique Visitors', value: fmtNum(t.total_unique_visitors) },
-- { icon: 'fa-id-badge', label: 'Sessions', value: fmtNum(ov.sessionCount || 0) },
++ { icon: 'fa-id-card', label: 'Sessions', value: fmtNum(ov.sessionCount || 0) },
{ icon: 'fa-robot', label: 'Bot Hits', value: fmtNum(t.total_bot_hits) },
{ icon: 'fa-circle-exclamation', label: 'Errors', value: fmtNum(t.total_errors) },
{ icon: 'fa-gauge-high', label: 'Avg Response', value: (t.avg_response_ms || 0) + 'ms' },
@@@ -393,7 +393,7 @@ async function loadServiceAnalytics(serviceName) {
{ icon: 'fa-code', label: 'API Calls', value: fmtNum(s.total_api_calls) },
{ icon: 'fa-users', label: 'Unique Visitors', value: fmtNum(s.total_unique_visitors) },
{ icon: 'fa-user', label: 'Logged-in Users', value: fmtNum(s.total_unique_users) },
-- { icon: 'fa-id-badge', label: 'Sessions', value: fmtNum(a.sessionCount || 0) },
++ { icon: 'fa-id-card', label: 'Sessions', value: fmtNum(a.sessionCount || 0) },
{ icon: 'fa-robot', label: 'Bot Hits', value: fmtNum(s.total_bot_hits) },
{ icon: 'fa-circle-exclamation', label: 'Errors', value: fmtNum(s.total_errors) },
{ icon: 'fa-gauge-high', label: 'Avg Response', value: (s.avg_response_ms || 0) + 'ms' },
ddiff --git a/public/js/admin/content.js b/public/js/admin/content.js
iindex 9ff1e03a..fb5a5890 100644
---- a/public/js/admin/content.js
++++ b/public/js/admin/content.js
@@@ -183,7 +183,7 @@ function buildStreamerTierHtml(tierData) {
+ '<tr style="border-top:2px solid var(--border);font-weight:700"><td style="padding:8px 10px">Total</td><td style="padding:8px 10px;text-align:right">' + totalVods.toLocaleString() + '</td><td style="padding:8px 10px;text-align:right">' + fmtBytes(totalBytes) + '</td><td style="padding:8px 10px;text-align:right">100%</td><td style="padding:8px 10px;text-align:right">' + totalClips.toLocaleString() + '</td><td style="padding:8px 10px">' + (tierData.sweepRunning ? '<span style="color:#f59e0b"><i class="fa-solid fa-spinner fa-spin"></i> sweeping</span>' : '<span style="color:var(--text-muted)">idle</span>') + '</td></tr>'
+ '</tbody></table></div>'
+ '<div style="display:flex;flex-wrap:wrap;gap:8px;font-size:12px;color:var(--text-muted);margin-top:12px">'
-- + '<span><i class="fa-solid ' + (ts.enabled ? 'fa-toggle-on' : 'fa-toggle-off') + '" style="color:' + (ts.enabled ? '#22c55e' : '#ef4444') + '"></i> Auto-offload: ' + (ts.enabled ? 'ON' : 'OFF') + '</span>'
++ + '<span><i class="fa-solid ' + (ts.enabled ? 'fa-circle-check' : 'fa-circle-xmark') + '" style="color:' + (ts.enabled ? '#22c55e' : '#ef4444') + '"></i> Auto-offload: ' + (ts.enabled ? 'ON' : 'OFF') + '</span>'
+ '<span>| Age &ge; ' + (ts.minAgeDays != null ? ts.minAgeDays : '?') + 'd</span><span>| Views &le; ' + (ts.maxViewsForCold != null ? ts.maxViewsForCold : '?') + '</span><span>| Idle &ge; ' + (ts.minLastAccessDays != null ? ts.minLastAccessDays : '?') + 'd</span>'
+ '</div></div>';
}
@@@ -224,7 +224,7 @@ async function loadStreamerCloudStorage(force) {
};
el.innerHTML = header
+ '<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:12px">' + card('b2', 'Backblaze B2', '#e21e2b') + card('r2', 'Cloudflare R2', '#f6821f') + '</div>'
-- + '<div class="card" style="display:flex;align-items:center;justify-content:space-between"><span><i class="fa-solid fa-file-invoice-dollar" style="color:var(--accent)"></i> <strong>Estimated cloud storage cost</strong> <span style="color:var(--text-muted);font-size:12px">(list prices; excludes egress/operations)</span></span><span style="font-size:22px;font-weight:800;color:var(--accent)">' + money(costs.totalStorageMonthly) + '<span style="font-size:13px;font-weight:400;color:var(--text-muted)">/mo</span></span></div>';
++ + '<div class="card" style="display:flex;align-items:center;justify-content:space-between"><span><i class="fa-solid fa-money-bill" style="color:var(--accent)"></i> <strong>Estimated cloud storage cost</strong> <span style="color:var(--text-muted);font-size:12px">(list prices; excludes egress/operations)</span></span><span style="font-size:22px;font-weight:800;color:var(--accent)">' + money(costs.totalStorageMonthly) + '<span style="font-size:13px;font-weight:400;color:var(--text-muted)">/mo</span></span></div>';
} catch (e) {
el.innerHTML = header + '<div style="color:var(--danger-text, var(--danger,#ef4444))">Failed to scan buckets: ' + esc(e.message) + '</div>';
}
@@@ -318,7 +318,7 @@ async function loadEmail() {
<div class="stat-card"><div class="value">${Number(summary.sent_24h || 0).toLocaleString()}</div><div class="label"><i class="fa-solid fa-clock"></i> Sent · 24h</div></div>
<div class="stat-card"><div class="value">${Number(summary.password_resets_24h || 0).toLocaleString()}</div><div class="label"><i class="fa-solid fa-key"></i> Password Resets · 24h</div></div>
<div class="stat-card"><div class="value">${Number(summary.notifications_24h || 0).toLocaleString()}</div><div class="label"><i class="fa-solid fa-bell"></i> Notification Emails · 24h</div></div>
-- <div class="stat-card"><div class="value">${Number(summary.tests_24h || 0).toLocaleString()}</div><div class="label"><i class="fa-solid fa-vial"></i> Test Emails · 24h</div></div>
++ <div class="stat-card"><div class="value">${Number(summary.tests_24h || 0).toLocaleString()}</div><div class="label"><i class="fa-solid fa-flask-vial"></i> Test Emails · 24h</div></div>
</div>
<div class="card" style="padding:16px;margin-bottom:16px">
ddiff --git a/public/js/admin/dashboard.js b/public/js/admin/dashboard.js
iindex e34f33ab..24f1ea9f 100644
---- a/public/js/admin/dashboard.js
++++ b/public/js/admin/dashboard.js
@@@ -27,7 +27,7 @@ async function loadDashboard() {
{ label: 'Total Streams', value: ss?.streams?.total || ss?.totalStreams || 0, icon: 'fa-video' },
{ label: 'Active Bans', value: ss?.users?.banned || ss?.activeBans || 0, icon: 'fa-ban' },
{ label: 'Notifications', value: th?.total_notifications || 0, icon: 'fa-bell' },
-- { label: 'Unread Notifs', value: th?.unread_notifications || 0, icon: 'fa-bell-slash' },
++ { label: 'Unread Notifs', value: th?.unread_notifications || 0, icon: 'fa-bell' },
{ label: 'Anon Users', value: th?.anon_users || 0, icon: 'fa-ghost' },
];
ddiff --git a/public/js/admin/deploy.js b/public/js/admin/deploy.js
iindex 6ea9b23e..58cd78f0 100644
---- a/public/js/admin/deploy.js
++++ b/public/js/admin/deploy.js
@@@ -413,7 +413,7 @@ async function showManualDnsInfo() {
<div style="font-size:13px;line-height:1.6">${instructions}</div>
<div style="margin-top:16px">
<button class="btn btn-primary" onclick="issueManualCert('${esc(domain)}')">
-- <i class="fa-solid fa-certificate"></i> DNS Records Created — Issue Certificate
++ <i class="fa-solid fa-ranking-star"></i> DNS Records Created — Issue Certificate
</button>
<div class="muted" style="font-size:11px;margin-top:4px">Only click after creating the required DNS TXT records</div>
</div>
ddiff --git a/public/js/my/machines.js b/public/js/my/machines.js
iindex 6a7aedfe..6f9f5751 100644
---- a/public/js/my/machines.js
++++ b/public/js/my/machines.js
@@@ -168,7 +168,7 @@ async function loadMachines() {
const machines = (out && out.machines) || [];
machines.sort((a, b) => (b.online === true) - (a.online === true));
if (!machines.length) {
-- list.replaceChildren(mcEl('div', 'mc-empty', [mcIcon('fa-laptop-code'), mcEl('p', '', [mcEl('b', '', ['No machines yet.']), ' Pair your computer and it can run your Codes jobs, host your desks and serve your own models, under your account.'])]));
++ list.replaceChildren(mcEl('div', 'mc-empty', [mcIcon('fa-laptop'), mcEl('p', '', [mcEl('b', '', ['No machines yet.']), ' Pair your computer and it can run your Codes jobs, host your desks and serve your own models, under your account.'])]));
return;
}
list.replaceChildren(...machines.map(mcCard));
ddiff --git a/public/js/my/security.js b/public/js/my/security.js
iindex c13f767f..5b46ff0b 100644
---- a/public/js/my/security.js
++++ b/public/js/my/security.js
@@@ -349,7 +349,7 @@ async function loadApps() {
for (const app of apps) {
const row = document.createElement('div'); row.className = 'linked-item';
const icon = document.createElement('span'); icon.className = 'svc-icon';
-- const i = document.createElement('i'); i.className = 'fa-solid fa-puzzle-piece'; icon.appendChild(i);
++ const i = document.createElement('i'); i.className = 'fa-solid fa-plug'; icon.appendChild(i);
const main = document.createElement('div');
const name = document.createElement('div'); name.className = 'svc-name'; name.textContent = app.name || app.client_id;
const meta = document.createElement('div'); meta.className = 'svc-user';
ddiff --git a/public/js/my/today.js b/public/js/my/today.js
iindex f4079439..d82d188a 100644
---- a/public/js/my/today.js
++++ b/public/js/my/today.js
@@@ -16,8 +16,8 @@ const TD_SITES = {
reviews: ['Reviews', 'fa-star', 'https://openvibe.reviews'], tips: ['Tips', 'fa-hand-holding-heart', 'https://openvibe.tips'],
vip: ['VIP', 'fa-crown', 'https://openvibe.vip'], trade: ['Trade', 'fa-chart-line', 'https://openvibe.trade'],
host: ['Host', 'fa-server', 'https://openvibe.host'], deals: ['Deals', 'fa-tags', 'https://openvibe.deals'],
-- coupons: ['Coupons', 'fa-ticket', 'https://openvibe.coupons'], openre: ['Restream', 'fa-arrows-split-up-and-left', 'https://openre.stream'],
-- design: ['Design', 'fa-palette', 'https://openvibe.design'], report: ['Report', 'fa-chart-pie', 'https://openvibe.report'],
++ coupons: ['Coupons', 'fa-ticket', 'https://openvibe.coupons'], openre: ['Restream', 'fa-share-nodes', 'https://openre.stream'],
++ design: ['Design', 'fa-palette', 'https://openvibe.design'], report: ['Report', 'fa-chart-column', 'https://openvibe.report'],
};
// What to try next, most used across the network first, each with the reason it is worth a look.
const TD_TRY = [
ddiff --git a/public/login.html b/public/login.html
iindex f0d7f16b..4ed4d8f3 100644
---- a/public/login.html
++++ b/public/login.html
@@@ -11,8 +11,9 @@
<link rel="apple-touch-icon" href="/assets/logo-192.png">
<link rel="manifest" href="/manifest.webmanifest">
<meta name="description" content="Sign in to OpenVibe — one account across OpenVibe.Live, OpenVibe.Tools, OpenVibe.Games, and OpenVibe.Media.">
--<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/login.css">
++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
++<script src="/shared/ov-glyphs.js" defer></script>
</head>
<body>
@@@ -54,7 +55,7 @@
<i class="fa-solid fa-wand-magic-sparkles"></i> Create a new account
</button>
<button class="action-btn" onclick="showForgotForm()">
-- <i class="fa-solid fa-lock-open"></i> Forgot password
++ <i class="fa-solid fa-unlock"></i> Forgot password
</button>
</div>
</div>
ddiff --git a/public/my.html b/public/my.html
iindex b3b783c3..4565516c 100644
---- a/public/my.html
++++ b/public/my.html
@@@ -21,7 +21,6 @@
<link rel="canonical" href="https://openvibe.network/my">
<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">
<!-- 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).
@@@ -39,7 +38,6 @@
} 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">
<!-- The OpenVibe Kit's fold rows on "Your data choices" (L24): /shared/kit.css is the shared kit, nothing global. -->
<link rel="stylesheet" href="/shared/kit.css">
@@@ -111,7 +109,7 @@
<button onclick="showSection('data',this)"><i class="fa-solid fa-database"></i> AI &amp; Data</button>
<button onclick="showSection('approvals',this)"><i class="fa-solid fa-user-check"></i> Approvals</button>
<button onclick="showSection('linked',this)"><i class="fa-solid fa-link"></i> Linked Services</button>
-- <button onclick="showSection('machines',this)"><i class="fa-solid fa-laptop-code"></i> Machines</button>
++ <button onclick="showSection('machines',this)"><i class="fa-solid fa-laptop"></i> Machines</button>
<button onclick="showSection('security',this)"><i class="fa-solid fa-lock"></i> Security</button>
<button onclick="showSection('anon',this)"><i class="fa-solid fa-user-secret"></i> Anon Info</button>
</div>
@@@ -119,7 +117,7 @@
<!-- Profile Section -->
<div class="section active" id="sec-profile">
<div class="card" id="avatar-card">
-- <h3><span class="icon"><i class="fa-solid fa-image-portrait"></i></span> Avatar</h3>
++ <h3><span class="icon"><i class="fa-solid fa-user-circle"></i></span> Avatar</h3>
<p class="muted" style="margin-top:0">One picture for every OpenVibe site. It always lives on <b>openvibe.media</b>. Paste the link to one of your image pastes (from <a href="https://openvibe.community/my" target="_blank" rel="noopener">your pastes</a>), or any picture link: we fetch it once, clean it, and keep our own copy.</p>
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<img id="avatar-preview" alt="" width="64" height="64" style="width:64px;height:64px;border-radius:50%;object-fit:cover;border:2px solid var(--accent)">
@@@ -206,8 +204,8 @@
<h3 style="margin:0"><span class="icon"><i class="fa-solid fa-share-nodes"></i></span> Share a theme</h3>
<p style="margin:6px 0 12px;color:var(--text-secondary);font-size:0.9rem">Export your active theme as a file, import one, or submit your colours as a community theme. Submissions are reviewed before anyone else sees them; you can use yours right away.</p>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
-- <button class="btn btn-outline" onclick="exportActiveTheme(this)"><i class="fa-solid fa-file-export"></i> Export active theme</button>
-- <label class="btn btn-outline" style="cursor:pointer"><i class="fa-solid fa-file-import"></i> Import a theme file<input type="file" accept=".json,application/json" onchange="importThemeFile(this)" style="display:none"></label>
++ <button class="btn btn-outline" onclick="exportActiveTheme(this)"><i class="fa-solid fa-download"></i> Export active theme</button>
++ <label class="btn btn-outline" style="cursor:pointer"><i class="fa-solid fa-upload"></i> Import a theme file<input type="file" accept=".json,application/json" onchange="importThemeFile(this)" style="display:none"></label>
</div>
<form class="theme-submit" onsubmit="submitMyTheme(event)">
<div class="form-field"><label for="theme-submit-name">Name</label><input id="theme-submit-name" required minlength="2" maxlength="60" placeholder="Midnight Arcade" autocomplete="off"></div>
@@@ -233,18 +231,18 @@
</div>
<!-- Account merge (ADR-029): fold a second account this person owns into this one. -->
<div class="card" id="merge-card" style="margin-top:16px">
-- <h3><span class="icon"><i class="fa-solid fa-code-merge"></i></span> Merge two accounts</h3>
++ <h3><span class="icon"><i class="fa-solid fa-code-fork"></i></span> Merge two accounts</h3>
<p>Own two accounts? Fold one into the other. Its follows, OpenCoins, linked sign-ins, sessions and settings move to the account you keep, which keeps its name and profile. The other account can't be used on its own afterwards: signing in to it signs you in to the one you kept. If it was a mistake, support can split it by hand within 30 days.</p>
<div id="merge-body" style="margin-top:12px"></div>
</div>
<!-- Your data (ADR-033): a copy of everything, and deleting the account after 30 days. -->
<div class="card" id="export-card" style="margin-top:16px">
-- <h3><span class="icon"><i class="fa-solid fa-file-zipper"></i></span> Download your data</h3>
++ <h3><span class="icon"><i class="fa-solid fa-box-archive"></i></span> Download your data</h3>
<p>Get a copy of what OpenVibe keeps about you: your account here, and your part of Live, Chat, Community, Media and Games, as JSON files in one .zip. Each service adds its own files, so it takes up to half an hour. You can download it for 7 days, and ask for one a day.</p>
<div id="export-body" style="margin-top:12px"></div>
</div>
<div class="card" id="delete-card" style="margin-top:16px">
-- <h3><span class="icon"><i class="fa-solid fa-user-xmark"></i></span> Delete your account</h3>
++ <h3><span class="icon"><i class="fa-solid fa-user-slash"></i></span> Delete your account</h3>
<p>Your account is deleted 30 days after you ask, and you can cancel any time before then; until that date everything works as usual. On the day, your account here is erased and every service erases what it keeps about you: messages, posts, follows, media and settings. Replies other people wrote under your posts stay, without your name. Payment records are kept without your name, as the law requires. After that it cannot be undone, and your username becomes free for someone else.</p>
<div id="delete-body" style="margin-top:12px"></div>
</div>
@@@ -253,7 +251,7 @@
<!-- Notifications Section -->
<div class="section" id="sec-notifications">
<div class="card" id="notif-verify-card" style="display:none">
-- <h3><span class="icon"><i class="fa-solid fa-envelope-circle-check"></i></span> Email alerts</h3>
++ <h3><span class="icon"><i class="fa-solid fa-envelope"></i></span> Email alerts</h3>
<div id="notif-verify-body" style="font-size:13px;line-height:1.5"></div>
</div>
<div class="card">
@@@ -312,7 +310,7 @@
per purpose, the wording, the kinds, the 18+ confirmation inside the row, the history, and equal On/Off
buttons. Nothing is pre-ticked; the rows are drawn from GET /api/v1/me/consent. -->
<div class="card" id="consent-card">
-- <h3><span class="icon"><i class="fa-solid fa-toggle-on"></i></span> Your data choices</h3>
++ <h3><span class="icon"><i class="fa-solid fa-circle-check"></i></span> Your data choices</h3>
<p>AI training, the open dataset, face recognition and apps that use your content are <b>off until you turn them on</b>. Each choice is yours, and you can turn it off again at any time. Nothing here is on by default. We never sell your data.</p>
<div id="consent-choices"><div class="loading-spinner">Loading your choices…</div></div>
<p class="muted consent-status" id="consent-status" role="status"></p>
@@@ -367,7 +365,7 @@
<div class="card">
<div class="mc-top">
<div>
-- <h3><span class="icon"><i class="fa-solid fa-laptop-code"></i></span> Your machines</h3>
++ <h3><span class="icon"><i class="fa-solid fa-laptop"></i></span> Your machines</h3>
<p class="muted">Computers and servers paired with your account. They run your Codes jobs, host your desks and serve your own models. Only you can pair one or change what it does.</p>
</div>
<button class="btn btn-primary" id="mc-pair-btn" type="button" onclick="mcPair()"><i class="fa-solid fa-plus"></i> Pair a machine</button>
@@@ -409,7 +407,7 @@
</div>
</div>
<div class="card">
-- <h3><span class="icon"><i class="fa-solid fa-puzzle-piece"></i></span> Signed-in apps</h3>
++ <h3><span class="icon"><i class="fa-solid fa-plug"></i></span> Signed-in apps</h3>
<p style="margin-bottom:12px;color:var(--text-secondary);font-size:13px">Extensions and apps that hold their own sign-in. Revoking one ends its refresh token; the next request asks you to sign in again.</p>
<div id="apps-list"><div class="loading-spinner">Loading apps...</div></div>
</div>
ddiff --git a/public/sso-fanout.html b/public/sso-fanout.html
iindex 3591b377..cf048bfa 100644
---- a/public/sso-fanout.html
++++ b/public/sso-fanout.html
@@@ -27,7 +27,6 @@
} 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>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--bg-primary:#0a0f1c;--bg-secondary:#101828;--bg-card:#131c2e;--border:#1f2d47;--text-primary:#e8ecf5;--text-secondary:#a9b3c9;--text-muted:#6b7590;--accent:#3b82f6;--accent-light:#60a5fa;--accent-rgb:59,130,246;--on-accent:#fff;--success:#22c55e}
@@@ -59,6 +58,8 @@ li.now .dot i{animation:breathe 1.2s ease-in-out infinite}
.ov-mark{width:38px;height:38px;flex:none}
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
</style>
++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
++<script src="/shared/ov-glyphs.js" defer></script>
</head>
<body>
<section class="card" aria-live="polite">
ddiff --git a/public/verify-email.html b/public/verify-email.html
iindex 9f99480d..426f43ed 100644
---- a/public/verify-email.html
++++ b/public/verify-email.html
@@@ -29,7 +29,6 @@
})();
</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">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--bg-primary:#0a0f1c;--bg-secondary:#101828;--bg-card:#131c2e;--bg-hover:#1c2a44;--border:#1f2d47;--text-primary:#e8ecf5;--text-secondary:#a9b3c9;--text-muted:#6b7590;--accent:#3b82f6;--accent-light:#60a5fa;--accent-rgb:59,130,246;--on-accent:#fff;--success:#22c55e;--danger:#ef4444;--warning:#f59e0b}
ddiff --git a/server/home/render.js b/server/home/render.js
iindex 28b895b0..a05e06c8 100644
---- a/server/home/render.js
++++ b/server/home/render.js
@@@ -59,10 +59,10 @@ const INTENTS = [
['services', 'fa-server', 'Build on OpenVibe', 'One console for projects, keys, grants and the API docs of every service.', 'https://openvibe.services/', 'build api sdk developer developers oauth webhook webhooks app integrate console platform'],
['codes', 'fa-code', 'Code with any agent', 'Claude Code, Codex, OpenCode or your own model on one task, with hand-offs.', 'https://openvibe.codes/', 'code coding agent agents ai claude codex opencode deepseek harness contribute'],
['actor', 'fa-user-astronaut', 'Give a task to an agent', 'Actor picks the best agent for it, works on a desk you can watch and take over, and shows the cost and why.', 'https://openvibe.actor/', 'agent agents ai assistant task tasks automate automation router openrouter delegate'],
-- ['bot', 'fa-computer', 'Put your computer to work', 'Desks for your agents, a spare laptop as a desk host, and the AI models you run (Ollama, LM Studio) for your own work.', 'https://openvibe.bot/machines', 'computer computers pc laptop desktop desk desks machine machines ollama lmstudio llama gpu homelab node'],
++ ['bot', 'fa-desktop', 'Put your computer to work', 'Desks for your agents, a spare laptop as a desk host, and the AI models you run (Ollama, LM Studio) for your own work.', 'https://openvibe.bot/machines', 'computer computers pc laptop desktop desk desks machine machines ollama lmstudio llama gpu homelab node'],
['work', 'fa-briefcase', 'Find work', 'Jobs from open boards in one search, each linked to where you apply.', 'https://openvibe.work/', 'job jobs work career hiring remote employment apply listings'],
['food', 'fa-utensils', 'Eat well for less', 'Food near you, meal plans and recipes for your budget, and delivery apps compared.', 'https://openvibe.food/', 'food eat meal meals grocery groceries recipe recipes foodbank pantry delivery doordash ubereats grubhub instacart hellofresh'],
-- ['quest', 'fa-medal', 'Earn badges', 'Quests across the network for the things you already do there.', 'https://openvibe.quest/', 'quest quests badge badges achievement achievements rewards opencoins earn'],
++ ['quest', 'fa-ranking-star', 'Earn badges', 'Quests across the network for the things you already do there.', 'https://openvibe.quest/', 'quest quests badge badges achievement achievements rewards opencoins earn'],
['rent', 'fa-house', 'Rent a place', 'Every rental site in one search, and one board for what you find there.', 'https://openvibe.rent/', 'rent rental rentals apartment apartments room house flat lease parking storage equipment craigslist zillow zumper'],
['homes', 'fa-house-chimney', 'Buy a home', 'Every for-sale site in one search, with county records and a mortgage calculator.', 'https://openvibe.homes/', 'home homes house houses sale realestate realtor redfin zillow mortgage property records land'],
['video', 'fa-film', 'Watch and upload videos', 'Upload or record a video and get AI captions, chapters and cuts, plus OpenVibe.Live\'s past streams.', 'https://openvibe.video/', 'video videos upload record vod vods recording recordings replay replays clip clips past stream streams highlights vlog captions'],
ddiff --git a/server/index.js b/server/index.js
iindex bc500470..1114f197 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -100,9 +100,9 @@ app.use(helmet({
defaultSrc: ["'self'"],
// Cloudflare Web Analytics: Cloudflare injects its beacon at the edge and the privacy text says it may
// measure performance; script-src loads the beacon, connect-src is where it reports.
-- scriptSrc: ["'self'", "'unsafe-inline'", "https://openvibe.network", "https://openvibe.live", "cdnjs.cloudflare.com", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://static.cloudflareinsights.com"],
-- styleSrc: ["'self'", "'unsafe-inline'", "cdnjs.cloudflare.com", "fonts.googleapis.com", "fonts.gstatic.com"],
-- fontSrc: ["'self'", "fonts.gstatic.com", "cdnjs.cloudflare.com"],
++ scriptSrc: ["'self'", "'unsafe-inline'", "https://openvibe.network", "https://openvibe.live", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://static.cloudflareinsights.com"],
++ styleSrc: ["'self'", "'unsafe-inline'", "fonts.googleapis.com", "fonts.gstatic.com"],
++ fontSrc: ["'self'", "fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:", "https://openvibe.media", "https://openvibe.live"], // avatars and media live on openvibe.media; live-stream thumbnails on the home page come from openvibe.live
connectSrc: [
"'self'",
ddiff --git a/test/look-boot.test.js b/test/look-boot.test.js
iindex 28cb1466..55e528ef 100644
---- a/test/look-boot.test.js
++++ b/test/look-boot.test.js
@@@ -30,7 +30,8 @@ const PAGES = [
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"/);
++ // A linked sheet or an inline <style>: /verify-email has only its own (its one link was Font Awesome's, L54 K4).
++ const firstSheet = html.search(/<link[^>]+rel="stylesheet"|<style\b/);
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);
ddiff --git a/test/no-font-awesome.test.js b/test/no-font-awesome.test.js
nnew file mode 100644
iindex 00000000..b92f4629
---- /dev/null
++++ b/test/no-font-awesome.test.js
@@@ -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.Network'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.Network 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 = ["public"];
++const CODE_ROOTS = ["public","server"];
++const LAYOUTS = []; // 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`);
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex 413edfd0..4c62806c 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -17,8 +17,8 @@ const BUDGETS = {
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: 337, // 334.1 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 333.2 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 330.1 with openvibe-shared 4.52.0 (from 4.47.2): the seasonal fall, rich embeds, the Dock and its Chats tab (2026-10-30); 323.2 with openvibe-shared 4.47.2 (the comfort settings in theme-loader 4.45, touch targets 4.46, the grouped updates log 4.47.2); 313.7 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 306.4 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 298.9 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 293.1 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 279.1 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 269.3 with shared 4.11.0's seasonal navbar (Pumpkin Night and the season badge, +2.6); 249.8 with boost.js (13.1); 228.3
jsBrotliKB: 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
-- cssFiles: 2, // 1 (Font Awesome, cross-origin)
-- externalFiles: 3, // 1 locally; production adds Cloudflare's beacon
++ cssFiles: 2, // 1 (the site's own; Font Awesome's stylesheet is gone since 2026-11-01)
++ externalFiles: 3, // none locally (Font Awesome was the one); production adds Cloudflare's beacon
};
const freePort = () => new Promise((resolve) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const { port } = s.address(); s.close(() => resolve(port)); }); });