openvibers/openvibe.live Commits
Live no longer carries the old game: OpenVibe.Games is being rewritten (its ADR-0007) and will never serve /game, /canvas or /api/game again, so the /game and /canvas redirects, the /api/game 410, the /ws/game and /ws/canvas upgrade branch, the SPA routes and their footer/nav/history special cases are gone (the paths answer 404, with no redirect; game and canvas stay reserved usernames). The home page's Games widgets (leaderboards and the canvas preview, switched off behind GAMES_HOME_WIDGETS because Games never served them) are deleted with their markup and CSS, and the network hub's Canvas card with them; the hero Game button opens openvibe.games. Leaderboards return when OpenVibe.Play serves them. Reviewed independently (VERDICT: SHIP). npm test 151/152, 1 skipped.
Changes
@@@ -406,51 +406,6 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }.home-pastes-list { grid-template-columns: 1fr; }.home-paste-media { height: 136px; }}--.home-leaderboards {-- display: grid;-- grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr));-- gap: 14px; margin-bottom: 32px;--}--.home-lb-card {-- background: var(--bg-card); border: 1px solid var(--border);-- border-radius: var(--radius-lg); padding: 16px; overflow: hidden;--}--.home-lb-title {-- font-weight: 650; font-size: 0.88rem; margin-bottom: 10px;-- color: var(--text-secondary);-- display: flex; align-items: center; gap: 6px;--}--.home-lb-entries { display: flex; flex-direction: column; gap: 6px; }--.home-lb-row {-- display: flex; align-items: center; gap: 8px;-- font-size: 0.82rem; padding: 4px 0;-- border-bottom: 1px solid var(--border);--}--.home-lb-row:last-child { border-bottom: none; }--.home-lb-rank {-- width: 20px; text-align: center;-- font-weight: 700; color: var(--text-muted); font-size: 0.75rem;--}--.home-lb-score {-- font-weight: 600; color: var(--accent); font-size: 0.78rem;-- white-space: nowrap;--}--.home-canvas-preview { margin-bottom: 32px; }--.home-canvas-wrap {-- display: flex; flex-direction: column; align-items: center;-- background: var(--bg-card); border: 1px solid var(--border);-- border-radius: var(--radius-lg); padding: 24px; text-align: center;--}--.home-canvas-wrap canvas {-- max-height: 300px; width: auto;--}--.home-canvas-stats {-- display: flex; gap: 24px; margin-top: 16px; flex-wrap: wrap;-- justify-content: center;--}--.home-canvas-stat {-- display: flex; flex-direction: column; align-items: center;--}.home-changelog-wrapper.collapsed .home-changelog-fade {display: block;}@@@ -2511,29 +2511,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }.founder-story-pullquote blockquote { font-size: 0.94rem; }}--/* ═══════════════════════════════════════════════════════════════-- HOME — OPENVIBEGAMES LEADERBOARDS-- ═══════════════════════════════════════════════════════════════ */--.home-lb-title i { color: var(--accent); font-size: 0.85rem; }--.home-lb-row:nth-child(1) .home-lb-rank { color: #ffd700; }--.home-lb-row:nth-child(2) .home-lb-rank { color: #c0c0c0; }--.home-lb-row:nth-child(3) .home-lb-rank { color: #cd7f32; }--.home-lb-name {-- flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;-- color: var(--text-primary);--}----/* ═══════════════════════════════════════════════════════════════-- HOME — CANVAS PREVIEW-- ═══════════════════════════════════════════════════════════════ */--.home-canvas-stat strong {-- font-size: 1.3rem; color: var(--accent); line-height: 1.2;--}--.home-canvas-stat span {-- font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase;-- letter-spacing: 0.5px;--}--/* ═══════════════════════════════════════════════════════════════STREAM VIEWER LAYOUT═══════════════════════════════════════════════════════════════ */@@@ -194,7 +194,7 @@<div class="hero-cta" data-fx-viewport><button style="--i:0" class="hb hb-stream" onclick="currentUser ? navigate('/broadcast') : showModal('register')"><span class="hb-ico"><i class="fa-solid fa-broadcast-tower"></i></span><span class="hb-label">Stream</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></button><a href="https://openvibe.tools" style="--i:1" class="hb hb-tools"><span class="hb-ico"><i class="fa-solid fa-screwdriver-wrench"></i></span><span class="hb-label">Tools</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a>-- <a href="https://openvibe.games/game" style="--i:2" class="hb hb-game"><span class="hb-ico"><i class="fa-solid fa-gamepad"></i></span><span class="hb-label">Game</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a>++ <a href="https://openvibe.games" style="--i:2" class="hb hb-game"><span class="hb-ico"><i class="fa-solid fa-gamepad"></i></span><span class="hb-label">Game</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a><a href="https://github.com/OpenVibers/OpenVibe.Live" target="_blank" style="--i:3" class="hb hb-github" rel="noopener"><span class="hb-ico"><i class="fa-brands fa-github"></i></span><span class="hb-label">GitHub</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a><a href="https://discord.gg/M6MuRUaeJj" target="_blank" style="--i:4" class="hb hb-discord" rel="noopener noreferrer"><span class="hb-ico"><i class="fa-brands fa-discord"></i></span><span class="hb-label">Discord</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a><a href="/documentation" onclick="return handleLinkClick(event, '/documentation')" style="--i:5" class="hb hb-vibe"@@@ -505,20 +505,6 @@</div><div class="container">-- <!-- Scraplandia Leaderboards -->-- <div class="section-header" style="--sc:#fbbf24;display:none" id="home-quest-header">-- <h2><i class="fa-solid fa-trophy"></i> Scraplandia Leaderboards<small class="section-kicker">the ladder</small></h2>-- <a href="https://openvibe.games/game" class="section-action">Play game</a>-- </div>-- <div class="home-leaderboards" id="home-leaderboards"></div>---- <!-- Canvas Preview -->-- <div class="section-header" style="--sc:#4ade80;display:none" id="home-canvas-header">-- <h2><i class="fa-solid fa-palette"></i> Community Canvas<small class="section-kicker">drawn together</small></h2>-- <a href="https://openvibe.games/canvas" class="section-action">Open canvas</a>-- </div>-- <div class="home-canvas-preview" id="home-canvas-preview"></div>--<div class="network-hub"><div class="network-hub-header"><i class="fa-solid fa-network-wired"></i>@@@ -560,14 +546,6 @@</div><i class="fa-solid fa-arrow-right network-card-arrow"></i></a>-- <a href="https://openvibe.games/canvas" class="network-card">-- <div class="network-card-icon" style="--card-accent:#3498db"><i class="fa-solid fa-palette"></i></div>-- <div class="network-card-info">-- <strong>Canvas</strong>-- <span>Collaborative pixel art</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a><a href="https://openvibe.media" class="network-card"><div class="network-card-icon" style="--card-accent:#f59e0b"><i class="fa-solid fa-photo-film"></i></div><div class="network-card-info">@@@ -730,8 +708,6 @@<!-- (page-themes removed: /themes redirects to openvibe.network in routeFromURL, so thissection was unreachable markup with five dead inline handlers.) -->--<!-- Game & Canvas migrated to openvibe.games -->--<!-- ═══════════════════════════════════════════════════════════ --><!-- UPDATES / CHANGELOG --><!-- ═══════════════════════════════════════════════════════════ -->@@@ -1248,10 +1248,6 @@ async function loadHome() {loadHomeClips();// Load recent pastesloadHomePastes();-- // Load Scraplandia leaderboards-- loadHomeLeaderboards();-- // Load Canvas preview-- loadHomeCanvas();startHomeRefresh(); // live grid + sections auto-update in real time}@@@ -1555,129 +1551,6 @@ async function loadHomePastes(opts) {} catch { rail.filling = false; }}--// OpenVibe.Games does not serve /api/game/leaderboard/* or /api/game/canvas/state (the pixel canvas is--// owner decision O15), so every first visit logged CORS errors for sections that stay hidden. Turn this--// on when Games serves them (public, CORS for first-party origins) — roadmap WS-M task 7.--const GAMES_HOME_WIDGETS = false;----async function loadHomeLeaderboards() {-- if (!GAMES_HOME_WIDGETS) { const h = document.getElementById('home-quest-header'); if (h) h.style.display = 'none'; return; }-- try {-- const boards = ['total_level', 'combat', 'mining', 'fishing'];-- const questUrl = getScraplandiaUrl();-- // One probe first: if the service is not answering, the other three would fail the same way.-- const first = await fetchServiceJson(`${questUrl}/api/game/leaderboard/${boards[0]}`);-- const rest = first ? await Promise.all(boards.slice(1).map(b => fetchServiceJson(`${questUrl}/api/game/leaderboard/${b}`))) : [];-- const results = [first, ...rest].map(r => r || { entries: [] });-- while (results.length < boards.length) results.push({ entries: [] });-- const header = document.getElementById('home-quest-header');-- const container = document.getElementById('home-leaderboards');-- const hasData = results.some(r => r.entries && r.entries.length);-- if (!hasData) { if (header) header.style.display = 'none'; return; }-- if (header) header.style.display = '';---- const labels = { total_level: 'Total Level', combat: 'Combat', mining: 'Mining', fishing: 'Fishing' };-- const icons = { total_level: 'fa-star', combat: 'fa-sword', mining: 'fa-gem', fishing: 'fa-fish' };-- container.innerHTML = boards.map((board, i) => {-- const entries = (results[i].entries || []).slice(0, 5);-- if (!entries.length) return '';-- return `-- <div class="home-lb-card">-- <div class="home-lb-title"><i class="fa-solid ${icons[board] || 'fa-trophy'}"></i> ${labels[board]}</div>-- <div class="home-lb-entries">-- ${entries.map((e, rank) => `-- <div class="home-lb-row">-- <span class="home-lb-rank">${rank + 1}</span>-- <span class="home-lb-name">${esc(e.display_name || e.username || 'Unknown')}</span>-- <span class="home-lb-score">${typeof e.score === 'number' ? e.score.toLocaleString() : e.score}</span>-- </div>-- `).join('')}-- </div>-- </div>`;-- }).join('');-- } catch { /* silent */ }--}----/**-- * Fetch JSON from another OpenVibe service, remembering when it is not there.-- *-- * The home page asks OpenVibe.Games for leaderboards and canvas state. Those endpoints currently-- * answer with the Games SPA's HTML and no CORS headers, so from openvibe.live all five requests-- * fail — on every single home page view. Failing is fine; failing five times per visit forever is-- * not. A miss is remembered for a few hours per browser, so the page stops asking a service that-- * is not answering and starts again on its own once the window lapses. A response that is not-- * JSON counts as a miss instead of throwing inside .json().-- */--const _XSVC_MISS_KEY = 'ov_xsvc_miss_v1';--const _XSVC_MISS_MS = 6 * 60 * 60 * 1000;--function _xsvcMisses() { try { return JSON.parse(localStorage.getItem(_XSVC_MISS_KEY) || '{}'); } catch { return {}; } }--async function fetchServiceJson(url) {-- // Keyed per URL, not per service prefix: one board that 404s must not switch off every other-- // endpoint on the same service for six hours.-- const misses = _xsvcMisses();-- if (misses[url] && Date.now() - misses[url] < _XSVC_MISS_MS) return null;-- const remember = () => { try { misses[url] = Date.now(); localStorage.setItem(_XSVC_MISS_KEY, JSON.stringify(misses)); } catch { /* */ } };-- let r;-- try { r = await fetch(url, { credentials: 'omit' }); }-- catch { remember(); return null; } // network / CORS: the service is not answering-- const type = r.headers.get('content-type') || '';-- if (!/json/i.test(type)) { remember(); return null; } // an HTML page where an API should be-- // A JSON error means the service is there and answered — do not remember it as absent.-- if (!r.ok) return null;-- try { return await r.json(); } catch { return null; }--}----async function loadHomeCanvas() {-- if (!GAMES_HOME_WIDGETS) { const h = document.getElementById('home-canvas-header'); if (h) h.style.display = 'none'; return; }-- try {-- const header = document.getElementById('home-canvas-header');-- const container = document.getElementById('home-canvas-preview');-- const data = await fetchServiceJson(`${getScraplandiaUrl()}/api/game/canvas/state`);-- if (!data || !data.board) { if (header) header.style.display = 'none'; return; }-- if (header) header.style.display = '';---- const tiles = data.tiles || [];-- const recentActions = data.recent_actions || [];-- const uniqueArtists = new Set(tiles.map(t => t.user_id).filter(Boolean)).size;-- const width = data.board.width || 64;-- const height = data.board.height || 64;-- const palette = data.board.palette || ['#000000'];---- // Render a mini canvas preview-- const scale = 4;-- container.innerHTML = `-- <div class="home-canvas-wrap">-- <canvas id="home-canvas-mini" width="${width * scale}" height="${height * scale}" style="image-rendering:pixelated;border-radius:var(--radius);border:1px solid var(--border);max-width:100%;"></canvas>-- <div class="home-canvas-stats">-- <div class="home-canvas-stat"><strong>${tiles.length.toLocaleString()}</strong> <span>pixels placed</span></div>-- <div class="home-canvas-stat"><strong>${uniqueArtists.toLocaleString()}</strong> <span>artists</span></div>-- <div class="home-canvas-stat"><strong>${width}×${height}</strong> <span>board size</span></div>-- <div class="home-canvas-stat"><strong>${recentActions.length}</strong> <span>recent actions</span></div>-- </div>-- <a href="${getScraplandiaUrl()}/canvas" class="btn btn-outline" style="margin-top:12px;">-- <i class="fa-solid fa-palette"></i> Open Canvas-- </a>-- </div>-- `;---- // Draw tiles on the mini canvas-- const canvas = document.getElementById('home-canvas-mini');-- if (canvas) {-- const ctx = canvas.getContext('2d');-- ctx.fillStyle = palette[0] || '#000';-- ctx.fillRect(0, 0, width * scale, height * scale);-- for (const tile of tiles) {-- const color = palette[tile.color_index] || '#fff';-- ctx.fillStyle = color;-- ctx.fillRect(tile.x * scale, tile.y * scale, scale, scale);-- }-- }-- } catch {-- const header = document.getElementById('home-canvas-header');-- if (header) header.style.display = 'none';-- }--}--// Markup for a single stream card (live or recent). Extracted so the home page// can reconcile the live grid in place for real-time updates.function streamCardHTML(s, isLive) {@@@ -1928,7 +1801,6 @@ function refreshHomeSections() {if (untouched('vods') && typeof loadHomeRecentVods === 'function') loadHomeRecentVods();if (untouched('clips') && typeof loadHomeClips === 'function') loadHomeClips();if (untouched('pastes') && typeof loadHomePastes === 'function') loadHomePastes();-- if (typeof loadHomeLeaderboards === 'function') loadHomeLeaderboards();}// The live grid follows OpenVibe.Events realtime (public/js/ov-live-realtime.js): a stream that@@@ -17,7 +17,6 @@ function getDefaultOpenVibeNetworkUrls() {const isTopenvibeAlias = ['topenvibe.tools', 'topenvibe.live', 'topenvibe.quest'].includes(host);return {tools: isLocalHost ? 'http://localhost:3100' : (isTopenvibeAlias ? 'https://topenvibe.tools' : 'https://openvibe.network'),-- quest: isLocalHost ? 'http://localhost:3200' : (isTopenvibeAlias ? 'https://topenvibe.quest' : 'https://openvibe.games'),};}@@@ -29,10 +28,6 @@ function getOpenVibeNetworkUrl(service) {function getOpenVibeToolsUrl() {return getOpenVibeNetworkUrl('tools');}----function getScraplandiaUrl() {-- return getOpenVibeNetworkUrl('quest');--}/** Cached external viewer count (Kick/Twitch/RS) — updated by cumulative viewer poll */let _cachedExternalViewerCount = 0;@@@ -587,7 +582,7 @@ function _ssoHint() {// What the signed-in account watches/opens here shows up in its network-wide History and the// "Recently used" rows of the shared navbar on every other site. Recorded after the route// rendered and its title settled; never for guests, and only for the pages worth remembering.--const _HISTORY_PAGES = { channel: 'stream', 'vod-player': 'vod', 'clip-player': 'clip', 'paste-viewer': 'paste', pastes: null, game: 'game', arena: 'page' };++const _HISTORY_PAGES = { channel: 'stream', 'vod-player': 'vod', 'clip-player': 'clip', 'paste-viewer': 'paste', pastes: null, arena: 'page' };let _historyTimer = null;function _recordHistory(pageId) {const type = _HISTORY_PAGES[pageId];@@@ -749,9 +744,6 @@ function onAuthChange() {if (window.LiveNav) window.LiveNav.sync(currentUser || null);if (currentUser) loadBalance();-- // Sync canvas auth state if canvas page is loaded-- if (typeof syncCanvasAuthState === 'function') syncCanvasAuthState();--try {window.dispatchEvent(new CustomEvent('openvibe-auth-changed', {detail: {@@@ -1108,12 +1100,6 @@ function routeFromURL() {} else if (segments[0] === 'chat') {showPage('chat');whenRouteReady('chat', () => loadChatPage());-- } else if (segments[0] === 'game') {-- window.location.href = `${getScraplandiaUrl()}/game`;-- return;-- } else if (segments[0] === 'canvas') {-- window.location.href = `${getScraplandiaUrl()}/canvas`;-- return;} else if (segments[0] === 'pastes') {// The Content feed's Pastes filter; ?edit=<slug> (from a paste's Edit button) opens the editor over it.showPage('content');@@@ -1294,20 +1280,15 @@ function showPage(page) {// of each attaching MutationObservers to every section's class attribute.try { document.dispatchEvent(new CustomEvent('ov:page', { detail: { page, changed } })); } catch { /* */ }-- // Game/Canvas: hide footer only, keep navbar visible; other pages restore both++ // Nothing hides the footer or blocks scrolling any more; a page that once did has to undo it.const navbar = document.querySelector('.navbar');const footer = document.querySelector('.footer');-- if (page === 'game' || page === 'canvas') {-- if (footer) footer.style.display = 'none';-- document.body.style.overflow = 'hidden';-- } else {-- if (navbar) navbar.style.display = '';-- if (footer) footer.style.display = '';-- document.body.style.overflow = '';-- }++ if (navbar) navbar.style.display = '';++ if (footer) footer.style.display = '';++ document.body.style.overflow = '';// Highlight nav link-- const pageToNav = { home: 'home', content: 'content', moments: 'moments', search: 'search', broadcast: 'broadcast', dashboard: 'dashboard', admin: 'admin', chat: 'chat', game: 'game', canvas: 'game', 'paste-viewer': 'content', arena: 'arena' };++ const pageToNav = { home: 'home', content: 'content', moments: 'moments', search: 'search', broadcast: 'broadcast', dashboard: 'dashboard', admin: 'admin', chat: 'chat', 'paste-viewer': 'content', arena: 'arena' };const navPage = pageToNav[page];if (navPage) {const link = document.querySelector(`.nav-link[data-page="${navPage}"]`);@@@ -11,7 +11,7 @@*/(function () {'use strict';-- const COMPACT_PAGES = ['channel', 'vod-player', 'clip-player', 'broadcast', 'game', 'canvas', 'chat', 'arena', 'recap'];++ const COMPACT_PAGES = ['channel', 'vod-player', 'clip-player', 'broadcast', 'chat', 'arena', 'recap'];const LINKS = [{heading: 'Watch',@@@ -26,7 +26,7 @@{ id: 'search', page: 'search', label: 'Search', href: '/search', icon: 'fa-magnifying-glass' },{ id: 'chat', page: 'chat', label: 'Chat', href: '/chat', icon: 'fa-comments' },{ id: 'arena', page: 'arena', label: 'Arena', href: '/arena', icon: 'fa-hand-fist' },-- { id: 'game', page: 'game', label: 'Game', href: 'https://openvibe.games', icon: 'fa-gamepad' },++ { id: 'game', label: 'Game', href: 'https://openvibe.games', icon: 'fa-gamepad' },{ id: 'broadcast', page: 'broadcast', elId: 'nav-broadcast', label: 'Go Live', href: '/broadcast', icon: 'fa-tower-broadcast', dot: false, dotId: 'nav-live-dot',onClick: (e) => call('goLiveNav', e),children: user ? [{ id: 'dashboard', label: 'Dashboard', href: '/dashboard', icon: 'fa-gauge-high', onClick: (e) => call('dashNav', e) }] : [] },@@@ -143,8 +143,6 @@ const analyticsModule = require('openvibe-shared/analytics'); // ADR-021: no IP/// WHIP (WebRTC-HTTP Ingestion Protocol)const whipHandler = require('./streaming/whip-handler');--// Game & Canvas — migrated to openvibe.games (game/canvas code removed)--// ── Express App ──────────────────────────────────────────────const app = express();const server = http.createServer(app);@@@ -715,10 +713,6 @@ app.use('/api/emotes', emoteRoutes);app.use('/api/sounds', chatRemote ? chatMoved : require('./chat/sounds-routes'));app.use('/api/ai-viewers', require('./ai/viewers/routes'));app.use('/api/powerchat', require('./integrations/powerchat-routes'));--// Game & Canvas — migrated to openvibe.games--app.get('/game', (req, res) => res.redirect(301, 'https://openvibe.games/game'));--app.get('/canvas', (req, res) => res.redirect(301, 'https://openvibe.games/canvas'));--app.use('/api/game', (req, res) => res.status(410).json({ error: 'Game has moved to https://openvibe.games/game' }));app.use('/api/meta', metaRoutes);app.use('/api/pastes', pasteRoutes);app.use('/api/content', require('./content/routes')); // Content (people's work) + Moments (AI) feeds@@@ -1107,8 +1101,6 @@ server.on('upgrade', (req, socket, head) => {controlServer.handleUpgrade(req, socket, head);} else if (url.startsWith('/ws/call')) {callServer.handleUpgrade(req, socket, head);-- } else if (url.startsWith('/ws/game') || url.startsWith('/ws/canvas')) {-- socket.destroy(); // migrated to openvibe.games} else if (url.startsWith('/ws/robotstreamer-publish')) {robotStreamerService.handleUpgrade(req, socket, head);} else {@@@ -1182,8 +1174,6 @@ async function start() {console.log(`[Server] Admin user "${adminUser}" created from ADMIN_USERNAME; sign in with the openvibe.network account of that name`);}-- // Game & Canvas migrated to openvibe.games — no local init needed--// 3. Initialize chat serverchatServer.init(server);vibeCodingPublishServer.init(server);@@@ -1199,8 +1189,6 @@ async function start() {// 4b. Initialize broadcast serverbroadcastServer.init(server);-- // Game & Canvas WebSocket servers migrated to openvibe.games--// 4d. Initialize group call signaling servercallServer.init(server);if (require('./streaming/calls-authority').isChat()) {@@@ -1329,7 +1317,6 @@ async function start() {console.log(`[Server] WebSocket: ws://${config.host}:${config.port}/ws/broadcast`);console.log(`[Server] WebSocket: ws://${config.host}:${config.port}/ws/control`);console.log(`[Server] WebSocket: ws://${config.host}:${config.port}/ws/call`);-- console.log(`[Server] Game/Canvas: migrated to openvibe.games`);console.log(`[Server] Environment: ${config.nodeEnv}`);console.log(`[Server] BASE_URL: ${config.baseUrl}`);console.log(`[Server] WHIP_PUBLIC_URL: ${config.whip?.publicUrl}`);@@@ -1676,7 +1663,6 @@ function shutdown() {restreamManager.stopViewerCountPolling();restreamManager.stopAll();try { recorder.stopAll(); } catch {}-- // canvasServer + gameServer migrated to openvibe.gamescallServer.close();chatServer.close();controlServer.close();@@@ -203,7 +203,7 @@ section.visible{opacity:1;transform:none}<p>The current server in <code>server/index.js</code> provides:</p><ul><li>HTTP on <code>PORT</code>, default <code>3000</code></li>--<li>WebSocket upgrades on <code>/ws/chat</code>, <code>/ws/broadcast</code>, <code>/ws/control</code>, <code>/ws/call</code>, and <code>/ws/game</code></li>++<li>WebSocket upgrades on <code>/ws/chat</code>, <code>/ws/broadcast</code>, <code>/ws/control</code>, and <code>/ws/call</code></li><li>Optional RTMP ingest</li><li>Optional JSMPEG relay</li><li>Optional WebRTC / <code>mediasoup</code></li>@@@ -58,7 +58,7 @@ check('nothing was removed', () => {for (const id of ['home-featured', 'stream-grid-live', 'home-digest', 'home-star-section', 'hero-egg', 'home-pulse-section','stream-grid-recent', 'stream-grid-recent-pagination', 'home-recent-vods-header', 'home-recent-vods-grid', 'home-clips-header','home-clips-grid', 'home-pastes-header', 'home-pastes-list', 'home-moments-section', 'home-tour-mount', 'home-cta-banner',-- 'home-about-body', 'home-quest-header', 'home-leaderboards', 'home-canvas-header', 'home-canvas-preview']) {++ 'home-about-body']) {at(`id="${id}"`);}assert.ok(home.includes('class="network-hub"'));
@@ -406,51 +406,6 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
@@ -406,51 +406,6 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
.home-pastes-list { grid-template-columns: 1fr; }
.home-pastes-list { grid-template-columns: 1fr; }
.home-paste-media { height: 136px; }
.home-paste-media { height: 136px; }
}
}
-.home-leaderboards {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr));
- gap: 14px; margin-bottom: 32px;
-}
-.home-lb-card {
- background: var(--bg-card); border: 1px solid var(--border);
- border-radius: var(--radius-lg); padding: 16px; overflow: hidden;
-}
-.home-lb-title {
- font-weight: 650; font-size: 0.88rem; margin-bottom: 10px;
- color: var(--text-secondary);
- display: flex; align-items: center; gap: 6px;
-}
-.home-lb-entries { display: flex; flex-direction: column; gap: 6px; }
-.home-lb-row {
- display: flex; align-items: center; gap: 8px;
- font-size: 0.82rem; padding: 4px 0;
- border-bottom: 1px solid var(--border);
-}
-.home-lb-row:last-child { border-bottom: none; }
-.home-lb-rank {
- width: 20px; text-align: center;
- font-weight: 700; color: var(--text-muted); font-size: 0.75rem;
-}
-.home-lb-score {
- font-weight: 600; color: var(--accent); font-size: 0.78rem;
- white-space: nowrap;
-}
-.home-canvas-preview { margin-bottom: 32px; }
-.home-canvas-wrap {
- display: flex; flex-direction: column; align-items: center;
- background: var(--bg-card); border: 1px solid var(--border);
- border-radius: var(--radius-lg); padding: 24px; text-align: center;
-}
-.home-canvas-wrap canvas {
- max-height: 300px; width: auto;
-}
-.home-canvas-stats {
- display: flex; gap: 24px; margin-top: 16px; flex-wrap: wrap;
- justify-content: center;
-}
-.home-canvas-stat {
- display: flex; flex-direction: column; align-items: center;
-}
.home-changelog-wrapper.collapsed .home-changelog-fade {
.home-changelog-wrapper.collapsed .home-changelog-fade {
display: block;
display: block;
}
}
@@ -2511,29 +2511,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
@@ -2511,29 +2511,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
.founder-story-pullquote blockquote { font-size: 0.94rem; }
.founder-story-pullquote blockquote { font-size: 0.94rem; }
}
}
-/* ═══════════════════════════════════════════════════════════════
- HOME — OPENVIBEGAMES LEADERBOARDS
- ═══════════════════════════════════════════════════════════════ */
-.home-lb-title i { color: var(--accent); font-size: 0.85rem; }
-.home-lb-row:nth-child(1) .home-lb-rank { color: #ffd700; }
-.home-lb-row:nth-child(2) .home-lb-rank { color: #c0c0c0; }
-.home-lb-row:nth-child(3) .home-lb-rank { color: #cd7f32; }
-.home-lb-name {
- flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
- color: var(--text-primary);
-}
-
-/* ═══════════════════════════════════════════════════════════════
- HOME — CANVAS PREVIEW
- ═══════════════════════════════════════════════════════════════ */
-.home-canvas-stat strong {
- font-size: 1.3rem; color: var(--accent); line-height: 1.2;
-}
-.home-canvas-stat span {
- font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
/* ═══════════════════════════════════════════════════════════════
/* ═══════════════════════════════════════════════════════════════
STREAM VIEWER LAYOUT
STREAM VIEWER LAYOUT
═══════════════════════════════════════════════════════════════ */
═══════════════════════════════════════════════════════════════ */
@@ -194,7 +194,7 @@
@@ -194,7 +194,7 @@
<div class="hero-cta" data-fx-viewport>
<div class="hero-cta" data-fx-viewport>
<button style="--i:0" class="hb hb-stream" onclick="currentUser ? navigate('/broadcast') : showModal('register')"><span class="hb-ico"><i class="fa-solid fa-broadcast-tower"></i></span><span class="hb-label">Stream</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></button>
<button style="--i:0" class="hb hb-stream" onclick="currentUser ? navigate('/broadcast') : showModal('register')"><span class="hb-ico"><i class="fa-solid fa-broadcast-tower"></i></span><span class="hb-label">Stream</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></button>
<a href="https://openvibe.tools" style="--i:1" class="hb hb-tools"><span class="hb-ico"><i class="fa-solid fa-screwdriver-wrench"></i></span><span class="hb-label">Tools</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a>
<a href="https://openvibe.tools" style="--i:1" class="hb hb-tools"><span class="hb-ico"><i class="fa-solid fa-screwdriver-wrench"></i></span><span class="hb-label">Tools</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a>
- <a href="https://openvibe.games/game" style="--i:2" class="hb hb-game"><span class="hb-ico"><i class="fa-solid fa-gamepad"></i></span><span class="hb-label">Game</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a>
+ <a href="https://openvibe.games" style="--i:2" class="hb hb-game"><span class="hb-ico"><i class="fa-solid fa-gamepad"></i></span><span class="hb-label">Game</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a>
<a href="https://github.com/OpenVibers/OpenVibe.Live" target="_blank" style="--i:3" class="hb hb-github" rel="noopener"><span class="hb-ico"><i class="fa-brands fa-github"></i></span><span class="hb-label">GitHub</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a>
<a href="https://github.com/OpenVibers/OpenVibe.Live" target="_blank" style="--i:3" class="hb hb-github" rel="noopener"><span class="hb-ico"><i class="fa-brands fa-github"></i></span><span class="hb-label">GitHub</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a>
<a href="https://discord.gg/M6MuRUaeJj" target="_blank" style="--i:4" class="hb hb-discord" rel="noopener noreferrer"><span class="hb-ico"><i class="fa-brands fa-discord"></i></span><span class="hb-label">Discord</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a>
<a href="https://discord.gg/M6MuRUaeJj" target="_blank" style="--i:4" class="hb hb-discord" rel="noopener noreferrer"><span class="hb-ico"><i class="fa-brands fa-discord"></i></span><span class="hb-label">Discord</span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></a>
<a href="/documentation" onclick="return handleLinkClick(event, '/documentation')" style="--i:5" class="hb hb-vibe"
<a href="/documentation" onclick="return handleLinkClick(event, '/documentation')" style="--i:5" class="hb hb-vibe"
@@ -505,20 +505,6 @@
@@ -505,20 +505,6 @@
</div>
</div>
<div class="container">
<div class="container">
- <!-- Scraplandia Leaderboards -->
- <div class="section-header" style="--sc:#fbbf24;display:none" id="home-quest-header">
- <h2><i class="fa-solid fa-trophy"></i> Scraplandia Leaderboards<small class="section-kicker">the ladder</small></h2>
- <a href="https://openvibe.games/game" class="section-action">Play game</a>
- </div>
- <div class="home-leaderboards" id="home-leaderboards"></div>
-
- <!-- Canvas Preview -->
- <div class="section-header" style="--sc:#4ade80;display:none" id="home-canvas-header">
- <h2><i class="fa-solid fa-palette"></i> Community Canvas<small class="section-kicker">drawn together</small></h2>
- <a href="https://openvibe.games/canvas" class="section-action">Open canvas</a>
- </div>
- <div class="home-canvas-preview" id="home-canvas-preview"></div>
-
<div class="network-hub">
<div class="network-hub">
<div class="network-hub-header">
<div class="network-hub-header">
<i class="fa-solid fa-network-wired"></i>
<i class="fa-solid fa-network-wired"></i>
@@ -560,14 +546,6 @@
@@ -560,14 +546,6 @@
</div>
</div>
<i class="fa-solid fa-arrow-right network-card-arrow"></i>
<i class="fa-solid fa-arrow-right network-card-arrow"></i>
</a>
</a>
- <a href="https://openvibe.games/canvas" class="network-card">
- <div class="network-card-icon" style="--card-accent:#3498db"><i class="fa-solid fa-palette"></i></div>
- <div class="network-card-info">
- <strong>Canvas</strong>
- <span>Collaborative pixel art</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
<a href="https://openvibe.media" class="network-card">
<a href="https://openvibe.media" class="network-card">
<div class="network-card-icon" style="--card-accent:#f59e0b"><i class="fa-solid fa-photo-film"></i></div>
<div class="network-card-icon" style="--card-accent:#f59e0b"><i class="fa-solid fa-photo-film"></i></div>
<div class="network-card-info">
<div class="network-card-info">
@@ -730,8 +708,6 @@
@@ -730,8 +708,6 @@
<!-- (page-themes removed: /themes redirects to openvibe.network in routeFromURL, so this
<!-- (page-themes removed: /themes redirects to openvibe.network in routeFromURL, so this
section was unreachable markup with five dead inline handlers.) -->
section was unreachable markup with five dead inline handlers.) -->
-<!-- Game & Canvas migrated to openvibe.games -->
-
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- UPDATES / CHANGELOG -->
<!-- UPDATES / CHANGELOG -->
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- ═══════════════════════════════════════════════════════════ -->
@@ -1248,10 +1248,6 @@ async function loadHome() {
@@ -1248,10 +1248,6 @@ async function loadHome() {
loadHomeClips();
loadHomeClips();
// Load recent pastes
// Load recent pastes
loadHomePastes();
loadHomePastes();
- // Load Scraplandia leaderboards
- loadHomeLeaderboards();
- // Load Canvas preview
- loadHomeCanvas();
startHomeRefresh(); // live grid + sections auto-update in real time
startHomeRefresh(); // live grid + sections auto-update in real time
}
}
@@ -1555,129 +1551,6 @@ async function loadHomePastes(opts) {
@@ -1555,129 +1551,6 @@ async function loadHomePastes(opts) {
} catch { rail.filling = false; }
} catch { rail.filling = false; }
}
}
-// OpenVibe.Games does not serve /api/game/leaderboard/* or /api/game/canvas/state (the pixel canvas is
-// owner decision O15), so every first visit logged CORS errors for sections that stay hidden. Turn this
-// on when Games serves them (public, CORS for first-party origins) — roadmap WS-M task 7.
-const GAMES_HOME_WIDGETS = false;
-
-async function loadHomeLeaderboards() {
- if (!GAMES_HOME_WIDGETS) { const h = document.getElementById('home-quest-header'); if (h) h.style.display = 'none'; return; }
- try {
- const boards = ['total_level', 'combat', 'mining', 'fishing'];
- const questUrl = getScraplandiaUrl();
- // One probe first: if the service is not answering, the other three would fail the same way.
- const first = await fetchServiceJson(`${questUrl}/api/game/leaderboard/${boards[0]}`);
- const rest = first ? await Promise.all(boards.slice(1).map(b => fetchServiceJson(`${questUrl}/api/game/leaderboard/${b}`))) : [];
- const results = [first, ...rest].map(r => r || { entries: [] });
- while (results.length < boards.length) results.push({ entries: [] });
- const header = document.getElementById('home-quest-header');
- const container = document.getElementById('home-leaderboards');
- const hasData = results.some(r => r.entries && r.entries.length);
- if (!hasData) { if (header) header.style.display = 'none'; return; }
- if (header) header.style.display = '';
-
- const labels = { total_level: 'Total Level', combat: 'Combat', mining: 'Mining', fishing: 'Fishing' };
- const icons = { total_level: 'fa-star', combat: 'fa-sword', mining: 'fa-gem', fishing: 'fa-fish' };
- container.innerHTML = boards.map((board, i) => {
- const entries = (results[i].entries || []).slice(0, 5);
- if (!entries.length) return '';
- return `
- <div class="home-lb-card">
- <div class="home-lb-title"><i class="fa-solid ${icons[board] || 'fa-trophy'}"></i> ${labels[board]}</div>
- <div class="home-lb-entries">
- ${entries.map((e, rank) => `
- <div class="home-lb-row">
- <span class="home-lb-rank">${rank + 1}</span>
- <span class="home-lb-name">${esc(e.display_name || e.username || 'Unknown')}</span>
- <span class="home-lb-score">${typeof e.score === 'number' ? e.score.toLocaleString() : e.score}</span>
- </div>
- `).join('')}
- </div>
- </div>`;
- }).join('');
- } catch { /* silent */ }
-}
-
-/**
- * Fetch JSON from another OpenVibe service, remembering when it is not there.
- *
- * The home page asks OpenVibe.Games for leaderboards and canvas state. Those endpoints currently
- * answer with the Games SPA's HTML and no CORS headers, so from openvibe.live all five requests
- * fail — on every single home page view. Failing is fine; failing five times per visit forever is
- * not. A miss is remembered for a few hours per browser, so the page stops asking a service that
- * is not answering and starts again on its own once the window lapses. A response that is not
- * JSON counts as a miss instead of throwing inside .json().
- */
-const _XSVC_MISS_KEY = 'ov_xsvc_miss_v1';
-const _XSVC_MISS_MS = 6 * 60 * 60 * 1000;
-function _xsvcMisses() { try { return JSON.parse(localStorage.getItem(_XSVC_MISS_KEY) || '{}'); } catch { return {}; } }
-async function fetchServiceJson(url) {
- // Keyed per URL, not per service prefix: one board that 404s must not switch off every other
- // endpoint on the same service for six hours.
- const misses = _xsvcMisses();
- if (misses[url] && Date.now() - misses[url] < _XSVC_MISS_MS) return null;
- const remember = () => { try { misses[url] = Date.now(); localStorage.setItem(_XSVC_MISS_KEY, JSON.stringify(misses)); } catch { /* */ } };
- let r;
- try { r = await fetch(url, { credentials: 'omit' }); }
- catch { remember(); return null; } // network / CORS: the service is not answering
- const type = r.headers.get('content-type') || '';
- if (!/json/i.test(type)) { remember(); return null; } // an HTML page where an API should be
- // A JSON error means the service is there and answered — do not remember it as absent.
- if (!r.ok) return null;
- try { return await r.json(); } catch { return null; }
-}
-
-async function loadHomeCanvas() {
- if (!GAMES_HOME_WIDGETS) { const h = document.getElementById('home-canvas-header'); if (h) h.style.display = 'none'; return; }
- try {
- const header = document.getElementById('home-canvas-header');
- const container = document.getElementById('home-canvas-preview');
- const data = await fetchServiceJson(`${getScraplandiaUrl()}/api/game/canvas/state`);
- if (!data || !data.board) { if (header) header.style.display = 'none'; return; }
- if (header) header.style.display = '';
-
- const tiles = data.tiles || [];
- const recentActions = data.recent_actions || [];
- const uniqueArtists = new Set(tiles.map(t => t.user_id).filter(Boolean)).size;
- const width = data.board.width || 64;
- const height = data.board.height || 64;
- const palette = data.board.palette || ['#000000'];
-
- // Render a mini canvas preview
- const scale = 4;
- container.innerHTML = `
- <div class="home-canvas-wrap">
- <canvas id="home-canvas-mini" width="${width * scale}" height="${height * scale}" style="image-rendering:pixelated;border-radius:var(--radius);border:1px solid var(--border);max-width:100%;"></canvas>
- <div class="home-canvas-stats">
- <div class="home-canvas-stat"><strong>${tiles.length.toLocaleString()}</strong> <span>pixels placed</span></div>
- <div class="home-canvas-stat"><strong>${uniqueArtists.toLocaleString()}</strong> <span>artists</span></div>
- <div class="home-canvas-stat"><strong>${width}×${height}</strong> <span>board size</span></div>
- <div class="home-canvas-stat"><strong>${recentActions.length}</strong> <span>recent actions</span></div>
- </div>
- <a href="${getScraplandiaUrl()}/canvas" class="btn btn-outline" style="margin-top:12px;">
- <i class="fa-solid fa-palette"></i> Open Canvas
- </a>
- </div>
- `;
-
- // Draw tiles on the mini canvas
- const canvas = document.getElementById('home-canvas-mini');
- if (canvas) {
- const ctx = canvas.getContext('2d');
- ctx.fillStyle = palette[0] || '#000';
- ctx.fillRect(0, 0, width * scale, height * scale);
- for (const tile of tiles) {
- const color = palette[tile.color_index] || '#fff';
- ctx.fillStyle = color;
- ctx.fillRect(tile.x * scale, tile.y * scale, scale, scale);
- }
- }
- } catch {
- const header = document.getElementById('home-canvas-header');
- if (header) header.style.display = 'none';
- }
-}
-
// Markup for a single stream card (live or recent). Extracted so the home page
// Markup for a single stream card (live or recent). Extracted so the home page
// can reconcile the live grid in place for real-time updates.
// can reconcile the live grid in place for real-time updates.
function streamCardHTML(s, isLive) {
function streamCardHTML(s, isLive) {
@@ -1928,7 +1801,6 @@ function refreshHomeSections() {
@@ -1928,7 +1801,6 @@ function refreshHomeSections() {
if (untouched('vods') && typeof loadHomeRecentVods === 'function') loadHomeRecentVods();
if (untouched('vods') && typeof loadHomeRecentVods === 'function') loadHomeRecentVods();
if (untouched('clips') && typeof loadHomeClips === 'function') loadHomeClips();
if (untouched('clips') && typeof loadHomeClips === 'function') loadHomeClips();
if (untouched('pastes') && typeof loadHomePastes === 'function') loadHomePastes();
if (untouched('pastes') && typeof loadHomePastes === 'function') loadHomePastes();
- if (typeof loadHomeLeaderboards === 'function') loadHomeLeaderboards();
}
}
// The live grid follows OpenVibe.Events realtime (public/js/ov-live-realtime.js): a stream that
// The live grid follows OpenVibe.Events realtime (public/js/ov-live-realtime.js): a stream that
@@ -17,7 +17,6 @@ function getDefaultOpenVibeNetworkUrls() {
@@ -17,7 +17,6 @@ function getDefaultOpenVibeNetworkUrls() {
const isTopenvibeAlias = ['topenvibe.tools', 'topenvibe.live', 'topenvibe.quest'].includes(host);
const isTopenvibeAlias = ['topenvibe.tools', 'topenvibe.live', 'topenvibe.quest'].includes(host);
return {
return {
tools: isLocalHost ? 'http://localhost:3100' : (isTopenvibeAlias ? 'https://topenvibe.tools' : 'https://openvibe.network'),
tools: isLocalHost ? 'http://localhost:3100' : (isTopenvibeAlias ? 'https://topenvibe.tools' : 'https://openvibe.network'),
- quest: isLocalHost ? 'http://localhost:3200' : (isTopenvibeAlias ? 'https://topenvibe.quest' : 'https://openvibe.games'),
};
};
}
}
@@ -29,10 +28,6 @@ function getOpenVibeNetworkUrl(service) {
@@ -29,10 +28,6 @@ function getOpenVibeNetworkUrl(service) {
function getOpenVibeToolsUrl() {
function getOpenVibeToolsUrl() {
return getOpenVibeNetworkUrl('tools');
return getOpenVibeNetworkUrl('tools');
}
}
-
-function getScraplandiaUrl() {
- return getOpenVibeNetworkUrl('quest');
-}
/** Cached external viewer count (Kick/Twitch/RS) — updated by cumulative viewer poll */
/** Cached external viewer count (Kick/Twitch/RS) — updated by cumulative viewer poll */
let _cachedExternalViewerCount = 0;
let _cachedExternalViewerCount = 0;
@@ -587,7 +582,7 @@ function _ssoHint() {
@@ -587,7 +582,7 @@ function _ssoHint() {
// What the signed-in account watches/opens here shows up in its network-wide History and the
// What the signed-in account watches/opens here shows up in its network-wide History and the
// "Recently used" rows of the shared navbar on every other site. Recorded after the route
// "Recently used" rows of the shared navbar on every other site. Recorded after the route
// rendered and its title settled; never for guests, and only for the pages worth remembering.
// rendered and its title settled; never for guests, and only for the pages worth remembering.
-const _HISTORY_PAGES = { channel: 'stream', 'vod-player': 'vod', 'clip-player': 'clip', 'paste-viewer': 'paste', pastes: null, game: 'game', arena: 'page' };
+const _HISTORY_PAGES = { channel: 'stream', 'vod-player': 'vod', 'clip-player': 'clip', 'paste-viewer': 'paste', pastes: null, arena: 'page' };
let _historyTimer = null;
let _historyTimer = null;
function _recordHistory(pageId) {
function _recordHistory(pageId) {
const type = _HISTORY_PAGES[pageId];
const type = _HISTORY_PAGES[pageId];
@@ -749,9 +744,6 @@ function onAuthChange() {
@@ -749,9 +744,6 @@ function onAuthChange() {
if (window.LiveNav) window.LiveNav.sync(currentUser || null);
if (window.LiveNav) window.LiveNav.sync(currentUser || null);
if (currentUser) loadBalance();
if (currentUser) loadBalance();
- // Sync canvas auth state if canvas page is loaded
- if (typeof syncCanvasAuthState === 'function') syncCanvasAuthState();
-
try {
try {
window.dispatchEvent(new CustomEvent('openvibe-auth-changed', {
window.dispatchEvent(new CustomEvent('openvibe-auth-changed', {
detail: {
detail: {
@@ -1108,12 +1100,6 @@ function routeFromURL() {
@@ -1108,12 +1100,6 @@ function routeFromURL() {
} else if (segments[0] === 'chat') {
} else if (segments[0] === 'chat') {
showPage('chat');
showPage('chat');
whenRouteReady('chat', () => loadChatPage());
whenRouteReady('chat', () => loadChatPage());
- } else if (segments[0] === 'game') {
- window.location.href = `${getScraplandiaUrl()}/game`;
- return;
- } else if (segments[0] === 'canvas') {
- window.location.href = `${getScraplandiaUrl()}/canvas`;
- return;
} else if (segments[0] === 'pastes') {
} else if (segments[0] === 'pastes') {
// The Content feed's Pastes filter; ?edit=<slug> (from a paste's Edit button) opens the editor over it.
// The Content feed's Pastes filter; ?edit=<slug> (from a paste's Edit button) opens the editor over it.
showPage('content');
showPage('content');
@@ -1294,20 +1280,15 @@ function showPage(page) {
@@ -1294,20 +1280,15 @@ function showPage(page) {
// of each attaching MutationObservers to every section's class attribute.
// of each attaching MutationObservers to every section's class attribute.
try { document.dispatchEvent(new CustomEvent('ov:page', { detail: { page, changed } })); } catch { /* */ }
try { document.dispatchEvent(new CustomEvent('ov:page', { detail: { page, changed } })); } catch { /* */ }
- // Game/Canvas: hide footer only, keep navbar visible; other pages restore both
+ // Nothing hides the footer or blocks scrolling any more; a page that once did has to undo it.
const navbar = document.querySelector('.navbar');
const navbar = document.querySelector('.navbar');
const footer = document.querySelector('.footer');
const footer = document.querySelector('.footer');
- if (page === 'game' || page === 'canvas') {
- if (footer) footer.style.display = 'none';
- document.body.style.overflow = 'hidden';
- } else {
- if (navbar) navbar.style.display = '';
- if (footer) footer.style.display = '';
- document.body.style.overflow = '';
- }
+ if (navbar) navbar.style.display = '';
+ if (footer) footer.style.display = '';
+ document.body.style.overflow = '';
// Highlight nav link
// Highlight nav link
- const pageToNav = { home: 'home', content: 'content', moments: 'moments', search: 'search', broadcast: 'broadcast', dashboard: 'dashboard', admin: 'admin', chat: 'chat', game: 'game', canvas: 'game', 'paste-viewer': 'content', arena: 'arena' };
+ const pageToNav = { home: 'home', content: 'content', moments: 'moments', search: 'search', broadcast: 'broadcast', dashboard: 'dashboard', admin: 'admin', chat: 'chat', 'paste-viewer': 'content', arena: 'arena' };
const navPage = pageToNav[page];
const navPage = pageToNav[page];
if (navPage) {
if (navPage) {
const link = document.querySelector(`.nav-link[data-page="${navPage}"]`);
const link = document.querySelector(`.nav-link[data-page="${navPage}"]`);
@@ -11,7 +11,7 @@
@@ -11,7 +11,7 @@
*/
*/
(function () {
(function () {
'use strict';
'use strict';
- const COMPACT_PAGES = ['channel', 'vod-player', 'clip-player', 'broadcast', 'game', 'canvas', 'chat', 'arena', 'recap'];
+ const COMPACT_PAGES = ['channel', 'vod-player', 'clip-player', 'broadcast', 'chat', 'arena', 'recap'];
const LINKS = [
const LINKS = [
{
{
heading: 'Watch',
heading: 'Watch',
@@ -26,7 +26,7 @@
@@ -26,7 +26,7 @@
{ id: 'search', page: 'search', label: 'Search', href: '/search', icon: 'fa-magnifying-glass' },
{ id: 'search', page: 'search', label: 'Search', href: '/search', icon: 'fa-magnifying-glass' },
{ id: 'chat', page: 'chat', label: 'Chat', href: '/chat', icon: 'fa-comments' },
{ id: 'chat', page: 'chat', label: 'Chat', href: '/chat', icon: 'fa-comments' },
{ id: 'arena', page: 'arena', label: 'Arena', href: '/arena', icon: 'fa-hand-fist' },
{ id: 'arena', page: 'arena', label: 'Arena', href: '/arena', icon: 'fa-hand-fist' },
- { id: 'game', page: 'game', label: 'Game', href: 'https://openvibe.games', icon: 'fa-gamepad' },
+ { id: 'game', label: 'Game', href: 'https://openvibe.games', icon: 'fa-gamepad' },
{ id: 'broadcast', page: 'broadcast', elId: 'nav-broadcast', label: 'Go Live', href: '/broadcast', icon: 'fa-tower-broadcast', dot: false, dotId: 'nav-live-dot',
{ id: 'broadcast', page: 'broadcast', elId: 'nav-broadcast', label: 'Go Live', href: '/broadcast', icon: 'fa-tower-broadcast', dot: false, dotId: 'nav-live-dot',
onClick: (e) => call('goLiveNav', e),
onClick: (e) => call('goLiveNav', e),
children: user ? [{ id: 'dashboard', label: 'Dashboard', href: '/dashboard', icon: 'fa-gauge-high', onClick: (e) => call('dashNav', e) }] : [] },
children: user ? [{ id: 'dashboard', label: 'Dashboard', href: '/dashboard', icon: 'fa-gauge-high', onClick: (e) => call('dashNav', e) }] : [] },
@@ -143,8 +143,6 @@ const analyticsModule = require('openvibe-shared/analytics'); // ADR-021: no IP/
@@ -143,8 +143,6 @@ const analyticsModule = require('openvibe-shared/analytics'); // ADR-021: no IP/
// WHIP (WebRTC-HTTP Ingestion Protocol)
// WHIP (WebRTC-HTTP Ingestion Protocol)
const whipHandler = require('./streaming/whip-handler');
const whipHandler = require('./streaming/whip-handler');
-// Game & Canvas — migrated to openvibe.games (game/canvas code removed)
-
// ── Express App ──────────────────────────────────────────────
// ── Express App ──────────────────────────────────────────────
const app = express();
const app = express();
const server = http.createServer(app);
const server = http.createServer(app);
@@ -715,10 +713,6 @@ app.use('/api/emotes', emoteRoutes);
@@ -715,10 +713,6 @@ app.use('/api/emotes', emoteRoutes);
app.use('/api/sounds', chatRemote ? chatMoved : require('./chat/sounds-routes'));
app.use('/api/sounds', chatRemote ? chatMoved : require('./chat/sounds-routes'));
app.use('/api/ai-viewers', require('./ai/viewers/routes'));
app.use('/api/ai-viewers', require('./ai/viewers/routes'));
app.use('/api/powerchat', require('./integrations/powerchat-routes'));
app.use('/api/powerchat', require('./integrations/powerchat-routes'));
-// Game & Canvas — migrated to openvibe.games
-app.get('/game', (req, res) => res.redirect(301, 'https://openvibe.games/game'));
-app.get('/canvas', (req, res) => res.redirect(301, 'https://openvibe.games/canvas'));
-app.use('/api/game', (req, res) => res.status(410).json({ error: 'Game has moved to https://openvibe.games/game' }));
app.use('/api/meta', metaRoutes);
app.use('/api/meta', metaRoutes);
app.use('/api/pastes', pasteRoutes);
app.use('/api/pastes', pasteRoutes);
app.use('/api/content', require('./content/routes')); // Content (people's work) + Moments (AI) feeds
app.use('/api/content', require('./content/routes')); // Content (people's work) + Moments (AI) feeds
@@ -1107,8 +1101,6 @@ server.on('upgrade', (req, socket, head) => {
@@ -1107,8 +1101,6 @@ server.on('upgrade', (req, socket, head) => {
controlServer.handleUpgrade(req, socket, head);
controlServer.handleUpgrade(req, socket, head);
} else if (url.startsWith('/ws/call')) {
} else if (url.startsWith('/ws/call')) {
callServer.handleUpgrade(req, socket, head);
callServer.handleUpgrade(req, socket, head);
- } else if (url.startsWith('/ws/game') || url.startsWith('/ws/canvas')) {
- socket.destroy(); // migrated to openvibe.games
} else if (url.startsWith('/ws/robotstreamer-publish')) {
} else if (url.startsWith('/ws/robotstreamer-publish')) {
robotStreamerService.handleUpgrade(req, socket, head);
robotStreamerService.handleUpgrade(req, socket, head);
} else {
} else {
@@ -1182,8 +1174,6 @@ async function start() {
@@ -1182,8 +1174,6 @@ async function start() {
console.log(`[Server] Admin user "${adminUser}" created from ADMIN_USERNAME; sign in with the openvibe.network account of that name`);
console.log(`[Server] Admin user "${adminUser}" created from ADMIN_USERNAME; sign in with the openvibe.network account of that name`);
}
}
- // Game & Canvas migrated to openvibe.games — no local init needed
-
// 3. Initialize chat server
// 3. Initialize chat server
chatServer.init(server);
chatServer.init(server);
vibeCodingPublishServer.init(server);
vibeCodingPublishServer.init(server);
@@ -1199,8 +1189,6 @@ async function start() {
@@ -1199,8 +1189,6 @@ async function start() {
// 4b. Initialize broadcast server
// 4b. Initialize broadcast server
broadcastServer.init(server);
broadcastServer.init(server);
- // Game & Canvas WebSocket servers migrated to openvibe.games
-
// 4d. Initialize group call signaling server
// 4d. Initialize group call signaling server
callServer.init(server);
callServer.init(server);
if (require('./streaming/calls-authority').isChat()) {
if (require('./streaming/calls-authority').isChat()) {
@@ -1329,7 +1317,6 @@ async function start() {
@@ -1329,7 +1317,6 @@ async function start() {
console.log(`[Server] WebSocket: ws://${config.host}:${config.port}/ws/broadcast`);
console.log(`[Server] WebSocket: ws://${config.host}:${config.port}/ws/broadcast`);
console.log(`[Server] WebSocket: ws://${config.host}:${config.port}/ws/control`);
console.log(`[Server] WebSocket: ws://${config.host}:${config.port}/ws/control`);
console.log(`[Server] WebSocket: ws://${config.host}:${config.port}/ws/call`);
console.log(`[Server] WebSocket: ws://${config.host}:${config.port}/ws/call`);
- console.log(`[Server] Game/Canvas: migrated to openvibe.games`);
console.log(`[Server] Environment: ${config.nodeEnv}`);
console.log(`[Server] Environment: ${config.nodeEnv}`);
console.log(`[Server] BASE_URL: ${config.baseUrl}`);
console.log(`[Server] BASE_URL: ${config.baseUrl}`);
console.log(`[Server] WHIP_PUBLIC_URL: ${config.whip?.publicUrl}`);
console.log(`[Server] WHIP_PUBLIC_URL: ${config.whip?.publicUrl}`);
@@ -1676,7 +1663,6 @@ function shutdown() {
@@ -1676,7 +1663,6 @@ function shutdown() {
restreamManager.stopViewerCountPolling();
restreamManager.stopViewerCountPolling();
restreamManager.stopAll();
restreamManager.stopAll();
try { recorder.stopAll(); } catch {}
try { recorder.stopAll(); } catch {}
- // canvasServer + gameServer migrated to openvibe.games
callServer.close();
callServer.close();
chatServer.close();
chatServer.close();
controlServer.close();
controlServer.close();
@@ -203,7 +203,7 @@ section.visible{opacity:1;transform:none}
@@ -203,7 +203,7 @@ section.visible{opacity:1;transform:none}
<p>The current server in <code>server/index.js</code> provides:</p>
<p>The current server in <code>server/index.js</code> provides:</p>
<ul>
<ul>
<li>HTTP on <code>PORT</code>, default <code>3000</code></li>
<li>HTTP on <code>PORT</code>, default <code>3000</code></li>
-<li>WebSocket upgrades on <code>/ws/chat</code>, <code>/ws/broadcast</code>, <code>/ws/control</code>, <code>/ws/call</code>, and <code>/ws/game</code></li>
+<li>WebSocket upgrades on <code>/ws/chat</code>, <code>/ws/broadcast</code>, <code>/ws/control</code>, and <code>/ws/call</code></li>
<li>Optional RTMP ingest</li>
<li>Optional RTMP ingest</li>
<li>Optional JSMPEG relay</li>
<li>Optional JSMPEG relay</li>
<li>Optional WebRTC / <code>mediasoup</code></li>
<li>Optional WebRTC / <code>mediasoup</code></li>
@@ -58,7 +58,7 @@ check('nothing was removed', () => {
@@ -58,7 +58,7 @@ check('nothing was removed', () => {
for (const id of ['home-featured', 'stream-grid-live', 'home-digest', 'home-star-section', 'hero-egg', 'home-pulse-section',
for (const id of ['home-featured', 'stream-grid-live', 'home-digest', 'home-star-section', 'hero-egg', 'home-pulse-section',
'stream-grid-recent', 'stream-grid-recent-pagination', 'home-recent-vods-header', 'home-recent-vods-grid', 'home-clips-header',
'stream-grid-recent', 'stream-grid-recent-pagination', 'home-recent-vods-header', 'home-recent-vods-grid', 'home-clips-header',
'home-clips-grid', 'home-pastes-header', 'home-pastes-list', 'home-moments-section', 'home-tour-mount', 'home-cta-banner',
'home-clips-grid', 'home-pastes-header', 'home-pastes-list', 'home-moments-section', 'home-tour-mount', 'home-cta-banner',
- 'home-about-body', 'home-quest-header', 'home-leaderboards', 'home-canvas-header', 'home-canvas-preview']) {
+ 'home-about-body']) {
at(`id="${id}"`);
at(`id="${id}"`);
}
}
assert.ok(home.includes('class="network-hub"'));
assert.ok(home.includes('class="network-hub"'));