openvibers/openvibe.network Commits
Font Awesome out, the OpenVibe glyphs in (plan L54 K4) (#484)
Changes
@@@ -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">@@@ -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 putsopenvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one alink 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}@@@ -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 & 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)}@@@ -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' },@@@ -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 ≥ ' + (ts.minAgeDays != null ? ts.minAgeDays : '?') + 'd</span><span>| Views ≤ ' + (ts.maxViewsForCold != null ? ts.maxViewsForCold : '?') + '</span><span>| Idle ≥ ' + (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">@@@ -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' },];@@@ -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>@@@ -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));@@@ -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';@@@ -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 = [@@@ -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>@@@ -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 putsopenvibe-shared's look-boot here: it paints the look this browser last painted here (ov_paint), or the one alink 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 & 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/Offbuttons. 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>@@@ -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">@@@ -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}@@@ -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'],@@@ -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.liveconnectSrc: ["'self'",@@@ -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);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.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`);@@@ -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.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-- 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)); }); });
@@ -26,7 +26,6 @@
@@ -26,7 +26,6 @@
} catch (_) { /* nothing kept: theme-loader.js paints shortly */ }
} 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="/css/admin.css">
<link rel="stylesheet" href="/css/admin.css">
</head>
</head>
<body>
<body>
@@ -69,7 +68,7 @@
@@ -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('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" 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 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('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 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>
<button class="admin-tab-btn" onclick="showTab('ssh',this)"><i class="fa-solid fa-terminal"></i> SSH</button>
@@ -374,11 +373,11 @@
@@ -374,11 +373,11 @@
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px">
<div class="card">
<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>
<canvas id="chart-service-share" height="260"></canvas>
</div>
</div>
<div class="card">
<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>
<canvas id="chart-service-visitors" height="260"></canvas>
</div>
</div>
</div>
</div>
@@ -469,7 +468,7 @@
@@ -469,7 +468,7 @@
<div id="svc-top-referers"></div>
<div id="svc-top-referers"></div>
</div>
</div>
<div class="card">
<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 id="svc-slowest"></div>
</div>
</div>
</div>
</div>
@@ -582,7 +581,7 @@
@@ -582,7 +581,7 @@
<!-- Deploy: Config -->
<!-- Deploy: Config -->
<div id="deploy-config-panel" style="display:none">
<div id="deploy-config-panel" style="display:none">
<div class="card">
<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">
<div class="form-group" style="margin:12px 0">
<label>ACME / Let's Encrypt Email</label>
<label>ACME / Let's Encrypt Email</label>
<input type="email" id="deploy-acme-email" placeholder="[email protected]" style="width:350px">
<input type="email" id="deploy-acme-email" placeholder="[email protected]" style="width:350px">
@@ -27,7 +27,6 @@
@@ -27,7 +27,6 @@
<meta property="og:site_name" content="OpenVibe">
<meta property="og:site_name" content="OpenVibe">
<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">
<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts
<!-- 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
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).
link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).
@@ -45,7 +44,6 @@
@@ -45,7 +44,6 @@
} catch (_) { /* nothing kept: theme-loader.js paints shortly */ }
} 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="/shared/showcase.css">
<link rel="stylesheet" href="/shared/showcase.css">
<style>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
@@ -289,7 +289,7 @@ async function loadStreamerMediaTools() {
@@ -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>
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">
<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 & Test</button>
<button type="button" class="btn btn-primary" onclick="saveStreamerMediaCookies()"><i class="fa-solid fa-floppy-disk"></i> Save & 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>
<button type="button" class="btn" onclick="deleteStreamerMediaCookies()" ${status.cookies_configured ? '' : 'disabled'}><i class="fa-solid fa-trash"></i> Remove Cookies</button>
</div>
</div>
<div id="streamer-media-cookie-verdict"></div>
<div id="streamer-media-cookie-verdict"></div>
@@ -485,7 +485,7 @@ async function loadTTS() {
@@ -485,7 +485,7 @@ async function loadTTS() {
</div>
</div>
<div class="card">
<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}
${ownerNote}
${secret('tts-aws-key', 'AWS Access Key ID', s.awsAccessKeyId)}
${secret('tts-aws-key', 'AWS Access Key ID', s.awsAccessKeyId)}
${secret('tts-aws-secret', 'AWS Secret Access Key', s.awsSecretAccessKey)}
${secret('tts-aws-secret', 'AWS Secret Access Key', s.awsSecretAccessKey)}
@@ -164,7 +164,7 @@ async function loadAnalyticsOverview() {
@@ -164,7 +164,7 @@ async function loadAnalyticsOverview() {
{ icon: 'fa-eye', label: 'Page Views', value: fmtNum(t.total_pageviews) },
{ 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-code', label: 'API Calls', value: fmtNum(t.total_api_calls) },
{ icon: 'fa-users', label: 'Unique Visitors', value: fmtNum(t.total_unique_visitors) },
{ 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-robot', label: 'Bot Hits', value: fmtNum(t.total_bot_hits) },
{ icon: 'fa-circle-exclamation', label: 'Errors', value: fmtNum(t.total_errors) },
{ 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' },
{ icon: 'fa-gauge-high', label: 'Avg Response', value: (t.avg_response_ms || 0) + 'ms' },
@@ -393,7 +393,7 @@ async function loadServiceAnalytics(serviceName) {
@@ -393,7 +393,7 @@ async function loadServiceAnalytics(serviceName) {
{ icon: 'fa-code', label: 'API Calls', value: fmtNum(s.total_api_calls) },
{ 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-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-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-robot', label: 'Bot Hits', value: fmtNum(s.total_bot_hits) },
{ icon: 'fa-circle-exclamation', label: 'Errors', value: fmtNum(s.total_errors) },
{ 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' },
{ icon: 'fa-gauge-high', label: 'Avg Response', value: (s.avg_response_ms || 0) + 'ms' },
@@ -183,7 +183,7 @@ function buildStreamerTierHtml(tierData) {
@@ -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>'
+ '<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>'
+ '</tbody></table></div>'
+ '<div style="display:flex;flex-wrap:wrap;gap:8px;font-size:12px;color:var(--text-muted);margin-top:12px">'
+ '<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 ≥ ' + (ts.minAgeDays != null ? ts.minAgeDays : '?') + 'd</span><span>| Views ≤ ' + (ts.maxViewsForCold != null ? ts.maxViewsForCold : '?') + '</span><span>| Idle ≥ ' + (ts.minLastAccessDays != null ? ts.minLastAccessDays : '?') + 'd</span>'
+ '<span>| Age ≥ ' + (ts.minAgeDays != null ? ts.minAgeDays : '?') + 'd</span><span>| Views ≤ ' + (ts.maxViewsForCold != null ? ts.maxViewsForCold : '?') + '</span><span>| Idle ≥ ' + (ts.minLastAccessDays != null ? ts.minLastAccessDays : '?') + 'd</span>'
+ '</div></div>';
+ '</div></div>';
}
}
@@ -224,7 +224,7 @@ async function loadStreamerCloudStorage(force) {
@@ -224,7 +224,7 @@ async function loadStreamerCloudStorage(force) {
};
};
el.innerHTML = header
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 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) {
} catch (e) {
el.innerHTML = header + '<div style="color:var(--danger-text, var(--danger,#ef4444))">Failed to scan buckets: ' + esc(e.message) + '</div>';
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() {
@@ -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.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.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.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>
<div class="card" style="padding:16px;margin-bottom:16px">
<div class="card" style="padding:16px;margin-bottom:16px">
@@ -27,7 +27,7 @@ async function loadDashboard() {
@@ -27,7 +27,7 @@ async function loadDashboard() {
{ label: 'Total Streams', value: ss?.streams?.total || ss?.totalStreams || 0, icon: 'fa-video' },
{ 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: 'Active Bans', value: ss?.users?.banned || ss?.activeBans || 0, icon: 'fa-ban' },
{ label: 'Notifications', value: th?.total_notifications || 0, icon: 'fa-bell' },
{ 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' },
{ label: 'Anon Users', value: th?.anon_users || 0, icon: 'fa-ghost' },
];
];
@@ -413,7 +413,7 @@ async function showManualDnsInfo() {
@@ -413,7 +413,7 @@ async function showManualDnsInfo() {
<div style="font-size:13px;line-height:1.6">${instructions}</div>
<div style="font-size:13px;line-height:1.6">${instructions}</div>
<div style="margin-top:16px">
<div style="margin-top:16px">
<button class="btn btn-primary" onclick="issueManualCert('${esc(domain)}')">
<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>
</button>
<div class="muted" style="font-size:11px;margin-top:4px">Only click after creating the required DNS TXT records</div>
<div class="muted" style="font-size:11px;margin-top:4px">Only click after creating the required DNS TXT records</div>
</div>
</div>
@@ -168,7 +168,7 @@ async function loadMachines() {
@@ -168,7 +168,7 @@ async function loadMachines() {
const machines = (out && out.machines) || [];
const machines = (out && out.machines) || [];
machines.sort((a, b) => (b.online === true) - (a.online === true));
machines.sort((a, b) => (b.online === true) - (a.online === true));
if (!machines.length) {
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;
return;
}
}
list.replaceChildren(...machines.map(mcCard));
list.replaceChildren(...machines.map(mcCard));
@@ -349,7 +349,7 @@ async function loadApps() {
@@ -349,7 +349,7 @@ async function loadApps() {
for (const app of apps) {
for (const app of apps) {
const row = document.createElement('div'); row.className = 'linked-item';
const row = document.createElement('div'); row.className = 'linked-item';
const icon = document.createElement('span'); icon.className = 'svc-icon';
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 main = document.createElement('div');
const name = document.createElement('div'); name.className = 'svc-name'; name.textContent = app.name || app.client_id;
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';
const meta = document.createElement('div'); meta.className = 'svc-user';
@@ -16,8 +16,8 @@ const TD_SITES = {
@@ -16,8 +16,8 @@ const TD_SITES = {
reviews: ['Reviews', 'fa-star', 'https://openvibe.reviews'], tips: ['Tips', 'fa-hand-holding-heart', 'https://openvibe.tips'],
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'],
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'],
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.
// What to try next, most used across the network first, each with the reason it is worth a look.
const TD_TRY = [
const TD_TRY = [
@@ -11,8 +11,9 @@
@@ -11,8 +11,9 @@
<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="description" content="Sign in to OpenVibe — one account across OpenVibe.Live, OpenVibe.Tools, OpenVibe.Games, and OpenVibe.Media.">
<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">
<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>
</head>
<body>
<body>
@@ -54,7 +55,7 @@
@@ -54,7 +55,7 @@
<i class="fa-solid fa-wand-magic-sparkles"></i> Create a new account
<i class="fa-solid fa-wand-magic-sparkles"></i> Create a new account
</button>
</button>
<button class="action-btn" onclick="showForgotForm()">
<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>
</button>
</div>
</div>
</div>
</div>
@@ -21,7 +21,6 @@
@@ -21,7 +21,6 @@
<link rel="canonical" href="https://openvibe.network/my">
<link rel="canonical" href="https://openvibe.network/my">
<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">
<!-- The look before first paint (ov-theme-boot): MUST stay ABOVE the stylesheets. server/look-boot.js puts
<!-- 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
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).
link from another OpenVibe page carried (?ov_look=), with the display settings (the season switch among them).
@@ -39,7 +38,6 @@
@@ -39,7 +38,6 @@
} catch (_) { /* nothing kept: theme-loader.js paints shortly */ }
} 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="/css/my.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. -->
<!-- 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">
<link rel="stylesheet" href="/shared/kit.css">
@@ -111,7 +109,7 @@
@@ -111,7 +109,7 @@
<button onclick="showSection('data',this)"><i class="fa-solid fa-database"></i> AI & Data</button>
<button onclick="showSection('data',this)"><i class="fa-solid fa-database"></i> AI & Data</button>
<button onclick="showSection('approvals',this)"><i class="fa-solid fa-user-check"></i> Approvals</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('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('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>
<button onclick="showSection('anon',this)"><i class="fa-solid fa-user-secret"></i> Anon Info</button>
</div>
</div>
@@ -119,7 +117,7 @@
@@ -119,7 +117,7 @@
<!-- Profile Section -->
<!-- Profile Section -->
<div class="section active" id="sec-profile">
<div class="section active" id="sec-profile">
<div class="card" id="avatar-card">
<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>
<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">
<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)">
<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 @@
@@ -206,8 +204,8 @@
<h3 style="margin:0"><span class="icon"><i class="fa-solid fa-share-nodes"></i></span> Share a theme</h3>
<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>
<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">
<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>
</div>
<form class="theme-submit" onsubmit="submitMyTheme(event)">
<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>
<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 @@
@@ -233,18 +231,18 @@
</div>
</div>
<!-- Account merge (ADR-029): fold a second account this person owns into this one. -->
<!-- Account merge (ADR-029): fold a second account this person owns into this one. -->
<div class="card" id="merge-card" style="margin-top:16px">
<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>
<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 id="merge-body" style="margin-top:12px"></div>
</div>
</div>
<!-- Your data (ADR-033): a copy of everything, and deleting the account after 30 days. -->
<!-- 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">
<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>
<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 id="export-body" style="margin-top:12px"></div>
</div>
</div>
<div class="card" id="delete-card" style="margin-top:16px">
<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>
<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 id="delete-body" style="margin-top:12px"></div>
</div>
</div>
@@ -253,7 +251,7 @@
@@ -253,7 +251,7 @@
<!-- Notifications Section -->
<!-- Notifications Section -->
<div class="section" id="sec-notifications">
<div class="section" id="sec-notifications">
<div class="card" id="notif-verify-card" style="display:none">
<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 id="notif-verify-body" style="font-size:13px;line-height:1.5"></div>
</div>
</div>
<div class="card">
<div class="card">
@@ -312,7 +310,7 @@
@@ -312,7 +310,7 @@
per purpose, the wording, the kinds, the 18+ confirmation inside the row, the history, and equal On/Off
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. -->
buttons. Nothing is pre-ticked; the rows are drawn from GET /api/v1/me/consent. -->
<div class="card" id="consent-card">
<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>
<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>
<div id="consent-choices"><div class="loading-spinner">Loading your choices…</div></div>
<p class="muted consent-status" id="consent-status" role="status"></p>
<p class="muted consent-status" id="consent-status" role="status"></p>
@@ -367,7 +365,7 @@
@@ -367,7 +365,7 @@
<div class="card">
<div class="card">
<div class="mc-top">
<div class="mc-top">
<div>
<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>
<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>
</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>
<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 @@
@@ -409,7 +407,7 @@
</div>
</div>
</div>
</div>
<div class="card">
<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>
<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 id="apps-list"><div class="loading-spinner">Loading apps...</div></div>
</div>
</div>
@@ -27,7 +27,6 @@
@@ -27,7 +27,6 @@
} catch (_) { /* nothing kept: theme-loader.js paints shortly */ }
} 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">
<style>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
*,*::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}
: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}
@@ -59,6 +58,8 @@ li.now .dot i{animation:breathe 1.2s ease-in-out infinite}
.ov-mark{width:38px;height:38px;flex:none}
.ov-mark{width:38px;height:38px;flex:none}
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
</style>
</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>
</head>
<body>
<body>
<section class="card" aria-live="polite">
<section class="card" aria-live="polite">
@@ -29,7 +29,6 @@
@@ -29,7 +29,6 @@
})();
})();
</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">
<style>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
*,*::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}
: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}
@@ -59,10 +59,10 @@ const INTENTS = [
@@ -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'],
['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'],
['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'],
['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'],
['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'],
['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'],
['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'],
['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'],
['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'],
@@ -100,9 +100,9 @@ app.use(helmet({
@@ -100,9 +100,9 @@ app.use(helmet({
defaultSrc: ["'self'"],
defaultSrc: ["'self'"],
// Cloudflare Web Analytics: Cloudflare injects its beacon at the edge and the privacy text says it may
// 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.
// 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
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: [
connectSrc: [
"'self'",
"'self'",
@@ -30,7 +30,8 @@ const PAGES = [
@@ -30,7 +30,8 @@ const PAGES = [
assert.ok(!html.includes(MARK), `${path}: the marker never reaches the page`);
assert.ok(!html.includes(MARK), `${path}: the marker never reaches the page`);
assert.strictEqual(html.split(BOOT).length, 2, `${path}: look-boot once`);
assert.strictEqual(html.split(BOOT).length, 2, `${path}: look-boot once`);
const at = html.indexOf(BOOT);
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`);
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.
// The one-time fallback for a browser from before look-boot: its theme only, colours only.
const legacy = html.slice(at, firstSheet);
const legacy = html.slice(at, firstSheet);
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.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`);
@@ -17,8 +17,8 @@ const BUDGETS = {
@@ -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)
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
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
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)); }); });
const freePort = () => new Promise((resolve) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const { port } = s.address(); s.close(() => resolve(port)); }); });