openvibers/openvibe.live Commits
Each page carries only its own markup and one <h1>: the home page, Updates and the Rundown are fragments inlined on their own route, so other pages lose the home hero's 37 KB and its <h1> (#343)
Changes
@@@ -30,7 +30,7 @@ BASE=http://127.0.0.1:3000 npm run test:browser # Browser smoke (running server- **Database:** [server/db/database.js](server/db/database.js) — all queries (async, `openvibe-sdk/db`), schema in [migrations/](migrations/) (`0001_analytics.sql`, `0002_live.sql`)- **Auth:** [server/auth/auth.js](server/auth/auth.js) — RS256 JWT from openvibe.tools SSO + `hbt_` API tokens- **Permissions:** [server/auth/permissions.js](server/auth/permissions.js) — role hierarchy: `user < streamer < global_mod < admin`--- **Frontend shell:** [public/index.html](public/index.html) — navbar, home page and empty `<section id="page-*">` shells; routing via `history.pushState` in [public/js/app.js](public/js/app.js)++- **Frontend shell:** [public/index.html](public/index.html) — navbar and `<section id="page-*">` shells (page markup, the home page's included, is in `public/fragments/`, inlined on its own route); routing via `history.pushState` in [public/js/app.js](public/js/app.js)- **Route loading:** [public/features.json](public/features.json) maps routes → features (fragment in `public/fragments/`, CSS in `public/css/features/`, scripts, deps, stubs); [public/js/ov-loader.js](public/js/ov-loader.js) loads them. New page code goes in a feature, not in `index.html`/core `app.js`. See [docs/architecture.md](docs/architecture.md#frontend-loading).Each feature lives in its own `server/<feature>/` directory with `routes.js` + service files. Frontend: one JS file per feature in `public/js/`, registered in `public/features.json`.@@@ -39,10 +39,11 @@ OpenVibe.Live is a self-hosted live streaming platform, one of the OpenVibe servThe SPA is plain JavaScript with no build step. What loads is decided per route:```--public/index.html shell: navbar, home page, empty <section id="page-*"> shells, core scripts++public/index.html shell: navbar, empty <section id="page-*"> shells, core scripts (no page markup)public/features.json registry: feature → fragment, stylesheets, scripts, dependencies, stubs, idle prefetchroutes: URL pattern → features--public/fragments/*.html page markup for channel, dashboard, broadcast, chat, documentation, content, moments (VOD and clip pages are OpenVibe.Video's)++public/fragments/*.html page markup for home, channel, dashboard, broadcast, chat, documentation, content, moments, search,++ rundown, updates (VOD and clip pages are OpenVibe.Video's)public/css/features/*.css rules used only by one feature (split out of style.css / broadcast.css / i18n-star.css)public/js/ov-loader.js configures /shared/web-runtime.js (openvibe-shared): ov.load(feature), ov.route(path), ov.prefetch(),route generations, scopes, stubs, transactional asset groups, ov.diagnostics()/ov.leaks()@@@ -52,7 +53,9 @@ public/js/app-*.js route code split out of app.js: home, channel, media, c- **First paint.** `server/web/assets.js` renders `index.html` for the requested path: the route'sstylesheets go in `<head>` (right after `style.css`), its scripts after the core scripts, and its-- fragment is inlined into the section shell. A direct visit needs no extra round trip.++ fragment is inlined into the section shell. A direct visit needs no extra round trip, and a page++ carries only its own markup: the home page's hero and lists are inlined on `/` alone, so every++ other page has one `<h1>`, its own (`test/shell-one-h1.test.js`).- **Navigation.** `routeFromURL()` calls `whenRouteReady(page, render)`: the loader inserts thefeature's markup first (modules bind to it on load), then dependencies, stylesheets and scripts inorder, runs the feature's `after` hook once, and renders only if the route generation is still@@@ -2,6 +2,7 @@"_comment": "Feature and route registry, read by server/web/assets.js (first paint: which assets the requested route needs) and by public/js/ov-loader.js (navigation: what to load before a route renders). Scripts in a feature run in the listed order, after the feature's dependencies. `fragment` names public/fragments/<name>.html, inserted into section #page-<section> before any of the feature's scripts run. `after` names a global function called once when the feature has loaded. `stubs` are global functions that inline handlers call; until the feature is loaded they load it and then call the real function. See docs/architecture.md#frontend-loading.","features": {"home": {++ "fragment": "home","css": ["/css/features/home.css","/css/home-fx.css",@@@ -232,7 +233,7 @@"regenerateDashStreamKey"]},-- "rundown": {"css": ["/css/features/rundown.css"], "js": ["/js/rundown.js"]},++ "rundown": {"fragment": "rundown", "css": ["/css/features/rundown.css"], "js": ["/js/rundown.js"]},"recap": {"css": ["/css/recap.css"@@@ -249,6 +250,9 @@"openCosmeticsModal"]},++ "updates": {++ "fragment": "updates"++ },"docs": {"fragment": "documentation","css": [@@@ -343,6 +347,7 @@]},{"path": "^/rundown", "features": ["rundown"]}, {"path": "^/arena", "features": ["rundown"]},++ {"path": "^/updates/?$", "features": ["updates"]},{"path": "^/documentation","features": [nnew file mode 100644@@@ -0,0 +1,500 @@++<div class="hero" data-fx-viewport>++ <div class="hero-bg" id="hero-bg" aria-hidden="true"><div class="hero-bg-layer"></div><div class="hero-bg-layer"></div></div>++ <div class="hero-aurora" aria-hidden="true"></div>++ <div class="hero-collage" id="hero-collage" aria-hidden="true"></div>++ <div class="hero-scrim" aria-hidden="true"></div>++ <div class="hero-content">++ <h1><span class="brand-mark brand-mark--hero" aria-hidden="true"><svg viewBox="0 0 48 48">++ <defs>++ <linearGradient id="ovh-g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="currentColor"/><stop offset="1" stop-color="currentColor" stop-opacity="0.45"/></linearGradient>++ <linearGradient id="ovh-v" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="currentColor"/><stop offset="1" stop-color="#fff" stop-opacity="0.9"/><animateTransform attributeName="gradientTransform" type="translate" from="-1 0" to="1 0" dur="3.2s" repeatCount="indefinite"/></linearGradient>++ <path id="ovh-ring" d="M24,6 A18,18 0 1,1 23.99,6 Z"/>++ </defs>++ <circle class="ov-glow" cx="24" cy="24" r="21"/>++ <circle class="ov-ring" cx="24" cy="24" r="18"/>++ <circle class="ov-comet" cx="24" cy="24" r="18" stroke="url(#ovh-g)"/>++ <path class="ov-v" d="M14.5,17 L24,34 L33.5,17" stroke="url(#ovh-v)"/>++ <circle class="ov-dot" cx="24" cy="34" r="2.6"/>++ <circle class="ov-orbit" r="2"><animateMotion dur="3.6s" repeatCount="indefinite" rotate="auto"><mpath href="#ovh-ring"/></animateMotion></circle>++ <circle class="ov-orbit ov-orbit--tail" r="1.4"><animateMotion dur="3.6s" begin="0.14s" repeatCount="indefinite"><mpath href="#ovh-ring"/></animateMotion></circle>++ <circle class="ov-spark" cx="24" cy="24" r="1.3"/><circle class="ov-spark ov-spark--2" cx="24" cy="24" r="1"/><circle class="ov-spark ov-spark--3" cx="24" cy="24" r="1.1"/>++</svg></span> OpenVibe.Live</h1>++ <!-- The first word and quip ship in the HTML so the hero is complete at first paint; the++ rotators take over once the page's data arrives. No aria-live: a slogan that changes++ every few seconds would be read out to screen-reader users over and over. -->++ <p class="hero-tagline">Live streaming for <span class="hero-rotate-wrap"><span class="hero-rotate visible" id="hero-rotate">stealth campers</span></span></p>++ <p class="hero-quip visible" id="hero-quip">No investors. No suits. Just vibes.</p>++ <!-- Holds its line from the first paint (the countdown fills it with the page's data), so the buttons below do not move. -->++ <div class="hero-slogan-timer" id="hero-slogan-timer"> </div>++ <!-- One primary action and its promise, then the other destinations (owner, 2026-10-18: "whole area is kinda++ messy and spacing is weird"; six equal buttons wrapped 5 + 1 with the no-OBS line floating between++ rows). The headline feature stays in the first screen: going live from a browser with no OBS and++ no follower minimum is what people search and ask AI assistants for. -->++ <div class="hero-cta" data-fx-viewport>++ <div class="hero-primary">++ <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">Go live <small>in your browser</small></span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></button>++ <p class="hero-golive-note"><span><i class="fa-solid fa-check" aria-hidden="true"></i> No OBS</span><span><i class="fa-solid fa-check" aria-hidden="true"></i> No downloads</span><span><i class="fa-solid fa-check" aria-hidden="true"></i> No follower minimum</span><a href="/docs/go-live-in-your-browser">How it works <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></p>++ </div>++ <nav class="hero-links" aria-label="More from OpenVibe">++ <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" 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="/documentation" onclick="return handleLinkClick(event, '/documentation')" style="--i:3" class="hb hb-vibe"++ data-ovtip-title="Mod it, bot it, build on it"++ data-ovtip-icon="fa-wand-magic-sparkles"++ data-ovtip-accent="#f472b6"++ data-ovtip-place="top"++ data-ovtip="Chat, streams, clips, overlays, robots, sound commands, whole games — every feature here has an <b>open API</b>, and the entire site is <b>open source</b>. <em>You do not need to know how to code.</em> Point an AI at these docs, describe the thing you want, and it writes it for you."++ data-ovtip-foot="<i class="fa-solid fa-heart"></i> We love what people build here — come make something"++ data-ovtip-go="Open the API docs"><span class="hb-ico"><i class="fa-solid fa-wand-magic-sparkles"></i></span><span class="hb-label">Build <span class="hb-amp">& Mod</span> <small>API docs</small></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:4" 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:5" 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>++ </nav>++ </div>++ <!-- Signed-in streamers with setup left to do see their next task here, in the row of++ things to do rather than a screen below (public/js/guides-live.js → setupHeroNextUp). -->++ <div class="tour-quest-slot hero-quest-slot" id="hero-next-up" data-fx-viewport hidden></div>++ <div class="hero-philosophy-links">++ <a href="#home-cta-banner" class="hero-philosophy-btn" onclick="event.preventDefault();openHomeAbout();">++ <i class="fa-solid fa-book-open"></i> What is OpenVibe?++ </a>++ <span class="hero-philosophy-sep" aria-hidden="true">•</span>++ <span class="hero-legal"><a href="/tos" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/tos')">Terms</a>++ <span aria-hidden="true">•</span>++ <a href="/privacy" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/privacy')">Privacy</a>++ <span aria-hidden="true">•</span>++ <a href="/dmca" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/dmca')">DMCA</a></span>++ </div>++ <!-- Newest shipped commit — built in the open, receipts included. Sits under the++ philosophy row so the proof is next to the promise. -->++ <a class="hero-latest" id="hero-latest" data-ov-shipped="latest" data-service="live" href="/updates" onclick="return handleLinkClick(event, '/updates')"></a>++ <div class="hero-stats" id="hero-stats" aria-label="Site stats"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div></div></div>++ </div>++</div>++++<div class="container">++ <!-- Featured live stream (public/js/home-featured.js): filled only when someone is live. -->++ <div class="home-featured" id="home-featured" hidden></div>++ <!-- Live Streams -->++ <div class="section-header" style="--sc:#ef4444">++ <h2><i class="fa-solid fa-circle live-dot"></i> Live Now<small class="section-kicker">on air</small></h2>++ <span id="live-count" class="badge">0</span>++ <button type="button" class="home-featured-toggle" id="home-featured-toggle" hidden><i class="fa-solid fa-satellite-dish"></i> Show featured stream</button>++ </div>++ <div class="stream-grid" id="stream-grid-live">++ <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>++ <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>++ <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>++ <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>++ <div class="empty-state" id="no-live-streams" style="display:none">++ <i class="fa-solid fa-tower-broadcast fa-3x"></i>++ <p>No one is streaming right now</p>++ <!-- This was a line of grey text telling people to go live with no way to do it.++ It is the same button as the one under the tour, routed through the same++ entry point, so signing in lands you back in the go-live journey. -->++ <button type="button" class="ovg-firstlive" onclick="if (window.startGoLiveJourney) startGoLiveJourney(); else location.href='/broadcast';">++ <span class="ovg-firstlive-ring" aria-hidden="true"></span>++ <span class="ovg-firstlive-sheen" aria-hidden="true"></span>++ <i class="fa-solid fa-tower-broadcast"></i>++ <span>Be the first to go live</span>++ <i class="fa-solid fa-arrow-right ovg-firstlive-go"></i>++ </button>++ </div>++ </div>++++ <!-- "While you were away" (signed-in, returning) / "Lately on OpenVibe" (everyone) — site activity digest -->++ <div class="home-digest" id="home-digest"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div></div></div>++++ <!-- Star of OpenVibe — featured streamer spotlight (star_streamer setting / STAR_STREAMER env) -->++ <div class="home-star-section" id="home-star-section"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div><div class="ovsk-bar" style="height:10px;width:80%"></div></div></div>++++ <div class="hero-egg" id="hero-egg" style="display:none">++ <button class="hero-egg-head" type="button" onclick="toggleHeroEgg()">++ <span class="hero-egg-icon">🥚</span>++ <span class="hero-egg-titles"><span class="hero-egg-kicker">Today's Secret</span><span class="hero-egg-title" id="hero-egg-title"></span></span>++ <span class="hero-egg-status" id="hero-egg-status"></span>++ <i class="fa-solid fa-chevron-down hero-egg-chev"></i>++ </button>++ <div class="hero-egg-body">++ <div class="hero-egg-inner">++ <p class="hero-egg-intro">A new secret every day. Each clue is one key, in order. Type the keys anywhere on the site, or tap them below.</p>++ <div class="hero-egg-slots" id="hero-egg-slots" role="group" aria-label="Your key sequence"></div>++ <ol class="hero-egg-clues" id="hero-egg-clues"></ol>++ <div class="hero-egg-pad" id="hero-egg-pad">++ <div class="hero-egg-pad-arrows">++ <button type="button" class="egg-key" data-tok="left" aria-label="Left">←</button>++ <button type="button" class="egg-key" data-tok="up" aria-label="Up">↑</button>++ <button type="button" class="egg-key" data-tok="down" aria-label="Down">↓</button>++ <button type="button" class="egg-key" data-tok="right" aria-label="Right">→</button>++ <button type="button" class="egg-key egg-key--ctl" data-act="back" aria-label="Backspace">⌫</button>++ <button type="button" class="egg-key egg-key--ctl" data-act="clear" aria-label="Clear">✕</button>++ </div>++ <div class="hero-egg-pad-letters" id="hero-egg-letters"></div>++ </div>++ <div class="hero-egg-foot">++ <button type="button" class="hero-egg-reveal" id="hero-egg-reveal" onclick="_eggReveal()"><i class="fa-solid fa-lightbulb"></i> Stuck? Reveal a key</button>++ <div class="hero-egg-meta"><span id="hero-egg-count"></span><span id="hero-egg-first"></span><span id="hero-egg-reset"></span></div>++ </div>++ </div>++ </div>++ </div>++++ <!-- Community pulse: goals near completion, latest activity, weekly leaders -->++ <div class="home-section-lazy" id="home-pulse-section" style="display:none">++ <div class="section-header" style="--sc:#f59e0b">++ <h2><i class="fa-solid fa-bolt"></i> Happening Now<small class="section-kicker">the pulse</small></h2>++ </div>++ <div class="pulse-grid" id="pulse-grid"></div>++ </div>++++ <!-- What people made, above the introduction to the site (roadmap 32.7: the home leads with live++ content and creators): channels that were just on, their recordings, the clips people took++ and their pastes. What the AI made follows it (AI Moments; 33.6: made, then derived). -->++ <!-- Recently Online -->++ <div class="section-header" style="--sc:#60a5fa">++ <h2><i class="fa-solid fa-clock-rotate-left"></i> Recently Online<small class="section-kicker">just missed them</small></h2>++ </div>++ <div class="stream-grid" id="stream-grid-recent">++ <div class="empty-state">++ <p class="muted">No recent streams</p>++ </div>++ </div>++ <div id="stream-grid-recent-pagination" class="pastes-pagination" style="display:none"></div>++++ <!-- Recent VODs -->++ <div class="section-header" style="--sc:#a78bfa;display:none" id="home-recent-vods-header">++ <h2><i class="fa-solid fa-video"></i> Recent VODs<small class="section-kicker">full streams</small></h2>++ <a href="/content?type=vods" onclick="return handleLinkClick(event, '/content?type=vods')" class="section-action">View all</a>++ </div>++ <div class="stream-grid" id="home-recent-vods-grid"></div>++ <div id="home-recent-vods-pagination" class="pastes-pagination" style="display:none"></div>++++ <!-- Recent Clips -->++ <div class="section-header" style="--sc:#f472b6;display:none" id="home-clips-header">++ <h2><i class="fa-solid fa-scissors"></i> Recent Clips<small class="section-kicker">the good bits</small></h2>++ <a href="/content?type=clips" onclick="return handleLinkClick(event, '/content?type=clips')" class="section-action">View all</a>++ </div>++ <div class="stream-grid home-clips-grid" id="home-clips-grid"></div>++ <div id="home-clips-pagination" class="pastes-pagination" style="display:none"></div>++++ <!-- Recent Pastes -->++ <div class="section-header" style="--sc:#2dd4bf;display:none" id="home-pastes-header">++ <h2><i class="fa-solid fa-paste"></i> Recent Pastes<small class="section-kicker">shots & snippets</small></h2>++ <a href="/content?type=pastes" onclick="return handleLinkClick(event, '/content?type=pastes')" class="section-action">View all</a>++ </div>++ <div class="home-pastes-list" id="home-pastes-list"></div>++ <div id="home-pastes-pagination" class="pastes-pagination" style="display:none"></div>++++ <!-- AI Moments showcase -->++ <div class="home-section-lazy" id="home-moments-section" style="display:none">++ <div class="section-header" style="--sc:#22d3ee">++ <h2><i class="fa-solid fa-brain"></i> AI Moments<small class="section-kicker">picked by AI</small></h2>++ <a href="/moments" onclick="return handleLinkClick(event, '/moments')" class="section-action">See all</a>++ </div>++ <div class="moments-row ov-rail-track" id="home-moments-row"></div>++ </div>++++ <!-- The two introductions travel together: what OpenVibe does (the restream diagram), then++ why it exists (the About banner). demoteHomeIntroSections() moves them as a pair. -->++ <!-- One stream, everywhere — restream pipeline + feature tour (public/js/home-tour.js) -->++ <div class="home-tour-mount home-section-lazy" id="home-tour-mount" data-fx-viewport></div>++ <!-- Streaming CTA -->++ <div class="home-cta-banner" id="home-cta-banner" data-fx-viewport>++ <div class="cta-ambient" aria-hidden="true"></div>++ <div class="cta-embers" aria-hidden="true">++ <span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>++ </div>++ <div class="home-cta-inner">++++ <!-- The all-caps eyebrow here ("HOME-GROWN & HOBBYIST-RUN — NOT ANOTHER CORPORATE++ STREAM SITE") was a louder restatement of the lede two lines below it, which++ already says the platform is built by one person who streams here. One of them++ is enough, and the lede is the one that reads like a human wrote it. -->++ <div class="cta-chips" aria-hidden="true">++ <span class="cta-chip"><i class="fa-solid fa-people-group"></i> Community-run</span>++ <span class="cta-chip"><i class="fa-solid fa-comment"></i> Free speech, light rules</span>++ <span class="cta-chip"><i class="fa-solid fa-code-branch"></i> Built in the open</span>++ </div>++ <h2 class="cta-hero">Streaming the way it <span class="cta-hero-grad">should</span> be.</h2>++ <p class="cta-lede">++ An <strong>open, community-run</strong> streaming platform, built in public by people who stream and hang out in chat. The ideas from chat are the ones that ship. <strong>Free speech, light-touch moderation, no mystery bans.</strong>++ </p>++++ <!-- Always shown (not behind "See what makes it different"): going live from the browser with++ no OBS and no follower minimum is what people search and ask AI assistants for. -->++ <div class="cta-actions">++ <a class="cta-btn cta-btn-primary" href="/broadcast" onclick="return goLiveNav(event)"><i class="fa-solid fa-bolt" aria-hidden="true"></i> Go live from your browser</a>++ <a class="cta-btn cta-btn-ghost" href="/docs/go-live-in-your-browser">How it works</a>++ </div>++ <ul class="cta-checks" aria-label="What going live needs">++ <li><i class="fa-solid fa-check" aria-hidden="true"></i> No OBS</li>++ <li><i class="fa-solid fa-check" aria-hidden="true"></i> No downloads</li>++ <li><i class="fa-solid fa-check" aria-hidden="true"></i> No follower minimum</li>++ <li><i class="fa-solid fa-check" aria-hidden="true"></i> Chromebook, laptop or phone</li>++ </ul>++++ <button class="home-about-toggle" type="button" onclick="toggleHomeAbout()" aria-expanded="false" aria-controls="home-about-body">++ <span class="home-about-toggle-more"><i class="fa-solid fa-circle-plus"></i> See what makes it different</span>++ <span class="home-about-toggle-less"><i class="fa-solid fa-circle-minus"></i> Show less</span>++ </button>++++ <div class="home-about-body" id="home-about-body">++ <!-- Three ways to go live -->++ <div class="cta-section-label" style="--sc:#60a5fa"><i class="fa-solid fa-tower-broadcast"></i> Three ways to go live</div>++ <div class="cta-methods">++ <div class="cta-method">++ <div class="cta-method-num">01</div>++ <div class="cta-method-icon"><i class="fa-solid fa-bolt"></i></div>++ <strong>Right in your browser</strong>++ <span>One click, zero software. Powered by <b>WebRTC</b> for <em>sub-second, ultra-low latency</em> — the fastest way to go live anywhere.</span>++ <div class="cta-method-tag">WebRTC · <1s latency</div>++ </div>++ <div class="cta-method">++ <div class="cta-method-num">02</div>++ <div class="cta-method-icon"><i class="fa-solid fa-sliders"></i></div>++ <strong>OBS & RTMP</strong>++ <span>Plug in OBS, Streamlabs, or any RTMP encoder for a full scene-based, pro-grade production workflow.</span>++ <div class="cta-method-tag">RTMP · pro setups</div>++ </div>++ <div class="cta-method">++ <div class="cta-method-num">03</div>++ <div class="cta-method-icon"><i class="fa-solid fa-terminal"></i></div>++ <strong>ffmpeg & CLI</strong>++ <span>Headless and fully scriptable — stream straight from a Raspberry Pi, a server, or an embedded device.</span>++ <div class="cta-method-tag">jsmpeg · headless</div>++ </div>++ </div>++++ <!-- Differentiators -->++ <div class="cta-section-label cta-diff-label" style="--sc:#fbbf24"><i class="fa-solid fa-crown"></i> Only on OpenVibe</div>++ <div class="home-cta-features">++ <div class="home-cta-feature feat-star">++ <i class="fa-solid fa-robot"></i>++ <strong>Viewer-controlled robots</strong>++ <span>Hand chat the controls: robots, PTZ cameras and interactive rigs over the same sub-second WebRTC pipeline — low enough latency to actually play.</span>++ </div>++ <div class="home-cta-feature">++ <i class="fa-solid fa-tower-broadcast"></i>++ <strong>Restream everywhere at once</strong>++ <span>Push a single broadcast to Twitch, YouTube, Kick, RobotStreamer & any RTMP server simultaneously — from one dashboard.</span>++ </div>++ <div class="home-cta-feature">++ <i class="fa-solid fa-gamepad"></i>++ <strong>A game next door</strong>++ <span>Scraplandia lives on OpenVibe.Games — same account, leaderboards and a shared canvas, one click from any stream.</span>++ </div>++ <div class="home-cta-feature">++ <i class="fa-solid fa-earth-americas"></i>++ <strong>Global chat + voice</strong>++ <span>A sitewide chatroom and drop-in voice channels connect everyone across every stream — no Discord required.</span>++ </div>++ <div class="home-cta-feature">++ <i class="fa-solid fa-bullseye"></i>++ <strong>Donation goals & Vibes</strong>++ <span>Set funding goals with custom art, celebrate live in chat, and keep a real creator economy — with fair, transparent payouts.</span>++ </div>++ <div class="home-cta-feature">++ <i class="fa-solid fa-wand-magic-sparkles"></i>++ <strong>AI clips, VODs & transcripts</strong>++ <span>Every broadcast auto-records, clips on the fly, and gets AI overviews + searchable audio transcripts.</span>++ </div>++ <div class="home-cta-feature">++ <i class="fa-solid fa-paste"></i>++ <strong>Pastes & file sharing</strong>++ <span>Share code, logs and files with syntax highlighting and a short link. Pastes live on OpenVibe.Community and still show up on your channel.</span>++ </div>++ <div class="home-cta-feature">++ <i class="fa-solid fa-screwdriver-wrench"></i>++ <strong>160+ online tools</strong>++ <span>Image & audio editing, PDF tools, video downloads, network diagnostics, dev tools and more — all at openvibe.tools.</span>++ </div>++ <div class="home-cta-feature feat-star">++ <i class="fa-solid fa-code"></i>++ <strong>100% open source</strong>++ <span>Every line is public on GitHub. Read it, fork it, or send a pull request — features here get built because someone asked.</span>++ </div>++ </div>++++ <div class="home-cta-buttons">++ <button class="btn btn-primary btn-lg cta-btn-glow" onclick="if (window.startGoLiveJourney) startGoLiveJourney(); else if (currentUser) navigate('/broadcast'); else showModal('register');">++ <i class="fa-solid fa-broadcast-tower"></i> <span class="cta-btn-long">Launch your stream</span><span class="cta-btn-short">Go live</span>++ </button>++ <a href="/chat" class="btn btn-outline btn-lg" onclick="return handleLinkClick(event, '/chat')">++ <i class="fa-solid fa-comments"></i> Say hi in chat++ </a>++ </div>++++ <!-- Where the money goes — part of the same About section -->++ <div class="cta-section-label cta-diff-label" style="--sc:#4ade80"><i class="fa-solid fa-seedling"></i> Where the money goes</div>++ <p class="cta-lede cta-money-lede">Whatever the network earns goes back into it — the servers, the storage and bandwidth that free streaming really costs, new features, and the creators and events that make this place worth showing up to.</p>++ <div class="home-cta-features cta-money-grid">++ <div class="home-cta-feature">++ <i class="fa-solid fa-plug-circle-bolt"></i>++ <strong>Keep it running</strong>++ <span>Hosting, bandwidth, storage and development come first. Streaming is not free to host, and being honest about that is how the network stays open to everyone.</span>++ </div>++ <div class="home-cta-feature">++ <i class="fa-solid fa-heart"></i>++ <strong>Back the creators</strong>++ <span>What is left over goes to the people who fill the place: creator payouts, community events, prize pools and the tools streamers ask for.</span>++ </div>++ <div class="home-cta-feature">++ <i class="fa-solid fa-code-branch"></i>++ <strong>No surprise pivot</strong>++ <span>Where it goes stays visible. No selling the community out, no bait-and-switch, no dark patterns.</span>++ </div>++ </div>++ </div><!-- /home-about-body -->++ </div>++ </div>++++</div>++++<div class="container">++ <div class="network-hub">++ <div class="network-hub-header">++ <i class="fa-solid fa-network-wired"></i>++ <div>++ <h3>The OpenVibe Network</h3>++ <p>One account, 14 services — streaming, games, maps, food, image tools, audio tools, text generators, logo makers, PDF & document tools, network diagnostics, developer tools, video downloads, and more.</p>++ </div>++ </div>++ <div class="network-hub-grid">++ <a href="https://openvibe.live" class="network-card network-card-featured">++ <div class="network-card-icon" style="--card-accent:#e74c3c"><i class="fa-solid fa-tower-broadcast"></i></div>++ <div class="network-card-info">++ <strong>OpenVibe.<wbr>Live</strong>++ <span>Live streaming, VODs, clips & chat</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://openvibe.tools" class="network-card">++ <div class="network-card-icon" style="--card-accent:#3b82f6"><i class="fa-solid fa-screwdriver-wrench"></i></div>++ <div class="network-card-info">++ <strong>OpenVibe.<wbr>Tools</strong>++ <span>Every online tool in one index</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://openvibe.community" class="network-card">++ <div class="network-card-icon" style="--card-accent:#22c55e"><i class="fa-solid fa-people-group"></i></div>++ <div class="network-card-info">++ <strong>OpenVibe.<wbr>Community</strong>++ <span>Pastes, posts and people</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://openvibe.games" class="network-card">++ <div class="network-card-icon" style="--card-accent:#a855f7"><i class="fa-solid fa-hat-wizard"></i></div>++ <div class="network-card-info">++ <strong>Scraplandia</strong>++ <span>Browser RPG & community games</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">++ <strong>OpenVibe.<wbr>Media</strong>++ <span>VODs, clips and files</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://yt.openvibe.tools" class="network-card">++ <div class="network-card-icon" style="--card-accent:#ef4444"><i class="fa-solid fa-circle-play"></i></div>++ <div class="network-card-info">++ <strong>YouTube Downloader</strong>++ <span>Save video or audio</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://openvibe.tools/image-tools" class="network-card">++ <div class="network-card-icon" style="--card-accent:#ec4899"><i class="fa-solid fa-image"></i></div>++ <div class="network-card-info">++ <strong>Image Tools</strong>++ <span>Convert, compress, resize, crop</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://openvibe.tools/audio-tools" class="network-card">++ <div class="network-card-icon" style="--card-accent:#14b8a6"><i class="fa-solid fa-music"></i></div>++ <div class="network-card-info">++ <strong>Audio Tools</strong>++ <span>Convert, trim, pitch and clean up</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://openvibe.tools/pdf-tools" class="network-card">++ <div class="network-card-icon" style="--card-accent:#f97316"><i class="fa-solid fa-file-pdf"></i></div>++ <div class="network-card-info">++ <strong>PDF & Documents</strong>++ <span>Merge, split, compress, protect</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://openvibe.tools/text-tools" class="network-card">++ <div class="network-card-icon" style="--card-accent:#8b5cf6"><i class="fa-solid fa-font"></i></div>++ <div class="network-card-info">++ <strong>Text & Logo Tools</strong>++ <span>Fancy text, logos, counters</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://openvibe.tools/developer-tools" class="network-card">++ <div class="network-card-icon" style="--card-accent:#06b6d4"><i class="fa-solid fa-code"></i></div>++ <div class="network-card-info">++ <strong>Developer Tools</strong>++ <span>JSON, regex, JWT, diff, hashes</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://openvibe.tools/network-tools" class="network-card">++ <div class="network-card-icon" style="--card-accent:#10b981"><i class="fa-solid fa-network-wired"></i></div>++ <div class="network-card-info">++ <strong>Network Tools</strong>++ <span>DNS, WHOIS, SSL, ping, ports</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://maps.openvibe.tools" class="network-card">++ <div class="network-card-icon" style="--card-accent:#2ecc71"><i class="fa-solid fa-map-location-dot"></i></div>++ <div class="network-card-info">++ <strong>Survival Map</strong>++ <span>Camps, resources & shelters</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://food.openvibe.tools" class="network-card">++ <div class="network-card-icon" style="--card-accent:#eab308"><i class="fa-solid fa-utensils"></i></div>++ <div class="network-card-info">++ <strong>Budget Food Finder</strong>++ <span>Cheap meals and food banks</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ <a href="https://openvibe.network/my" class="network-card">++ <div class="network-card-icon" style="--card-accent:var(--accent, #3b82f6)"><i class="fa-solid fa-user-circle"></i></div>++ <div class="network-card-info">++ <strong>My Account</strong>++ <span>Profile, themes, history, linked services</span>++ </div>++ <i class="fa-solid fa-arrow-right network-card-arrow"></i>++ </a>++ </div>++ </div>++++ <!-- OpenVibe.Live Changelog -->++ <div class="section-header" style="--sc:#94a3b8;margin-top:32px;">++ <h2><i class="fa-solid fa-code-commit"></i> Recent Changes<small class="section-kicker">built in the open</small></h2>++ <a href="/updates" onclick="return handleLinkClick(event, '/updates')" class="section-action">View all</a>++ </div>++ <div id="home-changelog-wrapper" class="home-changelog-wrapper collapsed">++ <div id="home-changelog" class="home-changelog">++ <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>++ </div>++ <div class="home-changelog-fade" id="home-changelog-fade"></div>++ </div>++ <!-- Expands the preview in place; "View all" in the header opens /updates. It sits under++ the list it expands, in the same button style as the rails' "Load more". -->++ <button type="button" id="home-changelog-toggle" class="home-load-more home-changelog-toggle" onclick="toggleHomeChangelog()" aria-expanded="false" aria-controls="home-changelog-wrapper"><i class="fa-solid fa-chevron-down" aria-hidden="true"></i><span>Show all recent changes</span></button>++++</div>nnew file mode 100644@@@ -0,0 +1,5 @@++<div class="content-container rundown-container">++ <div id="rundown-root">++ <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>++ </div>++</div>nnew file mode 100644@@@ -0,0 +1,9 @@++<div class="content-container">++ <div class="page-header">++ <h1><i class="fa-solid fa-code-commit"></i> Updates</h1>++ <p class="page-subtitle">Every change deployed to OpenVibe.Live, newest first: each line is a commit from the <a href="https://github.com/OpenVibers/OpenVibe.Live" target="_blank" rel="noopener">OpenVibers repositories</a>, and batches become <a href="https://openvibe.blog/@openvibe" rel="noopener">Patch notes on openvibe.blog</a>. See <a href="https://openvibe.network/updates">the whole network</a>.</p>++ </div>++ <div id="updates-list" class="updates-list">++ <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>++ </div>++</div>@@@ -167,511 +167,7 @@<main id="app"><!-- ── HOME: Stream List ─────────────────────────────── -->-- <section id="page-home" class="page active">-- <div class="hero" data-fx-viewport>-- <div class="hero-bg" id="hero-bg" aria-hidden="true"><div class="hero-bg-layer"></div><div class="hero-bg-layer"></div></div>-- <div class="hero-aurora" aria-hidden="true"></div>-- <div class="hero-collage" id="hero-collage" aria-hidden="true"></div>-- <div class="hero-scrim" aria-hidden="true"></div>-- <div class="hero-content">-- <h1><span class="brand-mark brand-mark--hero" aria-hidden="true"><svg viewBox="0 0 48 48">-- <defs>-- <linearGradient id="ovh-g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="currentColor"/><stop offset="1" stop-color="currentColor" stop-opacity="0.45"/></linearGradient>-- <linearGradient id="ovh-v" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="currentColor"/><stop offset="1" stop-color="#fff" stop-opacity="0.9"/><animateTransform attributeName="gradientTransform" type="translate" from="-1 0" to="1 0" dur="3.2s" repeatCount="indefinite"/></linearGradient>-- <path id="ovh-ring" d="M24,6 A18,18 0 1,1 23.99,6 Z"/>-- </defs>-- <circle class="ov-glow" cx="24" cy="24" r="21"/>-- <circle class="ov-ring" cx="24" cy="24" r="18"/>-- <circle class="ov-comet" cx="24" cy="24" r="18" stroke="url(#ovh-g)"/>-- <path class="ov-v" d="M14.5,17 L24,34 L33.5,17" stroke="url(#ovh-v)"/>-- <circle class="ov-dot" cx="24" cy="34" r="2.6"/>-- <circle class="ov-orbit" r="2"><animateMotion dur="3.6s" repeatCount="indefinite" rotate="auto"><mpath href="#ovh-ring"/></animateMotion></circle>-- <circle class="ov-orbit ov-orbit--tail" r="1.4"><animateMotion dur="3.6s" begin="0.14s" repeatCount="indefinite"><mpath href="#ovh-ring"/></animateMotion></circle>-- <circle class="ov-spark" cx="24" cy="24" r="1.3"/><circle class="ov-spark ov-spark--2" cx="24" cy="24" r="1"/><circle class="ov-spark ov-spark--3" cx="24" cy="24" r="1.1"/>-- </svg></span> OpenVibe.Live</h1>-- <!-- The first word and quip ship in the HTML so the hero is complete at first paint; the-- rotators take over once the page's data arrives. No aria-live: a slogan that changes-- every few seconds would be read out to screen-reader users over and over. -->-- <p class="hero-tagline">Live streaming for <span class="hero-rotate-wrap"><span class="hero-rotate visible" id="hero-rotate">stealth campers</span></span></p>-- <p class="hero-quip visible" id="hero-quip">No investors. No suits. Just vibes.</p>-- <!-- Holds its line from the first paint (the countdown fills it with the page's data), so the buttons below do not move. -->-- <div class="hero-slogan-timer" id="hero-slogan-timer"> </div>-- <!-- One primary action and its promise, then the other destinations (owner, 2026-10-18: "whole area is kinda-- messy and spacing is weird"; six equal buttons wrapped 5 + 1 with the no-OBS line floating between-- rows). The headline feature stays in the first screen: going live from a browser with no OBS and-- no follower minimum is what people search and ask AI assistants for. -->-- <div class="hero-cta" data-fx-viewport>-- <div class="hero-primary">-- <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">Go live <small>in your browser</small></span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></button>-- <p class="hero-golive-note"><span><i class="fa-solid fa-check" aria-hidden="true"></i> No OBS</span><span><i class="fa-solid fa-check" aria-hidden="true"></i> No downloads</span><span><i class="fa-solid fa-check" aria-hidden="true"></i> No follower minimum</span><a href="/docs/go-live-in-your-browser">How it works <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></p>-- </div>-- <nav class="hero-links" aria-label="More from OpenVibe">-- <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" 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="/documentation" onclick="return handleLinkClick(event, '/documentation')" style="--i:3" class="hb hb-vibe"-- data-ovtip-title="Mod it, bot it, build on it"-- data-ovtip-icon="fa-wand-magic-sparkles"-- data-ovtip-accent="#f472b6"-- data-ovtip-place="top"-- data-ovtip="Chat, streams, clips, overlays, robots, sound commands, whole games — every feature here has an <b>open API</b>, and the entire site is <b>open source</b>. <em>You do not need to know how to code.</em> Point an AI at these docs, describe the thing you want, and it writes it for you."-- data-ovtip-foot="<i class="fa-solid fa-heart"></i> We love what people build here — come make something"-- data-ovtip-go="Open the API docs"><span class="hb-ico"><i class="fa-solid fa-wand-magic-sparkles"></i></span><span class="hb-label">Build <span class="hb-amp">& Mod</span> <small>API docs</small></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:4" 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:5" 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>-- </nav>-- </div>-- <!-- Signed-in streamers with setup left to do see their next task here, in the row of-- things to do rather than a screen below (public/js/guides-live.js → setupHeroNextUp). -->-- <div class="tour-quest-slot hero-quest-slot" id="hero-next-up" data-fx-viewport hidden></div>-- <div class="hero-philosophy-links">-- <a href="#home-cta-banner" class="hero-philosophy-btn" onclick="event.preventDefault();openHomeAbout();">-- <i class="fa-solid fa-book-open"></i> What is OpenVibe?-- </a>-- <span class="hero-philosophy-sep" aria-hidden="true">•</span>-- <span class="hero-legal"><a href="/tos" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/tos')">Terms</a>-- <span aria-hidden="true">•</span>-- <a href="/privacy" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/privacy')">Privacy</a>-- <span aria-hidden="true">•</span>-- <a href="/dmca" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/dmca')">DMCA</a></span>-- </div>-- <!-- Newest shipped commit — built in the open, receipts included. Sits under the-- philosophy row so the proof is next to the promise. -->-- <a class="hero-latest" id="hero-latest" data-ov-shipped="latest" data-service="live" href="/updates" onclick="return handleLinkClick(event, '/updates')"></a>-- <div class="hero-stats" id="hero-stats" aria-label="Site stats"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div></div></div>-- </div>-- </div>---- <div class="container">-- <!-- Featured live stream (public/js/home-featured.js): filled only when someone is live. -->-- <div class="home-featured" id="home-featured" hidden></div>-- <!-- Live Streams -->-- <div class="section-header" style="--sc:#ef4444">-- <h2><i class="fa-solid fa-circle live-dot"></i> Live Now<small class="section-kicker">on air</small></h2>-- <span id="live-count" class="badge">0</span>-- <button type="button" class="home-featured-toggle" id="home-featured-toggle" hidden><i class="fa-solid fa-satellite-dish"></i> Show featured stream</button>-- </div>-- <div class="stream-grid" id="stream-grid-live">-- <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>-- <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>-- <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>-- <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>-- <div class="empty-state" id="no-live-streams" style="display:none">-- <i class="fa-solid fa-tower-broadcast fa-3x"></i>-- <p>No one is streaming right now</p>-- <!-- This was a line of grey text telling people to go live with no way to do it.-- It is the same button as the one under the tour, routed through the same-- entry point, so signing in lands you back in the go-live journey. -->-- <button type="button" class="ovg-firstlive" onclick="if (window.startGoLiveJourney) startGoLiveJourney(); else location.href='/broadcast';">-- <span class="ovg-firstlive-ring" aria-hidden="true"></span>-- <span class="ovg-firstlive-sheen" aria-hidden="true"></span>-- <i class="fa-solid fa-tower-broadcast"></i>-- <span>Be the first to go live</span>-- <i class="fa-solid fa-arrow-right ovg-firstlive-go"></i>-- </button>-- </div>-- </div>---- <!-- "While you were away" (signed-in, returning) / "Lately on OpenVibe" (everyone) — site activity digest -->-- <div class="home-digest" id="home-digest"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div></div></div>---- <!-- Star of OpenVibe — featured streamer spotlight (star_streamer setting / STAR_STREAMER env) -->-- <div class="home-star-section" id="home-star-section"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div><div class="ovsk-bar" style="height:10px;width:80%"></div></div></div>---- <div class="hero-egg" id="hero-egg" style="display:none">-- <button class="hero-egg-head" type="button" onclick="toggleHeroEgg()">-- <span class="hero-egg-icon">🥚</span>-- <span class="hero-egg-titles"><span class="hero-egg-kicker">Today's Secret</span><span class="hero-egg-title" id="hero-egg-title"></span></span>-- <span class="hero-egg-status" id="hero-egg-status"></span>-- <i class="fa-solid fa-chevron-down hero-egg-chev"></i>-- </button>-- <div class="hero-egg-body">-- <div class="hero-egg-inner">-- <p class="hero-egg-intro">A new secret every day. Each clue is one key, in order. Type the keys anywhere on the site, or tap them below.</p>-- <div class="hero-egg-slots" id="hero-egg-slots" role="group" aria-label="Your key sequence"></div>-- <ol class="hero-egg-clues" id="hero-egg-clues"></ol>-- <div class="hero-egg-pad" id="hero-egg-pad">-- <div class="hero-egg-pad-arrows">-- <button type="button" class="egg-key" data-tok="left" aria-label="Left">←</button>-- <button type="button" class="egg-key" data-tok="up" aria-label="Up">↑</button>-- <button type="button" class="egg-key" data-tok="down" aria-label="Down">↓</button>-- <button type="button" class="egg-key" data-tok="right" aria-label="Right">→</button>-- <button type="button" class="egg-key egg-key--ctl" data-act="back" aria-label="Backspace">⌫</button>-- <button type="button" class="egg-key egg-key--ctl" data-act="clear" aria-label="Clear">✕</button>-- </div>-- <div class="hero-egg-pad-letters" id="hero-egg-letters"></div>-- </div>-- <div class="hero-egg-foot">-- <button type="button" class="hero-egg-reveal" id="hero-egg-reveal" onclick="_eggReveal()"><i class="fa-solid fa-lightbulb"></i> Stuck? Reveal a key</button>-- <div class="hero-egg-meta"><span id="hero-egg-count"></span><span id="hero-egg-first"></span><span id="hero-egg-reset"></span></div>-- </div>-- </div>-- </div>-- </div>---- <!-- Community pulse: goals near completion, latest activity, weekly leaders -->-- <div class="home-section-lazy" id="home-pulse-section" style="display:none">-- <div class="section-header" style="--sc:#f59e0b">-- <h2><i class="fa-solid fa-bolt"></i> Happening Now<small class="section-kicker">the pulse</small></h2>-- </div>-- <div class="pulse-grid" id="pulse-grid"></div>-- </div>---- <!-- What people made, above the introduction to the site (roadmap 32.7: the home leads with live-- content and creators): channels that were just on, their recordings, the clips people took-- and their pastes. What the AI made follows it (AI Moments; 33.6: made, then derived). -->-- <!-- Recently Online -->-- <div class="section-header" style="--sc:#60a5fa">-- <h2><i class="fa-solid fa-clock-rotate-left"></i> Recently Online<small class="section-kicker">just missed them</small></h2>-- </div>-- <div class="stream-grid" id="stream-grid-recent">-- <div class="empty-state">-- <p class="muted">No recent streams</p>-- </div>-- </div>-- <div id="stream-grid-recent-pagination" class="pastes-pagination" style="display:none"></div>---- <!-- Recent VODs -->-- <div class="section-header" style="--sc:#a78bfa;display:none" id="home-recent-vods-header">-- <h2><i class="fa-solid fa-video"></i> Recent VODs<small class="section-kicker">full streams</small></h2>-- <a href="/content?type=vods" onclick="return handleLinkClick(event, '/content?type=vods')" class="section-action">View all</a>-- </div>-- <div class="stream-grid" id="home-recent-vods-grid"></div>-- <div id="home-recent-vods-pagination" class="pastes-pagination" style="display:none"></div>---- <!-- Recent Clips -->-- <div class="section-header" style="--sc:#f472b6;display:none" id="home-clips-header">-- <h2><i class="fa-solid fa-scissors"></i> Recent Clips<small class="section-kicker">the good bits</small></h2>-- <a href="/content?type=clips" onclick="return handleLinkClick(event, '/content?type=clips')" class="section-action">View all</a>-- </div>-- <div class="stream-grid home-clips-grid" id="home-clips-grid"></div>-- <div id="home-clips-pagination" class="pastes-pagination" style="display:none"></div>---- <!-- Recent Pastes -->-- <div class="section-header" style="--sc:#2dd4bf;display:none" id="home-pastes-header">-- <h2><i class="fa-solid fa-paste"></i> Recent Pastes<small class="section-kicker">shots & snippets</small></h2>-- <a href="/content?type=pastes" onclick="return handleLinkClick(event, '/content?type=pastes')" class="section-action">View all</a>-- </div>-- <div class="home-pastes-list" id="home-pastes-list"></div>-- <div id="home-pastes-pagination" class="pastes-pagination" style="display:none"></div>---- <!-- AI Moments showcase -->-- <div class="home-section-lazy" id="home-moments-section" style="display:none">-- <div class="section-header" style="--sc:#22d3ee">-- <h2><i class="fa-solid fa-brain"></i> AI Moments<small class="section-kicker">picked by AI</small></h2>-- <a href="/moments" onclick="return handleLinkClick(event, '/moments')" class="section-action">See all</a>-- </div>-- <div class="moments-row ov-rail-track" id="home-moments-row"></div>-- </div>---- <!-- The two introductions travel together: what OpenVibe does (the restream diagram), then-- why it exists (the About banner). demoteHomeIntroSections() moves them as a pair. -->-- <!-- One stream, everywhere — restream pipeline + feature tour (public/js/home-tour.js) -->-- <div class="home-tour-mount home-section-lazy" id="home-tour-mount" data-fx-viewport></div>-- <!-- Streaming CTA -->-- <div class="home-cta-banner" id="home-cta-banner" data-fx-viewport>-- <div class="cta-ambient" aria-hidden="true"></div>-- <div class="cta-embers" aria-hidden="true">-- <span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>-- </div>-- <div class="home-cta-inner">---- <!-- The all-caps eyebrow here ("HOME-GROWN & HOBBYIST-RUN — NOT ANOTHER CORPORATE-- STREAM SITE") was a louder restatement of the lede two lines below it, which-- already says the platform is built by one person who streams here. One of them-- is enough, and the lede is the one that reads like a human wrote it. -->-- <div class="cta-chips" aria-hidden="true">-- <span class="cta-chip"><i class="fa-solid fa-people-group"></i> Community-run</span>-- <span class="cta-chip"><i class="fa-solid fa-comment"></i> Free speech, light rules</span>-- <span class="cta-chip"><i class="fa-solid fa-code-branch"></i> Built in the open</span>-- </div>-- <h2 class="cta-hero">Streaming the way it <span class="cta-hero-grad">should</span> be.</h2>-- <p class="cta-lede">-- An <strong>open, community-run</strong> streaming platform, built in public by people who stream and hang out in chat. The ideas from chat are the ones that ship. <strong>Free speech, light-touch moderation, no mystery bans.</strong>-- </p>---- <!-- Always shown (not behind "See what makes it different"): going live from the browser with-- no OBS and no follower minimum is what people search and ask AI assistants for. -->-- <div class="cta-actions">-- <a class="cta-btn cta-btn-primary" href="/broadcast" onclick="return goLiveNav(event)"><i class="fa-solid fa-bolt" aria-hidden="true"></i> Go live from your browser</a>-- <a class="cta-btn cta-btn-ghost" href="/docs/go-live-in-your-browser">How it works</a>-- </div>-- <ul class="cta-checks" aria-label="What going live needs">-- <li><i class="fa-solid fa-check" aria-hidden="true"></i> No OBS</li>-- <li><i class="fa-solid fa-check" aria-hidden="true"></i> No downloads</li>-- <li><i class="fa-solid fa-check" aria-hidden="true"></i> No follower minimum</li>-- <li><i class="fa-solid fa-check" aria-hidden="true"></i> Chromebook, laptop or phone</li>-- </ul>---- <button class="home-about-toggle" type="button" onclick="toggleHomeAbout()" aria-expanded="false" aria-controls="home-about-body">-- <span class="home-about-toggle-more"><i class="fa-solid fa-circle-plus"></i> See what makes it different</span>-- <span class="home-about-toggle-less"><i class="fa-solid fa-circle-minus"></i> Show less</span>-- </button>---- <div class="home-about-body" id="home-about-body">-- <!-- Three ways to go live -->-- <div class="cta-section-label" style="--sc:#60a5fa"><i class="fa-solid fa-tower-broadcast"></i> Three ways to go live</div>-- <div class="cta-methods">-- <div class="cta-method">-- <div class="cta-method-num">01</div>-- <div class="cta-method-icon"><i class="fa-solid fa-bolt"></i></div>-- <strong>Right in your browser</strong>-- <span>One click, zero software. Powered by <b>WebRTC</b> for <em>sub-second, ultra-low latency</em> — the fastest way to go live anywhere.</span>-- <div class="cta-method-tag">WebRTC · <1s latency</div>-- </div>-- <div class="cta-method">-- <div class="cta-method-num">02</div>-- <div class="cta-method-icon"><i class="fa-solid fa-sliders"></i></div>-- <strong>OBS & RTMP</strong>-- <span>Plug in OBS, Streamlabs, or any RTMP encoder for a full scene-based, pro-grade production workflow.</span>-- <div class="cta-method-tag">RTMP · pro setups</div>-- </div>-- <div class="cta-method">-- <div class="cta-method-num">03</div>-- <div class="cta-method-icon"><i class="fa-solid fa-terminal"></i></div>-- <strong>ffmpeg & CLI</strong>-- <span>Headless and fully scriptable — stream straight from a Raspberry Pi, a server, or an embedded device.</span>-- <div class="cta-method-tag">jsmpeg · headless</div>-- </div>-- </div>---- <!-- Differentiators -->-- <div class="cta-section-label cta-diff-label" style="--sc:#fbbf24"><i class="fa-solid fa-crown"></i> Only on OpenVibe</div>-- <div class="home-cta-features">-- <div class="home-cta-feature feat-star">-- <i class="fa-solid fa-robot"></i>-- <strong>Viewer-controlled robots</strong>-- <span>Hand chat the controls: robots, PTZ cameras and interactive rigs over the same sub-second WebRTC pipeline — low enough latency to actually play.</span>-- </div>-- <div class="home-cta-feature">-- <i class="fa-solid fa-tower-broadcast"></i>-- <strong>Restream everywhere at once</strong>-- <span>Push a single broadcast to Twitch, YouTube, Kick, RobotStreamer & any RTMP server simultaneously — from one dashboard.</span>-- </div>-- <div class="home-cta-feature">-- <i class="fa-solid fa-gamepad"></i>-- <strong>A game next door</strong>-- <span>Scraplandia lives on OpenVibe.Games — same account, leaderboards and a shared canvas, one click from any stream.</span>-- </div>-- <div class="home-cta-feature">-- <i class="fa-solid fa-earth-americas"></i>-- <strong>Global chat + voice</strong>-- <span>A sitewide chatroom and drop-in voice channels connect everyone across every stream — no Discord required.</span>-- </div>-- <div class="home-cta-feature">-- <i class="fa-solid fa-bullseye"></i>-- <strong>Donation goals & Vibes</strong>-- <span>Set funding goals with custom art, celebrate live in chat, and keep a real creator economy — with fair, transparent payouts.</span>-- </div>-- <div class="home-cta-feature">-- <i class="fa-solid fa-wand-magic-sparkles"></i>-- <strong>AI clips, VODs & transcripts</strong>-- <span>Every broadcast auto-records, clips on the fly, and gets AI overviews + searchable audio transcripts.</span>-- </div>-- <div class="home-cta-feature">-- <i class="fa-solid fa-paste"></i>-- <strong>Pastes & file sharing</strong>-- <span>Share code, logs and files with syntax highlighting and a short link. Pastes live on OpenVibe.Community and still show up on your channel.</span>-- </div>-- <div class="home-cta-feature">-- <i class="fa-solid fa-screwdriver-wrench"></i>-- <strong>160+ online tools</strong>-- <span>Image & audio editing, PDF tools, video downloads, network diagnostics, dev tools and more — all at openvibe.tools.</span>-- </div>-- <div class="home-cta-feature feat-star">-- <i class="fa-solid fa-code"></i>-- <strong>100% open source</strong>-- <span>Every line is public on GitHub. Read it, fork it, or send a pull request — features here get built because someone asked.</span>-- </div>-- </div>---- <div class="home-cta-buttons">-- <button class="btn btn-primary btn-lg cta-btn-glow" onclick="if (window.startGoLiveJourney) startGoLiveJourney(); else if (currentUser) navigate('/broadcast'); else showModal('register');">-- <i class="fa-solid fa-broadcast-tower"></i> <span class="cta-btn-long">Launch your stream</span><span class="cta-btn-short">Go live</span>-- </button>-- <a href="/chat" class="btn btn-outline btn-lg" onclick="return handleLinkClick(event, '/chat')">-- <i class="fa-solid fa-comments"></i> Say hi in chat-- </a>-- </div>---- <!-- Where the money goes — part of the same About section -->-- <div class="cta-section-label cta-diff-label" style="--sc:#4ade80"><i class="fa-solid fa-seedling"></i> Where the money goes</div>-- <p class="cta-lede cta-money-lede">Whatever the network earns goes back into it — the servers, the storage and bandwidth that free streaming really costs, new features, and the creators and events that make this place worth showing up to.</p>-- <div class="home-cta-features cta-money-grid">-- <div class="home-cta-feature">-- <i class="fa-solid fa-plug-circle-bolt"></i>-- <strong>Keep it running</strong>-- <span>Hosting, bandwidth, storage and development come first. Streaming is not free to host, and being honest about that is how the network stays open to everyone.</span>-- </div>-- <div class="home-cta-feature">-- <i class="fa-solid fa-heart"></i>-- <strong>Back the creators</strong>-- <span>What is left over goes to the people who fill the place: creator payouts, community events, prize pools and the tools streamers ask for.</span>-- </div>-- <div class="home-cta-feature">-- <i class="fa-solid fa-code-branch"></i>-- <strong>No surprise pivot</strong>-- <span>Where it goes stays visible. No selling the community out, no bait-and-switch, no dark patterns.</span>-- </div>-- </div>-- </div><!-- /home-about-body -->-- </div>-- </div>---- </div>---- <div class="container">-- <div class="network-hub">-- <div class="network-hub-header">-- <i class="fa-solid fa-network-wired"></i>-- <div>-- <h3>The OpenVibe Network</h3>-- <p>One account, 14 services — streaming, games, maps, food, image tools, audio tools, text generators, logo makers, PDF & document tools, network diagnostics, developer tools, video downloads, and more.</p>-- </div>-- </div>-- <div class="network-hub-grid">-- <a href="https://openvibe.live" class="network-card network-card-featured">-- <div class="network-card-icon" style="--card-accent:#e74c3c"><i class="fa-solid fa-tower-broadcast"></i></div>-- <div class="network-card-info">-- <strong>OpenVibe.<wbr>Live</strong>-- <span>Live streaming, VODs, clips & chat</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://openvibe.tools" class="network-card">-- <div class="network-card-icon" style="--card-accent:#3b82f6"><i class="fa-solid fa-screwdriver-wrench"></i></div>-- <div class="network-card-info">-- <strong>OpenVibe.<wbr>Tools</strong>-- <span>Every online tool in one index</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://openvibe.community" class="network-card">-- <div class="network-card-icon" style="--card-accent:#22c55e"><i class="fa-solid fa-people-group"></i></div>-- <div class="network-card-info">-- <strong>OpenVibe.<wbr>Community</strong>-- <span>Pastes, posts and people</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://openvibe.games" class="network-card">-- <div class="network-card-icon" style="--card-accent:#a855f7"><i class="fa-solid fa-hat-wizard"></i></div>-- <div class="network-card-info">-- <strong>Scraplandia</strong>-- <span>Browser RPG & community games</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">-- <strong>OpenVibe.<wbr>Media</strong>-- <span>VODs, clips and files</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://yt.openvibe.tools" class="network-card">-- <div class="network-card-icon" style="--card-accent:#ef4444"><i class="fa-solid fa-circle-play"></i></div>-- <div class="network-card-info">-- <strong>YouTube Downloader</strong>-- <span>Save video or audio</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://openvibe.tools/image-tools" class="network-card">-- <div class="network-card-icon" style="--card-accent:#ec4899"><i class="fa-solid fa-image"></i></div>-- <div class="network-card-info">-- <strong>Image Tools</strong>-- <span>Convert, compress, resize, crop</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://openvibe.tools/audio-tools" class="network-card">-- <div class="network-card-icon" style="--card-accent:#14b8a6"><i class="fa-solid fa-music"></i></div>-- <div class="network-card-info">-- <strong>Audio Tools</strong>-- <span>Convert, trim, pitch and clean up</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://openvibe.tools/pdf-tools" class="network-card">-- <div class="network-card-icon" style="--card-accent:#f97316"><i class="fa-solid fa-file-pdf"></i></div>-- <div class="network-card-info">-- <strong>PDF & Documents</strong>-- <span>Merge, split, compress, protect</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://openvibe.tools/text-tools" class="network-card">-- <div class="network-card-icon" style="--card-accent:#8b5cf6"><i class="fa-solid fa-font"></i></div>-- <div class="network-card-info">-- <strong>Text & Logo Tools</strong>-- <span>Fancy text, logos, counters</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://openvibe.tools/developer-tools" class="network-card">-- <div class="network-card-icon" style="--card-accent:#06b6d4"><i class="fa-solid fa-code"></i></div>-- <div class="network-card-info">-- <strong>Developer Tools</strong>-- <span>JSON, regex, JWT, diff, hashes</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://openvibe.tools/network-tools" class="network-card">-- <div class="network-card-icon" style="--card-accent:#10b981"><i class="fa-solid fa-network-wired"></i></div>-- <div class="network-card-info">-- <strong>Network Tools</strong>-- <span>DNS, WHOIS, SSL, ping, ports</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://maps.openvibe.tools" class="network-card">-- <div class="network-card-icon" style="--card-accent:#2ecc71"><i class="fa-solid fa-map-location-dot"></i></div>-- <div class="network-card-info">-- <strong>Survival Map</strong>-- <span>Camps, resources & shelters</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://food.openvibe.tools" class="network-card">-- <div class="network-card-icon" style="--card-accent:#eab308"><i class="fa-solid fa-utensils"></i></div>-- <div class="network-card-info">-- <strong>Budget Food Finder</strong>-- <span>Cheap meals and food banks</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- <a href="https://openvibe.network/my" class="network-card">-- <div class="network-card-icon" style="--card-accent:var(--accent, #3b82f6)"><i class="fa-solid fa-user-circle"></i></div>-- <div class="network-card-info">-- <strong>My Account</strong>-- <span>Profile, themes, history, linked services</span>-- </div>-- <i class="fa-solid fa-arrow-right network-card-arrow"></i>-- </a>-- </div>-- </div>---- <!-- OpenVibe.Live Changelog -->-- <div class="section-header" style="--sc:#94a3b8;margin-top:32px;">-- <h2><i class="fa-solid fa-code-commit"></i> Recent Changes<small class="section-kicker">built in the open</small></h2>-- <a href="/updates" onclick="return handleLinkClick(event, '/updates')" class="section-action">View all</a>-- </div>-- <div id="home-changelog-wrapper" class="home-changelog-wrapper collapsed">-- <div id="home-changelog" class="home-changelog">-- <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>-- </div>-- <div class="home-changelog-fade" id="home-changelog-fade"></div>-- </div>-- <!-- Expands the preview in place; "View all" in the header opens /updates. It sits under-- the list it expands, in the same button style as the rails' "Load more". -->-- <button type="button" id="home-changelog-toggle" class="home-load-more home-changelog-toggle" onclick="toggleHomeChangelog()" aria-expanded="false" aria-controls="home-changelog-wrapper"><i class="fa-solid fa-chevron-down" aria-hidden="true"></i><span>Show all recent changes</span></button>---------- </div>-- </section>++ <section id="page-home" class="page active" data-fragment="home"><!--ov:fragment:home--><div class="ov-route-skel" aria-hidden="true"><span></span><span></span><span></span></div><!--/ov:fragment--></section><!-- ── STREAM VIEWER (legacy, redirect to channel) ──── --><section id="page-stream" class="page">@@@ -724,17 +220,7 @@<!-- ═══════════════════════════════════════════════════════════ --><!-- UPDATES / CHANGELOG --><!-- ═══════════════════════════════════════════════════════════ -->-- <section id="page-updates" class="page">-- <div class="content-container">-- <div class="page-header">-- <h1><i class="fa-solid fa-code-commit"></i> Updates</h1>-- <p class="page-subtitle">Every change deployed to OpenVibe.Live, newest first: each line is a commit from the <a href="https://github.com/OpenVibers/OpenVibe.Live" target="_blank" rel="noopener">OpenVibers repositories</a>, and batches become <a href="https://openvibe.blog/@openvibe" rel="noopener">Patch notes on openvibe.blog</a>. See <a href="https://openvibe.network/updates">the whole network</a>.</p>-- </div>-- <div id="updates-list" class="updates-list">-- <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>-- </div>-- </div>-- </section>++ <section id="page-updates" class="page" data-fragment="updates"><!--ov:fragment:updates--><div class="ov-route-skel" aria-hidden="true"><span></span><span></span><span></span></div><!--/ov:fragment--></section><!-- ═══════════════════════════════════════════════════════════ --><!-- PASTE VIEWER -->@@@ -757,13 +243,7 @@</div></section>-- <section id="page-rundown" class="page">-- <div class="content-container rundown-container">-- <div id="rundown-root">-- <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>-- </div>-- </div>-- </section>++ <section id="page-rundown" class="page" data-fragment="rundown"><!--ov:fragment:rundown--><div class="ov-route-skel" aria-hidden="true"><span></span><span></span><span></span></div><!--/ov:fragment--></section><section id="page-documentation" class="page" data-fragment="documentation"><!--ov:fragment:documentation--><div class="ov-route-skel" aria-hidden="true"><span></span><span></span><span></span></div><!--/ov:fragment--></section><!-- A path no route knows (the server answers it 404): filled by showNotFound() in app.js -->@@@ -1158,7 +1158,7 @@ function routeFromURL() {whenRouteReady('rundown', () => { if (typeof loadRundownPage === 'function') loadRundownPage(segments); });} else if (segments[0] === 'updates') {showPage('updates');-- loadUpdatesPage();++ whenRouteReady('updates', () => loadUpdatesPage());} else if (segments[0] === 'dmca') {window.location.replace('/dmca');return;@@@ -295,14 +295,15 @@ function renderRoute(html, urlPath) {* visit to a channel, a VOD or the chat painted the home page (its hero and lists) until the router ran and swapped* pages — a flash of the wrong page and the largest layout shifts measured on the site (CLS 1.0 on a channel page,* 0.5 on a VOD, phone profile). The route's last feature with a page fragment names its section; the router's own-- * showPage() then finds it already active.++ * showPage() then finds it already active. The home page is a fragment too (inlined on "/" only), so on any other++ * route the shell carries its placeholder, not the hero and its <h1>.*/function activePage(html, names, reg) {const frag = [...names].reverse().map((n) => reg.features[n] && reg.features[n].fragment).find((f) => f && html.includes(`class="page" data-fragment="${f}"`));if (!frag) return html;return html-- .replace('<section id="page-home" class="page active">', '<section id="page-home" class="page">')++ .replace('<section id="page-home" class="page active"', '<section id="page-home" class="page"').replace(`class="page" data-fragment="${frag}"`, `class="page active" data-fragment="${frag}"`);}@@@ -49,8 +49,9 @@ ok('VOD and clip pages live on OpenVibe.Video');// ── Home hero and early widgets ──const index = read('public/index.html');--assert.ok(/<div class="hero-slogan-timer" id="hero-slogan-timer"> <\/div>/.test(index), 'the countdown line is held from the first paint');--assert.ok(/<a class="hero-latest" id="hero-latest"[^>]*><\/a>/.test(index) && !/id="hero-latest"[^>]*\shidden>/.test(index), 'the shipped line is not hidden (shipped.js would collapse it, then refill it)');++const homeHtml = read('public/fragments/home.html');++assert.ok(/<div class="hero-slogan-timer" id="hero-slogan-timer"> <\/div>/.test(homeHtml), 'the countdown line is held from the first paint');++assert.ok(/<a class="hero-latest" id="hero-latest"[^>]*><\/a>/.test(homeHtml) && !/id="hero-latest"[^>]*\shidden>/.test(homeHtml), 'the shipped line is not hidden (shipped.js would collapse it, then refill it)');const home = read('public/css/features/home.css');assert.ok(home.includes('.hero-latest:empty { visibility: hidden; min-height: 33px; }'), 'the empty shipped line keeps its height, invisibly');assert.ok(/\.hero-stats \{ margin: 16px 0 10px; gap: 10px; min-height: 120px; \}/.test(home) && home.includes('.hero-stats { min-height: 204px; }') && home.includes('.hero-stats { min-height: 256px; margin-top: 14px; }'),nnew file mode 100644@@@ -0,0 +1,46 @@++'use strict';++/**++ * Every page's shell carries one <h1>: its own. The home page (hero and lists), the Updates page and the Rundown++ * are fragments like the other pages, inlined only on their own route (server/web/assets.js renderRoute). Before++ * 2026-10-24 every page shipped the home hero's <h1> and the Updates page's, so a channel page had three, and++ * every page that was not the home page carried the home page's 37 KB of markup.++ *++ * node test/shell-one-h1.test.js++ */++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const assets = require('../server/web/assets');++try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */ }++++const doc = assets.document('index.html').html;++const h1s = (html) => (html.replace(/<template[\s\S]*?<\/template>/gi, '').match(/<h1\b/gi) || []).length;++assert.strictEqual(h1s(doc), 0, 'the bare shell has no page markup with an <h1>');++++for (const url of ['/', '/@someone', '/updates', '/content', '/clips', '/moments', '/search', '/rundown', '/documentation', '/chat', '/dashboard', '/broadcast']) {++ const html = assets.renderRoute(doc, url);++ assert.ok(h1s(html) <= 1, `${url}: at most one <h1> (${h1s(html)})`);++ assert.strictEqual(html.includes('class="hero-tagline"'), url === '/', `${url}: the home hero ${url === '/' ? 'is' : 'is not'} in the page`);++}++for (const url of ['/', '/updates', '/content', '/moments', '/search', '/@someone']) {++ assert.strictEqual(h1s(assets.renderRoute(doc, url)), 1, `${url}: its page's <h1> is there from the first paint`);++}++++// The home, Updates and Rundown pages are features with fragments, and the router waits for them.++const reg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'public', 'features.json'), 'utf8'));++for (const [feature, frag] of [['home', 'home'], ['updates', 'updates'], ['rundown', 'rundown']]) {++ assert.strictEqual(reg.features[feature].fragment, frag, `${feature} has its fragment`);++ assert.ok(fs.existsSync(path.join(__dirname, '..', 'public', 'fragments', `${frag}.html`)), `${frag}.html exists`);++ assert.ok(doc.includes(`data-fragment="${frag}"><!--ov:fragment:${frag}-->`), `the shell holds ${frag}'s placeholder`);++}++assert.ok(reg.routes.some((r) => new RegExp(r.path).test('/updates') && r.features.includes('updates')), '/updates loads its feature');++const app = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');++assert.ok(app.includes("whenRouteReady('updates', () => loadUpdatesPage())"), 'the Updates page fills once its markup is in');++++// A route of another page keeps the home placeholder inactive; "/" keeps it active.++const active = (html) => [...html.matchAll(/<section id="(page-[a-z-]+)" class="page active"/g)].map((m) => m[1]);++assert.deepStrictEqual(active(assets.renderRoute(doc, '/')), ['page-home']);++assert.deepStrictEqual(active(assets.renderRoute(doc, '/rundown')), ['page-rundown']);++assert.deepStrictEqual(active(assets.renderRoute(doc, '/updates')), ['page-updates']);++++console.log('shell one h1: all passed');@@@ -11,8 +11,8 @@ const ROOT = path.join(__dirname, '..');const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');// The shared views--const index = read('public/index.html');--assert.ok(/id="hero-latest" data-ov-shipped="latest" data-service="live" href="\/updates"/.test(index), 'the hero one-liner is the shared view');++const homeHtml = read('public/fragments/home.html');++assert.ok(/id="hero-latest" data-ov-shipped="latest" data-service="live" href="\/updates"/.test(homeHtml), 'the hero one-liner is the shared view');const app = read('public/js/app.js');assert.ok(app.includes("sc.src = '/shared/shipped.js'") && app.includes("sc.setAttribute('data-ov-shipped-src', '')"), 'shipped.js from this site\'s own /shared/ pin, with the footer\'s marker');assert.ok(/shipped\.log\(container, \{ service: 'live'/.test(app), '/updates is the shared log');
@@ -30,7 +30,7 @@ BASE=http://127.0.0.1:3000 npm run test:browser # Browser smoke (running server
@@ -30,7 +30,7 @@ BASE=http://127.0.0.1:3000 npm run test:browser # Browser smoke (running server
- **Database:** [server/db/database.js](server/db/database.js) — all queries (async, `openvibe-sdk/db`), schema in [migrations/](migrations/) (`0001_analytics.sql`, `0002_live.sql`)
- **Database:** [server/db/database.js](server/db/database.js) — all queries (async, `openvibe-sdk/db`), schema in [migrations/](migrations/) (`0001_analytics.sql`, `0002_live.sql`)
- **Auth:** [server/auth/auth.js](server/auth/auth.js) — RS256 JWT from openvibe.tools SSO + `hbt_` API tokens
- **Auth:** [server/auth/auth.js](server/auth/auth.js) — RS256 JWT from openvibe.tools SSO + `hbt_` API tokens
- **Permissions:** [server/auth/permissions.js](server/auth/permissions.js) — role hierarchy: `user < streamer < global_mod < admin`
- **Permissions:** [server/auth/permissions.js](server/auth/permissions.js) — role hierarchy: `user < streamer < global_mod < admin`
-- **Frontend shell:** [public/index.html](public/index.html) — navbar, home page and empty `<section id="page-*">` shells; routing via `history.pushState` in [public/js/app.js](public/js/app.js)
+- **Frontend shell:** [public/index.html](public/index.html) — navbar and `<section id="page-*">` shells (page markup, the home page's included, is in `public/fragments/`, inlined on its own route); routing via `history.pushState` in [public/js/app.js](public/js/app.js)
- **Route loading:** [public/features.json](public/features.json) maps routes → features (fragment in `public/fragments/`, CSS in `public/css/features/`, scripts, deps, stubs); [public/js/ov-loader.js](public/js/ov-loader.js) loads them. New page code goes in a feature, not in `index.html`/core `app.js`. See [docs/architecture.md](docs/architecture.md#frontend-loading).
- **Route loading:** [public/features.json](public/features.json) maps routes → features (fragment in `public/fragments/`, CSS in `public/css/features/`, scripts, deps, stubs); [public/js/ov-loader.js](public/js/ov-loader.js) loads them. New page code goes in a feature, not in `index.html`/core `app.js`. See [docs/architecture.md](docs/architecture.md#frontend-loading).
Each feature lives in its own `server/<feature>/` directory with `routes.js` + service files. Frontend: one JS file per feature in `public/js/`, registered in `public/features.json`.
Each feature lives in its own `server/<feature>/` directory with `routes.js` + service files. Frontend: one JS file per feature in `public/js/`, registered in `public/features.json`.
@@ -39,10 +39,11 @@ OpenVibe.Live is a self-hosted live streaming platform, one of the OpenVibe serv
@@ -39,10 +39,11 @@ OpenVibe.Live is a self-hosted live streaming platform, one of the OpenVibe serv
The SPA is plain JavaScript with no build step. What loads is decided per route:
The SPA is plain JavaScript with no build step. What loads is decided per route:
```
```
-public/index.html shell: navbar, home page, empty <section id="page-*"> shells, core scripts
+public/index.html shell: navbar, empty <section id="page-*"> shells, core scripts (no page markup)
public/features.json registry: feature → fragment, stylesheets, scripts, dependencies, stubs, idle prefetch
public/features.json registry: feature → fragment, stylesheets, scripts, dependencies, stubs, idle prefetch
routes: URL pattern → features
routes: URL pattern → features
-public/fragments/*.html page markup for channel, dashboard, broadcast, chat, documentation, content, moments (VOD and clip pages are OpenVibe.Video's)
+public/fragments/*.html page markup for home, channel, dashboard, broadcast, chat, documentation, content, moments, search,
+ rundown, updates (VOD and clip pages are OpenVibe.Video's)
public/css/features/*.css rules used only by one feature (split out of style.css / broadcast.css / i18n-star.css)
public/css/features/*.css rules used only by one feature (split out of style.css / broadcast.css / i18n-star.css)
public/js/ov-loader.js configures /shared/web-runtime.js (openvibe-shared): ov.load(feature), ov.route(path), ov.prefetch(),
public/js/ov-loader.js configures /shared/web-runtime.js (openvibe-shared): ov.load(feature), ov.route(path), ov.prefetch(),
route generations, scopes, stubs, transactional asset groups, ov.diagnostics()/ov.leaks()
route generations, scopes, stubs, transactional asset groups, ov.diagnostics()/ov.leaks()
@@ -52,7 +53,9 @@ public/js/app-*.js route code split out of app.js: home, channel, media, c
@@ -52,7 +53,9 @@ public/js/app-*.js route code split out of app.js: home, channel, media, c
- **First paint.** `server/web/assets.js` renders `index.html` for the requested path: the route's
- **First paint.** `server/web/assets.js` renders `index.html` for the requested path: the route's
stylesheets go in `<head>` (right after `style.css`), its scripts after the core scripts, and its
stylesheets go in `<head>` (right after `style.css`), its scripts after the core scripts, and its
- fragment is inlined into the section shell. A direct visit needs no extra round trip.
+ fragment is inlined into the section shell. A direct visit needs no extra round trip, and a page
+ carries only its own markup: the home page's hero and lists are inlined on `/` alone, so every
+ other page has one `<h1>`, its own (`test/shell-one-h1.test.js`).
- **Navigation.** `routeFromURL()` calls `whenRouteReady(page, render)`: the loader inserts the
- **Navigation.** `routeFromURL()` calls `whenRouteReady(page, render)`: the loader inserts the
feature's markup first (modules bind to it on load), then dependencies, stylesheets and scripts in
feature's markup first (modules bind to it on load), then dependencies, stylesheets and scripts in
order, runs the feature's `after` hook once, and renders only if the route generation is still
order, runs the feature's `after` hook once, and renders only if the route generation is still
@@ -2,6 +2,7 @@
@@ -2,6 +2,7 @@
"_comment": "Feature and route registry, read by server/web/assets.js (first paint: which assets the requested route needs) and by public/js/ov-loader.js (navigation: what to load before a route renders). Scripts in a feature run in the listed order, after the feature's dependencies. `fragment` names public/fragments/<name>.html, inserted into section #page-<section> before any of the feature's scripts run. `after` names a global function called once when the feature has loaded. `stubs` are global functions that inline handlers call; until the feature is loaded they load it and then call the real function. See docs/architecture.md#frontend-loading.",
"_comment": "Feature and route registry, read by server/web/assets.js (first paint: which assets the requested route needs) and by public/js/ov-loader.js (navigation: what to load before a route renders). Scripts in a feature run in the listed order, after the feature's dependencies. `fragment` names public/fragments/<name>.html, inserted into section #page-<section> before any of the feature's scripts run. `after` names a global function called once when the feature has loaded. `stubs` are global functions that inline handlers call; until the feature is loaded they load it and then call the real function. See docs/architecture.md#frontend-loading.",
"features": {
"features": {
"home": {
"home": {
+ "fragment": "home",
"css": [
"css": [
"/css/features/home.css",
"/css/features/home.css",
"/css/home-fx.css",
"/css/home-fx.css",
@@ -232,7 +233,7 @@
@@ -232,7 +233,7 @@
"regenerateDashStreamKey"
"regenerateDashStreamKey"
]
]
},
},
- "rundown": {"css": ["/css/features/rundown.css"], "js": ["/js/rundown.js"]},
+ "rundown": {"fragment": "rundown", "css": ["/css/features/rundown.css"], "js": ["/js/rundown.js"]},
"recap": {
"recap": {
"css": [
"css": [
"/css/recap.css"
"/css/recap.css"
@@ -249,6 +250,9 @@
@@ -249,6 +250,9 @@
"openCosmeticsModal"
"openCosmeticsModal"
]
]
},
},
+ "updates": {
+ "fragment": "updates"
+ },
"docs": {
"docs": {
"fragment": "documentation",
"fragment": "documentation",
"css": [
"css": [
@@ -343,6 +347,7 @@
@@ -343,6 +347,7 @@
]
]
},
},
{"path": "^/rundown", "features": ["rundown"]}, {"path": "^/arena", "features": ["rundown"]},
{"path": "^/rundown", "features": ["rundown"]}, {"path": "^/arena", "features": ["rundown"]},
+ {"path": "^/updates/?$", "features": ["updates"]},
{
{
"path": "^/documentation",
"path": "^/documentation",
"features": [
"features": [
new file mode 100644
new file mode 100644
@@ -0,0 +1,500 @@
@@ -0,0 +1,500 @@
+<div class="hero" data-fx-viewport>
+ <div class="hero-bg" id="hero-bg" aria-hidden="true"><div class="hero-bg-layer"></div><div class="hero-bg-layer"></div></div>
+ <div class="hero-aurora" aria-hidden="true"></div>
+ <div class="hero-collage" id="hero-collage" aria-hidden="true"></div>
+ <div class="hero-scrim" aria-hidden="true"></div>
+ <div class="hero-content">
+ <h1><span class="brand-mark brand-mark--hero" aria-hidden="true"><svg viewBox="0 0 48 48">
+ <defs>
+ <linearGradient id="ovh-g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="currentColor"/><stop offset="1" stop-color="currentColor" stop-opacity="0.45"/></linearGradient>
+ <linearGradient id="ovh-v" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="currentColor"/><stop offset="1" stop-color="#fff" stop-opacity="0.9"/><animateTransform attributeName="gradientTransform" type="translate" from="-1 0" to="1 0" dur="3.2s" repeatCount="indefinite"/></linearGradient>
+ <path id="ovh-ring" d="M24,6 A18,18 0 1,1 23.99,6 Z"/>
+ </defs>
+ <circle class="ov-glow" cx="24" cy="24" r="21"/>
+ <circle class="ov-ring" cx="24" cy="24" r="18"/>
+ <circle class="ov-comet" cx="24" cy="24" r="18" stroke="url(#ovh-g)"/>
+ <path class="ov-v" d="M14.5,17 L24,34 L33.5,17" stroke="url(#ovh-v)"/>
+ <circle class="ov-dot" cx="24" cy="34" r="2.6"/>
+ <circle class="ov-orbit" r="2"><animateMotion dur="3.6s" repeatCount="indefinite" rotate="auto"><mpath href="#ovh-ring"/></animateMotion></circle>
+ <circle class="ov-orbit ov-orbit--tail" r="1.4"><animateMotion dur="3.6s" begin="0.14s" repeatCount="indefinite"><mpath href="#ovh-ring"/></animateMotion></circle>
+ <circle class="ov-spark" cx="24" cy="24" r="1.3"/><circle class="ov-spark ov-spark--2" cx="24" cy="24" r="1"/><circle class="ov-spark ov-spark--3" cx="24" cy="24" r="1.1"/>
+</svg></span> OpenVibe.Live</h1>
+ <!-- The first word and quip ship in the HTML so the hero is complete at first paint; the
+ rotators take over once the page's data arrives. No aria-live: a slogan that changes
+ every few seconds would be read out to screen-reader users over and over. -->
+ <p class="hero-tagline">Live streaming for <span class="hero-rotate-wrap"><span class="hero-rotate visible" id="hero-rotate">stealth campers</span></span></p>
+ <p class="hero-quip visible" id="hero-quip">No investors. No suits. Just vibes.</p>
+ <!-- Holds its line from the first paint (the countdown fills it with the page's data), so the buttons below do not move. -->
+ <div class="hero-slogan-timer" id="hero-slogan-timer"> </div>
+ <!-- One primary action and its promise, then the other destinations (owner, 2026-10-18: "whole area is kinda
+ messy and spacing is weird"; six equal buttons wrapped 5 + 1 with the no-OBS line floating between
+ rows). The headline feature stays in the first screen: going live from a browser with no OBS and
+ no follower minimum is what people search and ask AI assistants for. -->
+ <div class="hero-cta" data-fx-viewport>
+ <div class="hero-primary">
+ <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">Go live <small>in your browser</small></span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></button>
+ <p class="hero-golive-note"><span><i class="fa-solid fa-check" aria-hidden="true"></i> No OBS</span><span><i class="fa-solid fa-check" aria-hidden="true"></i> No downloads</span><span><i class="fa-solid fa-check" aria-hidden="true"></i> No follower minimum</span><a href="/docs/go-live-in-your-browser">How it works <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></p>
+ </div>
+ <nav class="hero-links" aria-label="More from OpenVibe">
+ <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" 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="/documentation" onclick="return handleLinkClick(event, '/documentation')" style="--i:3" class="hb hb-vibe"
+ data-ovtip-title="Mod it, bot it, build on it"
+ data-ovtip-icon="fa-wand-magic-sparkles"
+ data-ovtip-accent="#f472b6"
+ data-ovtip-place="top"
+ data-ovtip="Chat, streams, clips, overlays, robots, sound commands, whole games — every feature here has an <b>open API</b>, and the entire site is <b>open source</b>. <em>You do not need to know how to code.</em> Point an AI at these docs, describe the thing you want, and it writes it for you."
+ data-ovtip-foot="<i class="fa-solid fa-heart"></i> We love what people build here — come make something"
+ data-ovtip-go="Open the API docs"><span class="hb-ico"><i class="fa-solid fa-wand-magic-sparkles"></i></span><span class="hb-label">Build <span class="hb-amp">& Mod</span> <small>API docs</small></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:4" 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:5" 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>
+ </nav>
+ </div>
+ <!-- Signed-in streamers with setup left to do see their next task here, in the row of
+ things to do rather than a screen below (public/js/guides-live.js → setupHeroNextUp). -->
+ <div class="tour-quest-slot hero-quest-slot" id="hero-next-up" data-fx-viewport hidden></div>
+ <div class="hero-philosophy-links">
+ <a href="#home-cta-banner" class="hero-philosophy-btn" onclick="event.preventDefault();openHomeAbout();">
+ <i class="fa-solid fa-book-open"></i> What is OpenVibe?
+ </a>
+ <span class="hero-philosophy-sep" aria-hidden="true">•</span>
+ <span class="hero-legal"><a href="/tos" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/tos')">Terms</a>
+ <span aria-hidden="true">•</span>
+ <a href="/privacy" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/privacy')">Privacy</a>
+ <span aria-hidden="true">•</span>
+ <a href="/dmca" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/dmca')">DMCA</a></span>
+ </div>
+ <!-- Newest shipped commit — built in the open, receipts included. Sits under the
+ philosophy row so the proof is next to the promise. -->
+ <a class="hero-latest" id="hero-latest" data-ov-shipped="latest" data-service="live" href="/updates" onclick="return handleLinkClick(event, '/updates')"></a>
+ <div class="hero-stats" id="hero-stats" aria-label="Site stats"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div></div></div>
+ </div>
+</div>
+
+<div class="container">
+ <!-- Featured live stream (public/js/home-featured.js): filled only when someone is live. -->
+ <div class="home-featured" id="home-featured" hidden></div>
+ <!-- Live Streams -->
+ <div class="section-header" style="--sc:#ef4444">
+ <h2><i class="fa-solid fa-circle live-dot"></i> Live Now<small class="section-kicker">on air</small></h2>
+ <span id="live-count" class="badge">0</span>
+ <button type="button" class="home-featured-toggle" id="home-featured-toggle" hidden><i class="fa-solid fa-satellite-dish"></i> Show featured stream</button>
+ </div>
+ <div class="stream-grid" id="stream-grid-live">
+ <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>
+ <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>
+ <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>
+ <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>
+ <div class="empty-state" id="no-live-streams" style="display:none">
+ <i class="fa-solid fa-tower-broadcast fa-3x"></i>
+ <p>No one is streaming right now</p>
+ <!-- This was a line of grey text telling people to go live with no way to do it.
+ It is the same button as the one under the tour, routed through the same
+ entry point, so signing in lands you back in the go-live journey. -->
+ <button type="button" class="ovg-firstlive" onclick="if (window.startGoLiveJourney) startGoLiveJourney(); else location.href='/broadcast';">
+ <span class="ovg-firstlive-ring" aria-hidden="true"></span>
+ <span class="ovg-firstlive-sheen" aria-hidden="true"></span>
+ <i class="fa-solid fa-tower-broadcast"></i>
+ <span>Be the first to go live</span>
+ <i class="fa-solid fa-arrow-right ovg-firstlive-go"></i>
+ </button>
+ </div>
+ </div>
+
+ <!-- "While you were away" (signed-in, returning) / "Lately on OpenVibe" (everyone) — site activity digest -->
+ <div class="home-digest" id="home-digest"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div></div></div>
+
+ <!-- Star of OpenVibe — featured streamer spotlight (star_streamer setting / STAR_STREAMER env) -->
+ <div class="home-star-section" id="home-star-section"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div><div class="ovsk-bar" style="height:10px;width:80%"></div></div></div>
+
+ <div class="hero-egg" id="hero-egg" style="display:none">
+ <button class="hero-egg-head" type="button" onclick="toggleHeroEgg()">
+ <span class="hero-egg-icon">🥚</span>
+ <span class="hero-egg-titles"><span class="hero-egg-kicker">Today's Secret</span><span class="hero-egg-title" id="hero-egg-title"></span></span>
+ <span class="hero-egg-status" id="hero-egg-status"></span>
+ <i class="fa-solid fa-chevron-down hero-egg-chev"></i>
+ </button>
+ <div class="hero-egg-body">
+ <div class="hero-egg-inner">
+ <p class="hero-egg-intro">A new secret every day. Each clue is one key, in order. Type the keys anywhere on the site, or tap them below.</p>
+ <div class="hero-egg-slots" id="hero-egg-slots" role="group" aria-label="Your key sequence"></div>
+ <ol class="hero-egg-clues" id="hero-egg-clues"></ol>
+ <div class="hero-egg-pad" id="hero-egg-pad">
+ <div class="hero-egg-pad-arrows">
+ <button type="button" class="egg-key" data-tok="left" aria-label="Left">←</button>
+ <button type="button" class="egg-key" data-tok="up" aria-label="Up">↑</button>
+ <button type="button" class="egg-key" data-tok="down" aria-label="Down">↓</button>
+ <button type="button" class="egg-key" data-tok="right" aria-label="Right">→</button>
+ <button type="button" class="egg-key egg-key--ctl" data-act="back" aria-label="Backspace">⌫</button>
+ <button type="button" class="egg-key egg-key--ctl" data-act="clear" aria-label="Clear">✕</button>
+ </div>
+ <div class="hero-egg-pad-letters" id="hero-egg-letters"></div>
+ </div>
+ <div class="hero-egg-foot">
+ <button type="button" class="hero-egg-reveal" id="hero-egg-reveal" onclick="_eggReveal()"><i class="fa-solid fa-lightbulb"></i> Stuck? Reveal a key</button>
+ <div class="hero-egg-meta"><span id="hero-egg-count"></span><span id="hero-egg-first"></span><span id="hero-egg-reset"></span></div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Community pulse: goals near completion, latest activity, weekly leaders -->
+ <div class="home-section-lazy" id="home-pulse-section" style="display:none">
+ <div class="section-header" style="--sc:#f59e0b">
+ <h2><i class="fa-solid fa-bolt"></i> Happening Now<small class="section-kicker">the pulse</small></h2>
+ </div>
+ <div class="pulse-grid" id="pulse-grid"></div>
+ </div>
+
+ <!-- What people made, above the introduction to the site (roadmap 32.7: the home leads with live
+ content and creators): channels that were just on, their recordings, the clips people took
+ and their pastes. What the AI made follows it (AI Moments; 33.6: made, then derived). -->
+ <!-- Recently Online -->
+ <div class="section-header" style="--sc:#60a5fa">
+ <h2><i class="fa-solid fa-clock-rotate-left"></i> Recently Online<small class="section-kicker">just missed them</small></h2>
+ </div>
+ <div class="stream-grid" id="stream-grid-recent">
+ <div class="empty-state">
+ <p class="muted">No recent streams</p>
+ </div>
+ </div>
+ <div id="stream-grid-recent-pagination" class="pastes-pagination" style="display:none"></div>
+
+ <!-- Recent VODs -->
+ <div class="section-header" style="--sc:#a78bfa;display:none" id="home-recent-vods-header">
+ <h2><i class="fa-solid fa-video"></i> Recent VODs<small class="section-kicker">full streams</small></h2>
+ <a href="/content?type=vods" onclick="return handleLinkClick(event, '/content?type=vods')" class="section-action">View all</a>
+ </div>
+ <div class="stream-grid" id="home-recent-vods-grid"></div>
+ <div id="home-recent-vods-pagination" class="pastes-pagination" style="display:none"></div>
+
+ <!-- Recent Clips -->
+ <div class="section-header" style="--sc:#f472b6;display:none" id="home-clips-header">
+ <h2><i class="fa-solid fa-scissors"></i> Recent Clips<small class="section-kicker">the good bits</small></h2>
+ <a href="/content?type=clips" onclick="return handleLinkClick(event, '/content?type=clips')" class="section-action">View all</a>
+ </div>
+ <div class="stream-grid home-clips-grid" id="home-clips-grid"></div>
+ <div id="home-clips-pagination" class="pastes-pagination" style="display:none"></div>
+
+ <!-- Recent Pastes -->
+ <div class="section-header" style="--sc:#2dd4bf;display:none" id="home-pastes-header">
+ <h2><i class="fa-solid fa-paste"></i> Recent Pastes<small class="section-kicker">shots & snippets</small></h2>
+ <a href="/content?type=pastes" onclick="return handleLinkClick(event, '/content?type=pastes')" class="section-action">View all</a>
+ </div>
+ <div class="home-pastes-list" id="home-pastes-list"></div>
+ <div id="home-pastes-pagination" class="pastes-pagination" style="display:none"></div>
+
+ <!-- AI Moments showcase -->
+ <div class="home-section-lazy" id="home-moments-section" style="display:none">
+ <div class="section-header" style="--sc:#22d3ee">
+ <h2><i class="fa-solid fa-brain"></i> AI Moments<small class="section-kicker">picked by AI</small></h2>
+ <a href="/moments" onclick="return handleLinkClick(event, '/moments')" class="section-action">See all</a>
+ </div>
+ <div class="moments-row ov-rail-track" id="home-moments-row"></div>
+ </div>
+
+ <!-- The two introductions travel together: what OpenVibe does (the restream diagram), then
+ why it exists (the About banner). demoteHomeIntroSections() moves them as a pair. -->
+ <!-- One stream, everywhere — restream pipeline + feature tour (public/js/home-tour.js) -->
+ <div class="home-tour-mount home-section-lazy" id="home-tour-mount" data-fx-viewport></div>
+ <!-- Streaming CTA -->
+ <div class="home-cta-banner" id="home-cta-banner" data-fx-viewport>
+ <div class="cta-ambient" aria-hidden="true"></div>
+ <div class="cta-embers" aria-hidden="true">
+ <span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
+ </div>
+ <div class="home-cta-inner">
+
+ <!-- The all-caps eyebrow here ("HOME-GROWN & HOBBYIST-RUN — NOT ANOTHER CORPORATE
+ STREAM SITE") was a louder restatement of the lede two lines below it, which
+ already says the platform is built by one person who streams here. One of them
+ is enough, and the lede is the one that reads like a human wrote it. -->
+ <div class="cta-chips" aria-hidden="true">
+ <span class="cta-chip"><i class="fa-solid fa-people-group"></i> Community-run</span>
+ <span class="cta-chip"><i class="fa-solid fa-comment"></i> Free speech, light rules</span>
+ <span class="cta-chip"><i class="fa-solid fa-code-branch"></i> Built in the open</span>
+ </div>
+ <h2 class="cta-hero">Streaming the way it <span class="cta-hero-grad">should</span> be.</h2>
+ <p class="cta-lede">
+ An <strong>open, community-run</strong> streaming platform, built in public by people who stream and hang out in chat. The ideas from chat are the ones that ship. <strong>Free speech, light-touch moderation, no mystery bans.</strong>
+ </p>
+
+ <!-- Always shown (not behind "See what makes it different"): going live from the browser with
+ no OBS and no follower minimum is what people search and ask AI assistants for. -->
+ <div class="cta-actions">
+ <a class="cta-btn cta-btn-primary" href="/broadcast" onclick="return goLiveNav(event)"><i class="fa-solid fa-bolt" aria-hidden="true"></i> Go live from your browser</a>
+ <a class="cta-btn cta-btn-ghost" href="/docs/go-live-in-your-browser">How it works</a>
+ </div>
+ <ul class="cta-checks" aria-label="What going live needs">
+ <li><i class="fa-solid fa-check" aria-hidden="true"></i> No OBS</li>
+ <li><i class="fa-solid fa-check" aria-hidden="true"></i> No downloads</li>
+ <li><i class="fa-solid fa-check" aria-hidden="true"></i> No follower minimum</li>
+ <li><i class="fa-solid fa-check" aria-hidden="true"></i> Chromebook, laptop or phone</li>
+ </ul>
+
+ <button class="home-about-toggle" type="button" onclick="toggleHomeAbout()" aria-expanded="false" aria-controls="home-about-body">
+ <span class="home-about-toggle-more"><i class="fa-solid fa-circle-plus"></i> See what makes it different</span>
+ <span class="home-about-toggle-less"><i class="fa-solid fa-circle-minus"></i> Show less</span>
+ </button>
+
+ <div class="home-about-body" id="home-about-body">
+ <!-- Three ways to go live -->
+ <div class="cta-section-label" style="--sc:#60a5fa"><i class="fa-solid fa-tower-broadcast"></i> Three ways to go live</div>
+ <div class="cta-methods">
+ <div class="cta-method">
+ <div class="cta-method-num">01</div>
+ <div class="cta-method-icon"><i class="fa-solid fa-bolt"></i></div>
+ <strong>Right in your browser</strong>
+ <span>One click, zero software. Powered by <b>WebRTC</b> for <em>sub-second, ultra-low latency</em> — the fastest way to go live anywhere.</span>
+ <div class="cta-method-tag">WebRTC · <1s latency</div>
+ </div>
+ <div class="cta-method">
+ <div class="cta-method-num">02</div>
+ <div class="cta-method-icon"><i class="fa-solid fa-sliders"></i></div>
+ <strong>OBS & RTMP</strong>
+ <span>Plug in OBS, Streamlabs, or any RTMP encoder for a full scene-based, pro-grade production workflow.</span>
+ <div class="cta-method-tag">RTMP · pro setups</div>
+ </div>
+ <div class="cta-method">
+ <div class="cta-method-num">03</div>
+ <div class="cta-method-icon"><i class="fa-solid fa-terminal"></i></div>
+ <strong>ffmpeg & CLI</strong>
+ <span>Headless and fully scriptable — stream straight from a Raspberry Pi, a server, or an embedded device.</span>
+ <div class="cta-method-tag">jsmpeg · headless</div>
+ </div>
+ </div>
+
+ <!-- Differentiators -->
+ <div class="cta-section-label cta-diff-label" style="--sc:#fbbf24"><i class="fa-solid fa-crown"></i> Only on OpenVibe</div>
+ <div class="home-cta-features">
+ <div class="home-cta-feature feat-star">
+ <i class="fa-solid fa-robot"></i>
+ <strong>Viewer-controlled robots</strong>
+ <span>Hand chat the controls: robots, PTZ cameras and interactive rigs over the same sub-second WebRTC pipeline — low enough latency to actually play.</span>
+ </div>
+ <div class="home-cta-feature">
+ <i class="fa-solid fa-tower-broadcast"></i>
+ <strong>Restream everywhere at once</strong>
+ <span>Push a single broadcast to Twitch, YouTube, Kick, RobotStreamer & any RTMP server simultaneously — from one dashboard.</span>
+ </div>
+ <div class="home-cta-feature">
+ <i class="fa-solid fa-gamepad"></i>
+ <strong>A game next door</strong>
+ <span>Scraplandia lives on OpenVibe.Games — same account, leaderboards and a shared canvas, one click from any stream.</span>
+ </div>
+ <div class="home-cta-feature">
+ <i class="fa-solid fa-earth-americas"></i>
+ <strong>Global chat + voice</strong>
+ <span>A sitewide chatroom and drop-in voice channels connect everyone across every stream — no Discord required.</span>
+ </div>
+ <div class="home-cta-feature">
+ <i class="fa-solid fa-bullseye"></i>
+ <strong>Donation goals & Vibes</strong>
+ <span>Set funding goals with custom art, celebrate live in chat, and keep a real creator economy — with fair, transparent payouts.</span>
+ </div>
+ <div class="home-cta-feature">
+ <i class="fa-solid fa-wand-magic-sparkles"></i>
+ <strong>AI clips, VODs & transcripts</strong>
+ <span>Every broadcast auto-records, clips on the fly, and gets AI overviews + searchable audio transcripts.</span>
+ </div>
+ <div class="home-cta-feature">
+ <i class="fa-solid fa-paste"></i>
+ <strong>Pastes & file sharing</strong>
+ <span>Share code, logs and files with syntax highlighting and a short link. Pastes live on OpenVibe.Community and still show up on your channel.</span>
+ </div>
+ <div class="home-cta-feature">
+ <i class="fa-solid fa-screwdriver-wrench"></i>
+ <strong>160+ online tools</strong>
+ <span>Image & audio editing, PDF tools, video downloads, network diagnostics, dev tools and more — all at openvibe.tools.</span>
+ </div>
+ <div class="home-cta-feature feat-star">
+ <i class="fa-solid fa-code"></i>
+ <strong>100% open source</strong>
+ <span>Every line is public on GitHub. Read it, fork it, or send a pull request — features here get built because someone asked.</span>
+ </div>
+ </div>
+
+ <div class="home-cta-buttons">
+ <button class="btn btn-primary btn-lg cta-btn-glow" onclick="if (window.startGoLiveJourney) startGoLiveJourney(); else if (currentUser) navigate('/broadcast'); else showModal('register');">
+ <i class="fa-solid fa-broadcast-tower"></i> <span class="cta-btn-long">Launch your stream</span><span class="cta-btn-short">Go live</span>
+ </button>
+ <a href="/chat" class="btn btn-outline btn-lg" onclick="return handleLinkClick(event, '/chat')">
+ <i class="fa-solid fa-comments"></i> Say hi in chat
+ </a>
+ </div>
+
+ <!-- Where the money goes — part of the same About section -->
+ <div class="cta-section-label cta-diff-label" style="--sc:#4ade80"><i class="fa-solid fa-seedling"></i> Where the money goes</div>
+ <p class="cta-lede cta-money-lede">Whatever the network earns goes back into it — the servers, the storage and bandwidth that free streaming really costs, new features, and the creators and events that make this place worth showing up to.</p>
+ <div class="home-cta-features cta-money-grid">
+ <div class="home-cta-feature">
+ <i class="fa-solid fa-plug-circle-bolt"></i>
+ <strong>Keep it running</strong>
+ <span>Hosting, bandwidth, storage and development come first. Streaming is not free to host, and being honest about that is how the network stays open to everyone.</span>
+ </div>
+ <div class="home-cta-feature">
+ <i class="fa-solid fa-heart"></i>
+ <strong>Back the creators</strong>
+ <span>What is left over goes to the people who fill the place: creator payouts, community events, prize pools and the tools streamers ask for.</span>
+ </div>
+ <div class="home-cta-feature">
+ <i class="fa-solid fa-code-branch"></i>
+ <strong>No surprise pivot</strong>
+ <span>Where it goes stays visible. No selling the community out, no bait-and-switch, no dark patterns.</span>
+ </div>
+ </div>
+ </div><!-- /home-about-body -->
+ </div>
+ </div>
+
+</div>
+
+<div class="container">
+ <div class="network-hub">
+ <div class="network-hub-header">
+ <i class="fa-solid fa-network-wired"></i>
+ <div>
+ <h3>The OpenVibe Network</h3>
+ <p>One account, 14 services — streaming, games, maps, food, image tools, audio tools, text generators, logo makers, PDF & document tools, network diagnostics, developer tools, video downloads, and more.</p>
+ </div>
+ </div>
+ <div class="network-hub-grid">
+ <a href="https://openvibe.live" class="network-card network-card-featured">
+ <div class="network-card-icon" style="--card-accent:#e74c3c"><i class="fa-solid fa-tower-broadcast"></i></div>
+ <div class="network-card-info">
+ <strong>OpenVibe.<wbr>Live</strong>
+ <span>Live streaming, VODs, clips & chat</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://openvibe.tools" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#3b82f6"><i class="fa-solid fa-screwdriver-wrench"></i></div>
+ <div class="network-card-info">
+ <strong>OpenVibe.<wbr>Tools</strong>
+ <span>Every online tool in one index</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://openvibe.community" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#22c55e"><i class="fa-solid fa-people-group"></i></div>
+ <div class="network-card-info">
+ <strong>OpenVibe.<wbr>Community</strong>
+ <span>Pastes, posts and people</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://openvibe.games" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#a855f7"><i class="fa-solid fa-hat-wizard"></i></div>
+ <div class="network-card-info">
+ <strong>Scraplandia</strong>
+ <span>Browser RPG & community games</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">
+ <strong>OpenVibe.<wbr>Media</strong>
+ <span>VODs, clips and files</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://yt.openvibe.tools" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#ef4444"><i class="fa-solid fa-circle-play"></i></div>
+ <div class="network-card-info">
+ <strong>YouTube Downloader</strong>
+ <span>Save video or audio</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://openvibe.tools/image-tools" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#ec4899"><i class="fa-solid fa-image"></i></div>
+ <div class="network-card-info">
+ <strong>Image Tools</strong>
+ <span>Convert, compress, resize, crop</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://openvibe.tools/audio-tools" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#14b8a6"><i class="fa-solid fa-music"></i></div>
+ <div class="network-card-info">
+ <strong>Audio Tools</strong>
+ <span>Convert, trim, pitch and clean up</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://openvibe.tools/pdf-tools" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#f97316"><i class="fa-solid fa-file-pdf"></i></div>
+ <div class="network-card-info">
+ <strong>PDF & Documents</strong>
+ <span>Merge, split, compress, protect</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://openvibe.tools/text-tools" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#8b5cf6"><i class="fa-solid fa-font"></i></div>
+ <div class="network-card-info">
+ <strong>Text & Logo Tools</strong>
+ <span>Fancy text, logos, counters</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://openvibe.tools/developer-tools" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#06b6d4"><i class="fa-solid fa-code"></i></div>
+ <div class="network-card-info">
+ <strong>Developer Tools</strong>
+ <span>JSON, regex, JWT, diff, hashes</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://openvibe.tools/network-tools" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#10b981"><i class="fa-solid fa-network-wired"></i></div>
+ <div class="network-card-info">
+ <strong>Network Tools</strong>
+ <span>DNS, WHOIS, SSL, ping, ports</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://maps.openvibe.tools" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#2ecc71"><i class="fa-solid fa-map-location-dot"></i></div>
+ <div class="network-card-info">
+ <strong>Survival Map</strong>
+ <span>Camps, resources & shelters</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://food.openvibe.tools" class="network-card">
+ <div class="network-card-icon" style="--card-accent:#eab308"><i class="fa-solid fa-utensils"></i></div>
+ <div class="network-card-info">
+ <strong>Budget Food Finder</strong>
+ <span>Cheap meals and food banks</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ <a href="https://openvibe.network/my" class="network-card">
+ <div class="network-card-icon" style="--card-accent:var(--accent, #3b82f6)"><i class="fa-solid fa-user-circle"></i></div>
+ <div class="network-card-info">
+ <strong>My Account</strong>
+ <span>Profile, themes, history, linked services</span>
+ </div>
+ <i class="fa-solid fa-arrow-right network-card-arrow"></i>
+ </a>
+ </div>
+ </div>
+
+ <!-- OpenVibe.Live Changelog -->
+ <div class="section-header" style="--sc:#94a3b8;margin-top:32px;">
+ <h2><i class="fa-solid fa-code-commit"></i> Recent Changes<small class="section-kicker">built in the open</small></h2>
+ <a href="/updates" onclick="return handleLinkClick(event, '/updates')" class="section-action">View all</a>
+ </div>
+ <div id="home-changelog-wrapper" class="home-changelog-wrapper collapsed">
+ <div id="home-changelog" class="home-changelog">
+ <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>
+ </div>
+ <div class="home-changelog-fade" id="home-changelog-fade"></div>
+ </div>
+ <!-- Expands the preview in place; "View all" in the header opens /updates. It sits under
+ the list it expands, in the same button style as the rails' "Load more". -->
+ <button type="button" id="home-changelog-toggle" class="home-load-more home-changelog-toggle" onclick="toggleHomeChangelog()" aria-expanded="false" aria-controls="home-changelog-wrapper"><i class="fa-solid fa-chevron-down" aria-hidden="true"></i><span>Show all recent changes</span></button>
+
+</div>
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+<div class="content-container rundown-container">
+ <div id="rundown-root">
+ <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>
+ </div>
+</div>
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+<div class="content-container">
+ <div class="page-header">
+ <h1><i class="fa-solid fa-code-commit"></i> Updates</h1>
+ <p class="page-subtitle">Every change deployed to OpenVibe.Live, newest first: each line is a commit from the <a href="https://github.com/OpenVibers/OpenVibe.Live" target="_blank" rel="noopener">OpenVibers repositories</a>, and batches become <a href="https://openvibe.blog/@openvibe" rel="noopener">Patch notes on openvibe.blog</a>. See <a href="https://openvibe.network/updates">the whole network</a>.</p>
+ </div>
+ <div id="updates-list" class="updates-list">
+ <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>
+ </div>
+</div>
@@ -167,511 +167,7 @@
@@ -167,511 +167,7 @@
<main id="app">
<main id="app">
<!-- ── HOME: Stream List ─────────────────────────────── -->
<!-- ── HOME: Stream List ─────────────────────────────── -->
- <section id="page-home" class="page active">
- <div class="hero" data-fx-viewport>
- <div class="hero-bg" id="hero-bg" aria-hidden="true"><div class="hero-bg-layer"></div><div class="hero-bg-layer"></div></div>
- <div class="hero-aurora" aria-hidden="true"></div>
- <div class="hero-collage" id="hero-collage" aria-hidden="true"></div>
- <div class="hero-scrim" aria-hidden="true"></div>
- <div class="hero-content">
- <h1><span class="brand-mark brand-mark--hero" aria-hidden="true"><svg viewBox="0 0 48 48">
- <defs>
- <linearGradient id="ovh-g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="currentColor"/><stop offset="1" stop-color="currentColor" stop-opacity="0.45"/></linearGradient>
- <linearGradient id="ovh-v" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="currentColor"/><stop offset="1" stop-color="#fff" stop-opacity="0.9"/><animateTransform attributeName="gradientTransform" type="translate" from="-1 0" to="1 0" dur="3.2s" repeatCount="indefinite"/></linearGradient>
- <path id="ovh-ring" d="M24,6 A18,18 0 1,1 23.99,6 Z"/>
- </defs>
- <circle class="ov-glow" cx="24" cy="24" r="21"/>
- <circle class="ov-ring" cx="24" cy="24" r="18"/>
- <circle class="ov-comet" cx="24" cy="24" r="18" stroke="url(#ovh-g)"/>
- <path class="ov-v" d="M14.5,17 L24,34 L33.5,17" stroke="url(#ovh-v)"/>
- <circle class="ov-dot" cx="24" cy="34" r="2.6"/>
- <circle class="ov-orbit" r="2"><animateMotion dur="3.6s" repeatCount="indefinite" rotate="auto"><mpath href="#ovh-ring"/></animateMotion></circle>
- <circle class="ov-orbit ov-orbit--tail" r="1.4"><animateMotion dur="3.6s" begin="0.14s" repeatCount="indefinite"><mpath href="#ovh-ring"/></animateMotion></circle>
- <circle class="ov-spark" cx="24" cy="24" r="1.3"/><circle class="ov-spark ov-spark--2" cx="24" cy="24" r="1"/><circle class="ov-spark ov-spark--3" cx="24" cy="24" r="1.1"/>
- </svg></span> OpenVibe.Live</h1>
- <!-- The first word and quip ship in the HTML so the hero is complete at first paint; the
- rotators take over once the page's data arrives. No aria-live: a slogan that changes
- every few seconds would be read out to screen-reader users over and over. -->
- <p class="hero-tagline">Live streaming for <span class="hero-rotate-wrap"><span class="hero-rotate visible" id="hero-rotate">stealth campers</span></span></p>
- <p class="hero-quip visible" id="hero-quip">No investors. No suits. Just vibes.</p>
- <!-- Holds its line from the first paint (the countdown fills it with the page's data), so the buttons below do not move. -->
- <div class="hero-slogan-timer" id="hero-slogan-timer"> </div>
- <!-- One primary action and its promise, then the other destinations (owner, 2026-10-18: "whole area is kinda
- messy and spacing is weird"; six equal buttons wrapped 5 + 1 with the no-OBS line floating between
- rows). The headline feature stays in the first screen: going live from a browser with no OBS and
- no follower minimum is what people search and ask AI assistants for. -->
- <div class="hero-cta" data-fx-viewport>
- <div class="hero-primary">
- <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">Go live <small>in your browser</small></span><i class="fa-solid fa-chevron-right hb-go" aria-hidden="true"></i></button>
- <p class="hero-golive-note"><span><i class="fa-solid fa-check" aria-hidden="true"></i> No OBS</span><span><i class="fa-solid fa-check" aria-hidden="true"></i> No downloads</span><span><i class="fa-solid fa-check" aria-hidden="true"></i> No follower minimum</span><a href="/docs/go-live-in-your-browser">How it works <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></p>
- </div>
- <nav class="hero-links" aria-label="More from OpenVibe">
- <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" 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="/documentation" onclick="return handleLinkClick(event, '/documentation')" style="--i:3" class="hb hb-vibe"
- data-ovtip-title="Mod it, bot it, build on it"
- data-ovtip-icon="fa-wand-magic-sparkles"
- data-ovtip-accent="#f472b6"
- data-ovtip-place="top"
- data-ovtip="Chat, streams, clips, overlays, robots, sound commands, whole games — every feature here has an <b>open API</b>, and the entire site is <b>open source</b>. <em>You do not need to know how to code.</em> Point an AI at these docs, describe the thing you want, and it writes it for you."
- data-ovtip-foot="<i class="fa-solid fa-heart"></i> We love what people build here — come make something"
- data-ovtip-go="Open the API docs"><span class="hb-ico"><i class="fa-solid fa-wand-magic-sparkles"></i></span><span class="hb-label">Build <span class="hb-amp">& Mod</span> <small>API docs</small></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:4" 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:5" 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>
- </nav>
- </div>
- <!-- Signed-in streamers with setup left to do see their next task here, in the row of
- things to do rather than a screen below (public/js/guides-live.js → setupHeroNextUp). -->
- <div class="tour-quest-slot hero-quest-slot" id="hero-next-up" data-fx-viewport hidden></div>
- <div class="hero-philosophy-links">
- <a href="#home-cta-banner" class="hero-philosophy-btn" onclick="event.preventDefault();openHomeAbout();">
- <i class="fa-solid fa-book-open"></i> What is OpenVibe?
- </a>
- <span class="hero-philosophy-sep" aria-hidden="true">•</span>
- <span class="hero-legal"><a href="/tos" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/tos')">Terms</a>
- <span aria-hidden="true">•</span>
- <a href="/privacy" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/privacy')">Privacy</a>
- <span aria-hidden="true">•</span>
- <a href="/dmca" class="hero-philosophy-link" onclick="return handleLinkClick(event, '/dmca')">DMCA</a></span>
- </div>
- <!-- Newest shipped commit — built in the open, receipts included. Sits under the
- philosophy row so the proof is next to the promise. -->
- <a class="hero-latest" id="hero-latest" data-ov-shipped="latest" data-service="live" href="/updates" onclick="return handleLinkClick(event, '/updates')"></a>
- <div class="hero-stats" id="hero-stats" aria-label="Site stats"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div></div></div>
- </div>
- </div>
-
- <div class="container">
- <!-- Featured live stream (public/js/home-featured.js): filled only when someone is live. -->
- <div class="home-featured" id="home-featured" hidden></div>
- <!-- Live Streams -->
- <div class="section-header" style="--sc:#ef4444">
- <h2><i class="fa-solid fa-circle live-dot"></i> Live Now<small class="section-kicker">on air</small></h2>
- <span id="live-count" class="badge">0</span>
- <button type="button" class="home-featured-toggle" id="home-featured-toggle" hidden><i class="fa-solid fa-satellite-dish"></i> Show featured stream</button>
- </div>
- <div class="stream-grid" id="stream-grid-live">
- <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>
- <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>
- <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>
- <div class="ovsk ovsk-card" aria-hidden="true"><div class="ovsk-thumb"></div><div class="ovsk-meta"><div class="ovsk-bar ovsk-av"></div><div class="ovsk-lines"><div class="ovsk-bar" style="height:11px;width:72%"></div><div class="ovsk-bar" style="height:9px;width:45%"></div></div></div></div>
- <div class="empty-state" id="no-live-streams" style="display:none">
- <i class="fa-solid fa-tower-broadcast fa-3x"></i>
- <p>No one is streaming right now</p>
- <!-- This was a line of grey text telling people to go live with no way to do it.
- It is the same button as the one under the tour, routed through the same
- entry point, so signing in lands you back in the go-live journey. -->
- <button type="button" class="ovg-firstlive" onclick="if (window.startGoLiveJourney) startGoLiveJourney(); else location.href='/broadcast';">
- <span class="ovg-firstlive-ring" aria-hidden="true"></span>
- <span class="ovg-firstlive-sheen" aria-hidden="true"></span>
- <i class="fa-solid fa-tower-broadcast"></i>
- <span>Be the first to go live</span>
- <i class="fa-solid fa-arrow-right ovg-firstlive-go"></i>
- </button>
- </div>
- </div>
-
- <!-- "While you were away" (signed-in, returning) / "Lately on OpenVibe" (everyone) — site activity digest -->
- <div class="home-digest" id="home-digest"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div></div></div>
-
- <!-- Star of OpenVibe — featured streamer spotlight (star_streamer setting / STAR_STREAMER env) -->
- <div class="home-star-section" id="home-star-section"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div><div class="ovsk-bar" style="height:10px;width:80%"></div></div></div>
-
- <div class="hero-egg" id="hero-egg" style="display:none">
- <button class="hero-egg-head" type="button" onclick="toggleHeroEgg()">
- <span class="hero-egg-icon">🥚</span>
- <span class="hero-egg-titles"><span class="hero-egg-kicker">Today's Secret</span><span class="hero-egg-title" id="hero-egg-title"></span></span>
- <span class="hero-egg-status" id="hero-egg-status"></span>
- <i class="fa-solid fa-chevron-down hero-egg-chev"></i>
- </button>
- <div class="hero-egg-body">
- <div class="hero-egg-inner">
- <p class="hero-egg-intro">A new secret every day. Each clue is one key, in order. Type the keys anywhere on the site, or tap them below.</p>
- <div class="hero-egg-slots" id="hero-egg-slots" role="group" aria-label="Your key sequence"></div>
- <ol class="hero-egg-clues" id="hero-egg-clues"></ol>
- <div class="hero-egg-pad" id="hero-egg-pad">
- <div class="hero-egg-pad-arrows">
- <button type="button" class="egg-key" data-tok="left" aria-label="Left">←</button>
- <button type="button" class="egg-key" data-tok="up" aria-label="Up">↑</button>
- <button type="button" class="egg-key" data-tok="down" aria-label="Down">↓</button>
- <button type="button" class="egg-key" data-tok="right" aria-label="Right">→</button>
- <button type="button" class="egg-key egg-key--ctl" data-act="back" aria-label="Backspace">⌫</button>
- <button type="button" class="egg-key egg-key--ctl" data-act="clear" aria-label="Clear">✕</button>
- </div>
- <div class="hero-egg-pad-letters" id="hero-egg-letters"></div>
- </div>
- <div class="hero-egg-foot">
- <button type="button" class="hero-egg-reveal" id="hero-egg-reveal" onclick="_eggReveal()"><i class="fa-solid fa-lightbulb"></i> Stuck? Reveal a key</button>
- <div class="hero-egg-meta"><span id="hero-egg-count"></span><span id="hero-egg-first"></span><span id="hero-egg-reset"></span></div>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Community pulse: goals near completion, latest activity, weekly leaders -->
- <div class="home-section-lazy" id="home-pulse-section" style="display:none">
- <div class="section-header" style="--sc:#f59e0b">
- <h2><i class="fa-solid fa-bolt"></i> Happening Now<small class="section-kicker">the pulse</small></h2>
- </div>
- <div class="pulse-grid" id="pulse-grid"></div>
- </div>
-
- <!-- What people made, above the introduction to the site (roadmap 32.7: the home leads with live
- content and creators): channels that were just on, their recordings, the clips people took
- and their pastes. What the AI made follows it (AI Moments; 33.6: made, then derived). -->
- <!-- Recently Online -->
- <div class="section-header" style="--sc:#60a5fa">
- <h2><i class="fa-solid fa-clock-rotate-left"></i> Recently Online<small class="section-kicker">just missed them</small></h2>
- </div>
- <div class="stream-grid" id="stream-grid-recent">
- <div class="empty-state">
- <p class="muted">No recent streams</p>
- </div>
- </div>
- <div id="stream-grid-recent-pagination" class="pastes-pagination" style="display:none"></div>
-
- <!-- Recent VODs -->
- <div class="section-header" style="--sc:#a78bfa;display:none" id="home-recent-vods-header">
- <h2><i class="fa-solid fa-video"></i> Recent VODs<small class="section-kicker">full streams</small></h2>
- <a href="/content?type=vods" onclick="return handleLinkClick(event, '/content?type=vods')" class="section-action">View all</a>
- </div>
- <div class="stream-grid" id="home-recent-vods-grid"></div>
- <div id="home-recent-vods-pagination" class="pastes-pagination" style="display:none"></div>
-
- <!-- Recent Clips -->
- <div class="section-header" style="--sc:#f472b6;display:none" id="home-clips-header">
- <h2><i class="fa-solid fa-scissors"></i> Recent Clips<small class="section-kicker">the good bits</small></h2>
- <a href="/content?type=clips" onclick="return handleLinkClick(event, '/content?type=clips')" class="section-action">View all</a>
- </div>
- <div class="stream-grid home-clips-grid" id="home-clips-grid"></div>
- <div id="home-clips-pagination" class="pastes-pagination" style="display:none"></div>
-
- <!-- Recent Pastes -->
- <div class="section-header" style="--sc:#2dd4bf;display:none" id="home-pastes-header">
- <h2><i class="fa-solid fa-paste"></i> Recent Pastes<small class="section-kicker">shots & snippets</small></h2>
- <a href="/content?type=pastes" onclick="return handleLinkClick(event, '/content?type=pastes')" class="section-action">View all</a>
- </div>
- <div class="home-pastes-list" id="home-pastes-list"></div>
- <div id="home-pastes-pagination" class="pastes-pagination" style="display:none"></div>
-
- <!-- AI Moments showcase -->
- <div class="home-section-lazy" id="home-moments-section" style="display:none">
- <div class="section-header" style="--sc:#22d3ee">
- <h2><i class="fa-solid fa-brain"></i> AI Moments<small class="section-kicker">picked by AI</small></h2>
- <a href="/moments" onclick="return handleLinkClick(event, '/moments')" class="section-action">See all</a>
- </div>
- <div class="moments-row ov-rail-track" id="home-moments-row"></div>
- </div>
-
- <!-- The two introductions travel together: what OpenVibe does (the restream diagram), then
- why it exists (the About banner). demoteHomeIntroSections() moves them as a pair. -->
- <!-- One stream, everywhere — restream pipeline + feature tour (public/js/home-tour.js) -->
- <div class="home-tour-mount home-section-lazy" id="home-tour-mount" data-fx-viewport></div>
- <!-- Streaming CTA -->
- <div class="home-cta-banner" id="home-cta-banner" data-fx-viewport>
- <div class="cta-ambient" aria-hidden="true"></div>
- <div class="cta-embers" aria-hidden="true">
- <span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
- </div>
- <div class="home-cta-inner">
-
- <!-- The all-caps eyebrow here ("HOME-GROWN & HOBBYIST-RUN — NOT ANOTHER CORPORATE
- STREAM SITE") was a louder restatement of the lede two lines below it, which
- already says the platform is built by one person who streams here. One of them
- is enough, and the lede is the one that reads like a human wrote it. -->
- <div class="cta-chips" aria-hidden="true">
- <span class="cta-chip"><i class="fa-solid fa-people-group"></i> Community-run</span>
- <span class="cta-chip"><i class="fa-solid fa-comment"></i> Free speech, light rules</span>
- <span class="cta-chip"><i class="fa-solid fa-code-branch"></i> Built in the open</span>
- </div>
- <h2 class="cta-hero">Streaming the way it <span class="cta-hero-grad">should</span> be.</h2>
- <p class="cta-lede">
- An <strong>open, community-run</strong> streaming platform, built in public by people who stream and hang out in chat. The ideas from chat are the ones that ship. <strong>Free speech, light-touch moderation, no mystery bans.</strong>
- </p>
-
- <!-- Always shown (not behind "See what makes it different"): going live from the browser with
- no OBS and no follower minimum is what people search and ask AI assistants for. -->
- <div class="cta-actions">
- <a class="cta-btn cta-btn-primary" href="/broadcast" onclick="return goLiveNav(event)"><i class="fa-solid fa-bolt" aria-hidden="true"></i> Go live from your browser</a>
- <a class="cta-btn cta-btn-ghost" href="/docs/go-live-in-your-browser">How it works</a>
- </div>
- <ul class="cta-checks" aria-label="What going live needs">
- <li><i class="fa-solid fa-check" aria-hidden="true"></i> No OBS</li>
- <li><i class="fa-solid fa-check" aria-hidden="true"></i> No downloads</li>
- <li><i class="fa-solid fa-check" aria-hidden="true"></i> No follower minimum</li>
- <li><i class="fa-solid fa-check" aria-hidden="true"></i> Chromebook, laptop or phone</li>
- </ul>
-
- <button class="home-about-toggle" type="button" onclick="toggleHomeAbout()" aria-expanded="false" aria-controls="home-about-body">
- <span class="home-about-toggle-more"><i class="fa-solid fa-circle-plus"></i> See what makes it different</span>
- <span class="home-about-toggle-less"><i class="fa-solid fa-circle-minus"></i> Show less</span>
- </button>
-
- <div class="home-about-body" id="home-about-body">
- <!-- Three ways to go live -->
- <div class="cta-section-label" style="--sc:#60a5fa"><i class="fa-solid fa-tower-broadcast"></i> Three ways to go live</div>
- <div class="cta-methods">
- <div class="cta-method">
- <div class="cta-method-num">01</div>
- <div class="cta-method-icon"><i class="fa-solid fa-bolt"></i></div>
- <strong>Right in your browser</strong>
- <span>One click, zero software. Powered by <b>WebRTC</b> for <em>sub-second, ultra-low latency</em> — the fastest way to go live anywhere.</span>
- <div class="cta-method-tag">WebRTC · <1s latency</div>
- </div>
- <div class="cta-method">
- <div class="cta-method-num">02</div>
- <div class="cta-method-icon"><i class="fa-solid fa-sliders"></i></div>
- <strong>OBS & RTMP</strong>
- <span>Plug in OBS, Streamlabs, or any RTMP encoder for a full scene-based, pro-grade production workflow.</span>
- <div class="cta-method-tag">RTMP · pro setups</div>
- </div>
- <div class="cta-method">
- <div class="cta-method-num">03</div>
- <div class="cta-method-icon"><i class="fa-solid fa-terminal"></i></div>
- <strong>ffmpeg & CLI</strong>
- <span>Headless and fully scriptable — stream straight from a Raspberry Pi, a server, or an embedded device.</span>
- <div class="cta-method-tag">jsmpeg · headless</div>
- </div>
- </div>
-
- <!-- Differentiators -->
- <div class="cta-section-label cta-diff-label" style="--sc:#fbbf24"><i class="fa-solid fa-crown"></i> Only on OpenVibe</div>
- <div class="home-cta-features">
- <div class="home-cta-feature feat-star">
- <i class="fa-solid fa-robot"></i>
- <strong>Viewer-controlled robots</strong>
- <span>Hand chat the controls: robots, PTZ cameras and interactive rigs over the same sub-second WebRTC pipeline — low enough latency to actually play.</span>
- </div>
- <div class="home-cta-feature">
- <i class="fa-solid fa-tower-broadcast"></i>
- <strong>Restream everywhere at once</strong>
- <span>Push a single broadcast to Twitch, YouTube, Kick, RobotStreamer & any RTMP server simultaneously — from one dashboard.</span>
- </div>
- <div class="home-cta-feature">
- <i class="fa-solid fa-gamepad"></i>
- <strong>A game next door</strong>
- <span>Scraplandia lives on OpenVibe.Games — same account, leaderboards and a shared canvas, one click from any stream.</span>
- </div>
- <div class="home-cta-feature">
- <i class="fa-solid fa-earth-americas"></i>
- <strong>Global chat + voice</strong>
- <span>A sitewide chatroom and drop-in voice channels connect everyone across every stream — no Discord required.</span>
- </div>
- <div class="home-cta-feature">
- <i class="fa-solid fa-bullseye"></i>
- <strong>Donation goals & Vibes</strong>
- <span>Set funding goals with custom art, celebrate live in chat, and keep a real creator economy — with fair, transparent payouts.</span>
- </div>
- <div class="home-cta-feature">
- <i class="fa-solid fa-wand-magic-sparkles"></i>
- <strong>AI clips, VODs & transcripts</strong>
- <span>Every broadcast auto-records, clips on the fly, and gets AI overviews + searchable audio transcripts.</span>
- </div>
- <div class="home-cta-feature">
- <i class="fa-solid fa-paste"></i>
- <strong>Pastes & file sharing</strong>
- <span>Share code, logs and files with syntax highlighting and a short link. Pastes live on OpenVibe.Community and still show up on your channel.</span>
- </div>
- <div class="home-cta-feature">
- <i class="fa-solid fa-screwdriver-wrench"></i>
- <strong>160+ online tools</strong>
- <span>Image & audio editing, PDF tools, video downloads, network diagnostics, dev tools and more — all at openvibe.tools.</span>
- </div>
- <div class="home-cta-feature feat-star">
- <i class="fa-solid fa-code"></i>
- <strong>100% open source</strong>
- <span>Every line is public on GitHub. Read it, fork it, or send a pull request — features here get built because someone asked.</span>
- </div>
- </div>
-
- <div class="home-cta-buttons">
- <button class="btn btn-primary btn-lg cta-btn-glow" onclick="if (window.startGoLiveJourney) startGoLiveJourney(); else if (currentUser) navigate('/broadcast'); else showModal('register');">
- <i class="fa-solid fa-broadcast-tower"></i> <span class="cta-btn-long">Launch your stream</span><span class="cta-btn-short">Go live</span>
- </button>
- <a href="/chat" class="btn btn-outline btn-lg" onclick="return handleLinkClick(event, '/chat')">
- <i class="fa-solid fa-comments"></i> Say hi in chat
- </a>
- </div>
-
- <!-- Where the money goes — part of the same About section -->
- <div class="cta-section-label cta-diff-label" style="--sc:#4ade80"><i class="fa-solid fa-seedling"></i> Where the money goes</div>
- <p class="cta-lede cta-money-lede">Whatever the network earns goes back into it — the servers, the storage and bandwidth that free streaming really costs, new features, and the creators and events that make this place worth showing up to.</p>
- <div class="home-cta-features cta-money-grid">
- <div class="home-cta-feature">
- <i class="fa-solid fa-plug-circle-bolt"></i>
- <strong>Keep it running</strong>
- <span>Hosting, bandwidth, storage and development come first. Streaming is not free to host, and being honest about that is how the network stays open to everyone.</span>
- </div>
- <div class="home-cta-feature">
- <i class="fa-solid fa-heart"></i>
- <strong>Back the creators</strong>
- <span>What is left over goes to the people who fill the place: creator payouts, community events, prize pools and the tools streamers ask for.</span>
- </div>
- <div class="home-cta-feature">
- <i class="fa-solid fa-code-branch"></i>
- <strong>No surprise pivot</strong>
- <span>Where it goes stays visible. No selling the community out, no bait-and-switch, no dark patterns.</span>
- </div>
- </div>
- </div><!-- /home-about-body -->
- </div>
- </div>
-
- </div>
-
- <div class="container">
- <div class="network-hub">
- <div class="network-hub-header">
- <i class="fa-solid fa-network-wired"></i>
- <div>
- <h3>The OpenVibe Network</h3>
- <p>One account, 14 services — streaming, games, maps, food, image tools, audio tools, text generators, logo makers, PDF & document tools, network diagnostics, developer tools, video downloads, and more.</p>
- </div>
- </div>
- <div class="network-hub-grid">
- <a href="https://openvibe.live" class="network-card network-card-featured">
- <div class="network-card-icon" style="--card-accent:#e74c3c"><i class="fa-solid fa-tower-broadcast"></i></div>
- <div class="network-card-info">
- <strong>OpenVibe.<wbr>Live</strong>
- <span>Live streaming, VODs, clips & chat</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://openvibe.tools" class="network-card">
- <div class="network-card-icon" style="--card-accent:#3b82f6"><i class="fa-solid fa-screwdriver-wrench"></i></div>
- <div class="network-card-info">
- <strong>OpenVibe.<wbr>Tools</strong>
- <span>Every online tool in one index</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://openvibe.community" class="network-card">
- <div class="network-card-icon" style="--card-accent:#22c55e"><i class="fa-solid fa-people-group"></i></div>
- <div class="network-card-info">
- <strong>OpenVibe.<wbr>Community</strong>
- <span>Pastes, posts and people</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://openvibe.games" class="network-card">
- <div class="network-card-icon" style="--card-accent:#a855f7"><i class="fa-solid fa-hat-wizard"></i></div>
- <div class="network-card-info">
- <strong>Scraplandia</strong>
- <span>Browser RPG & community games</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">
- <strong>OpenVibe.<wbr>Media</strong>
- <span>VODs, clips and files</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://yt.openvibe.tools" class="network-card">
- <div class="network-card-icon" style="--card-accent:#ef4444"><i class="fa-solid fa-circle-play"></i></div>
- <div class="network-card-info">
- <strong>YouTube Downloader</strong>
- <span>Save video or audio</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://openvibe.tools/image-tools" class="network-card">
- <div class="network-card-icon" style="--card-accent:#ec4899"><i class="fa-solid fa-image"></i></div>
- <div class="network-card-info">
- <strong>Image Tools</strong>
- <span>Convert, compress, resize, crop</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://openvibe.tools/audio-tools" class="network-card">
- <div class="network-card-icon" style="--card-accent:#14b8a6"><i class="fa-solid fa-music"></i></div>
- <div class="network-card-info">
- <strong>Audio Tools</strong>
- <span>Convert, trim, pitch and clean up</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://openvibe.tools/pdf-tools" class="network-card">
- <div class="network-card-icon" style="--card-accent:#f97316"><i class="fa-solid fa-file-pdf"></i></div>
- <div class="network-card-info">
- <strong>PDF & Documents</strong>
- <span>Merge, split, compress, protect</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://openvibe.tools/text-tools" class="network-card">
- <div class="network-card-icon" style="--card-accent:#8b5cf6"><i class="fa-solid fa-font"></i></div>
- <div class="network-card-info">
- <strong>Text & Logo Tools</strong>
- <span>Fancy text, logos, counters</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://openvibe.tools/developer-tools" class="network-card">
- <div class="network-card-icon" style="--card-accent:#06b6d4"><i class="fa-solid fa-code"></i></div>
- <div class="network-card-info">
- <strong>Developer Tools</strong>
- <span>JSON, regex, JWT, diff, hashes</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://openvibe.tools/network-tools" class="network-card">
- <div class="network-card-icon" style="--card-accent:#10b981"><i class="fa-solid fa-network-wired"></i></div>
- <div class="network-card-info">
- <strong>Network Tools</strong>
- <span>DNS, WHOIS, SSL, ping, ports</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://maps.openvibe.tools" class="network-card">
- <div class="network-card-icon" style="--card-accent:#2ecc71"><i class="fa-solid fa-map-location-dot"></i></div>
- <div class="network-card-info">
- <strong>Survival Map</strong>
- <span>Camps, resources & shelters</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://food.openvibe.tools" class="network-card">
- <div class="network-card-icon" style="--card-accent:#eab308"><i class="fa-solid fa-utensils"></i></div>
- <div class="network-card-info">
- <strong>Budget Food Finder</strong>
- <span>Cheap meals and food banks</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- <a href="https://openvibe.network/my" class="network-card">
- <div class="network-card-icon" style="--card-accent:var(--accent, #3b82f6)"><i class="fa-solid fa-user-circle"></i></div>
- <div class="network-card-info">
- <strong>My Account</strong>
- <span>Profile, themes, history, linked services</span>
- </div>
- <i class="fa-solid fa-arrow-right network-card-arrow"></i>
- </a>
- </div>
- </div>
-
- <!-- OpenVibe.Live Changelog -->
- <div class="section-header" style="--sc:#94a3b8;margin-top:32px;">
- <h2><i class="fa-solid fa-code-commit"></i> Recent Changes<small class="section-kicker">built in the open</small></h2>
- <a href="/updates" onclick="return handleLinkClick(event, '/updates')" class="section-action">View all</a>
- </div>
- <div id="home-changelog-wrapper" class="home-changelog-wrapper collapsed">
- <div id="home-changelog" class="home-changelog">
- <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>
- </div>
- <div class="home-changelog-fade" id="home-changelog-fade"></div>
- </div>
- <!-- Expands the preview in place; "View all" in the header opens /updates. It sits under
- the list it expands, in the same button style as the rails' "Load more". -->
- <button type="button" id="home-changelog-toggle" class="home-load-more home-changelog-toggle" onclick="toggleHomeChangelog()" aria-expanded="false" aria-controls="home-changelog-wrapper"><i class="fa-solid fa-chevron-down" aria-hidden="true"></i><span>Show all recent changes</span></button>
-
-
-
-
- </div>
- </section>
+ <section id="page-home" class="page active" data-fragment="home"><!--ov:fragment:home--><div class="ov-route-skel" aria-hidden="true"><span></span><span></span><span></span></div><!--/ov:fragment--></section>
<!-- ── STREAM VIEWER (legacy, redirect to channel) ──── -->
<!-- ── STREAM VIEWER (legacy, redirect to channel) ──── -->
<section id="page-stream" class="page">
<section id="page-stream" class="page">
@@ -724,17 +220,7 @@
@@ -724,17 +220,7 @@
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- UPDATES / CHANGELOG -->
<!-- UPDATES / CHANGELOG -->
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- ═══════════════════════════════════════════════════════════ -->
- <section id="page-updates" class="page">
- <div class="content-container">
- <div class="page-header">
- <h1><i class="fa-solid fa-code-commit"></i> Updates</h1>
- <p class="page-subtitle">Every change deployed to OpenVibe.Live, newest first: each line is a commit from the <a href="https://github.com/OpenVibers/OpenVibe.Live" target="_blank" rel="noopener">OpenVibers repositories</a>, and batches become <a href="https://openvibe.blog/@openvibe" rel="noopener">Patch notes on openvibe.blog</a>. See <a href="https://openvibe.network/updates">the whole network</a>.</p>
- </div>
- <div id="updates-list" class="updates-list">
- <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>
- </div>
- </div>
- </section>
+ <section id="page-updates" class="page" data-fragment="updates"><!--ov:fragment:updates--><div class="ov-route-skel" aria-hidden="true"><span></span><span></span><span></span></div><!--/ov:fragment--></section>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- PASTE VIEWER -->
<!-- PASTE VIEWER -->
@@ -757,13 +243,7 @@
@@ -757,13 +243,7 @@
</div>
</div>
</section>
</section>
- <section id="page-rundown" class="page">
- <div class="content-container rundown-container">
- <div id="rundown-root">
- <div class="loading-spinner"><i class="fa-solid fa-circle-notch fa-spin"></i></div>
- </div>
- </div>
- </section>
+ <section id="page-rundown" class="page" data-fragment="rundown"><!--ov:fragment:rundown--><div class="ov-route-skel" aria-hidden="true"><span></span><span></span><span></span></div><!--/ov:fragment--></section>
<section id="page-documentation" class="page" data-fragment="documentation"><!--ov:fragment:documentation--><div class="ov-route-skel" aria-hidden="true"><span></span><span></span><span></span></div><!--/ov:fragment--></section>
<section id="page-documentation" class="page" data-fragment="documentation"><!--ov:fragment:documentation--><div class="ov-route-skel" aria-hidden="true"><span></span><span></span><span></span></div><!--/ov:fragment--></section>
<!-- A path no route knows (the server answers it 404): filled by showNotFound() in app.js -->
<!-- A path no route knows (the server answers it 404): filled by showNotFound() in app.js -->
@@ -1158,7 +1158,7 @@ function routeFromURL() {
@@ -1158,7 +1158,7 @@ function routeFromURL() {
whenRouteReady('rundown', () => { if (typeof loadRundownPage === 'function') loadRundownPage(segments); });
whenRouteReady('rundown', () => { if (typeof loadRundownPage === 'function') loadRundownPage(segments); });
} else if (segments[0] === 'updates') {
} else if (segments[0] === 'updates') {
showPage('updates');
showPage('updates');
- loadUpdatesPage();
+ whenRouteReady('updates', () => loadUpdatesPage());
} else if (segments[0] === 'dmca') {
} else if (segments[0] === 'dmca') {
window.location.replace('/dmca');
window.location.replace('/dmca');
return;
return;
@@ -295,14 +295,15 @@ function renderRoute(html, urlPath) {
@@ -295,14 +295,15 @@ function renderRoute(html, urlPath) {
* visit to a channel, a VOD or the chat painted the home page (its hero and lists) until the router ran and swapped
* visit to a channel, a VOD or the chat painted the home page (its hero and lists) until the router ran and swapped
* pages — a flash of the wrong page and the largest layout shifts measured on the site (CLS 1.0 on a channel page,
* pages — a flash of the wrong page and the largest layout shifts measured on the site (CLS 1.0 on a channel page,
* 0.5 on a VOD, phone profile). The route's last feature with a page fragment names its section; the router's own
* 0.5 on a VOD, phone profile). The route's last feature with a page fragment names its section; the router's own
- * showPage() then finds it already active.
+ * showPage() then finds it already active. The home page is a fragment too (inlined on "/" only), so on any other
+ * route the shell carries its placeholder, not the hero and its <h1>.
*/
*/
function activePage(html, names, reg) {
function activePage(html, names, reg) {
const frag = [...names].reverse().map((n) => reg.features[n] && reg.features[n].fragment)
const frag = [...names].reverse().map((n) => reg.features[n] && reg.features[n].fragment)
.find((f) => f && html.includes(`class="page" data-fragment="${f}"`));
.find((f) => f && html.includes(`class="page" data-fragment="${f}"`));
if (!frag) return html;
if (!frag) return html;
return html
return html
- .replace('<section id="page-home" class="page active">', '<section id="page-home" class="page">')
+ .replace('<section id="page-home" class="page active"', '<section id="page-home" class="page"')
.replace(`class="page" data-fragment="${frag}"`, `class="page active" data-fragment="${frag}"`);
.replace(`class="page" data-fragment="${frag}"`, `class="page active" data-fragment="${frag}"`);
}
}
@@ -49,8 +49,9 @@ ok('VOD and clip pages live on OpenVibe.Video');
@@ -49,8 +49,9 @@ ok('VOD and clip pages live on OpenVibe.Video');
// ── Home hero and early widgets ──
// ── Home hero and early widgets ──
const index = read('public/index.html');
const index = read('public/index.html');
-assert.ok(/<div class="hero-slogan-timer" id="hero-slogan-timer"> <\/div>/.test(index), 'the countdown line is held from the first paint');
-assert.ok(/<a class="hero-latest" id="hero-latest"[^>]*><\/a>/.test(index) && !/id="hero-latest"[^>]*\shidden>/.test(index), 'the shipped line is not hidden (shipped.js would collapse it, then refill it)');
+const homeHtml = read('public/fragments/home.html');
+assert.ok(/<div class="hero-slogan-timer" id="hero-slogan-timer"> <\/div>/.test(homeHtml), 'the countdown line is held from the first paint');
+assert.ok(/<a class="hero-latest" id="hero-latest"[^>]*><\/a>/.test(homeHtml) && !/id="hero-latest"[^>]*\shidden>/.test(homeHtml), 'the shipped line is not hidden (shipped.js would collapse it, then refill it)');
const home = read('public/css/features/home.css');
const home = read('public/css/features/home.css');
assert.ok(home.includes('.hero-latest:empty { visibility: hidden; min-height: 33px; }'), 'the empty shipped line keeps its height, invisibly');
assert.ok(home.includes('.hero-latest:empty { visibility: hidden; min-height: 33px; }'), 'the empty shipped line keeps its height, invisibly');
assert.ok(/\.hero-stats \{ margin: 16px 0 10px; gap: 10px; min-height: 120px; \}/.test(home) && home.includes('.hero-stats { min-height: 204px; }') && home.includes('.hero-stats { min-height: 256px; margin-top: 14px; }'),
assert.ok(/\.hero-stats \{ margin: 16px 0 10px; gap: 10px; min-height: 120px; \}/.test(home) && home.includes('.hero-stats { min-height: 204px; }') && home.includes('.hero-stats { min-height: 256px; margin-top: 14px; }'),
new file mode 100644
new file mode 100644
@@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
+'use strict';
+/**
+ * Every page's shell carries one <h1>: its own. The home page (hero and lists), the Updates page and the Rundown
+ * are fragments like the other pages, inlined only on their own route (server/web/assets.js renderRoute). Before
+ * 2026-10-24 every page shipped the home hero's <h1> and the Updates page's, so a channel page had three, and
+ * every page that was not the home page carried the home page's 37 KB of markup.
+ *
+ * node test/shell-one-h1.test.js
+ */
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const assets = require('../server/web/assets');
+try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */ }
+
+const doc = assets.document('index.html').html;
+const h1s = (html) => (html.replace(/<template[\s\S]*?<\/template>/gi, '').match(/<h1\b/gi) || []).length;
+assert.strictEqual(h1s(doc), 0, 'the bare shell has no page markup with an <h1>');
+
+for (const url of ['/', '/@someone', '/updates', '/content', '/clips', '/moments', '/search', '/rundown', '/documentation', '/chat', '/dashboard', '/broadcast']) {
+ const html = assets.renderRoute(doc, url);
+ assert.ok(h1s(html) <= 1, `${url}: at most one <h1> (${h1s(html)})`);
+ assert.strictEqual(html.includes('class="hero-tagline"'), url === '/', `${url}: the home hero ${url === '/' ? 'is' : 'is not'} in the page`);
+}
+for (const url of ['/', '/updates', '/content', '/moments', '/search', '/@someone']) {
+ assert.strictEqual(h1s(assets.renderRoute(doc, url)), 1, `${url}: its page's <h1> is there from the first paint`);
+}
+
+// The home, Updates and Rundown pages are features with fragments, and the router waits for them.
+const reg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'public', 'features.json'), 'utf8'));
+for (const [feature, frag] of [['home', 'home'], ['updates', 'updates'], ['rundown', 'rundown']]) {
+ assert.strictEqual(reg.features[feature].fragment, frag, `${feature} has its fragment`);
+ assert.ok(fs.existsSync(path.join(__dirname, '..', 'public', 'fragments', `${frag}.html`)), `${frag}.html exists`);
+ assert.ok(doc.includes(`data-fragment="${frag}"><!--ov:fragment:${frag}-->`), `the shell holds ${frag}'s placeholder`);
+}
+assert.ok(reg.routes.some((r) => new RegExp(r.path).test('/updates') && r.features.includes('updates')), '/updates loads its feature');
+const app = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');
+assert.ok(app.includes("whenRouteReady('updates', () => loadUpdatesPage())"), 'the Updates page fills once its markup is in');
+
+// A route of another page keeps the home placeholder inactive; "/" keeps it active.
+const active = (html) => [...html.matchAll(/<section id="(page-[a-z-]+)" class="page active"/g)].map((m) => m[1]);
+assert.deepStrictEqual(active(assets.renderRoute(doc, '/')), ['page-home']);
+assert.deepStrictEqual(active(assets.renderRoute(doc, '/rundown')), ['page-rundown']);
+assert.deepStrictEqual(active(assets.renderRoute(doc, '/updates')), ['page-updates']);
+
+console.log('shell one h1: all passed');
@@ -11,8 +11,8 @@ const ROOT = path.join(__dirname, '..');
@@ -11,8 +11,8 @@ const ROOT = path.join(__dirname, '..');
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
// The shared views
// The shared views
-const index = read('public/index.html');
-assert.ok(/id="hero-latest" data-ov-shipped="latest" data-service="live" href="\/updates"/.test(index), 'the hero one-liner is the shared view');
+const homeHtml = read('public/fragments/home.html');
+assert.ok(/id="hero-latest" data-ov-shipped="latest" data-service="live" href="\/updates"/.test(homeHtml), 'the hero one-liner is the shared view');
const app = read('public/js/app.js');
const app = read('public/js/app.js');
assert.ok(app.includes("sc.src = '/shared/shipped.js'") && app.includes("sc.setAttribute('data-ov-shipped-src', '')"), 'shipped.js from this site\'s own /shared/ pin, with the footer\'s marker');
assert.ok(app.includes("sc.src = '/shared/shipped.js'") && app.includes("sc.setAttribute('data-ov-shipped-src', '')"), 'shipped.js from this site\'s own /shared/ pin, with the footer\'s marker');
assert.ok(/shipped\.log\(container, \{ service: 'live'/.test(app), '/updates is the shared log');
assert.ok(/shipped\.log\(container, \{ service: 'live'/.test(app), '/updates is the shared log');