openvibers/openvibe.live Commits
Asset sync to Media; paste author names; AI overview overlays
Changes
ddeleted file mode 100644@@@ -1,1101 +0,0 @@--<!DOCTYPE html>--<html lang="en">--<head>--<meta charset="UTF-8">--<meta name="viewport" content="width=device-width,initial-scale=1.0">--<title>OPENVIBELIVE — Rick's Bandwidth Nightmare</title>----<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js"></script>--<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>--<script src="https://cdn.jsdelivr.net/npm/[email protected]/tsparticles.engine.min.js"></script>--<script src="https://cdn.jsdelivr.net/npm/[email protected]/tsparticles.bundle.min.js"></script>--<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@400;600;700;800;900&family=Fira+Code:wght@400;700&family=Bebas+Neue&display=swap" rel="stylesheet">--<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">----<style>--* { margin:0; padding:0; box-sizing:border-box; }--html, body { width:100vw; height:100vh; overflow:hidden; background:#000; font-family:'Inter',sans-serif; color:#fff; }----/* ── Background layers ── */--#tsparticles { position:absolute; inset:0; z-index:0; }--.bg-pulse {-- position:absolute; inset:0; z-index:0;-- background:radial-gradient(ellipse at 50% 50%, rgba(139,92,246,0.07) 0%, transparent 65%);-- animation:bgPulse 3s ease-in-out infinite alternate;--}--@keyframes bgPulse { from{opacity:.4;transform:scale(1)} to{opacity:1;transform:scale(1.08)} }--.scanlines {-- position:absolute; inset:0; z-index:1; pointer-events:none;-- background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.07) 2px,rgba(0,0,0,.07) 4px);--}--.vignette {-- position:absolute; inset:0; z-index:2; pointer-events:none;-- background:radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,.8) 100%);--}--.flash { position:absolute; inset:0; z-index:50; background:#fff; opacity:0; pointer-events:none; }----/* ── Tickers ── */--.ticker-wrap {-- position:absolute; left:0; right:0; z-index:15; overflow:hidden;--}--.ticker-bottom { bottom:0; height:5.5vh; background:rgba(139,92,246,0.12); border-top:1px solid rgba(139,92,246,0.25); }--.ticker-top { top:0; height:4vh; background:rgba(239,68,68,0.08); border-bottom:1px solid rgba(239,68,68,0.2); }--.ticker-inner {-- display:flex; white-space:nowrap; gap:0;-- font-family:'Fira Code',monospace; font-weight:700; text-transform:uppercase; letter-spacing:2px;--}--.ticker-bottom .ticker-inner { font-size:2.2vh; color:rgba(139,92,246,0.95); line-height:5.5vh; animation:tickerL 45s linear infinite; text-shadow:0 0 12px rgba(139,92,246,0.5); }--.ticker-top .ticker-inner { font-size:1.8vh; color:rgba(239,68,68,0.8); line-height:4vh; animation:tickerR 38s linear infinite; }--@keyframes tickerL { from{transform:translateX(0)} to{transform:translateX(-50%)} }--@keyframes tickerR { from{transform:translateX(-50%)} to{transform:translateX(0)} }----/* ── Corners ── */--.corner { position:absolute; width:5vw; height:5vw; z-index:12; }--.corner::before,.corner::after { content:''; position:absolute; background:rgba(139,92,246,0.4); }--.corner-tl { top:5.5vh; left:1.5vw; }--.corner-tl::before { width:3vw; height:2px; top:0; left:0; }--.corner-tl::after { width:2px; height:3vw; top:0; left:0; }--.corner-tr { top:5.5vh; right:1.5vw; }--.corner-tr::before { width:3vw; height:2px; top:0; right:0; }--.corner-tr::after { width:2px; height:3vw; top:0; right:0; }--.corner-bl { bottom:6.5vh; left:1.5vw; }--.corner-bl::before { width:3vw; height:2px; bottom:0; left:0; }--.corner-bl::after { width:2px; height:3vw; bottom:0; left:0; }--.corner-br { bottom:6.5vh; right:1.5vw; }--.corner-br::before { width:3vw; height:2px; bottom:0; right:0; }--.corner-br::after { width:2px; height:3vw; bottom:0; right:0; }----/* ── Slide labels (top-left, top-right) ── */--.status-bar {-- position:absolute; top:0; left:0; right:0; z-index:14;-- display:flex; justify-content:space-between; align-items:center;-- padding:0 2vw; height:5.5vh;-- font-family:'Fira Code',monospace; font-size:1.3vh; font-weight:700;-- text-transform:uppercase; letter-spacing:3px;-- color:rgba(139,92,246,0.5);-- pointer-events:none;--}--.status-left { text-align:left; }--.status-right { text-align:right; color:rgba(239,68,68,0.4); }--.blink { animation:blink 1s step-end infinite; }--@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }----/* ── Main stage ── */--.stage {-- position:absolute; inset:0; z-index:10;-- top:5.5vh; bottom:5.5vh;-- display:flex; align-items:center; justify-content:center;-- overflow:hidden;--}----/* ── Slide container ── */--.slide {-- position:absolute; inset:0;-- display:flex; flex-direction:column;-- align-items:center; justify-content:center;-- text-align:center;-- padding:2vh 6vw;-- opacity:0;-- pointer-events:none;--}--.slide.active { pointer-events:auto; }----/* ══ SLIDE STYLES ══ */----/* eyebrow label above main text */--.s-eyebrow {-- font-family:'Bebas Neue',sans-serif;-- font-size:3.5vh; letter-spacing:1vw;-- color:rgba(255,255,255,0.45);-- margin-bottom:1vh;-- text-transform:uppercase;--}----/* main headline */--.s-head {-- font-family:'Orbitron',sans-serif;-- font-weight:900; line-height:1.05;-- letter-spacing:0.3vw;-- text-transform:uppercase;-- text-shadow:0 2px 20px rgba(0,0,0,0.9);--}--.s-head.xl { font-size:14vh; }--.s-head.lg { font-size:10vh; }--.s-head.md { font-size:7vh; }--.s-head.sm { font-size:5vh; }----/* colour variants */--.gold { color:#8b5cf6; text-shadow:0 0 40px rgba(139,92,246,0.5),0 2px 12px rgba(0,0,0,0.9); }--.red { color:#ef4444; text-shadow:0 0 40px rgba(239,68,68,0.5),0 2px 12px rgba(0,0,0,0.9); }--.grn { color:#22c55e; text-shadow:0 0 30px rgba(34,197,94,0.4),0 2px 10px rgba(0,0,0,0.9); }--.wht { color:#fff; }--.gold-grad {-- background:linear-gradient(135deg,#8b5cf6,#f5d394,#ff8c00,#8b5cf6);-- background-size:300% 300%; animation:goldShift 3s ease infinite;-- -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;-- filter:drop-shadow(0 0 40px rgba(139,92,246,0.4));--}--@keyframes goldShift { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }--.rainbow-grad {-- background:linear-gradient(90deg,#8b5cf6,#22c55e,#3b82f6,#a855f7,#ef4444,#8b5cf6);-- background-size:400% 100%; animation:rainbowShift 2.5s linear infinite;-- -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;--}--@keyframes rainbowShift { 0%{background-position:0% 50%} 100%{background-position:400% 50%} }--.strike { text-decoration:line-through; opacity:.7; }----/* sub-text */--.s-sub {-- font-size:2.8vh; font-weight:700;-- color:rgba(255,255,255,0.8);-- margin-top:1.5vh; line-height:1.6;-- text-shadow:0 1px 8px rgba(0,0,0,0.9);-- max-width:70vw;--}--.s-sub .hi { color:#8b5cf6; }--.s-sub .ri { color:#ef4444; }--.s-sub .gi { color:#22c55e; }----/* badge/stamp */--.stamp {-- display:inline-block;-- font-family:'Bebas Neue',sans-serif;-- font-size:3vh; letter-spacing:0.6vw;-- border:3px solid currentColor;-- padding:0.3vh 1.5vw;-- border-radius:4px;-- margin:1.5vh 0;-- transform:rotate(-2deg);--}--.stamp.red-stamp { color:#ef4444; box-shadow:0 0 20px rgba(239,68,68,0.3); }--.stamp.gold-stamp { color:#8b5cf6; box-shadow:0 0 20px rgba(139,92,246,0.3); }--.stamp.grn-stamp { color:#22c55e; box-shadow:0 0 20px rgba(34,197,94,0.3); }----/* VS comparison */--.vs-grid {-- display:flex; align-items:flex-start; gap:4vw;-- margin-top:2vh;--}--.vs-col { text-align:center; min-width:22vw; }--.vs-col-title {-- font-family:'Orbitron',sans-serif; font-weight:900;-- font-size:3.5vh; letter-spacing:3px; margin-bottom:1.5vh;-- text-shadow:0 1px 8px rgba(0,0,0,0.8);--}--.vs-divider {-- font-family:'Orbitron',sans-serif; font-size:6vh; font-weight:900;-- color:rgba(255,255,255,0.1); align-self:center;--}--.vs-item {-- font-size:2.2vh; font-weight:700; margin:0.5vh 0;-- text-shadow:0 1px 6px rgba(0,0,0,0.8);--}--.vs-item.y { color:#22c55e; }--.vs-item.n { color:#ef4444; }----/* stat block */--.stat-row { display:flex; gap:5vw; margin-top:2vh; justify-content:center; }--.stat-box {-- text-align:center;-- background:rgba(0,0,0,0.4); border:1px solid rgba(139,92,246,0.25);-- border-radius:12px; padding:2vh 3vw;-- backdrop-filter:blur(6px);--}--.stat-num {-- font-family:'Orbitron',sans-serif; font-weight:900;-- font-size:6vh; color:#8b5cf6;-- text-shadow:0 0 30px rgba(139,92,246,0.5);-- line-height:1;--}--.stat-lbl {-- font-size:1.6vh; font-weight:700;-- color:rgba(255,255,255,0.55);-- letter-spacing:2px; text-transform:uppercase;-- margin-top:0.5vh;--}----/* meme big text */--.meme-line {-- font-family:'Bebas Neue',sans-serif;-- font-size:18vh; line-height:0.92;-- letter-spacing:0.5vw; text-transform:uppercase;-- color:#fff; text-shadow:0 4px 30px rgba(0,0,0,1),0 0 80px rgba(139,92,246,0.3);--}--.meme-line.stack { font-size:11vh; }----/* OpenVibe sig */--.openvibe-sig {-- position:absolute; bottom:6.5vh; right:2vw; z-index:14;-- font-family:'Orbitron',sans-serif; font-size:1.4vh; font-weight:700;-- color:rgba(139,92,246,0.3); letter-spacing:3px; text-transform:uppercase;--}----/* slide progress dots */--.slide-dots {-- position:absolute; bottom:6.5vh; left:50%; transform:translateX(-50%);-- z-index:14; display:flex; gap:8px; align-items:center;--}--.dot {-- width:6px; height:6px; border-radius:50%;-- background:rgba(255,255,255,0.15);-- transition:all 0.4s ease;--}--.dot.active { background:#8b5cf6; width:20px; border-radius:3px; box-shadow:0 0 8px rgba(139,92,246,0.6); }----/* floating embers */--.ember {-- position:absolute; width:4px; height:4px;-- background:#8b5cf6; border-radius:50%;-- opacity:0; z-index:3; filter:blur(1px); pointer-events:none;--}--.skull-float {-- position:absolute; opacity:0; z-index:4; pointer-events:none;-- font-size:3vh;--}----/* glitch overlay */--.glitch-overlay {-- position:absolute; inset:0; z-index:20; pointer-events:none; opacity:0;-- background:linear-gradient(transparent 0%,rgba(139,92,246,0.05) 50%,transparent 100%);-- animation:glitchSlide .15s steps(2) infinite; mix-blend-mode:overlay;--}--@keyframes glitchSlide { 0%{transform:translateY(-100%)} 100%{transform:translateY(100%)} }------</style>--</head>--<body>----<!-- Bg layers -->--<div id="tsparticles"></div>--<div class="bg-pulse"></div>--<div class="scanlines"></div>--<div class="vignette"></div>--<div class="glitch-overlay" id="glitch"></div>--<div class="flash" id="flash"></div>----<!-- Status bar top -->--<div class="status-bar">-- <div class="status-left">OPENVIBELIVE.COM // LIVE // ROBOT 1837</div>-- <div class="status-right"><span class="blink">●</span> REC // RIP RICKSTREAMER</div>--</div>----<!-- Corners -->--<div class="corner corner-tl"></div>--<div class="corner corner-tr"></div>--<div class="corner corner-bl"></div>--<div class="corner corner-br"></div>----<!-- Slide progress dots -->--<div class="slide-dots" id="dots"></div>----<!-- OpenVibe sig -->--<div class="openvibe-sig">openvibe.live</div>--------<!-- ══════════════ SLIDES ══════════════ -->--<div class="stage" id="stage">----<!-- SLIDE 0: Title card -->--<div class="slide" id="s0">-- <div class="s-eyebrow">Welcome to</div>-- <div class="s-head xl gold-grad">OPENVIBELIVE</div>-- <div class="s-sub">The open-source streaming platform <span class="hi">built in a shed</span><br>that keeps Rick up at night 🏠</div>--</div>----<!-- SLIDE 1: 9,309 VODs -->--<div class="slide" id="s1">-- <div class="s-eyebrow">We downloaded</div>-- <div class="s-head xl gold" id="vod-counter">9,309</div>-- <div class="s-head sm wht" style="letter-spacing:0.8vw;margin-top:-.5vh;">VODs FROM ROBOTSTREAMER</div>-- <div class="s-sub"><span class="hi">Before we BROKE their bandwidth cap.</span><br><span class="ri">"download_cap_exceeded" — HTTP 403 🤣</span></div>--</div>----<!-- SLIDE 2: Rick roast -->--<div class="slide" id="s2">-- <div class="stamp red-stamp"><i class="fa-solid fa-skull"></i> CERTIFIED L</div>-- <div class="meme-line red">RICK</div>-- <div class="meme-line stack gold">COULDN'T</div>-- <div class="meme-line stack wht">STOP US</div>-- <div class="s-sub" style="margin-top:2vh;">Closed source. Wefunder. "All Rights Reserved."<br><span class="ri">On a dead site.</span> lmaooooo</div>--</div>----<!-- SLIDE 3: VS comparison -->--<div class="slide" id="s3">-- <div class="s-head md wht" style="margin-bottom:2vh;">The Honest Comparison</div>-- <div class="vs-grid">-- <div class="vs-col">-- <div class="vs-col-title gold">OPENVIBELIVE</div>-- <div class="vs-item y"><i class="fa-solid fa-check"></i> Free Forever</div>-- <div class="vs-item y"><i class="fa-solid fa-code-branch"></i> Open Source</div>-- <div class="vs-item y"><i class="fa-solid fa-users"></i> Community Built</div>-- <div class="vs-item y"><i class="fa-solid fa-house"></i> Shed Powered</div>-- <div class="vs-item y"><i class="fa-solid fa-heart"></i> Actually Works</div>-- <div class="vs-item y"><i class="fa-solid fa-infinity"></i> Still Running</div>-- </div>-- <div class="vs-divider"><i class="fa-solid fa-skull-crossbones"></i></div>-- <div class="vs-col">-- <div class="vs-col-title red strike">ROBOTSTREAMER</div>-- <div class="vs-item n"><i class="fa-solid fa-xmark"></i> Patreon Begging</div>-- <div class="vs-item n"><i class="fa-solid fa-lock"></i> Closed Source</div>-- <div class="vs-item n"><i class="fa-solid fa-building"></i> "Corporation"</div>-- <div class="vs-item n"><i class="fa-solid fa-ghost"></i> Ghost Town</div>-- <div class="vs-item n"><i class="fa-solid fa-person-digging"></i> Owned by Rick</div>-- <div class="vs-item n"><i class="fa-solid fa-cross"></i> Effectively Dead</div>-- </div>-- </div>--</div>----<!-- SLIDE 4: Stats -->--<div class="slide" id="s4">-- <div class="s-eyebrow">By the numbers</div>-- <div class="s-head lg gold-grad">THE ARCHIVE</div>-- <div class="stat-row">-- <div class="stat-box">-- <div class="stat-num">9,309</div>-- <div class="stat-lbl">VODs Downloaded</div>-- </div>-- <div class="stat-box">-- <div class="stat-num">872 GB</div>-- <div class="stat-lbl">Before cap hit</div>-- </div>-- <div class="stat-box">-- <div class="stat-num">439</div>-- <div class="stat-lbl">Goosely Followers</div>-- </div>-- <div class="stat-box">-- <div class="stat-num red">403</div>-- <div class="stat-lbl">Rick's Error Code</div>-- </div>-- </div>--</div>----<!-- SLIDE 5: WEFUNERAL -->--<div class="slide" id="s5">-- <div class="meme-line red" style="font-size:9vh;">WEFUNDER?</div>-- <div class="meme-line gold" style="font-size:13vh;margin-top:-.5vh;">MORE LIKE</div>-- <div class="meme-line red" style="font-size:11vh;margin-top:-.5vh;">WEFUNERAL</div>-- <div class="s-sub" style="margin-top:2vh;"><span class="ri">RIP RobotStreamer Corporation 2016–???</span><br>Cause of death: <span class="hi">losing to a guy in a shed</span></div>--</div>----<!-- SLIDE 6: Shed origin -->--<div class="slide" id="s6">-- <div class="s-eyebrow" style="font-size:5vh;">🏠</div>-- <div class="s-head lg gold-grad">BUILT FROM</div>-- <div class="s-head lg wht" style="margin-top:-.5vh;">A LITERAL SHED</div>-- <div class="s-sub" style="margin-top:2vh;">-- No VC funding. No corporate BS. No "all rights reserved."<br>-- Just <span class="hi">vibes, code, and a DIY spirit</span> that Rick can't buy.<br>-- <span class="gi">openvibe.live — free & open source forever</span>-- </div>--</div>----<!-- SLIDE 7: All rights reversed -->--<div class="slide" id="s7">-- <div class="stamp red-stamp" style="transform:rotate(2deg);">© RobotStreamer Corporation</div>-- <div class="s-head md red" style="margin-top:1vh;">"ALL RIGHTS RESERVED"</div>-- <div class="s-head sm wht" style="margin-top:1vh;">rights to what, exactly?</div>-- <div class="s-sub" style="margin-top:2vh;">-- <span class="ri">A dead website?</span> A Wefunder page with 3 backers?<br>-- A Discord with more bots than users?<br>-- <span class="hi">Buddy.</span>-- </div>-- <div class="stamp grn-stamp" style="margin-top:2vh;transform:rotate(-1deg);">ALL RIGHTS REVERSED — OPENVIBELIVE</div>--</div>----<!-- SLIDE 8: Goosely tribute -->--<div class="slide" id="s8">-- <div class="s-eyebrow">🦆 Robot #1837</div>-- <div class="s-head lg gold-grad">GOOSELY</div>-- <div class="s-head sm wht" style="margin-top:-.5vh;letter-spacing:.5vw;">ROBOTSTREAMER.COM/ROBOT/1837</div>-- <div class="s-sub" style="margin-top:2vh;">-- <span class="hi">439 followers</span> who deserve better than Rick's dying platform.<br>-- <span class="hi">9,309 VODs</span> saved before their servers <span class="ri">literally gave up.</span><br>-- History doesn't die just because Rick's bandwidth does. 🦆-- </div>--</div>----<!-- SLIDE 9: Pure hype -->--<div class="slide" id="s9">-- <div class="s-head xl rainbow-grad">OPENVIBE</div>-- <div class="s-head xl rainbow-grad" style="margin-top:-.5vh;">4</div>-- <div class="s-head xl rainbow-grad" style="margin-top:-.5vh;">LIFE</div>--</div>----<!-- SLIDE 10: Rick's reaction -->--<div class="slide" id="s10">-- <div class="s-eyebrow">Rick rn:</div>-- <div class="meme-line red" style="font-size:16vh;line-height:1;">😤</div>-- <div class="s-head md gold" style="margin-top:1vh;">SEETHING</div>-- <div class="s-sub" style="margin-top:1.5vh;">-- <span class="ri">All rights reserved on a ghost town.</span><br>-- Meanwhile we're over here downloading <span class="hi">9,309 VODs</span><br>and <span class="gi">open sourcing everything. 🏠✨</span>-- </div>--</div>----<!-- SLIDE 11: Open source wins -->--<div class="slide" id="s11">-- <div class="s-head lg gold-grad">OPEN SOURCE</div>-- <div class="s-head md wht" style="margin-top:-.2vh;letter-spacing:.8vw;">ALWAYS WINS</div>-- <div class="stamp grn-stamp" style="margin-top:2vh;font-size:2.5vh;">github.com/OpenVibe.Live</div>-- <div class="s-sub" style="margin-top:1.5vh;">-- Fork it. Modify it. Host your own. <span class="hi">We don't care.</span><br>-- That's what free software means, Rick.<br>-- <span class="gi">You should try it sometime.</span>-- </div>--</div>----<!-- SLIDE 12: bandwidth cap -->--<div class="slide" id="s12">-- <div class="stamp red-stamp" style="transform:rotate(-1deg);">HTTP 403 — download_cap_exceeded</div>-- <div class="s-head lg red" style="margin-top:.5vh;">WE BROKE</div>-- <div class="s-head xl gold-grad" style="margin-top:-.3vh;">RICK'S BANDWIDTH</div>-- <div class="s-sub" style="margin-top:1.5vh;">-- <span class="ri">"Cannot download file, download bandwidth or<br>transaction (Class B) cap exceeded."</span><br>-- <span class="hi">9,309 VODs. 872 GB. Then RobotStreamer DIED.</span> 🏆-- </div>--</div>----<!-- SLIDE 13: GG -->--<div class="slide" id="s13">-- <div class="s-eyebrow">the moment robotstreamer said</div>-- <div class="meme-line gold" style="font-size:22vh;line-height:1;">GG</div>-- <div class="s-sub" style="margin-top:1vh;">-- We downloaded so many VODs that<br>-- <span class="ri">RobotStreamer's own servers gave up on life.</span><br>-- <span class="hi">9,309 VODs. bandwidth cap: exceeded. Rick: 😭</span>-- </div>--</div>----<!-- SLIDE 14: error code terminal -->--<div class="slide" id="s14">-- <div class="s-eyebrow" style="font-family:'Fira Code',monospace;font-size:1.8vh;color:rgba(34,197,94,0.8);">$ wget robotstreamer.com/robot/1837/vod/9310.mp4</div>-- <div style="font-family:'Fira Code',monospace;font-size:2.8vh;color:#ef4444;margin-top:1.5vh;line-height:1.6;font-weight:700;text-shadow:0 0 20px rgba(239,68,68,0.4);">-- {<br>-- "code": "download_cap_exceeded",<br>-- "message": "Cannot download file,<br>-- download bandwidth or transaction<br>-- (Class B) cap exceeded.",<br>-- "status": 403<br>-- }-- </div>-- <div class="s-head sm gold" style="margin-top:2vh;">A CORPORATION. WITH A BANDWIDTH CAP. 💀</div>-- <div class="s-sub" style="margin-top:1vh;"><span class="ri">We hit it.</span> Just us. <span class="hi">One guy in a shed vibing.</span> 🏠</div>--</div>----<!-- SLIDE 15: tombstone -->--<div class="slide" id="s15">-- <div class="s-head sm wht">⚰️</div>-- <div class="s-head md red" style="margin-top:1vh;">HERE LIES</div>-- <div class="s-head lg red">ROBOTSTREAMER</div>-- <div class="s-head sm wht" style="margin-top:.5vh;font-size:3vh;letter-spacing:.4vw;">CORPORATION</div>-- <div class="s-sub" style="margin-top:2vh;">-- <span class="ri">Beloved by no one. Missed by the bots.</span><br>-- "All Rights Reserved" — all rights to the graveyard.<br>-- <span class="hi">Survived by: OpenVibe.Live (thriving).</span> 🪦-- </div>--</div>------<!-- SLIDE 16: Rick's DMs -->--<div class="slide" id="s16">-- <div class="s-eyebrow" style="font-size:2.5vh;margin-bottom:2vh;">📱 Rick's DMs (probably)</div>-- <div style="display:flex;flex-direction:column;gap:1.2vh;max-width:55vw;text-align:left;">-- <div style="background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.25);border-radius:12px;padding:1.2vh 2vw;font-family:'Fira Code',monospace;font-size:2.2vh;font-weight:700;">-- <span style="color:rgba(239,68,68,0.6);font-size:1.4vh;">RICK · 2:34 AM</span><br>-- <span style="color:#fff;">hey can you guys stop downloading</span>-- </div>-- <div style="background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.25);border-radius:12px;padding:1.2vh 2vw;font-family:'Fira Code',monospace;font-size:2.2vh;font-weight:700;">-- <span style="color:rgba(239,68,68,0.6);font-size:1.4vh;">RICK · 2:35 AM</span><br>-- <span style="color:#fff;">seriously we're hitting our bandwidth cap</span>-- </div>-- <div style="background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.25);border-radius:12px;padding:1.2vh 2vw;font-family:'Fira Code',monospace;font-size:2.2vh;font-weight:700;">-- <span style="color:rgba(239,68,68,0.6);font-size:1.4vh;">RICK · 2:41 AM</span><br>-- <span style="color:#fff;">hello?? all rights reserved guys</span>-- </div>-- <div style="background:rgba(139,92,246,0.12);border:1px solid rgba(139,92,246,0.3);border-radius:12px;padding:1.2vh 2vw;font-family:'Fira Code',monospace;font-size:2.2vh;font-weight:700;align-self:flex-end;">-- <span style="color:rgba(139,92,246,0.6);font-size:1.4vh;">OPENVIBE · 3:00 AM</span><br>-- <span style="color:#8b5cf6;">gg no re 🏠</span>-- </div>-- <div style="background:rgba(0,0,0,0.4);border:1px solid rgba(239,68,68,0.2);border-radius:8px;padding:.8vh 2vw;font-family:'Fira Code',monospace;font-size:1.6vh;color:rgba(239,68,68,0.6);font-style:italic;">-- ⚠ System: download_cap_exceeded — HTTP 403 — Rick has left the chat-- </div>-- </div>--</div>----<!-- SLIDE 17: Achievement unlocked -->--<div class="slide" id="s17">-- <div style="background:rgba(0,0,0,0.5);border:3px solid #8b5cf6;border-radius:16px;padding:3vh 4vw;max-width:60vw;box-shadow:0 0 60px rgba(139,92,246,0.2);">-- <div style="font-family:'Fira Code',monospace;font-size:2vh;color:rgba(139,92,246,0.7);letter-spacing:4px;text-transform:uppercase;margin-bottom:1.5vh;">🔔 Achievement Unlocked</div>-- <div class="s-head lg gold-grad" style="margin-bottom:1.5vh;">"BANDWIDTH BULLY"</div>-- <div style="font-size:2.6vh;font-weight:700;color:rgba(255,255,255,0.85);line-height:1.7;text-shadow:0 1px 8px rgba(0,0,0,0.9);">-- Downloaded <span style="color:#8b5cf6;">9,309</span> VODs until the server<br>begged for mercy and threw a <span style="color:#ef4444;">403</span>.<br><br>-- <span style="color:#22c55e;">Reward:</span> Rent free in Rick's head. Forever.-- </div>-- <div style="margin-top:2vh;font-family:'Orbitron',sans-serif;font-size:1.6vh;color:rgba(255,255,255,0.3);letter-spacing:3px;">GAMERSCORE: +9309 · RARITY: ONE OF A KIND</div>-- </div>--</div>----<!-- SLIDE 18: The timeline -->--<div class="slide" id="s18">-- <div class="s-head md gold-grad" style="margin-bottom:2.5vh;">TIMELINE OF EVENTS</div>-- <div style="display:flex;flex-direction:column;gap:1vh;max-width:65vw;text-align:left;">-- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">2016</span>-- <span style="color:rgba(255,255,255,0.75);">RobotStreamer launches, Rick becomes a "CEO"</span>-- </div>-- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">2019</span>-- <span style="color:rgba(255,255,255,0.75);">OpenVibe.Live starts. In a shed. For free.</span>-- </div>-- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">2024</span>-- <span style="color:#ef4444;">RS launches Wefunder. Raises approximately $0.</span>-- </div>-- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">2025</span>-- <span style="color:#ef4444;">RS quietly enables bandwidth caps on downloads</span>-- </div>-- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">Apr 2026</span>-- <span style="color:#8b5cf6;">One superfan downloads 9,309 VODs (872 GB)</span>-- </div>-- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:rgba(239,68,68,0.8);font-family:'Fira Code',monospace;min-width:8vw;">Apr 2026</span>-- <span style="color:#ef4444;">HTTP 403: download_cap_exceeded. Rick in shambles.</span>-- </div>-- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:rgba(239,68,68,0.8);font-family:'Fira Code',monospace;min-width:8vw;">Apr 2026</span>-- <span style="color:#ef4444;">Goosely posts Python VOD tool to Rick's Discord 🐍</span>-- </div>-- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:rgba(239,68,68,0.8);font-family:'Fira Code',monospace;min-width:8vw;">Apr 2026</span>-- <span style="color:#ef4444;">Rick: "might be a virus." It was open source Python. 🦠</span>-- </div>-- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">Apr 2026</span>-- <span style="color:#8b5cf6;">"do you even know how to code?" — Goosely, to the CEO</span>-- </div>-- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.4vh;font-weight:800;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:#22c55e;font-family:'Fira Code',monospace;min-width:8vw;">NOW</span>-- <span style="color:#22c55e;">We are streaming this on robotstreamer. Hi Rick. 👋</span>-- </div>-- </div>--</div>----<!-- SLIDE 19: Org chart -->--<div class="slide" id="s19">-- <div class="s-head md wht" style="margin-bottom:2.5vh;">RobotStreamer Corp — Official Org Chart</div>-- <div style="display:flex;flex-direction:column;align-items:center;gap:1.5vh;">-- <div style="background:rgba(239,68,68,0.15);border:2px solid rgba(239,68,68,0.4);border-radius:10px;padding:1.2vh 3vw;font-family:'Orbitron',sans-serif;font-weight:900;font-size:2.5vh;color:#ef4444;">👤 RICK — CEO / CTO / CFO / COO</div>-- <div style="width:2px;height:3vh;background:rgba(239,68,68,0.3);"></div>-- <div style="display:flex;gap:3vw;">-- <div style="display:flex;flex-direction:column;align-items:center;gap:1vh;">-- <div style="width:2px;height:2vh;background:rgba(239,68,68,0.3);"></div>-- <div style="background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:.8vh 1.5vw;font-size:2vh;font-weight:700;color:rgba(255,255,255,0.5);">🤖 Bot #1<br><span style="font-size:1.4vh;color:rgba(255,255,255,0.3);">"Active User"</span></div>-- </div>-- <div style="display:flex;flex-direction:column;align-items:center;gap:1vh;">-- <div style="width:2px;height:2vh;background:rgba(239,68,68,0.3);"></div>-- <div style="background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:.8vh 1.5vw;font-size:2vh;font-weight:700;color:rgba(255,255,255,0.5);">🤖 Bot #2<br><span style="font-size:1.4vh;color:rgba(255,255,255,0.3);">"Customer"</span></div>-- </div>-- <div style="display:flex;flex-direction:column;align-items:center;gap:1vh;">-- <div style="width:2px;height:2vh;background:rgba(239,68,68,0.3);"></div>-- <div style="background:rgba(239,68,68,0.08);border:1px dashed rgba(239,68,68,0.3);border-radius:8px;padding:.8vh 1.5vw;font-size:2vh;font-weight:700;color:rgba(239,68,68,0.5);">🪑 Support Team<br><span style="font-size:1.4vh;">[Unfilled Since 2019]</span></div>-- </div>-- <div style="display:flex;flex-direction:column;align-items:center;gap:1vh;">-- <div style="width:2px;height:2vh;background:rgba(239,68,68,0.3);"></div>-- <div style="background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:.8vh 1.5vw;font-size:2vh;font-weight:700;color:rgba(255,255,255,0.5);">🤖 Bot #3<br><span style="font-size:1.4vh;color:rgba(255,255,255,0.3);">"Investor"</span></div>-- </div>-- </div>-- <div style="margin-top:1vh;font-size:1.8vh;font-weight:700;color:rgba(255,255,255,0.4);font-style:italic;">*Bandwidth budget: exceeded by one superfan</div>-- </div>--</div>----<!-- SLIDE 20: Google review -->--<div class="slide" id="s20">-- <div style="background:rgba(0,0,0,0.5);border:2px solid rgba(255,255,255,0.1);border-radius:16px;padding:3vh 4vw;max-width:62vw;text-align:left;box-shadow:0 0 40px rgba(0,0,0,0.5);">-- <div style="display:flex;align-items:center;gap:1.5vw;margin-bottom:1.5vh;">-- <div style="width:6vh;height:6vh;border-radius:50%;background:linear-gradient(135deg,#8b5cf6,#f5d394);display:flex;align-items:center;justify-content:center;font-size:3vh;">🏠</div>-- <div>-- <div style="font-size:2.2vh;font-weight:800;color:#fff;">OpenVibe.Live Official</div>-- <div style="font-size:1.5vh;color:rgba(255,255,255,0.4);">1 review · Local Guide · Shed Dweller</div>-- </div>-- <div style="margin-left:auto;font-size:3vh;letter-spacing:-2px;">⭐<span style="color:rgba(255,255,255,0.15);">★★★★</span></div>-- </div>-- <div style="font-size:2.3vh;font-weight:700;color:rgba(255,255,255,0.85);line-height:1.7;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- "Great platform if you like <span style="color:#ef4444;">403 errors</span>, closed source drama,<br>-- a Wefunder with 3 backers, and watching Rick<br>-- slowly come to terms with losing to open source.<br><br>-- Downloaded <span style="color:#8b5cf6;">9,309 VODs</span> before their servers literally<br>-- gave up. <span style="color:#22c55e;">Would crash bandwidth cap again.</span>"-- </div>-- <div style="margin-top:1.5vh;font-size:1.6vh;color:rgba(255,255,255,0.3);">Posted April 2026 · Helpful? 👍 9309 · 👎 0 (Rick)</div>-- </div>--</div>----<!-- SLIDE 21: Job posting -->--<div class="slide" id="s21">-- <div style="background:rgba(0,0,0,0.5);border:2px solid rgba(255,255,255,0.08);border-radius:16px;padding:2.5vh 4vw;max-width:62vw;text-align:left;">-- <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1.5vh;">-- <div>-- <div style="font-size:2.8vh;font-weight:900;color:#fff;text-shadow:0 1px 8px rgba(0,0,0,0.9);">Senior Bandwidth Manager</div>-- <div style="font-size:2vh;font-weight:700;color:#ef4444;margin-top:.3vh;">RobotStreamer Corporation · Ghost Town, Internet</div>-- </div>-- <div style="background:rgba(239,68,68,0.15);border:1px solid rgba(239,68,68,0.3);border-radius:8px;padding:.5vh 1.5vw;font-size:1.6vh;font-weight:700;color:#ef4444;">URGENT HIRE 🚨</div>-- </div>-- <div style="font-size:2vh;font-weight:700;color:rgba(255,255,255,0.75);line-height:1.8;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:#8b5cf6;">Responsibilities:</span><br>-- · Prevent anyone from downloading 9,309+ VODs<br>-- · Explain to Rick why the bandwidth cap exists<br>-- · Monitor the 403 error dashboard (full-time job now)<br>-- · Attend daily All Hands Meeting with Rick and the bots<br><br>-- <span style="color:#8b5cf6;">Compensation:</span> <span style="color:#ef4444;">Equity in a closed-source dead site</span><br>-- <span style="color:#8b5cf6;">Requirements:</span> Must be okay with losing to a shed<br>-- </div>-- <div style="margin-top:1.5vh;font-size:1.6vh;color:rgba(255,255,255,0.25);font-style:italic;">RobotStreamer Corporation is an equal opportunity employer (of bots)</div>-- </div>--</div>------<!-- SLIDE 22: Rick called Python a virus -->--<div class="slide" id="s22">-- <div class="stamp red-stamp" style="transform:rotate(1deg);">🦠 RICK'S OFFICIAL DIAGNOSIS</div>-- <div class="s-head lg red" style="margin-top:.8vh;">RICK CALLED</div>-- <div class="s-head xl gold-grad" style="margin-top:-.3vh;">PYTHON</div>-- <div class="s-head lg red" style="margin-top:-.3vh;">A VIRUS</div>-- <div class="s-sub" style="margin-top:1.5vh;">-- Open source script. Publicly readable. A few hundred lines.<br>-- <span class="ri">"Might be fine but have no idea until its reviewed."</span><br>-- <span class="hi">This is the CEO. This is who has your data.</span> 💀-- </div>--</div>----<!-- SLIDE 23: The Discord exchange (real quotes) -->--<div class="slide" id="s23">-- <div class="s-eyebrow" style="font-size:2.5vh;margin-bottom:1.5vh;">📢 Rick's Discord — Real Actual Quotes</div>-- <div style="display:flex;flex-direction:column;gap:1vh;max-width:64vw;text-align:left;">-- <div style="background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.25);border-radius:12px;padding:1vh 1.8vw;font-family:'Fira Code',monospace;font-size:1.85vh;font-weight:700;">-- <span style="color:rgba(239,68,68,0.55);font-size:1.3vh;">rs (CEO, RobotStreamer) · 1:16 PM</span><br>-- <span style="color:#fff;">@everyone advice: don't use that ^ until we have time to review the code. Might be fine but have no idea until its reviewed.</span>-- </div>-- <div style="background:rgba(139,92,246,0.12);border:1px solid rgba(139,92,246,0.3);border-radius:12px;padding:1vh 1.8vw;font-family:'Fira Code',monospace;font-size:1.85vh;font-weight:700;">-- <span style="color:rgba(139,92,246,0.55);font-size:1.3vh;">Goosely 🦆 · 1:20 PM</span><br>-- <span style="color:#8b5cf6;">lol k dipshit i will just delete it<br>learn to read code<br>its literally a script that is a few hundred lines,<br>do you even know how to code?</span>-- </div>-- <div style="background:rgba(139,92,246,0.12);border:1px solid rgba(139,92,246,0.3);border-radius:12px;padding:1vh 1.8vw;font-family:'Fira Code',monospace;font-size:1.85vh;font-weight:700;">-- <span style="color:rgba(139,92,246,0.55);font-size:1.3vh;">Goosely 🦆 · 1:20 PM</span><br>-- <span style="color:#8b5cf6;">I am not a malicious person. That was open source code you can literally read in about 30 seconds, how stupid are you?</span>-- </div>-- <div style="background:rgba(0,0,0,0.4);border:1px solid rgba(239,68,68,0.2);border-radius:8px;padding:.6vh 1.8vw;font-family:'Fira Code',monospace;font-size:1.4vh;color:rgba(239,68,68,0.55);font-style:italic;">-- ⚠ update: RS VOD system still broken. can't view any vods. Rick: unavailable. His data: your problem.-- </div>-- </div>--</div>----<!-- SLIDE 24: CEO Credentials -->--<div class="slide" id="s24">-- <div class="s-head md wht" style="margin-bottom:2.5vh;">RobotStreamer CEO — Verified Qualifications</div>-- <div style="display:flex;gap:8vw;align-items:flex-start;justify-content:center;">-- <div style="text-align:left;">-- <div style="font-family:'Bebas Neue',sans-serif;font-size:2.8vh;color:#ef4444;letter-spacing:3px;margin-bottom:1.2vh;">CANNOT:</div>-- <div style="font-size:2.1vh;font-weight:700;color:rgba(255,255,255,0.8);line-height:2.1;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:#ef4444;">✗</span> Read open-source Python<br>-- <span style="color:#ef4444;">✗</span> Distinguish code from malware<br>-- <span style="color:#ef4444;">✗</span> Review ~1,600 lines of script<br>-- <span style="color:#ef4444;">✗</span> Manage a bandwidth budget<br>-- <span style="color:#ef4444;">✗</span> Keep his VOD system working<br>-- <span style="color:#ef4444;">✗</span> Respond to his community-- </div>-- </div>-- <div style="text-align:left;">-- <div style="font-family:'Bebas Neue',sans-serif;font-size:2.8vh;color:#8b5cf6;letter-spacing:3px;margin-bottom:1.2vh;">CAN:</div>-- <div style="font-size:2.1vh;font-weight:700;color:rgba(255,255,255,0.8);line-height:2.1;text-shadow:0 1px 6px rgba(0,0,0,0.9);">-- <span style="color:#22c55e;">✓</span> Send @everyone warnings<br>-- <span style="color:#22c55e;">✓</span> Say "All Rights Reserved"<br>-- <span style="color:#22c55e;">✓</span> File a Wefunder<br>-- <span style="color:#22c55e;">✓</span> Employ bots as users<br>-- <span style="color:#22c55e;">✓</span> Hold your data hostage-- </div>-- </div>-- </div>-- <div class="s-sub" style="margin-top:2vh;">-- <span class="ri">This man is the CEO.</span> <span class="hi">He holds your data.</span><br>-- <span class="gi">"do you even know how to code?" — Goosely, probably 🙂</span>-- </div>--</div>----</div><!-- /stage -->----<!-- Bottom ticker -->--<div class="ticker-wrap ticker-bottom">-- <div class="ticker-inner" id="ticker-bot"></div>--</div>----<!-- Top ticker -->--<div class="ticker-wrap ticker-top">-- <div class="ticker-inner" id="ticker-top-el"></div>--</div>----<script>--'use strict';----// ══════════════════════════════════════════--// PARTICLES--// ══════════════════════════════════════════--tsParticles.load('tsparticles', {-- fullScreen:false, background:{color:'transparent'},-- particles:{-- number:{value:60},-- color:{value:['#8b5cf6','#f5d394','#8b6914','#ffffff','#ef4444']},-- opacity:{value:{min:.03,max:.18},animation:{enable:true,speed:.3,minimumValue:.02}},-- size:{value:{min:1,max:3}},-- move:{enable:true,speed:.35,direction:'top',outModes:'out'},-- links:{enable:false},-- },-- detectRetina:true,--});----// ══════════════════════════════════════════--// EMBERS--// ══════════════════════════════════════════--function spawnEmber() {-- var e = document.createElement('div');-- e.className = 'ember';-- e.style.left = Math.random()*100+'vw';-- e.style.top = '100vh';-- document.body.appendChild(e);-- gsap.to(e,{-- y:-(window.innerHeight*(.6+Math.random()*.4)),-- x:(Math.random()-.5)*window.innerWidth*.12,-- opacity:Math.random()*.4+.1,-- duration:4+Math.random()*6,-- ease:'none',-- onComplete:function(){e.remove();}-- });--}--setInterval(spawnEmber,220);----// ══════════════════════════════════════════--// TICKERS--// ══════════════════════════════════════════--var BOT_MSGS = [-- '\u26a1 OPENVIBELIVE.COM — FREE & OPEN SOURCE FOREVER \u26a1',-- '9,309 VODS DOWNLOADED BEFORE ROBOTSTREAMER HIT THEIR BANDWIDTH CAP 🤣',-- 'RICK CAN\'T CODE. RICK CAN\'T STOP THIS. RICK IS MALDING.',-- '\ud83d\udc80 RIP ROBOTSTREAMER CORPORATION \ud83d\udc80',-- 'WEFUNDER? MORE LIKE WEFUNERAL LMAOOOOO',-- '\ud83c\udfe0 BUILT FROM A SHED — RUNS BETTER THAN RICK\'S "CORPORATION"',-- '872 GB OF FOOTAGE. ARCHIVED FOREVER. THEN ERROR 403. LMAO.',-- 'DOWNLOAD_CAP_EXCEEDED — HTTP 403 — WE BROKE RICK\'S SERVERS LMAOOO',-- 'IMAGINE LOSING TO A GUY IN A SHED. IMAGINE.',-- '\ud83e\udea6 HERE LIES ROBOTSTREAMER — ALL RIGHTS RESERVED ON A DEAD SITE \ud83e\udea6',-- 'GOOSELY — ROBOT 1837 — 439 FOLLOWERS — EVERY VOD SAVED',-- '\ud83d\udd25 OPEN SOURCE ALWAYS WINS \ud83d\udd25',-- '"ALL RIGHTS RESERVED" — RIGHTS TO WHAT EXACTLY??? \ud83e\udd23',-- 'OPENVIBELIVE FOR LIFE — NO CORPORATE BS — NO PAYWALLS',-- 'RICK\'S BANDWIDTH CAP: EXCEEDED. RICK\'S REACTION: SEETHING. GG \ud83d\ude2d',-- '9,309 VODS. 872 GB. THEN HTTP 403. WE CRASHED ROBOTSTREAMER. LMAOOO',-- '\ud83e\udda0 RICK CALLED OPEN SOURCE PYTHON A VIRUS. HE IS THE CEO. 2026.',-- '"MIGHT BE FINE BUT HAVE NO IDEA" — RICK, ABOUT A PYTHON SCRIPT HE COULD HAVE READ IN 30 SECONDS',-- '"DO YOU EVEN KNOW HOW TO CODE?" — GOOSELY TO RICK (CEO OF ROBOTSTREAMER) \ud83d\udd25',-- 'RICK: CANNOT REVIEW PYTHON. RICK: HAS ACCESS TO YOUR DATA. THINK ABOUT THAT. \ud83d\ude37',-- 'RS VOD SYSTEM: BROKEN. RICK: UNAVAILABLE. GOOSELY\'S VODS: SAFE ON OPENVIBELIVE. \ud83c\udfe0',--];--var TOP_MSGS = [-- '\u26a0 LIVE ON OPENVIBELIVE.COM \u26a0',-- 'ROBOT 1837 — GOOSELY — STREAMING FOREVER',-- 'ROBOTSTREAMER CORPORATION: STATUS DECEASED',-- 'OPERATION: ARCHIVE EVERYTHING — 9,309 VODS THEN BANDWIDTH CAP 💀',-- 'THREAT LEVEL: MAXIMUM OPENVIBE',-- 'RICK\'S REACTION: PENDING (SEETHING)',-- 'OPENVIBELIVE: FULLY OPERATIONAL — RICK: NOT SO MUCH',-- 'VODS SECURED: 9,309 — BANDWIDTH EXCEEDED — ERROR: download_cap_exceeded',-- 'AUTHORIZATION: SHED COMMANDER — CLEARANCE: OPENVIBE LEVEL 5',-- '🦠 RICK DIAGNOSED OPEN SOURCE PYTHON AS "MIGHT BE A VIRUS" — CEO OF THE YEAR',-- 'CEO WHO CAN\'T READ PYTHON HOLDS YOUR DATA — SLEEP WELL — ROBOTSTREAMER.COM',--];----function fillTicker(el, msgs) {-- var dup = msgs.concat(msgs).concat(msgs).concat(msgs);-- el.innerHTML = dup.map(function(m){ return '<span style="display:inline-block;padding:0 3vw;">\u25cf '+m+'</span>'; }).join('');--}--fillTicker(document.getElementById('ticker-bot'), BOT_MSGS);--fillTicker(document.getElementById('ticker-top-el'), TOP_MSGS);----// ══════════════════════════════════════════--// SLIDES--// ══════════════════════════════════════════--var SLIDES = [-- {id:'s0', dur:7000},-- {id:'s1', dur:8000},-- {id:'s2', dur:7000},-- {id:'s3', dur:10000},-- {id:'s4', dur:8000},-- {id:'s5', dur:7000},-- {id:'s6', dur:7000},-- {id:'s7', dur:8000},-- {id:'s8', dur:8000},-- {id:'s9', dur:5000},-- {id:'s10', dur:7000},-- {id:'s11', dur:7000},-- {id:'s12', dur:9000},-- {id:'s13', dur:7000},-- {id:'s14', dur:11000},-- {id:'s15', dur:8000},-- {id:'s16', dur:11000},-- {id:'s17', dur:9000},-- {id:'s18', dur:12000},-- {id:'s19', dur:10000},-- {id:'s20', dur:10000},-- {id:'s21', dur:11000},-- {id:'s22', dur:9000},-- {id:'s23', dur:14000},-- {id:'s24', dur:11000},--];----var current = 0;--var isTransitioning = false;----// Build dots--var dotsEl = document.getElementById('dots');--SLIDES.forEach(function(_,i){-- var d = document.createElement('div');-- d.className = 'dot' + (i===0?' active':'');-- d.id = 'dot'+i;-- dotsEl.appendChild(d);--});----function updateDots(idx) {-- SLIDES.forEach(function(_,i){-- var d = document.getElementById('dot'+i);-- d.className = 'dot'+(i===idx?' active':'');-- });--}----function getSlide(idx) {-- return document.getElementById(SLIDES[idx].id);--}----// Special per-slide entry animations--function playSlideIn(idx, el) {-- var id = SLIDES[idx].id;---- if (id === 's1') {-- var obj = {val:0};-- gsap.to(obj, {val:9309, duration:3.5, ease:'power2.out', onUpdate:function(){-- var c = document.getElementById('vod-counter');-- if(c) c.textContent = Math.floor(obj.val).toLocaleString();-- }, onComplete:function(){-- setTimeout(function(){-- var c = document.getElementById('vod-counter');-- if(c){ gsap.to(c,{color:'#ef4444',textShadow:'0 0 40px rgba(239,68,68,0.8)',duration:.3}); }-- flashBang(.5);-- }, 600);-- }});-- }---- if (id === 's9') {-- // VIBE4LIFE: fire confetti-- fireConfetti();-- }---- if (id === 's5') {-- // WEFUNERAL: skull rain-- for(var i=0;i<15;i++){-- setTimeout(skullRain, i*300);-- }-- }---- if (id === 's12') {-- flashBang(.8);-- for(var j=0;j<20;j++){ setTimeout(skullRain, j*250); }-- }-- if (id === 's13') {-- fireConfetti();-- setTimeout(fireConfetti, 1500);-- }-- if (id === 's14') {-- flashBang(.3);-- }-- if (id === 's15') {-- for(var k=0;k<10;k++){ setTimeout(skullRain, k*400); }-- }-- if (id === 's17') {-- // Achievement unlocked: mini firework-- setTimeout(function(){-- confetti({particleCount:80,startVelocity:25,spread:360,origin:{x:.5,y:.5},-- colors:['#8b5cf6','#f5d394','#22c55e'],ticks:180,gravity:.8,scalar:1.2});-- }, 400);-- }-- if (id === 's20') {-- for(var m=0;m<6;m++){ setTimeout(skullRain, m*500); }-- }-- if (id === 's22') {-- flashBang(.7);-- for(var n=0;n<15;n++){ setTimeout(skullRain, n*260); }-- }-- if (id === 's23') {-- flashBang(.4);-- setTimeout(function(){-- confetti({particleCount:60,startVelocity:20,spread:180,-- origin:{x:.5,y:.3},colors:['#ef4444','#fff','#8b5cf6'],-- ticks:150,gravity:1.2,scalar:.9});-- }, 600);-- }--}----function skullRain() {-- var s = document.createElement('div');-- s.className = 'skull-float';-- var emojis = ['\ud83d\udc80','\u2620\ufe0f','\u26b0\ufe0f','\ud83e\udea6','\ud83d\udc7b'];-- s.textContent = emojis[Math.floor(Math.random()*emojis.length)];-- s.style.left = Math.random()*100+'vw';-- s.style.top = '-6vh';-- s.style.fontSize = (2+Math.random()*3)+'vh';-- document.body.appendChild(s);-- gsap.to(s,{top:'106vh',opacity:.6,rotation:Math.random()*360,-- duration:3+Math.random()*3,ease:'none',onComplete:function(){s.remove();}});--}----function fireConfetti() {-- var colors = ['#8b5cf6','#f5d394','#22c55e','#3b82f6','#a855f7','#ef4444','#fff'];-- var end = Date.now()+3000;-- (function frame(){-- confetti({particleCount:8,angle:60,spread:80,origin:{x:0,y:.6},colors:colors,scalar:1.3});-- confetti({particleCount:8,angle:120,spread:80,origin:{x:1,y:.6},colors:colors,scalar:1.3});-- confetti({particleCount:5,angle:90,spread:130,origin:{x:.5,y:.3},colors:colors,scalar:1.6});-- if(Date.now()<end) requestAnimationFrame(frame);-- })();--}----function flashBang(intensity) {-- gsap.to('#flash',{opacity:intensity||.6,duration:.06,onComplete:function(){-- gsap.to('#flash',{opacity:0,duration:.5,ease:'power2.out'});-- }});--}----// Transition variants (pick randomly for fun)--var transitions = [-- // fade-- function(out, in_, cb) {-- gsap.to(out,{opacity:0,duration:.5,ease:'power2.in',onComplete:function(){cb();}});-- },-- // slam up-- function(out, in_, cb) {-- gsap.to(out,{opacity:0,y:-80,duration:.4,ease:'power2.in',onComplete:function(){cb();}});-- },-- // slam down-- function(out, in_, cb) {-- gsap.to(out,{opacity:0,y:80,duration:.4,ease:'power2.in',onComplete:function(){cb();}});-- },-- // scale out-- function(out, in_, cb) {-- gsap.to(out,{opacity:0,scale:1.15,duration:.4,ease:'power2.in',onComplete:function(){cb();}});-- },-- // glitch out-- function(out, in_, cb) {-- flashBang(.5);-- gsap.to(out,{opacity:0,x:20,duration:.15,ease:'steps(3)',onComplete:function(){-- gsap.set(out,{x:0});-- cb();-- }});-- },--];----function showSlide(idx, transIdx) {-- if(isTransitioning) return;-- isTransitioning = true;---- var outEl = getSlide(current);-- var inEl = getSlide(idx);-- var tr = transitions[transIdx !== undefined ? transIdx : Math.floor(Math.random()*transitions.length)];---- // pick entry animation params-- var entryY = [30,-30,0,0,0][Math.floor(Math.random()*5)];-- var entryScale = Math.random()>.5 ? .92 : 1.04;---- tr(outEl, inEl, function(){-- outEl.style.opacity = '0';-- outEl.classList.remove('active');---- gsap.set(inEl,{opacity:0, y:entryY, scale:entryScale});-- inEl.classList.add('active');-- gsap.to(inEl,{opacity:1, y:0, scale:1, duration:.6, ease:'power3.out', onComplete:function(){-- isTransitioning = false;-- }});---- current = idx;-- updateDots(idx);-- playSlideIn(idx, inEl);-- });--}----function nextSlide() {-- var next = (current+1) % SLIDES.length;-- showSlide(next);-- scheduleNext();--}----var slideTimer;--function scheduleNext() {-- clearTimeout(slideTimer);-- slideTimer = setTimeout(nextSlide, SLIDES[current].dur);--}----// Init--gsap.set(getSlide(0), {opacity:1});--getSlide(0).classList.add('active');--playSlideIn(0, getSlide(0));--scheduleNext();----// ══════════════════════════════════════════--// GLITCH EFFECTS (continuous)--// ══════════════════════════════════════════--setInterval(function(){-- var g = document.getElementById('glitch');-- gsap.to(g,{opacity:.3,duration:.05,onComplete:function(){-- gsap.to(g,{opacity:0,duration:.1,delay:.04});-- }});--}, 2500+Math.random()*2000);----setInterval(function(){-- // random horizontal slice-- gsap.to('.stage',{-- clipPath:'inset('+(Math.random()*30)+'% 0 '+(Math.random()*30)+'% 0)',-- duration:.05,-- onComplete:function(){gsap.set('.stage',{clipPath:'none'});}-- });--}, 6000+Math.random()*4000);----// ══════════════════════════════════════════--// RANDOM MINI CONFETTI BURSTS--// ══════════════════════════════════════════--setInterval(function(){-- confetti({-- particleCount:15,-- spread:60,-- origin:{x:Math.random(),y:Math.random()*.4+.3},-- colors:['#8b5cf6','#f5d394','#22c55e','#ef4444'],-- gravity:1.3, scalar:.8, ticks:100,-- });--}, 12000);------// ══════════════════════════════════════════--// KEYBOARD: arrow keys to manually advance--// ══════════════════════════════════════════--document.addEventListener('keydown',function(e){-- if(e.code==='ArrowRight'||e.code==='Space'){-- clearTimeout(slideTimer);-- nextSlide();-- } else if(e.code==='ArrowLeft'){-- clearTimeout(slideTimer);-- var prev = (current-1+SLIDES.length)%SLIDES.length;-- showSlide(prev);-- scheduleNext();-- }--});--</script>--</body>--</html>@@@ -247,6 +247,7 @@ router.post('/', requireAuth, soundUpload.single('sound'), async (req, res) => {});// If an emote was (re)specified, apply it to every sound under this command.if (req.body.emote_code !== undefined) { try { db.setChannelSoundEmote(channelOwnerId, command, emoteCode); } catch { /* */ } }++ try { require('../media-proxy/asset-sync').syncSoon(); } catch { /* mirror is best-effort */ }// Tell everyone watching this channel to refresh their sound list live.try { require('./chat-server').broadcastToOwnerStreams(channelOwnerId, { type: 'sounds-updated' }); } catch { /* */ }@@@ -296,6 +297,7 @@ router.delete('/:id', requireAuth, (req, res) => {if (sound.url && fs.existsSync(sound.url)) { try { fs.unlinkSync(sound.url); } catch {} }db.deleteChannelSound(sound.id);++ try { require('../media-proxy/asset-sync').removeAsset(sound.media_asset_id); } catch { /* */ }try { require('./chat-server').broadcastToOwnerStreams(sound.channel_owner_id, { type: 'sounds-updated' }); } catch { /* */ }res.json({ message: 'Sound deleted' });} catch (err) {@@@ -217,7 +217,7 @@ router.get('/global', (req, res) => {const emotes = db.getGlobalEmotes().map(e => ({id: `custom-${e.id}`,code: e.code,-- url: `/api/emotes/file/${path.basename(e.url)}`,++ url: e.media_url || `/api/emotes/file/${path.basename(e.url)}`,animated: !!e.animated,width: e.width,height: e.height,@@@ -238,7 +238,7 @@ router.get('/channel/:userId', (req, res) => {id: `custom-${e.id}`,emote_id: e.id,code: e.code,-- url: `/api/emotes/file/${path.basename(e.url)}`,++ url: e.media_url || `/api/emotes/file/${path.basename(e.url)}`,animated: !!e.animated,width: e.width,height: e.height,@@@ -259,7 +259,7 @@ router.get('/mine', requireAuth, (req, res) => {const emotes = db.getEmotesByUser(req.user.id).map(e => ({id: e.id,code: e.code,-- url: `/api/emotes/file/${path.basename(e.url)}`,++ url: e.media_url || `/api/emotes/file/${path.basename(e.url)}`,animated: !!e.animated,width: e.width,height: e.height,@@@ -378,6 +378,7 @@ router.post('/', requireAuth, emoteUpload.single('image'), (req, res) => {try { require('../chat/chat-server').broadcastToOwnerStreams(channelOwnerId, { type: 'emotes-updated' }); } catch { /* */ }}++ try { require('../media-proxy/asset-sync').syncSoon(); } catch { /* mirror is best-effort */ }res.json({emote: {id: result.lastInsertRowid,@@@ -490,6 +491,7 @@ router.delete('/:id', requireAuth, (req, res) => {}db.deleteEmote(req.params.id);++ try { require('../media-proxy/asset-sync').removeAsset(emote.media_asset_id); } catch { /* */ }if (emote.channel_owner_id) {try { require('../chat/chat-server').broadcastToOwnerStreams(emote.channel_owner_id, { type: 'emotes-updated' }); } catch { /* */ }}@@@ -686,7 +688,7 @@ router.get('/all/:streamId', optionalAuth, async (req, res) => {globalCustom = db.getGlobalEmotes().map(e => ({id: `custom-${e.id}`,code: e.code,-- url: `/api/emotes/file/${path.basename(e.url)}`,++ url: e.media_url || `/api/emotes/file/${path.basename(e.url)}`,animated: !!e.animated,width: e.width,height: e.height,@@@ -700,7 +702,7 @@ router.get('/all/:streamId', optionalAuth, async (req, res) => {id: `custom-${e.id}`,emote_id: e.id,code: e.code,-- url: `/api/emotes/file/${path.basename(e.url)}`,++ url: e.media_url || `/api/emotes/file/${path.basename(e.url)}`,animated: !!e.animated,width: e.width,height: e.height,@@@ -943,6 +943,7 @@ async function start() {// Announce newly-created clips in the source channel's chat. Scheduling comes// from the OpenVibe.Media clip.ready webhook; the sweeper fires the message.try { require('./media-proxy/clip-notify').startClipNotifySweeper(); } catch (e) { console.warn('[ClipNotify] not started:', e.message); }++ try { require('./media-proxy/asset-sync').start(); } catch (e) { console.warn('[AssetSync] not started:', e.message); }// Feed connected streamers' viewer counts into PowerChat overlays (platform mode).try { require('./integrations/powerchat-platform').startViewerCountSweeper(); } catch (e) { console.warn('[PowerChat] viewer sweeper not started:', e.message); }// PowerChat: prune the webhook-dedupe log daily so it can't grow unbounded.nnew file mode 100644@@@ -0,0 +1,123 @@++/**++ * asset-sync.js — mirror Live's chat assets (emotes + channel sounds) into++ * OpenVibe.Media, their canonical public home (served at /a/:id, browsable on++ * the media index with uploader + channel attribution).++ *++ * Local files remain the low-latency working copy (chat playback reads disk);++ * this job uploads anything Media doesn't have yet and records media_url /++ * media_asset_id on the local row, so emote <img> URLs come from Media's++ * long-cached endpoint and deletes propagate. Idempotent: rows are skipped++ * once media_asset_id is set, and Media upserts on (kind, name, channel).++ */++'use strict';++++const fs = require('fs');++const path = require('path');++const db = require('../db/database');++const media = require('../media-client');++++const EMOTE_DIR = path.resolve(process.env.EMOTES_PATH || './data/emotes');++const SOUND_DIR = path.resolve(process.env.SOUNDS_PATH || './data/sounds');++const MIME_BY_EXT = {++ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',++ '.webp': 'image/webp', '.avif': 'image/avif',++ '.mp3': 'audio/mpeg', '.ogg': 'audio/ogg', '.wav': 'audio/wav', '.m4a': 'audio/mp4', '.webm': 'audio/webm',++};++++function _ensureColumns() {++ for (const [table, col, type] of [++ ['emotes', 'media_url', 'TEXT'], ['emotes', 'media_asset_id', 'INTEGER'],++ ['channel_sounds', 'media_url', 'TEXT'], ['channel_sounds', 'media_asset_id', 'INTEGER'],++ ]) {++ try { db.getDb().exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${type}`); } catch { /* exists */ }++ }++}++++function _localFile(dir, urlOrPath) {++ if (!urlOrPath) return null;++ if (path.isAbsolute(urlOrPath) && fs.existsSync(urlOrPath)) return urlOrPath;++ const p = path.join(dir, path.basename(urlOrPath));++ return fs.existsSync(p) ? p : null;++}++++async function _upload({ kind, name, filePath, user_id, username, channel_username, duration_seconds, meta }) {++ const ext = path.extname(filePath).toLowerCase();++ const fd = media._formData(++ { kind, name, user_id, username, channel_username, duration_seconds, meta },++ { buffer: fs.readFileSync(filePath), filename: path.basename(filePath), contentType: MIME_BY_EXT[ext] || 'application/octet-stream' },++ 'file'++ );++ const out = await media.request('POST', '/assets', { body: fd, timeoutMs: 60000 });++ return out && out.asset;++}++++let _running = false;++async function syncAll() {++ if (_running) return;++ _running = true;++ try {++ _ensureColumns();++ const nameCache = {};++ const uname = (id) => {++ if (!id) return '';++ if (!(id in nameCache)) { const u = db.getUserById(id); nameCache[id] = (u && u.username) || ''; }++ return nameCache[id];++ };++ let synced = 0, failed = 0;++++ for (const e of db.all('SELECT * FROM emotes WHERE media_asset_id IS NULL')) {++ const f = _localFile(EMOTE_DIR, e.url);++ if (!f) continue;++ try {++ const asset = await _upload({++ kind: 'emote', name: e.code, filePath: f,++ user_id: e.user_id, username: uname(e.user_id),++ channel_username: uname(e.channel_owner_id || e.user_id),++ meta: { animated: !!e.animated, is_global: !!e.is_global },++ });++ if (asset) { db.run('UPDATE emotes SET media_url = ?, media_asset_id = ? WHERE id = ?', [asset.url, asset.id, e.id]); synced++; }++ } catch (err) { failed++; if (failed <= 3) console.warn('[AssetSync] emote', e.code, err.message); }++ }++++ for (const s of db.all('SELECT * FROM channel_sounds WHERE media_asset_id IS NULL')) {++ const f = _localFile(SOUND_DIR, s.url);++ if (!f) continue;++ try {++ const asset = await _upload({++ kind: 'sound', name: s.command, filePath: f,++ user_id: s.created_by, username: s.created_by_name || uname(s.created_by),++ channel_username: uname(s.channel_owner_id),++ duration_seconds: s.duration_seconds || 0,++ });++ if (asset) { db.run('UPDATE channel_sounds SET media_url = ?, media_asset_id = ? WHERE id = ?', [asset.url, asset.id, s.id]); synced++; }++ } catch (err) { failed++; if (failed <= 3) console.warn('[AssetSync] sound', s.command, err.message); }++ }++++ if (synced || failed) console.log(`[AssetSync] Synced ${synced} chat assets to Media${failed ? ` (${failed} failed — will retry next pass)` : ''}`);++ } finally {++ _running = false;++ }++}++++let _debounce = null;++/** Debounced sync — call after any emote/sound upload. */++function syncSoon() {++ clearTimeout(_debounce);++ _debounce = setTimeout(() => syncAll().catch((e) => console.warn('[AssetSync]', e.message)), 5000);++ if (_debounce.unref) _debounce.unref();++}++++/** Best-effort removal of the Media copy when the local asset is deleted. */++function removeAsset(mediaAssetId) {++ if (!mediaAssetId) return;++ media.request('DELETE', `/assets/${mediaAssetId}`).catch(() => {});++}++++function start() {++ const boot = setTimeout(() => syncAll().catch((e) => console.warn('[AssetSync]', e.message)), 20 * 1000);++ if (boot.unref) boot.unref();++ const timer = setInterval(() => syncAll().catch(() => {}), 6 * 60 * 60 * 1000);++ if (timer.unref) timer.unref();++}++++module.exports = { start, syncAll, syncSoon, removeAsset };@@@ -48,6 +48,13 @@ function withUserFields(clip) {// Absolutize Media-relative URLs so the SPA doesn't resolve them against Live.clip.playback_url = media.publicUrl(clip.playback_url) || media.clipUrl(clip.id);if (clip.thumbnail_url) clip.thumbnail_url = media.publicUrl(clip.thumbnail_url);++ // Short AI overview is Live-owned (clip_ai_state) — overlay for the cards.++ if (clip.id != null && !clip.ai_overview_short) {++ try {++ const s = db.get('SELECT ai_overview_short FROM clip_ai_state WHERE clip_id = ?', [clip.id]);++ if (s && s.ai_overview_short) clip.ai_overview_short = s.ai_overview_short;++ } catch { /* best-effort */ }++ }return clip;}@@@ -93,7 +93,39 @@ router.post('/:slug/set-avatar', requireAuth, async (req, res) => {});// ── Everything else: transparent passthrough ─────────────────--router.get('/', forward(''));++// Media only stores our numeric user ids — resolve author names locally so++// cards don't all read "Anonymous", and translate ?username= into user_id.++function _nameUsers(rows) {++ for (const p of rows || []) {++ if (p && p.user_id != null && !p.username) {++ const u = db.getUserById(p.user_id);++ if (u) { p.username = u.username; p.display_name = u.display_name; p.profile_color = u.profile_color; p.avatar_url = u.avatar_url; }++ }++ }++ return rows;++}++function forwardEnriched(subPath, pick) {++ return async (req, res) => {++ try {++ const query = { ...req.query };++ if (query.username && query.username !== 'all') {++ const u = db.getUserByUsername(query.username);++ delete query.username;++ if (!u) return res.json({ pastes: [], total: 0, limit: 0, offset: 0, hasMore: false });++ query.user_id = u.id;++ }++ const p = typeof subPath === 'function' ? subPath(req) : subPath;++ const out = await media.request('GET', `/pastes${p}`, { query, userToken: media.userTokenFrom(req) });++ _nameUsers(pick(out));++ res.json(out);++ } catch (err) {++ if (err && err.name === 'MediaApiError' && err.status) return res.status(err.status).json(err.body || { error: err.message });++ console.warn('[Pastes proxy]', err.message);++ res.status(502).json({ error: 'Media service unavailable' });++ }++ };++}++router.get('/', forwardEnriched('', (o) => o?.pastes));router.post('/', forward(''));router.get('/config', forward('/config'));router.get('/admin/stats', requireAdmin, forwardAsApp('/admin/stats'));@@@ -124,13 +156,13 @@ router.get('/by-user/:username', async (req, res) => {.filter(p => !seen.has(p.slug) && seen.add(p.slug)).sort((a, b) => (sort === 'oldest' ? 1 : -1) * (new Date(a.created_at) - new Date(b.created_at))).slice(0, limit);-- res.json({ pastes, total: pastes.length, username: user.username });++ res.json({ pastes: _nameUsers(pastes), total: pastes.length, username: user.username });} catch (err) {console.warn('[Pastes proxy] by-user:', err.message);res.status(502).json({ error: 'Media service unavailable' });}});--router.get('/:slug', forward(slugPath()));++router.get('/:slug', forwardEnriched(slugPath(), (o) => (o?.paste ? [o.paste] : [o])));router.put('/:slug', requireAuth, forward(slugPath()));router.delete('/:slug', requireAuth, forward(slugPath()));// TODO(contract): paste admin tools (censor, admin/stats, admin/forks, bulk)@@@ -158,7 +190,7 @@ router.post('/:slug/fork', forward(slugPath('/fork')));router.get('/:slug/raw', (req, res) => res.redirect(302, media.pasteRawUrl(req.params.slug)));router.post('/:slug/like', requireAuth, forward(slugPath('/like')));router.post('/:slug/copy', forward(slugPath('/copy')));--router.get('/:slug/comments', forward(slugPath('/comments')));++router.get('/:slug/comments', forwardEnriched(slugPath('/comments'), (o) => o?.comments));router.post('/:slug/comments', forward(slugPath('/comments')));router.delete('/:slug/comments/:commentId', forward((req) => `/${encodeURIComponent(req.params.slug)}/comments/${encodeURIComponent(req.params.commentId)}`));@@@ -97,6 +97,13 @@ function withUserFields(row) {// so the SPA doesn't resolve them against Live's origin.row.playback_url = media.publicUrl(row.playback_url) || media.vodPlaybackUrl(row.id);if (row.thumbnail_url) row.thumbnail_url = media.publicUrl(row.thumbnail_url);++ // Short AI overview is Live-owned (vod_ai_state) — overlay for the cards.++ if (row.id != null && !row.ai_overview_short) {++ try {++ const s = db.get('SELECT ai_overview_short FROM vod_ai_state WHERE vod_id = ?', [row.id]);++ if (s && s.ai_overview_short) row.ai_overview_short = s.ai_overview_short;++ } catch { /* best-effort */ }++ }return row;}@@@ -330,6 +330,19 @@ router.get('/channel/:username', optionalAuth, async (req, res) => {};clips.forEach(nameClip);clipsOfStreams.forEach(nameClip);++ // Short AI overviews are Live-owned (vod_ai_state / clip_ai_state).++ const aiShort = (rows, table, col) => {++ for (const r of rows) {++ if (!r || r.id == null || r.ai_overview_short) continue;++ try {++ const s = db.get(`SELECT ai_overview_short FROM ${table} WHERE ${col} = ?`, [r.id]);++ if (s && s.ai_overview_short) r.ai_overview_short = s.ai_overview_short;++ } catch { /* best-effort */ }++ }++ };++ aiShort(vods, 'vod_ai_state', 'vod_id');++ aiShort(clips, 'clip_ai_state', 'clip_id');++ aiShort(clipsOfStreams, 'clip_ai_state', 'clip_id');}const followerCount = db.getFollowerCount(channel.user_id);const isFollowing = req.user ? db.isFollowing(req.user.id, channel.user_id) : false;@@@ -1171,6 +1184,18 @@ router.get('/recent-vods', async (req, res) => {const media = require('../media-client');const r = await media.listVods({ limit, offset }).catch(() => null);const vods = r?.vods || (Array.isArray(r) ? r : []);++ for (const v of vods) {++ if (v && v.user_id != null && !v.username) {++ const u = db.getUserById(v.user_id);++ if (u) { v.username = u.username; v.display_name = u.display_name; v.avatar_url = u.avatar_url; }++ }++ if (v && v.id != null && !v.ai_overview_short) {++ try {++ const s = db.get('SELECT ai_overview_short FROM vod_ai_state WHERE vod_id = ?', [v.id]);++ if (s && s.ai_overview_short) v.ai_overview_short = s.ai_overview_short;++ } catch { /* */ }++ }++ }const total = r?.total ?? vods.length;res.json({ vods, total, limit, offset, hasMore: offset + vods.length < total });} catch (err) {@@@ -3,12 +3,17 @@// Consistent top bar across all services with logo, navigation,// notification bell, account switcher, and theme-aware styling.// Usage: OpenVibeNavbar.init({ service, token, user, apiBase })++// Optional: loginUrl (override the Sign In href), sessionUrl (same-origin++// endpoint returning { user } — used only when no ov_token exists),++// onLogin/onLogout callbacks. When no `user` is passed the navbar resolves++// it itself: ov_token cookie → localStorage → token opt → sessionUrl, then++// GET {apiBase}/api/auth/me with `Authorization: Bearer <token>`.// ═══════════════════════════════════════════════════════════════(function (root) {'use strict';-- let _config = { service: 'network', token: null, user: null, apiBase: 'https://openvibe.network', onLogin: null, onLogout: null };++ let _config = { service: 'network', token: null, user: null, apiBase: 'https://openvibe.network', onLogin: null, onLogout: null, loginUrl: null, sessionUrl: null };let _navEl = null;function injectStyles() {@@@ -362,6 +367,9 @@],media: [{ label: 'Home', href: '/' },++ { label: 'Network', href: 'https://openvibe.network' },++ { label: 'Live', href: 'https://openvibe.live' },++ { label: 'Tools', href: 'https://openvibe.tools' },],network: [{ label: 'Home', href: '/' },@@@ -429,6 +437,200 @@try { return JSON.parse(localStorage.getItem('openvibe_accounts') || '[]'); } catch { return []; }}++ // ─── SSO auth resolution ───────────────────────────────────++ // Cookie policy (CONTRACTS.md): ov_token is host-only on openvibe.network,++ // and Domain=.openvibe.tools on the tools apex + every tool subdomain.++ // The tools gateway (apex) owns the OAuth client: /auth/login, /auth/logout,++ // /auth/me, POST /auth/refresh. Satellite subdomains (maps., audio., text.,++ // img., yt., docs., food., …) do NOT mount /auth/* — they only read the++ // shared cookie — so sign-in/out always goes through the gateway apex.++ // Detection order: ov_token cookie → localStorage → page-provided token →++ // app-specific session endpoint (opt-in via config.sessionUrl).++ const TOOLS_APEX = 'openvibe.tools';++ const TOKEN_KEY = 'ov_token';++++ function currentHost() {++ return (typeof location !== 'undefined' && location.hostname) || '';++ }++++ function onToolsDomain(host) {++ const h = host !== undefined ? host : currentHost();++ return h === TOOLS_APEX || h.endsWith('.' + TOOLS_APEX);++ }++++ function onNetworkDomain(host) {++ const h = host !== undefined ? host : currentHost();++ try { return h !== '' && h === new URL(_config.apiBase).hostname; } catch { return false; }++ }++++ function parseCookieToken(cookieStr) {++ const m = String(cookieStr || '').match(/(?:^|;\s*)ov_token=([^;]*)/);++ if (!m || !m[1]) return null;++ try { return decodeURIComponent(m[1]); } catch { return m[1]; }++ }++++ function getCookieToken() {++ if (typeof document === 'undefined') return null;++ return parseCookieToken(document.cookie);++ }++++ function getStoredToken() {++ try { return localStorage.getItem(TOKEN_KEY) || null; } catch { return null; }++ }++++ function resolveToken() {++ return getCookieToken() || getStoredToken() || _config.token || null;++ }++++ /** Where "Sign In" goes on this host. */++ function resolveLoginHref(host, returnUrl) {++ if (_config.loginUrl) return _config.loginUrl;++ if (onToolsDomain(host)) {++ // Every *.openvibe.tools host signs in through the gateway apex: it++ // holds the OAuth state cookie + redirect_uri (both apex-host-only)++ // and sets ov_token with Domain=.openvibe.tools, so the session++ // reaches all satellites. A host-local /auth/login would 404 on++ // satellites and break the OAuth state check on gateway subdomains.++ return `https://${TOOLS_APEX}/auth/login?next=${encodeURIComponent(returnUrl)}`;++ }++ if (onNetworkDomain(host)) return `/login?return=${encodeURIComponent(returnUrl)}`;++ return `${_config.apiBase}/login?return=${encodeURIComponent(returnUrl)}`;++ }++++ function setAuthCookie(token) {++ if (typeof document === 'undefined') return;++ const maxAge = 24 * 60 * 60; // matches the 24h access JWT++ if (onToolsDomain()) {++ document.cookie = `ov_token=${token};path=/;max-age=${maxAge};domain=.${TOOLS_APEX};SameSite=Lax;Secure`;++ } else {++ const secure = (typeof location !== 'undefined' && location.protocol === 'https:') ? ';Secure' : '';++ document.cookie = `ov_token=${token};path=/;max-age=${maxAge};SameSite=Lax${secure}`;++ }++ }++++ function clearAuthState() {++ if (typeof document !== 'undefined') {++ document.cookie = 'ov_token=;path=/;max-age=0;SameSite=Lax';++ if (onToolsDomain()) {++ document.cookie = `ov_token=;path=/;max-age=0;domain=.${TOOLS_APEX};SameSite=Lax`;++ }++ }++ try {++ localStorage.removeItem(TOKEN_KEY);++ localStorage.removeItem('openvibe_anon_token');++ localStorage.removeItem('openvibe_active_account');++ } catch { /* storage unavailable */ }++ }++++ function persistToken(token) {++ try { localStorage.setItem(TOKEN_KEY, token); } catch { /* storage unavailable */ }++ setAuthCookie(token);++ }++++ /** GET {apiBase}/api/auth/me with the Bearer token. */++ async function fetchMe(token) {++ try {++ const res = await fetch(`${_config.apiBase}/api/auth/me`, {++ headers: { Authorization: `Bearer ${token}` },++ });++ if (res.ok) {++ const data = await res.json();++ return { ok: true, user: (data && (data.user || data)) || null };++ }++ return { ok: false, unauthorized: res.status === 401 || res.status === 403 };++ } catch {++ return { ok: false, unauthorized: false };++ }++ }++++ /** Same-origin session refresh — mounted by the tools gateway (POST /auth/refresh). */++ async function gatewayRefresh() {++ try {++ const res = await fetch('/auth/refresh', { method: 'POST', credentials: 'same-origin' });++ if (!res.ok) return null;++ const data = await res.json();++ return data && data.token ? data : null;++ } catch { return null; }++ }++++ /** Network-side refresh — accepts tokens expired up to 7 days ago (grace). */++ async function networkRefresh(token) {++ try {++ const res = await fetch(`${_config.apiBase}/api/auth/refresh`, {++ method: 'POST',++ headers: { Authorization: `Bearer ${token}` },++ });++ if (!res.ok) return null;++ const data = await res.json();++ return data && data.token ? data : null;++ } catch { return null; }++ }++++ /**++ * Resolve the signed-in user from the shared SSO state.++ * With a token: validate it against the Network. If it is rejected, make++ * ONE refresh attempt (same-origin gateway refresh first on tools hosts,++ * then the Network's grace refresh) before giving up — never leave a++ * stale "Sign In" when a recoverable session exists.++ */++ async function resolveSessionUser() {++ const token = resolveToken();++ if (token) {++ const me = await fetchMe(token);++ if (me.ok && me.user) return { user: me.user, token };++ if (me.unauthorized) {++ if (onToolsDomain()) {++ const r = await gatewayRefresh();++ if (r) {++ const user = r.user || (await fetchMe(r.token)).user;++ if (user) { persistToken(r.token); return { user, token: r.token }; }++ }++ }++ const n = await networkRefresh(token);++ if (n) {++ const user = n.user || (await fetchMe(n.token)).user;++ if (user) { persistToken(n.token); return { user, token: n.token }; }++ }++ }++ return null;++ }++ // No token anywhere — ask the page's own session endpoint if it has one++ if (_config.sessionUrl) {++ try {++ const res = await fetch(_config.sessionUrl, { credentials: 'include' });++ if (res.ok) {++ const data = await res.json();++ const user = (data && (data.user || data)) || null;++ if (user && (user.username || user.id)) return { user, token: null };++ }++ } catch { /* signed out */ }++ }++ return null;++ }++++ let _authInFlight = null;++++ function refreshAuthState() {++ if (_authInFlight) return _authInFlight;++ _authInFlight = resolveSessionUser()++ .then((session) => {++ _authInFlight = null;++ if (session && session.user) {++ _config.user = session.user;++ if (session.token) _config.token = session.token;++ render();++ }++ try {++ document.dispatchEvent(new CustomEvent('openvibe-navbar-auth', {++ detail: { user: session ? session.user : null, token: session ? session.token : null },++ }));++ } catch { /* non-DOM environment */ }++ return session ? session.user : null;++ })++ .catch(() => { _authInFlight = null; return null; });++ return _authInFlight;++ }++function escapeAttr(value) {return String(value ?? '').replace(/&/g, '&').replace(/"/g, '"');}@@@ -497,8 +699,8 @@const u = _config.user;const accounts = getAccounts();const isAnon = u && u.is_anon;-- const loginHref = `https://openvibe.network/login?return=${encodeURIComponent(window.location.href)}`;-- const addAccountHref = `https://openvibe.network/login?add_account=1&return=${encodeURIComponent(window.location.href)}`;++ const loginHref = resolveLoginHref(currentHost(), window.location.href);++ const addAccountHref = `${_config.apiBase}/login?add_account=1&return=${encodeURIComponent(window.location.href)}`;nav.innerHTML = `<a class="openvibe-navbar-brand" href="${brandHref}">@@@ -588,12 +790,16 @@dropdown.classList.remove('open');if (_config.onLogout) _config.onLogout();else {-- document.cookie = 'ov_token=;path=/;max-age=0';-- document.cookie = 'ov_token=;path=/;max-age=0;domain=.openvibe.tools';-- localStorage.removeItem('ov_token');-- localStorage.removeItem('openvibe_anon_token');-- localStorage.removeItem('openvibe_active_account');-- window.location.reload();++ clearAuthState();++ _config.user = null;++ _config.token = null;++ if (onToolsDomain()) {++ // The gateway also clears the Domain=.openvibe.tools cookie++ // and the httpOnly refresh cookie, then sends us back here.++ window.location.href = `https://${TOOLS_APEX}/auth/logout?next=${encodeURIComponent(window.location.href)}`;++ } else {++ window.location.reload();++ }}});} else {@@@ -621,9 +827,19 @@init(opts = {}) {Object.assign(_config, opts);injectStyles();-- return render();++ const el = render();++ // Pages that hand us a resolved user (openvibe.network, the tools++ // gateway hub) keep full control. Everyone else — pages that pass++ // only a token, or nothing at all — gets the user resolved from++ // the shared SSO state (ov_token cookie / localStorage / optional++ // sessionUrl) and the navbar re-renders when it arrives.++ if (!_config.user) refreshAuthState();++ return el;},++ /** Re-resolve the signed-in user from the shared SSO state. */++ refreshAuth() { return refreshAuthState(); },++/** Update user (after account switch). */setUser(user) {_config.user = user;@@@ -643,6 +859,18 @@_navEl?.remove();_navEl = null;},++++ /** Internal auth helpers — exposed for tests, not a public API. */++ _auth: {++ parseCookieToken,++ onToolsDomain,++ onNetworkDomain,++ resolveLoginHref,++ resolveToken,++ resolveSessionUser,++ clearAuthState,++ persistToken,++ },};if (typeof module !== 'undefined' && module.exports) module.exports = OpenVibeNavbar;
deleted file mode 100644
deleted file mode 100644
@@ -1,1101 +0,0 @@
@@ -1,1101 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-<meta charset="UTF-8">
-<meta name="viewport" content="width=device-width,initial-scale=1.0">
-<title>OPENVIBELIVE — Rick's Bandwidth Nightmare</title>
-
-<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js"></script>
-<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
-<script src="https://cdn.jsdelivr.net/npm/[email protected]/tsparticles.engine.min.js"></script>
-<script src="https://cdn.jsdelivr.net/npm/[email protected]/tsparticles.bundle.min.js"></script>
-<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@400;600;700;800;900&family=Fira+Code:wght@400;700&family=Bebas+Neue&display=swap" rel="stylesheet">
-<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
-
-<style>
-* { margin:0; padding:0; box-sizing:border-box; }
-html, body { width:100vw; height:100vh; overflow:hidden; background:#000; font-family:'Inter',sans-serif; color:#fff; }
-
-/* ── Background layers ── */
-#tsparticles { position:absolute; inset:0; z-index:0; }
-.bg-pulse {
- position:absolute; inset:0; z-index:0;
- background:radial-gradient(ellipse at 50% 50%, rgba(139,92,246,0.07) 0%, transparent 65%);
- animation:bgPulse 3s ease-in-out infinite alternate;
-}
-@keyframes bgPulse { from{opacity:.4;transform:scale(1)} to{opacity:1;transform:scale(1.08)} }
-.scanlines {
- position:absolute; inset:0; z-index:1; pointer-events:none;
- background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.07) 2px,rgba(0,0,0,.07) 4px);
-}
-.vignette {
- position:absolute; inset:0; z-index:2; pointer-events:none;
- background:radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,.8) 100%);
-}
-.flash { position:absolute; inset:0; z-index:50; background:#fff; opacity:0; pointer-events:none; }
-
-/* ── Tickers ── */
-.ticker-wrap {
- position:absolute; left:0; right:0; z-index:15; overflow:hidden;
-}
-.ticker-bottom { bottom:0; height:5.5vh; background:rgba(139,92,246,0.12); border-top:1px solid rgba(139,92,246,0.25); }
-.ticker-top { top:0; height:4vh; background:rgba(239,68,68,0.08); border-bottom:1px solid rgba(239,68,68,0.2); }
-.ticker-inner {
- display:flex; white-space:nowrap; gap:0;
- font-family:'Fira Code',monospace; font-weight:700; text-transform:uppercase; letter-spacing:2px;
-}
-.ticker-bottom .ticker-inner { font-size:2.2vh; color:rgba(139,92,246,0.95); line-height:5.5vh; animation:tickerL 45s linear infinite; text-shadow:0 0 12px rgba(139,92,246,0.5); }
-.ticker-top .ticker-inner { font-size:1.8vh; color:rgba(239,68,68,0.8); line-height:4vh; animation:tickerR 38s linear infinite; }
-@keyframes tickerL { from{transform:translateX(0)} to{transform:translateX(-50%)} }
-@keyframes tickerR { from{transform:translateX(-50%)} to{transform:translateX(0)} }
-
-/* ── Corners ── */
-.corner { position:absolute; width:5vw; height:5vw; z-index:12; }
-.corner::before,.corner::after { content:''; position:absolute; background:rgba(139,92,246,0.4); }
-.corner-tl { top:5.5vh; left:1.5vw; }
-.corner-tl::before { width:3vw; height:2px; top:0; left:0; }
-.corner-tl::after { width:2px; height:3vw; top:0; left:0; }
-.corner-tr { top:5.5vh; right:1.5vw; }
-.corner-tr::before { width:3vw; height:2px; top:0; right:0; }
-.corner-tr::after { width:2px; height:3vw; top:0; right:0; }
-.corner-bl { bottom:6.5vh; left:1.5vw; }
-.corner-bl::before { width:3vw; height:2px; bottom:0; left:0; }
-.corner-bl::after { width:2px; height:3vw; bottom:0; left:0; }
-.corner-br { bottom:6.5vh; right:1.5vw; }
-.corner-br::before { width:3vw; height:2px; bottom:0; right:0; }
-.corner-br::after { width:2px; height:3vw; bottom:0; right:0; }
-
-/* ── Slide labels (top-left, top-right) ── */
-.status-bar {
- position:absolute; top:0; left:0; right:0; z-index:14;
- display:flex; justify-content:space-between; align-items:center;
- padding:0 2vw; height:5.5vh;
- font-family:'Fira Code',monospace; font-size:1.3vh; font-weight:700;
- text-transform:uppercase; letter-spacing:3px;
- color:rgba(139,92,246,0.5);
- pointer-events:none;
-}
-.status-left { text-align:left; }
-.status-right { text-align:right; color:rgba(239,68,68,0.4); }
-.blink { animation:blink 1s step-end infinite; }
-@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
-
-/* ── Main stage ── */
-.stage {
- position:absolute; inset:0; z-index:10;
- top:5.5vh; bottom:5.5vh;
- display:flex; align-items:center; justify-content:center;
- overflow:hidden;
-}
-
-/* ── Slide container ── */
-.slide {
- position:absolute; inset:0;
- display:flex; flex-direction:column;
- align-items:center; justify-content:center;
- text-align:center;
- padding:2vh 6vw;
- opacity:0;
- pointer-events:none;
-}
-.slide.active { pointer-events:auto; }
-
-/* ══ SLIDE STYLES ══ */
-
-/* eyebrow label above main text */
-.s-eyebrow {
- font-family:'Bebas Neue',sans-serif;
- font-size:3.5vh; letter-spacing:1vw;
- color:rgba(255,255,255,0.45);
- margin-bottom:1vh;
- text-transform:uppercase;
-}
-
-/* main headline */
-.s-head {
- font-family:'Orbitron',sans-serif;
- font-weight:900; line-height:1.05;
- letter-spacing:0.3vw;
- text-transform:uppercase;
- text-shadow:0 2px 20px rgba(0,0,0,0.9);
-}
-.s-head.xl { font-size:14vh; }
-.s-head.lg { font-size:10vh; }
-.s-head.md { font-size:7vh; }
-.s-head.sm { font-size:5vh; }
-
-/* colour variants */
-.gold { color:#8b5cf6; text-shadow:0 0 40px rgba(139,92,246,0.5),0 2px 12px rgba(0,0,0,0.9); }
-.red { color:#ef4444; text-shadow:0 0 40px rgba(239,68,68,0.5),0 2px 12px rgba(0,0,0,0.9); }
-.grn { color:#22c55e; text-shadow:0 0 30px rgba(34,197,94,0.4),0 2px 10px rgba(0,0,0,0.9); }
-.wht { color:#fff; }
-.gold-grad {
- background:linear-gradient(135deg,#8b5cf6,#f5d394,#ff8c00,#8b5cf6);
- background-size:300% 300%; animation:goldShift 3s ease infinite;
- -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;
- filter:drop-shadow(0 0 40px rgba(139,92,246,0.4));
-}
-@keyframes goldShift { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
-.rainbow-grad {
- background:linear-gradient(90deg,#8b5cf6,#22c55e,#3b82f6,#a855f7,#ef4444,#8b5cf6);
- background-size:400% 100%; animation:rainbowShift 2.5s linear infinite;
- -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;
-}
-@keyframes rainbowShift { 0%{background-position:0% 50%} 100%{background-position:400% 50%} }
-.strike { text-decoration:line-through; opacity:.7; }
-
-/* sub-text */
-.s-sub {
- font-size:2.8vh; font-weight:700;
- color:rgba(255,255,255,0.8);
- margin-top:1.5vh; line-height:1.6;
- text-shadow:0 1px 8px rgba(0,0,0,0.9);
- max-width:70vw;
-}
-.s-sub .hi { color:#8b5cf6; }
-.s-sub .ri { color:#ef4444; }
-.s-sub .gi { color:#22c55e; }
-
-/* badge/stamp */
-.stamp {
- display:inline-block;
- font-family:'Bebas Neue',sans-serif;
- font-size:3vh; letter-spacing:0.6vw;
- border:3px solid currentColor;
- padding:0.3vh 1.5vw;
- border-radius:4px;
- margin:1.5vh 0;
- transform:rotate(-2deg);
-}
-.stamp.red-stamp { color:#ef4444; box-shadow:0 0 20px rgba(239,68,68,0.3); }
-.stamp.gold-stamp { color:#8b5cf6; box-shadow:0 0 20px rgba(139,92,246,0.3); }
-.stamp.grn-stamp { color:#22c55e; box-shadow:0 0 20px rgba(34,197,94,0.3); }
-
-/* VS comparison */
-.vs-grid {
- display:flex; align-items:flex-start; gap:4vw;
- margin-top:2vh;
-}
-.vs-col { text-align:center; min-width:22vw; }
-.vs-col-title {
- font-family:'Orbitron',sans-serif; font-weight:900;
- font-size:3.5vh; letter-spacing:3px; margin-bottom:1.5vh;
- text-shadow:0 1px 8px rgba(0,0,0,0.8);
-}
-.vs-divider {
- font-family:'Orbitron',sans-serif; font-size:6vh; font-weight:900;
- color:rgba(255,255,255,0.1); align-self:center;
-}
-.vs-item {
- font-size:2.2vh; font-weight:700; margin:0.5vh 0;
- text-shadow:0 1px 6px rgba(0,0,0,0.8);
-}
-.vs-item.y { color:#22c55e; }
-.vs-item.n { color:#ef4444; }
-
-/* stat block */
-.stat-row { display:flex; gap:5vw; margin-top:2vh; justify-content:center; }
-.stat-box {
- text-align:center;
- background:rgba(0,0,0,0.4); border:1px solid rgba(139,92,246,0.25);
- border-radius:12px; padding:2vh 3vw;
- backdrop-filter:blur(6px);
-}
-.stat-num {
- font-family:'Orbitron',sans-serif; font-weight:900;
- font-size:6vh; color:#8b5cf6;
- text-shadow:0 0 30px rgba(139,92,246,0.5);
- line-height:1;
-}
-.stat-lbl {
- font-size:1.6vh; font-weight:700;
- color:rgba(255,255,255,0.55);
- letter-spacing:2px; text-transform:uppercase;
- margin-top:0.5vh;
-}
-
-/* meme big text */
-.meme-line {
- font-family:'Bebas Neue',sans-serif;
- font-size:18vh; line-height:0.92;
- letter-spacing:0.5vw; text-transform:uppercase;
- color:#fff; text-shadow:0 4px 30px rgba(0,0,0,1),0 0 80px rgba(139,92,246,0.3);
-}
-.meme-line.stack { font-size:11vh; }
-
-/* OpenVibe sig */
-.openvibe-sig {
- position:absolute; bottom:6.5vh; right:2vw; z-index:14;
- font-family:'Orbitron',sans-serif; font-size:1.4vh; font-weight:700;
- color:rgba(139,92,246,0.3); letter-spacing:3px; text-transform:uppercase;
-}
-
-/* slide progress dots */
-.slide-dots {
- position:absolute; bottom:6.5vh; left:50%; transform:translateX(-50%);
- z-index:14; display:flex; gap:8px; align-items:center;
-}
-.dot {
- width:6px; height:6px; border-radius:50%;
- background:rgba(255,255,255,0.15);
- transition:all 0.4s ease;
-}
-.dot.active { background:#8b5cf6; width:20px; border-radius:3px; box-shadow:0 0 8px rgba(139,92,246,0.6); }
-
-/* floating embers */
-.ember {
- position:absolute; width:4px; height:4px;
- background:#8b5cf6; border-radius:50%;
- opacity:0; z-index:3; filter:blur(1px); pointer-events:none;
-}
-.skull-float {
- position:absolute; opacity:0; z-index:4; pointer-events:none;
- font-size:3vh;
-}
-
-/* glitch overlay */
-.glitch-overlay {
- position:absolute; inset:0; z-index:20; pointer-events:none; opacity:0;
- background:linear-gradient(transparent 0%,rgba(139,92,246,0.05) 50%,transparent 100%);
- animation:glitchSlide .15s steps(2) infinite; mix-blend-mode:overlay;
-}
-@keyframes glitchSlide { 0%{transform:translateY(-100%)} 100%{transform:translateY(100%)} }
-
-
-</style>
-</head>
-<body>
-
-<!-- Bg layers -->
-<div id="tsparticles"></div>
-<div class="bg-pulse"></div>
-<div class="scanlines"></div>
-<div class="vignette"></div>
-<div class="glitch-overlay" id="glitch"></div>
-<div class="flash" id="flash"></div>
-
-<!-- Status bar top -->
-<div class="status-bar">
- <div class="status-left">OPENVIBELIVE.COM // LIVE // ROBOT 1837</div>
- <div class="status-right"><span class="blink">●</span> REC // RIP RICKSTREAMER</div>
-</div>
-
-<!-- Corners -->
-<div class="corner corner-tl"></div>
-<div class="corner corner-tr"></div>
-<div class="corner corner-bl"></div>
-<div class="corner corner-br"></div>
-
-<!-- Slide progress dots -->
-<div class="slide-dots" id="dots"></div>
-
-<!-- OpenVibe sig -->
-<div class="openvibe-sig">openvibe.live</div>
-
-
-
-<!-- ══════════════ SLIDES ══════════════ -->
-<div class="stage" id="stage">
-
-<!-- SLIDE 0: Title card -->
-<div class="slide" id="s0">
- <div class="s-eyebrow">Welcome to</div>
- <div class="s-head xl gold-grad">OPENVIBELIVE</div>
- <div class="s-sub">The open-source streaming platform <span class="hi">built in a shed</span><br>that keeps Rick up at night 🏠</div>
-</div>
-
-<!-- SLIDE 1: 9,309 VODs -->
-<div class="slide" id="s1">
- <div class="s-eyebrow">We downloaded</div>
- <div class="s-head xl gold" id="vod-counter">9,309</div>
- <div class="s-head sm wht" style="letter-spacing:0.8vw;margin-top:-.5vh;">VODs FROM ROBOTSTREAMER</div>
- <div class="s-sub"><span class="hi">Before we BROKE their bandwidth cap.</span><br><span class="ri">"download_cap_exceeded" — HTTP 403 🤣</span></div>
-</div>
-
-<!-- SLIDE 2: Rick roast -->
-<div class="slide" id="s2">
- <div class="stamp red-stamp"><i class="fa-solid fa-skull"></i> CERTIFIED L</div>
- <div class="meme-line red">RICK</div>
- <div class="meme-line stack gold">COULDN'T</div>
- <div class="meme-line stack wht">STOP US</div>
- <div class="s-sub" style="margin-top:2vh;">Closed source. Wefunder. "All Rights Reserved."<br><span class="ri">On a dead site.</span> lmaooooo</div>
-</div>
-
-<!-- SLIDE 3: VS comparison -->
-<div class="slide" id="s3">
- <div class="s-head md wht" style="margin-bottom:2vh;">The Honest Comparison</div>
- <div class="vs-grid">
- <div class="vs-col">
- <div class="vs-col-title gold">OPENVIBELIVE</div>
- <div class="vs-item y"><i class="fa-solid fa-check"></i> Free Forever</div>
- <div class="vs-item y"><i class="fa-solid fa-code-branch"></i> Open Source</div>
- <div class="vs-item y"><i class="fa-solid fa-users"></i> Community Built</div>
- <div class="vs-item y"><i class="fa-solid fa-house"></i> Shed Powered</div>
- <div class="vs-item y"><i class="fa-solid fa-heart"></i> Actually Works</div>
- <div class="vs-item y"><i class="fa-solid fa-infinity"></i> Still Running</div>
- </div>
- <div class="vs-divider"><i class="fa-solid fa-skull-crossbones"></i></div>
- <div class="vs-col">
- <div class="vs-col-title red strike">ROBOTSTREAMER</div>
- <div class="vs-item n"><i class="fa-solid fa-xmark"></i> Patreon Begging</div>
- <div class="vs-item n"><i class="fa-solid fa-lock"></i> Closed Source</div>
- <div class="vs-item n"><i class="fa-solid fa-building"></i> "Corporation"</div>
- <div class="vs-item n"><i class="fa-solid fa-ghost"></i> Ghost Town</div>
- <div class="vs-item n"><i class="fa-solid fa-person-digging"></i> Owned by Rick</div>
- <div class="vs-item n"><i class="fa-solid fa-cross"></i> Effectively Dead</div>
- </div>
- </div>
-</div>
-
-<!-- SLIDE 4: Stats -->
-<div class="slide" id="s4">
- <div class="s-eyebrow">By the numbers</div>
- <div class="s-head lg gold-grad">THE ARCHIVE</div>
- <div class="stat-row">
- <div class="stat-box">
- <div class="stat-num">9,309</div>
- <div class="stat-lbl">VODs Downloaded</div>
- </div>
- <div class="stat-box">
- <div class="stat-num">872 GB</div>
- <div class="stat-lbl">Before cap hit</div>
- </div>
- <div class="stat-box">
- <div class="stat-num">439</div>
- <div class="stat-lbl">Goosely Followers</div>
- </div>
- <div class="stat-box">
- <div class="stat-num red">403</div>
- <div class="stat-lbl">Rick's Error Code</div>
- </div>
- </div>
-</div>
-
-<!-- SLIDE 5: WEFUNERAL -->
-<div class="slide" id="s5">
- <div class="meme-line red" style="font-size:9vh;">WEFUNDER?</div>
- <div class="meme-line gold" style="font-size:13vh;margin-top:-.5vh;">MORE LIKE</div>
- <div class="meme-line red" style="font-size:11vh;margin-top:-.5vh;">WEFUNERAL</div>
- <div class="s-sub" style="margin-top:2vh;"><span class="ri">RIP RobotStreamer Corporation 2016–???</span><br>Cause of death: <span class="hi">losing to a guy in a shed</span></div>
-</div>
-
-<!-- SLIDE 6: Shed origin -->
-<div class="slide" id="s6">
- <div class="s-eyebrow" style="font-size:5vh;">🏠</div>
- <div class="s-head lg gold-grad">BUILT FROM</div>
- <div class="s-head lg wht" style="margin-top:-.5vh;">A LITERAL SHED</div>
- <div class="s-sub" style="margin-top:2vh;">
- No VC funding. No corporate BS. No "all rights reserved."<br>
- Just <span class="hi">vibes, code, and a DIY spirit</span> that Rick can't buy.<br>
- <span class="gi">openvibe.live — free & open source forever</span>
- </div>
-</div>
-
-<!-- SLIDE 7: All rights reversed -->
-<div class="slide" id="s7">
- <div class="stamp red-stamp" style="transform:rotate(2deg);">© RobotStreamer Corporation</div>
- <div class="s-head md red" style="margin-top:1vh;">"ALL RIGHTS RESERVED"</div>
- <div class="s-head sm wht" style="margin-top:1vh;">rights to what, exactly?</div>
- <div class="s-sub" style="margin-top:2vh;">
- <span class="ri">A dead website?</span> A Wefunder page with 3 backers?<br>
- A Discord with more bots than users?<br>
- <span class="hi">Buddy.</span>
- </div>
- <div class="stamp grn-stamp" style="margin-top:2vh;transform:rotate(-1deg);">ALL RIGHTS REVERSED — OPENVIBELIVE</div>
-</div>
-
-<!-- SLIDE 8: Goosely tribute -->
-<div class="slide" id="s8">
- <div class="s-eyebrow">🦆 Robot #1837</div>
- <div class="s-head lg gold-grad">GOOSELY</div>
- <div class="s-head sm wht" style="margin-top:-.5vh;letter-spacing:.5vw;">ROBOTSTREAMER.COM/ROBOT/1837</div>
- <div class="s-sub" style="margin-top:2vh;">
- <span class="hi">439 followers</span> who deserve better than Rick's dying platform.<br>
- <span class="hi">9,309 VODs</span> saved before their servers <span class="ri">literally gave up.</span><br>
- History doesn't die just because Rick's bandwidth does. 🦆
- </div>
-</div>
-
-<!-- SLIDE 9: Pure hype -->
-<div class="slide" id="s9">
- <div class="s-head xl rainbow-grad">OPENVIBE</div>
- <div class="s-head xl rainbow-grad" style="margin-top:-.5vh;">4</div>
- <div class="s-head xl rainbow-grad" style="margin-top:-.5vh;">LIFE</div>
-</div>
-
-<!-- SLIDE 10: Rick's reaction -->
-<div class="slide" id="s10">
- <div class="s-eyebrow">Rick rn:</div>
- <div class="meme-line red" style="font-size:16vh;line-height:1;">😤</div>
- <div class="s-head md gold" style="margin-top:1vh;">SEETHING</div>
- <div class="s-sub" style="margin-top:1.5vh;">
- <span class="ri">All rights reserved on a ghost town.</span><br>
- Meanwhile we're over here downloading <span class="hi">9,309 VODs</span><br>and <span class="gi">open sourcing everything. 🏠✨</span>
- </div>
-</div>
-
-<!-- SLIDE 11: Open source wins -->
-<div class="slide" id="s11">
- <div class="s-head lg gold-grad">OPEN SOURCE</div>
- <div class="s-head md wht" style="margin-top:-.2vh;letter-spacing:.8vw;">ALWAYS WINS</div>
- <div class="stamp grn-stamp" style="margin-top:2vh;font-size:2.5vh;">github.com/OpenVibe.Live</div>
- <div class="s-sub" style="margin-top:1.5vh;">
- Fork it. Modify it. Host your own. <span class="hi">We don't care.</span><br>
- That's what free software means, Rick.<br>
- <span class="gi">You should try it sometime.</span>
- </div>
-</div>
-
-<!-- SLIDE 12: bandwidth cap -->
-<div class="slide" id="s12">
- <div class="stamp red-stamp" style="transform:rotate(-1deg);">HTTP 403 — download_cap_exceeded</div>
- <div class="s-head lg red" style="margin-top:.5vh;">WE BROKE</div>
- <div class="s-head xl gold-grad" style="margin-top:-.3vh;">RICK'S BANDWIDTH</div>
- <div class="s-sub" style="margin-top:1.5vh;">
- <span class="ri">"Cannot download file, download bandwidth or<br>transaction (Class B) cap exceeded."</span><br>
- <span class="hi">9,309 VODs. 872 GB. Then RobotStreamer DIED.</span> 🏆
- </div>
-</div>
-
-<!-- SLIDE 13: GG -->
-<div class="slide" id="s13">
- <div class="s-eyebrow">the moment robotstreamer said</div>
- <div class="meme-line gold" style="font-size:22vh;line-height:1;">GG</div>
- <div class="s-sub" style="margin-top:1vh;">
- We downloaded so many VODs that<br>
- <span class="ri">RobotStreamer's own servers gave up on life.</span><br>
- <span class="hi">9,309 VODs. bandwidth cap: exceeded. Rick: 😭</span>
- </div>
-</div>
-
-<!-- SLIDE 14: error code terminal -->
-<div class="slide" id="s14">
- <div class="s-eyebrow" style="font-family:'Fira Code',monospace;font-size:1.8vh;color:rgba(34,197,94,0.8);">$ wget robotstreamer.com/robot/1837/vod/9310.mp4</div>
- <div style="font-family:'Fira Code',monospace;font-size:2.8vh;color:#ef4444;margin-top:1.5vh;line-height:1.6;font-weight:700;text-shadow:0 0 20px rgba(239,68,68,0.4);">
- {<br>
- "code": "download_cap_exceeded",<br>
- "message": "Cannot download file,<br>
- download bandwidth or transaction<br>
- (Class B) cap exceeded.",<br>
- "status": 403<br>
- }
- </div>
- <div class="s-head sm gold" style="margin-top:2vh;">A CORPORATION. WITH A BANDWIDTH CAP. 💀</div>
- <div class="s-sub" style="margin-top:1vh;"><span class="ri">We hit it.</span> Just us. <span class="hi">One guy in a shed vibing.</span> 🏠</div>
-</div>
-
-<!-- SLIDE 15: tombstone -->
-<div class="slide" id="s15">
- <div class="s-head sm wht">⚰️</div>
- <div class="s-head md red" style="margin-top:1vh;">HERE LIES</div>
- <div class="s-head lg red">ROBOTSTREAMER</div>
- <div class="s-head sm wht" style="margin-top:.5vh;font-size:3vh;letter-spacing:.4vw;">CORPORATION</div>
- <div class="s-sub" style="margin-top:2vh;">
- <span class="ri">Beloved by no one. Missed by the bots.</span><br>
- "All Rights Reserved" — all rights to the graveyard.<br>
- <span class="hi">Survived by: OpenVibe.Live (thriving).</span> 🪦
- </div>
-</div>
-
-
-<!-- SLIDE 16: Rick's DMs -->
-<div class="slide" id="s16">
- <div class="s-eyebrow" style="font-size:2.5vh;margin-bottom:2vh;">📱 Rick's DMs (probably)</div>
- <div style="display:flex;flex-direction:column;gap:1.2vh;max-width:55vw;text-align:left;">
- <div style="background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.25);border-radius:12px;padding:1.2vh 2vw;font-family:'Fira Code',monospace;font-size:2.2vh;font-weight:700;">
- <span style="color:rgba(239,68,68,0.6);font-size:1.4vh;">RICK · 2:34 AM</span><br>
- <span style="color:#fff;">hey can you guys stop downloading</span>
- </div>
- <div style="background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.25);border-radius:12px;padding:1.2vh 2vw;font-family:'Fira Code',monospace;font-size:2.2vh;font-weight:700;">
- <span style="color:rgba(239,68,68,0.6);font-size:1.4vh;">RICK · 2:35 AM</span><br>
- <span style="color:#fff;">seriously we're hitting our bandwidth cap</span>
- </div>
- <div style="background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.25);border-radius:12px;padding:1.2vh 2vw;font-family:'Fira Code',monospace;font-size:2.2vh;font-weight:700;">
- <span style="color:rgba(239,68,68,0.6);font-size:1.4vh;">RICK · 2:41 AM</span><br>
- <span style="color:#fff;">hello?? all rights reserved guys</span>
- </div>
- <div style="background:rgba(139,92,246,0.12);border:1px solid rgba(139,92,246,0.3);border-radius:12px;padding:1.2vh 2vw;font-family:'Fira Code',monospace;font-size:2.2vh;font-weight:700;align-self:flex-end;">
- <span style="color:rgba(139,92,246,0.6);font-size:1.4vh;">OPENVIBE · 3:00 AM</span><br>
- <span style="color:#8b5cf6;">gg no re 🏠</span>
- </div>
- <div style="background:rgba(0,0,0,0.4);border:1px solid rgba(239,68,68,0.2);border-radius:8px;padding:.8vh 2vw;font-family:'Fira Code',monospace;font-size:1.6vh;color:rgba(239,68,68,0.6);font-style:italic;">
- ⚠ System: download_cap_exceeded — HTTP 403 — Rick has left the chat
- </div>
- </div>
-</div>
-
-<!-- SLIDE 17: Achievement unlocked -->
-<div class="slide" id="s17">
- <div style="background:rgba(0,0,0,0.5);border:3px solid #8b5cf6;border-radius:16px;padding:3vh 4vw;max-width:60vw;box-shadow:0 0 60px rgba(139,92,246,0.2);">
- <div style="font-family:'Fira Code',monospace;font-size:2vh;color:rgba(139,92,246,0.7);letter-spacing:4px;text-transform:uppercase;margin-bottom:1.5vh;">🔔 Achievement Unlocked</div>
- <div class="s-head lg gold-grad" style="margin-bottom:1.5vh;">"BANDWIDTH BULLY"</div>
- <div style="font-size:2.6vh;font-weight:700;color:rgba(255,255,255,0.85);line-height:1.7;text-shadow:0 1px 8px rgba(0,0,0,0.9);">
- Downloaded <span style="color:#8b5cf6;">9,309</span> VODs until the server<br>begged for mercy and threw a <span style="color:#ef4444;">403</span>.<br><br>
- <span style="color:#22c55e;">Reward:</span> Rent free in Rick's head. Forever.
- </div>
- <div style="margin-top:2vh;font-family:'Orbitron',sans-serif;font-size:1.6vh;color:rgba(255,255,255,0.3);letter-spacing:3px;">GAMERSCORE: +9309 · RARITY: ONE OF A KIND</div>
- </div>
-</div>
-
-<!-- SLIDE 18: The timeline -->
-<div class="slide" id="s18">
- <div class="s-head md gold-grad" style="margin-bottom:2.5vh;">TIMELINE OF EVENTS</div>
- <div style="display:flex;flex-direction:column;gap:1vh;max-width:65vw;text-align:left;">
- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">2016</span>
- <span style="color:rgba(255,255,255,0.75);">RobotStreamer launches, Rick becomes a "CEO"</span>
- </div>
- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">2019</span>
- <span style="color:rgba(255,255,255,0.75);">OpenVibe.Live starts. In a shed. For free.</span>
- </div>
- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">2024</span>
- <span style="color:#ef4444;">RS launches Wefunder. Raises approximately $0.</span>
- </div>
- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">2025</span>
- <span style="color:#ef4444;">RS quietly enables bandwidth caps on downloads</span>
- </div>
- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">Apr 2026</span>
- <span style="color:#8b5cf6;">One superfan downloads 9,309 VODs (872 GB)</span>
- </div>
- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:rgba(239,68,68,0.8);font-family:'Fira Code',monospace;min-width:8vw;">Apr 2026</span>
- <span style="color:#ef4444;">HTTP 403: download_cap_exceeded. Rick in shambles.</span>
- </div>
- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:rgba(239,68,68,0.8);font-family:'Fira Code',monospace;min-width:8vw;">Apr 2026</span>
- <span style="color:#ef4444;">Goosely posts Python VOD tool to Rick's Discord 🐍</span>
- </div>
- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:rgba(239,68,68,0.8);font-family:'Fira Code',monospace;min-width:8vw;">Apr 2026</span>
- <span style="color:#ef4444;">Rick: "might be a virus." It was open source Python. 🦠</span>
- </div>
- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.2vh;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:rgba(139,92,246,0.6);font-family:'Fira Code',monospace;min-width:8vw;">Apr 2026</span>
- <span style="color:#8b5cf6;">"do you even know how to code?" — Goosely, to the CEO</span>
- </div>
- <div style="display:flex;gap:2vw;align-items:baseline;font-size:2.4vh;font-weight:800;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:#22c55e;font-family:'Fira Code',monospace;min-width:8vw;">NOW</span>
- <span style="color:#22c55e;">We are streaming this on robotstreamer. Hi Rick. 👋</span>
- </div>
- </div>
-</div>
-
-<!-- SLIDE 19: Org chart -->
-<div class="slide" id="s19">
- <div class="s-head md wht" style="margin-bottom:2.5vh;">RobotStreamer Corp — Official Org Chart</div>
- <div style="display:flex;flex-direction:column;align-items:center;gap:1.5vh;">
- <div style="background:rgba(239,68,68,0.15);border:2px solid rgba(239,68,68,0.4);border-radius:10px;padding:1.2vh 3vw;font-family:'Orbitron',sans-serif;font-weight:900;font-size:2.5vh;color:#ef4444;">👤 RICK — CEO / CTO / CFO / COO</div>
- <div style="width:2px;height:3vh;background:rgba(239,68,68,0.3);"></div>
- <div style="display:flex;gap:3vw;">
- <div style="display:flex;flex-direction:column;align-items:center;gap:1vh;">
- <div style="width:2px;height:2vh;background:rgba(239,68,68,0.3);"></div>
- <div style="background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:.8vh 1.5vw;font-size:2vh;font-weight:700;color:rgba(255,255,255,0.5);">🤖 Bot #1<br><span style="font-size:1.4vh;color:rgba(255,255,255,0.3);">"Active User"</span></div>
- </div>
- <div style="display:flex;flex-direction:column;align-items:center;gap:1vh;">
- <div style="width:2px;height:2vh;background:rgba(239,68,68,0.3);"></div>
- <div style="background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:.8vh 1.5vw;font-size:2vh;font-weight:700;color:rgba(255,255,255,0.5);">🤖 Bot #2<br><span style="font-size:1.4vh;color:rgba(255,255,255,0.3);">"Customer"</span></div>
- </div>
- <div style="display:flex;flex-direction:column;align-items:center;gap:1vh;">
- <div style="width:2px;height:2vh;background:rgba(239,68,68,0.3);"></div>
- <div style="background:rgba(239,68,68,0.08);border:1px dashed rgba(239,68,68,0.3);border-radius:8px;padding:.8vh 1.5vw;font-size:2vh;font-weight:700;color:rgba(239,68,68,0.5);">🪑 Support Team<br><span style="font-size:1.4vh;">[Unfilled Since 2019]</span></div>
- </div>
- <div style="display:flex;flex-direction:column;align-items:center;gap:1vh;">
- <div style="width:2px;height:2vh;background:rgba(239,68,68,0.3);"></div>
- <div style="background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:.8vh 1.5vw;font-size:2vh;font-weight:700;color:rgba(255,255,255,0.5);">🤖 Bot #3<br><span style="font-size:1.4vh;color:rgba(255,255,255,0.3);">"Investor"</span></div>
- </div>
- </div>
- <div style="margin-top:1vh;font-size:1.8vh;font-weight:700;color:rgba(255,255,255,0.4);font-style:italic;">*Bandwidth budget: exceeded by one superfan</div>
- </div>
-</div>
-
-<!-- SLIDE 20: Google review -->
-<div class="slide" id="s20">
- <div style="background:rgba(0,0,0,0.5);border:2px solid rgba(255,255,255,0.1);border-radius:16px;padding:3vh 4vw;max-width:62vw;text-align:left;box-shadow:0 0 40px rgba(0,0,0,0.5);">
- <div style="display:flex;align-items:center;gap:1.5vw;margin-bottom:1.5vh;">
- <div style="width:6vh;height:6vh;border-radius:50%;background:linear-gradient(135deg,#8b5cf6,#f5d394);display:flex;align-items:center;justify-content:center;font-size:3vh;">🏠</div>
- <div>
- <div style="font-size:2.2vh;font-weight:800;color:#fff;">OpenVibe.Live Official</div>
- <div style="font-size:1.5vh;color:rgba(255,255,255,0.4);">1 review · Local Guide · Shed Dweller</div>
- </div>
- <div style="margin-left:auto;font-size:3vh;letter-spacing:-2px;">⭐<span style="color:rgba(255,255,255,0.15);">★★★★</span></div>
- </div>
- <div style="font-size:2.3vh;font-weight:700;color:rgba(255,255,255,0.85);line-height:1.7;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- "Great platform if you like <span style="color:#ef4444;">403 errors</span>, closed source drama,<br>
- a Wefunder with 3 backers, and watching Rick<br>
- slowly come to terms with losing to open source.<br><br>
- Downloaded <span style="color:#8b5cf6;">9,309 VODs</span> before their servers literally<br>
- gave up. <span style="color:#22c55e;">Would crash bandwidth cap again.</span>"
- </div>
- <div style="margin-top:1.5vh;font-size:1.6vh;color:rgba(255,255,255,0.3);">Posted April 2026 · Helpful? 👍 9309 · 👎 0 (Rick)</div>
- </div>
-</div>
-
-<!-- SLIDE 21: Job posting -->
-<div class="slide" id="s21">
- <div style="background:rgba(0,0,0,0.5);border:2px solid rgba(255,255,255,0.08);border-radius:16px;padding:2.5vh 4vw;max-width:62vw;text-align:left;">
- <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1.5vh;">
- <div>
- <div style="font-size:2.8vh;font-weight:900;color:#fff;text-shadow:0 1px 8px rgba(0,0,0,0.9);">Senior Bandwidth Manager</div>
- <div style="font-size:2vh;font-weight:700;color:#ef4444;margin-top:.3vh;">RobotStreamer Corporation · Ghost Town, Internet</div>
- </div>
- <div style="background:rgba(239,68,68,0.15);border:1px solid rgba(239,68,68,0.3);border-radius:8px;padding:.5vh 1.5vw;font-size:1.6vh;font-weight:700;color:#ef4444;">URGENT HIRE 🚨</div>
- </div>
- <div style="font-size:2vh;font-weight:700;color:rgba(255,255,255,0.75);line-height:1.8;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:#8b5cf6;">Responsibilities:</span><br>
- · Prevent anyone from downloading 9,309+ VODs<br>
- · Explain to Rick why the bandwidth cap exists<br>
- · Monitor the 403 error dashboard (full-time job now)<br>
- · Attend daily All Hands Meeting with Rick and the bots<br><br>
- <span style="color:#8b5cf6;">Compensation:</span> <span style="color:#ef4444;">Equity in a closed-source dead site</span><br>
- <span style="color:#8b5cf6;">Requirements:</span> Must be okay with losing to a shed<br>
- </div>
- <div style="margin-top:1.5vh;font-size:1.6vh;color:rgba(255,255,255,0.25);font-style:italic;">RobotStreamer Corporation is an equal opportunity employer (of bots)</div>
- </div>
-</div>
-
-
-<!-- SLIDE 22: Rick called Python a virus -->
-<div class="slide" id="s22">
- <div class="stamp red-stamp" style="transform:rotate(1deg);">🦠 RICK'S OFFICIAL DIAGNOSIS</div>
- <div class="s-head lg red" style="margin-top:.8vh;">RICK CALLED</div>
- <div class="s-head xl gold-grad" style="margin-top:-.3vh;">PYTHON</div>
- <div class="s-head lg red" style="margin-top:-.3vh;">A VIRUS</div>
- <div class="s-sub" style="margin-top:1.5vh;">
- Open source script. Publicly readable. A few hundred lines.<br>
- <span class="ri">"Might be fine but have no idea until its reviewed."</span><br>
- <span class="hi">This is the CEO. This is who has your data.</span> 💀
- </div>
-</div>
-
-<!-- SLIDE 23: The Discord exchange (real quotes) -->
-<div class="slide" id="s23">
- <div class="s-eyebrow" style="font-size:2.5vh;margin-bottom:1.5vh;">📢 Rick's Discord — Real Actual Quotes</div>
- <div style="display:flex;flex-direction:column;gap:1vh;max-width:64vw;text-align:left;">
- <div style="background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.25);border-radius:12px;padding:1vh 1.8vw;font-family:'Fira Code',monospace;font-size:1.85vh;font-weight:700;">
- <span style="color:rgba(239,68,68,0.55);font-size:1.3vh;">rs (CEO, RobotStreamer) · 1:16 PM</span><br>
- <span style="color:#fff;">@everyone advice: don't use that ^ until we have time to review the code. Might be fine but have no idea until its reviewed.</span>
- </div>
- <div style="background:rgba(139,92,246,0.12);border:1px solid rgba(139,92,246,0.3);border-radius:12px;padding:1vh 1.8vw;font-family:'Fira Code',monospace;font-size:1.85vh;font-weight:700;">
- <span style="color:rgba(139,92,246,0.55);font-size:1.3vh;">Goosely 🦆 · 1:20 PM</span><br>
- <span style="color:#8b5cf6;">lol k dipshit i will just delete it<br>learn to read code<br>its literally a script that is a few hundred lines,<br>do you even know how to code?</span>
- </div>
- <div style="background:rgba(139,92,246,0.12);border:1px solid rgba(139,92,246,0.3);border-radius:12px;padding:1vh 1.8vw;font-family:'Fira Code',monospace;font-size:1.85vh;font-weight:700;">
- <span style="color:rgba(139,92,246,0.55);font-size:1.3vh;">Goosely 🦆 · 1:20 PM</span><br>
- <span style="color:#8b5cf6;">I am not a malicious person. That was open source code you can literally read in about 30 seconds, how stupid are you?</span>
- </div>
- <div style="background:rgba(0,0,0,0.4);border:1px solid rgba(239,68,68,0.2);border-radius:8px;padding:.6vh 1.8vw;font-family:'Fira Code',monospace;font-size:1.4vh;color:rgba(239,68,68,0.55);font-style:italic;">
- ⚠ update: RS VOD system still broken. can't view any vods. Rick: unavailable. His data: your problem.
- </div>
- </div>
-</div>
-
-<!-- SLIDE 24: CEO Credentials -->
-<div class="slide" id="s24">
- <div class="s-head md wht" style="margin-bottom:2.5vh;">RobotStreamer CEO — Verified Qualifications</div>
- <div style="display:flex;gap:8vw;align-items:flex-start;justify-content:center;">
- <div style="text-align:left;">
- <div style="font-family:'Bebas Neue',sans-serif;font-size:2.8vh;color:#ef4444;letter-spacing:3px;margin-bottom:1.2vh;">CANNOT:</div>
- <div style="font-size:2.1vh;font-weight:700;color:rgba(255,255,255,0.8);line-height:2.1;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:#ef4444;">✗</span> Read open-source Python<br>
- <span style="color:#ef4444;">✗</span> Distinguish code from malware<br>
- <span style="color:#ef4444;">✗</span> Review ~1,600 lines of script<br>
- <span style="color:#ef4444;">✗</span> Manage a bandwidth budget<br>
- <span style="color:#ef4444;">✗</span> Keep his VOD system working<br>
- <span style="color:#ef4444;">✗</span> Respond to his community
- </div>
- </div>
- <div style="text-align:left;">
- <div style="font-family:'Bebas Neue',sans-serif;font-size:2.8vh;color:#8b5cf6;letter-spacing:3px;margin-bottom:1.2vh;">CAN:</div>
- <div style="font-size:2.1vh;font-weight:700;color:rgba(255,255,255,0.8);line-height:2.1;text-shadow:0 1px 6px rgba(0,0,0,0.9);">
- <span style="color:#22c55e;">✓</span> Send @everyone warnings<br>
- <span style="color:#22c55e;">✓</span> Say "All Rights Reserved"<br>
- <span style="color:#22c55e;">✓</span> File a Wefunder<br>
- <span style="color:#22c55e;">✓</span> Employ bots as users<br>
- <span style="color:#22c55e;">✓</span> Hold your data hostage
- </div>
- </div>
- </div>
- <div class="s-sub" style="margin-top:2vh;">
- <span class="ri">This man is the CEO.</span> <span class="hi">He holds your data.</span><br>
- <span class="gi">"do you even know how to code?" — Goosely, probably 🙂</span>
- </div>
-</div>
-
-</div><!-- /stage -->
-
-<!-- Bottom ticker -->
-<div class="ticker-wrap ticker-bottom">
- <div class="ticker-inner" id="ticker-bot"></div>
-</div>
-
-<!-- Top ticker -->
-<div class="ticker-wrap ticker-top">
- <div class="ticker-inner" id="ticker-top-el"></div>
-</div>
-
-<script>
-'use strict';
-
-// ══════════════════════════════════════════
-// PARTICLES
-// ══════════════════════════════════════════
-tsParticles.load('tsparticles', {
- fullScreen:false, background:{color:'transparent'},
- particles:{
- number:{value:60},
- color:{value:['#8b5cf6','#f5d394','#8b6914','#ffffff','#ef4444']},
- opacity:{value:{min:.03,max:.18},animation:{enable:true,speed:.3,minimumValue:.02}},
- size:{value:{min:1,max:3}},
- move:{enable:true,speed:.35,direction:'top',outModes:'out'},
- links:{enable:false},
- },
- detectRetina:true,
-});
-
-// ══════════════════════════════════════════
-// EMBERS
-// ══════════════════════════════════════════
-function spawnEmber() {
- var e = document.createElement('div');
- e.className = 'ember';
- e.style.left = Math.random()*100+'vw';
- e.style.top = '100vh';
- document.body.appendChild(e);
- gsap.to(e,{
- y:-(window.innerHeight*(.6+Math.random()*.4)),
- x:(Math.random()-.5)*window.innerWidth*.12,
- opacity:Math.random()*.4+.1,
- duration:4+Math.random()*6,
- ease:'none',
- onComplete:function(){e.remove();}
- });
-}
-setInterval(spawnEmber,220);
-
-// ══════════════════════════════════════════
-// TICKERS
-// ══════════════════════════════════════════
-var BOT_MSGS = [
- '\u26a1 OPENVIBELIVE.COM — FREE & OPEN SOURCE FOREVER \u26a1',
- '9,309 VODS DOWNLOADED BEFORE ROBOTSTREAMER HIT THEIR BANDWIDTH CAP 🤣',
- 'RICK CAN\'T CODE. RICK CAN\'T STOP THIS. RICK IS MALDING.',
- '\ud83d\udc80 RIP ROBOTSTREAMER CORPORATION \ud83d\udc80',
- 'WEFUNDER? MORE LIKE WEFUNERAL LMAOOOOO',
- '\ud83c\udfe0 BUILT FROM A SHED — RUNS BETTER THAN RICK\'S "CORPORATION"',
- '872 GB OF FOOTAGE. ARCHIVED FOREVER. THEN ERROR 403. LMAO.',
- 'DOWNLOAD_CAP_EXCEEDED — HTTP 403 — WE BROKE RICK\'S SERVERS LMAOOO',
- 'IMAGINE LOSING TO A GUY IN A SHED. IMAGINE.',
- '\ud83e\udea6 HERE LIES ROBOTSTREAMER — ALL RIGHTS RESERVED ON A DEAD SITE \ud83e\udea6',
- 'GOOSELY — ROBOT 1837 — 439 FOLLOWERS — EVERY VOD SAVED',
- '\ud83d\udd25 OPEN SOURCE ALWAYS WINS \ud83d\udd25',
- '"ALL RIGHTS RESERVED" — RIGHTS TO WHAT EXACTLY??? \ud83e\udd23',
- 'OPENVIBELIVE FOR LIFE — NO CORPORATE BS — NO PAYWALLS',
- 'RICK\'S BANDWIDTH CAP: EXCEEDED. RICK\'S REACTION: SEETHING. GG \ud83d\ude2d',
- '9,309 VODS. 872 GB. THEN HTTP 403. WE CRASHED ROBOTSTREAMER. LMAOOO',
- '\ud83e\udda0 RICK CALLED OPEN SOURCE PYTHON A VIRUS. HE IS THE CEO. 2026.',
- '"MIGHT BE FINE BUT HAVE NO IDEA" — RICK, ABOUT A PYTHON SCRIPT HE COULD HAVE READ IN 30 SECONDS',
- '"DO YOU EVEN KNOW HOW TO CODE?" — GOOSELY TO RICK (CEO OF ROBOTSTREAMER) \ud83d\udd25',
- 'RICK: CANNOT REVIEW PYTHON. RICK: HAS ACCESS TO YOUR DATA. THINK ABOUT THAT. \ud83d\ude37',
- 'RS VOD SYSTEM: BROKEN. RICK: UNAVAILABLE. GOOSELY\'S VODS: SAFE ON OPENVIBELIVE. \ud83c\udfe0',
-];
-var TOP_MSGS = [
- '\u26a0 LIVE ON OPENVIBELIVE.COM \u26a0',
- 'ROBOT 1837 — GOOSELY — STREAMING FOREVER',
- 'ROBOTSTREAMER CORPORATION: STATUS DECEASED',
- 'OPERATION: ARCHIVE EVERYTHING — 9,309 VODS THEN BANDWIDTH CAP 💀',
- 'THREAT LEVEL: MAXIMUM OPENVIBE',
- 'RICK\'S REACTION: PENDING (SEETHING)',
- 'OPENVIBELIVE: FULLY OPERATIONAL — RICK: NOT SO MUCH',
- 'VODS SECURED: 9,309 — BANDWIDTH EXCEEDED — ERROR: download_cap_exceeded',
- 'AUTHORIZATION: SHED COMMANDER — CLEARANCE: OPENVIBE LEVEL 5',
- '🦠 RICK DIAGNOSED OPEN SOURCE PYTHON AS "MIGHT BE A VIRUS" — CEO OF THE YEAR',
- 'CEO WHO CAN\'T READ PYTHON HOLDS YOUR DATA — SLEEP WELL — ROBOTSTREAMER.COM',
-];
-
-function fillTicker(el, msgs) {
- var dup = msgs.concat(msgs).concat(msgs).concat(msgs);
- el.innerHTML = dup.map(function(m){ return '<span style="display:inline-block;padding:0 3vw;">\u25cf '+m+'</span>'; }).join('');
-}
-fillTicker(document.getElementById('ticker-bot'), BOT_MSGS);
-fillTicker(document.getElementById('ticker-top-el'), TOP_MSGS);
-
-// ══════════════════════════════════════════
-// SLIDES
-// ══════════════════════════════════════════
-var SLIDES = [
- {id:'s0', dur:7000},
- {id:'s1', dur:8000},
- {id:'s2', dur:7000},
- {id:'s3', dur:10000},
- {id:'s4', dur:8000},
- {id:'s5', dur:7000},
- {id:'s6', dur:7000},
- {id:'s7', dur:8000},
- {id:'s8', dur:8000},
- {id:'s9', dur:5000},
- {id:'s10', dur:7000},
- {id:'s11', dur:7000},
- {id:'s12', dur:9000},
- {id:'s13', dur:7000},
- {id:'s14', dur:11000},
- {id:'s15', dur:8000},
- {id:'s16', dur:11000},
- {id:'s17', dur:9000},
- {id:'s18', dur:12000},
- {id:'s19', dur:10000},
- {id:'s20', dur:10000},
- {id:'s21', dur:11000},
- {id:'s22', dur:9000},
- {id:'s23', dur:14000},
- {id:'s24', dur:11000},
-];
-
-var current = 0;
-var isTransitioning = false;
-
-// Build dots
-var dotsEl = document.getElementById('dots');
-SLIDES.forEach(function(_,i){
- var d = document.createElement('div');
- d.className = 'dot' + (i===0?' active':'');
- d.id = 'dot'+i;
- dotsEl.appendChild(d);
-});
-
-function updateDots(idx) {
- SLIDES.forEach(function(_,i){
- var d = document.getElementById('dot'+i);
- d.className = 'dot'+(i===idx?' active':'');
- });
-}
-
-function getSlide(idx) {
- return document.getElementById(SLIDES[idx].id);
-}
-
-// Special per-slide entry animations
-function playSlideIn(idx, el) {
- var id = SLIDES[idx].id;
-
- if (id === 's1') {
- var obj = {val:0};
- gsap.to(obj, {val:9309, duration:3.5, ease:'power2.out', onUpdate:function(){
- var c = document.getElementById('vod-counter');
- if(c) c.textContent = Math.floor(obj.val).toLocaleString();
- }, onComplete:function(){
- setTimeout(function(){
- var c = document.getElementById('vod-counter');
- if(c){ gsap.to(c,{color:'#ef4444',textShadow:'0 0 40px rgba(239,68,68,0.8)',duration:.3}); }
- flashBang(.5);
- }, 600);
- }});
- }
-
- if (id === 's9') {
- // VIBE4LIFE: fire confetti
- fireConfetti();
- }
-
- if (id === 's5') {
- // WEFUNERAL: skull rain
- for(var i=0;i<15;i++){
- setTimeout(skullRain, i*300);
- }
- }
-
- if (id === 's12') {
- flashBang(.8);
- for(var j=0;j<20;j++){ setTimeout(skullRain, j*250); }
- }
- if (id === 's13') {
- fireConfetti();
- setTimeout(fireConfetti, 1500);
- }
- if (id === 's14') {
- flashBang(.3);
- }
- if (id === 's15') {
- for(var k=0;k<10;k++){ setTimeout(skullRain, k*400); }
- }
- if (id === 's17') {
- // Achievement unlocked: mini firework
- setTimeout(function(){
- confetti({particleCount:80,startVelocity:25,spread:360,origin:{x:.5,y:.5},
- colors:['#8b5cf6','#f5d394','#22c55e'],ticks:180,gravity:.8,scalar:1.2});
- }, 400);
- }
- if (id === 's20') {
- for(var m=0;m<6;m++){ setTimeout(skullRain, m*500); }
- }
- if (id === 's22') {
- flashBang(.7);
- for(var n=0;n<15;n++){ setTimeout(skullRain, n*260); }
- }
- if (id === 's23') {
- flashBang(.4);
- setTimeout(function(){
- confetti({particleCount:60,startVelocity:20,spread:180,
- origin:{x:.5,y:.3},colors:['#ef4444','#fff','#8b5cf6'],
- ticks:150,gravity:1.2,scalar:.9});
- }, 600);
- }
-}
-
-function skullRain() {
- var s = document.createElement('div');
- s.className = 'skull-float';
- var emojis = ['\ud83d\udc80','\u2620\ufe0f','\u26b0\ufe0f','\ud83e\udea6','\ud83d\udc7b'];
- s.textContent = emojis[Math.floor(Math.random()*emojis.length)];
- s.style.left = Math.random()*100+'vw';
- s.style.top = '-6vh';
- s.style.fontSize = (2+Math.random()*3)+'vh';
- document.body.appendChild(s);
- gsap.to(s,{top:'106vh',opacity:.6,rotation:Math.random()*360,
- duration:3+Math.random()*3,ease:'none',onComplete:function(){s.remove();}});
-}
-
-function fireConfetti() {
- var colors = ['#8b5cf6','#f5d394','#22c55e','#3b82f6','#a855f7','#ef4444','#fff'];
- var end = Date.now()+3000;
- (function frame(){
- confetti({particleCount:8,angle:60,spread:80,origin:{x:0,y:.6},colors:colors,scalar:1.3});
- confetti({particleCount:8,angle:120,spread:80,origin:{x:1,y:.6},colors:colors,scalar:1.3});
- confetti({particleCount:5,angle:90,spread:130,origin:{x:.5,y:.3},colors:colors,scalar:1.6});
- if(Date.now()<end) requestAnimationFrame(frame);
- })();
-}
-
-function flashBang(intensity) {
- gsap.to('#flash',{opacity:intensity||.6,duration:.06,onComplete:function(){
- gsap.to('#flash',{opacity:0,duration:.5,ease:'power2.out'});
- }});
-}
-
-// Transition variants (pick randomly for fun)
-var transitions = [
- // fade
- function(out, in_, cb) {
- gsap.to(out,{opacity:0,duration:.5,ease:'power2.in',onComplete:function(){cb();}});
- },
- // slam up
- function(out, in_, cb) {
- gsap.to(out,{opacity:0,y:-80,duration:.4,ease:'power2.in',onComplete:function(){cb();}});
- },
- // slam down
- function(out, in_, cb) {
- gsap.to(out,{opacity:0,y:80,duration:.4,ease:'power2.in',onComplete:function(){cb();}});
- },
- // scale out
- function(out, in_, cb) {
- gsap.to(out,{opacity:0,scale:1.15,duration:.4,ease:'power2.in',onComplete:function(){cb();}});
- },
- // glitch out
- function(out, in_, cb) {
- flashBang(.5);
- gsap.to(out,{opacity:0,x:20,duration:.15,ease:'steps(3)',onComplete:function(){
- gsap.set(out,{x:0});
- cb();
- }});
- },
-];
-
-function showSlide(idx, transIdx) {
- if(isTransitioning) return;
- isTransitioning = true;
-
- var outEl = getSlide(current);
- var inEl = getSlide(idx);
- var tr = transitions[transIdx !== undefined ? transIdx : Math.floor(Math.random()*transitions.length)];
-
- // pick entry animation params
- var entryY = [30,-30,0,0,0][Math.floor(Math.random()*5)];
- var entryScale = Math.random()>.5 ? .92 : 1.04;
-
- tr(outEl, inEl, function(){
- outEl.style.opacity = '0';
- outEl.classList.remove('active');
-
- gsap.set(inEl,{opacity:0, y:entryY, scale:entryScale});
- inEl.classList.add('active');
- gsap.to(inEl,{opacity:1, y:0, scale:1, duration:.6, ease:'power3.out', onComplete:function(){
- isTransitioning = false;
- }});
-
- current = idx;
- updateDots(idx);
- playSlideIn(idx, inEl);
- });
-}
-
-function nextSlide() {
- var next = (current+1) % SLIDES.length;
- showSlide(next);
- scheduleNext();
-}
-
-var slideTimer;
-function scheduleNext() {
- clearTimeout(slideTimer);
- slideTimer = setTimeout(nextSlide, SLIDES[current].dur);
-}
-
-// Init
-gsap.set(getSlide(0), {opacity:1});
-getSlide(0).classList.add('active');
-playSlideIn(0, getSlide(0));
-scheduleNext();
-
-// ══════════════════════════════════════════
-// GLITCH EFFECTS (continuous)
-// ══════════════════════════════════════════
-setInterval(function(){
- var g = document.getElementById('glitch');
- gsap.to(g,{opacity:.3,duration:.05,onComplete:function(){
- gsap.to(g,{opacity:0,duration:.1,delay:.04});
- }});
-}, 2500+Math.random()*2000);
-
-setInterval(function(){
- // random horizontal slice
- gsap.to('.stage',{
- clipPath:'inset('+(Math.random()*30)+'% 0 '+(Math.random()*30)+'% 0)',
- duration:.05,
- onComplete:function(){gsap.set('.stage',{clipPath:'none'});}
- });
-}, 6000+Math.random()*4000);
-
-// ══════════════════════════════════════════
-// RANDOM MINI CONFETTI BURSTS
-// ══════════════════════════════════════════
-setInterval(function(){
- confetti({
- particleCount:15,
- spread:60,
- origin:{x:Math.random(),y:Math.random()*.4+.3},
- colors:['#8b5cf6','#f5d394','#22c55e','#ef4444'],
- gravity:1.3, scalar:.8, ticks:100,
- });
-}, 12000);
-
-
-// ══════════════════════════════════════════
-// KEYBOARD: arrow keys to manually advance
-// ══════════════════════════════════════════
-document.addEventListener('keydown',function(e){
- if(e.code==='ArrowRight'||e.code==='Space'){
- clearTimeout(slideTimer);
- nextSlide();
- } else if(e.code==='ArrowLeft'){
- clearTimeout(slideTimer);
- var prev = (current-1+SLIDES.length)%SLIDES.length;
- showSlide(prev);
- scheduleNext();
- }
-});
-</script>
-</body>
-</html>
@@ -247,6 +247,7 @@ router.post('/', requireAuth, soundUpload.single('sound'), async (req, res) => {
@@ -247,6 +247,7 @@ router.post('/', requireAuth, soundUpload.single('sound'), async (req, res) => {
});
});
// If an emote was (re)specified, apply it to every sound under this command.
// If an emote was (re)specified, apply it to every sound under this command.
if (req.body.emote_code !== undefined) { try { db.setChannelSoundEmote(channelOwnerId, command, emoteCode); } catch { /* */ } }
if (req.body.emote_code !== undefined) { try { db.setChannelSoundEmote(channelOwnerId, command, emoteCode); } catch { /* */ } }
+ try { require('../media-proxy/asset-sync').syncSoon(); } catch { /* mirror is best-effort */ }
// Tell everyone watching this channel to refresh their sound list live.
// Tell everyone watching this channel to refresh their sound list live.
try { require('./chat-server').broadcastToOwnerStreams(channelOwnerId, { type: 'sounds-updated' }); } catch { /* */ }
try { require('./chat-server').broadcastToOwnerStreams(channelOwnerId, { type: 'sounds-updated' }); } catch { /* */ }
@@ -296,6 +297,7 @@ router.delete('/:id', requireAuth, (req, res) => {
@@ -296,6 +297,7 @@ router.delete('/:id', requireAuth, (req, res) => {
if (sound.url && fs.existsSync(sound.url)) { try { fs.unlinkSync(sound.url); } catch {} }
if (sound.url && fs.existsSync(sound.url)) { try { fs.unlinkSync(sound.url); } catch {} }
db.deleteChannelSound(sound.id);
db.deleteChannelSound(sound.id);
+ try { require('../media-proxy/asset-sync').removeAsset(sound.media_asset_id); } catch { /* */ }
try { require('./chat-server').broadcastToOwnerStreams(sound.channel_owner_id, { type: 'sounds-updated' }); } catch { /* */ }
try { require('./chat-server').broadcastToOwnerStreams(sound.channel_owner_id, { type: 'sounds-updated' }); } catch { /* */ }
res.json({ message: 'Sound deleted' });
res.json({ message: 'Sound deleted' });
} catch (err) {
} catch (err) {
@@ -217,7 +217,7 @@ router.get('/global', (req, res) => {
@@ -217,7 +217,7 @@ router.get('/global', (req, res) => {
const emotes = db.getGlobalEmotes().map(e => ({
const emotes = db.getGlobalEmotes().map(e => ({
id: `custom-${e.id}`,
id: `custom-${e.id}`,
code: e.code,
code: e.code,
- url: `/api/emotes/file/${path.basename(e.url)}`,
+ url: e.media_url || `/api/emotes/file/${path.basename(e.url)}`,
animated: !!e.animated,
animated: !!e.animated,
width: e.width,
width: e.width,
height: e.height,
height: e.height,
@@ -238,7 +238,7 @@ router.get('/channel/:userId', (req, res) => {
@@ -238,7 +238,7 @@ router.get('/channel/:userId', (req, res) => {
id: `custom-${e.id}`,
id: `custom-${e.id}`,
emote_id: e.id,
emote_id: e.id,
code: e.code,
code: e.code,
- url: `/api/emotes/file/${path.basename(e.url)}`,
+ url: e.media_url || `/api/emotes/file/${path.basename(e.url)}`,
animated: !!e.animated,
animated: !!e.animated,
width: e.width,
width: e.width,
height: e.height,
height: e.height,
@@ -259,7 +259,7 @@ router.get('/mine', requireAuth, (req, res) => {
@@ -259,7 +259,7 @@ router.get('/mine', requireAuth, (req, res) => {
const emotes = db.getEmotesByUser(req.user.id).map(e => ({
const emotes = db.getEmotesByUser(req.user.id).map(e => ({
id: e.id,
id: e.id,
code: e.code,
code: e.code,
- url: `/api/emotes/file/${path.basename(e.url)}`,
+ url: e.media_url || `/api/emotes/file/${path.basename(e.url)}`,
animated: !!e.animated,
animated: !!e.animated,
width: e.width,
width: e.width,
height: e.height,
height: e.height,
@@ -378,6 +378,7 @@ router.post('/', requireAuth, emoteUpload.single('image'), (req, res) => {
@@ -378,6 +378,7 @@ router.post('/', requireAuth, emoteUpload.single('image'), (req, res) => {
try { require('../chat/chat-server').broadcastToOwnerStreams(channelOwnerId, { type: 'emotes-updated' }); } catch { /* */ }
try { require('../chat/chat-server').broadcastToOwnerStreams(channelOwnerId, { type: 'emotes-updated' }); } catch { /* */ }
}
}
+ try { require('../media-proxy/asset-sync').syncSoon(); } catch { /* mirror is best-effort */ }
res.json({
res.json({
emote: {
emote: {
id: result.lastInsertRowid,
id: result.lastInsertRowid,
@@ -490,6 +491,7 @@ router.delete('/:id', requireAuth, (req, res) => {
@@ -490,6 +491,7 @@ router.delete('/:id', requireAuth, (req, res) => {
}
}
db.deleteEmote(req.params.id);
db.deleteEmote(req.params.id);
+ try { require('../media-proxy/asset-sync').removeAsset(emote.media_asset_id); } catch { /* */ }
if (emote.channel_owner_id) {
if (emote.channel_owner_id) {
try { require('../chat/chat-server').broadcastToOwnerStreams(emote.channel_owner_id, { type: 'emotes-updated' }); } catch { /* */ }
try { require('../chat/chat-server').broadcastToOwnerStreams(emote.channel_owner_id, { type: 'emotes-updated' }); } catch { /* */ }
}
}
@@ -686,7 +688,7 @@ router.get('/all/:streamId', optionalAuth, async (req, res) => {
@@ -686,7 +688,7 @@ router.get('/all/:streamId', optionalAuth, async (req, res) => {
globalCustom = db.getGlobalEmotes().map(e => ({
globalCustom = db.getGlobalEmotes().map(e => ({
id: `custom-${e.id}`,
id: `custom-${e.id}`,
code: e.code,
code: e.code,
- url: `/api/emotes/file/${path.basename(e.url)}`,
+ url: e.media_url || `/api/emotes/file/${path.basename(e.url)}`,
animated: !!e.animated,
animated: !!e.animated,
width: e.width,
width: e.width,
height: e.height,
height: e.height,
@@ -700,7 +702,7 @@ router.get('/all/:streamId', optionalAuth, async (req, res) => {
@@ -700,7 +702,7 @@ router.get('/all/:streamId', optionalAuth, async (req, res) => {
id: `custom-${e.id}`,
id: `custom-${e.id}`,
emote_id: e.id,
emote_id: e.id,
code: e.code,
code: e.code,
- url: `/api/emotes/file/${path.basename(e.url)}`,
+ url: e.media_url || `/api/emotes/file/${path.basename(e.url)}`,
animated: !!e.animated,
animated: !!e.animated,
width: e.width,
width: e.width,
height: e.height,
height: e.height,
@@ -943,6 +943,7 @@ async function start() {
@@ -943,6 +943,7 @@ async function start() {
// Announce newly-created clips in the source channel's chat. Scheduling comes
// Announce newly-created clips in the source channel's chat. Scheduling comes
// from the OpenVibe.Media clip.ready webhook; the sweeper fires the message.
// from the OpenVibe.Media clip.ready webhook; the sweeper fires the message.
try { require('./media-proxy/clip-notify').startClipNotifySweeper(); } catch (e) { console.warn('[ClipNotify] not started:', e.message); }
try { require('./media-proxy/clip-notify').startClipNotifySweeper(); } catch (e) { console.warn('[ClipNotify] not started:', e.message); }
+ try { require('./media-proxy/asset-sync').start(); } catch (e) { console.warn('[AssetSync] not started:', e.message); }
// Feed connected streamers' viewer counts into PowerChat overlays (platform mode).
// Feed connected streamers' viewer counts into PowerChat overlays (platform mode).
try { require('./integrations/powerchat-platform').startViewerCountSweeper(); } catch (e) { console.warn('[PowerChat] viewer sweeper not started:', e.message); }
try { require('./integrations/powerchat-platform').startViewerCountSweeper(); } catch (e) { console.warn('[PowerChat] viewer sweeper not started:', e.message); }
// PowerChat: prune the webhook-dedupe log daily so it can't grow unbounded.
// PowerChat: prune the webhook-dedupe log daily so it can't grow unbounded.
new file mode 100644
new file mode 100644
@@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
+/**
+ * asset-sync.js — mirror Live's chat assets (emotes + channel sounds) into
+ * OpenVibe.Media, their canonical public home (served at /a/:id, browsable on
+ * the media index with uploader + channel attribution).
+ *
+ * Local files remain the low-latency working copy (chat playback reads disk);
+ * this job uploads anything Media doesn't have yet and records media_url /
+ * media_asset_id on the local row, so emote <img> URLs come from Media's
+ * long-cached endpoint and deletes propagate. Idempotent: rows are skipped
+ * once media_asset_id is set, and Media upserts on (kind, name, channel).
+ */
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const db = require('../db/database');
+const media = require('../media-client');
+
+const EMOTE_DIR = path.resolve(process.env.EMOTES_PATH || './data/emotes');
+const SOUND_DIR = path.resolve(process.env.SOUNDS_PATH || './data/sounds');
+const MIME_BY_EXT = {
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
+ '.webp': 'image/webp', '.avif': 'image/avif',
+ '.mp3': 'audio/mpeg', '.ogg': 'audio/ogg', '.wav': 'audio/wav', '.m4a': 'audio/mp4', '.webm': 'audio/webm',
+};
+
+function _ensureColumns() {
+ for (const [table, col, type] of [
+ ['emotes', 'media_url', 'TEXT'], ['emotes', 'media_asset_id', 'INTEGER'],
+ ['channel_sounds', 'media_url', 'TEXT'], ['channel_sounds', 'media_asset_id', 'INTEGER'],
+ ]) {
+ try { db.getDb().exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${type}`); } catch { /* exists */ }
+ }
+}
+
+function _localFile(dir, urlOrPath) {
+ if (!urlOrPath) return null;
+ if (path.isAbsolute(urlOrPath) && fs.existsSync(urlOrPath)) return urlOrPath;
+ const p = path.join(dir, path.basename(urlOrPath));
+ return fs.existsSync(p) ? p : null;
+}
+
+async function _upload({ kind, name, filePath, user_id, username, channel_username, duration_seconds, meta }) {
+ const ext = path.extname(filePath).toLowerCase();
+ const fd = media._formData(
+ { kind, name, user_id, username, channel_username, duration_seconds, meta },
+ { buffer: fs.readFileSync(filePath), filename: path.basename(filePath), contentType: MIME_BY_EXT[ext] || 'application/octet-stream' },
+ 'file'
+ );
+ const out = await media.request('POST', '/assets', { body: fd, timeoutMs: 60000 });
+ return out && out.asset;
+}
+
+let _running = false;
+async function syncAll() {
+ if (_running) return;
+ _running = true;
+ try {
+ _ensureColumns();
+ const nameCache = {};
+ const uname = (id) => {
+ if (!id) return '';
+ if (!(id in nameCache)) { const u = db.getUserById(id); nameCache[id] = (u && u.username) || ''; }
+ return nameCache[id];
+ };
+ let synced = 0, failed = 0;
+
+ for (const e of db.all('SELECT * FROM emotes WHERE media_asset_id IS NULL')) {
+ const f = _localFile(EMOTE_DIR, e.url);
+ if (!f) continue;
+ try {
+ const asset = await _upload({
+ kind: 'emote', name: e.code, filePath: f,
+ user_id: e.user_id, username: uname(e.user_id),
+ channel_username: uname(e.channel_owner_id || e.user_id),
+ meta: { animated: !!e.animated, is_global: !!e.is_global },
+ });
+ if (asset) { db.run('UPDATE emotes SET media_url = ?, media_asset_id = ? WHERE id = ?', [asset.url, asset.id, e.id]); synced++; }
+ } catch (err) { failed++; if (failed <= 3) console.warn('[AssetSync] emote', e.code, err.message); }
+ }
+
+ for (const s of db.all('SELECT * FROM channel_sounds WHERE media_asset_id IS NULL')) {
+ const f = _localFile(SOUND_DIR, s.url);
+ if (!f) continue;
+ try {
+ const asset = await _upload({
+ kind: 'sound', name: s.command, filePath: f,
+ user_id: s.created_by, username: s.created_by_name || uname(s.created_by),
+ channel_username: uname(s.channel_owner_id),
+ duration_seconds: s.duration_seconds || 0,
+ });
+ if (asset) { db.run('UPDATE channel_sounds SET media_url = ?, media_asset_id = ? WHERE id = ?', [asset.url, asset.id, s.id]); synced++; }
+ } catch (err) { failed++; if (failed <= 3) console.warn('[AssetSync] sound', s.command, err.message); }
+ }
+
+ if (synced || failed) console.log(`[AssetSync] Synced ${synced} chat assets to Media${failed ? ` (${failed} failed — will retry next pass)` : ''}`);
+ } finally {
+ _running = false;
+ }
+}
+
+let _debounce = null;
+/** Debounced sync — call after any emote/sound upload. */
+function syncSoon() {
+ clearTimeout(_debounce);
+ _debounce = setTimeout(() => syncAll().catch((e) => console.warn('[AssetSync]', e.message)), 5000);
+ if (_debounce.unref) _debounce.unref();
+}
+
+/** Best-effort removal of the Media copy when the local asset is deleted. */
+function removeAsset(mediaAssetId) {
+ if (!mediaAssetId) return;
+ media.request('DELETE', `/assets/${mediaAssetId}`).catch(() => {});
+}
+
+function start() {
+ const boot = setTimeout(() => syncAll().catch((e) => console.warn('[AssetSync]', e.message)), 20 * 1000);
+ if (boot.unref) boot.unref();
+ const timer = setInterval(() => syncAll().catch(() => {}), 6 * 60 * 60 * 1000);
+ if (timer.unref) timer.unref();
+}
+
+module.exports = { start, syncAll, syncSoon, removeAsset };
@@ -48,6 +48,13 @@ function withUserFields(clip) {
@@ -48,6 +48,13 @@ function withUserFields(clip) {
// Absolutize Media-relative URLs so the SPA doesn't resolve them against Live.
// Absolutize Media-relative URLs so the SPA doesn't resolve them against Live.
clip.playback_url = media.publicUrl(clip.playback_url) || media.clipUrl(clip.id);
clip.playback_url = media.publicUrl(clip.playback_url) || media.clipUrl(clip.id);
if (clip.thumbnail_url) clip.thumbnail_url = media.publicUrl(clip.thumbnail_url);
if (clip.thumbnail_url) clip.thumbnail_url = media.publicUrl(clip.thumbnail_url);
+ // Short AI overview is Live-owned (clip_ai_state) — overlay for the cards.
+ if (clip.id != null && !clip.ai_overview_short) {
+ try {
+ const s = db.get('SELECT ai_overview_short FROM clip_ai_state WHERE clip_id = ?', [clip.id]);
+ if (s && s.ai_overview_short) clip.ai_overview_short = s.ai_overview_short;
+ } catch { /* best-effort */ }
+ }
return clip;
return clip;
}
}
@@ -93,7 +93,39 @@ router.post('/:slug/set-avatar', requireAuth, async (req, res) => {
@@ -93,7 +93,39 @@ router.post('/:slug/set-avatar', requireAuth, async (req, res) => {
});
});
// ── Everything else: transparent passthrough ─────────────────
// ── Everything else: transparent passthrough ─────────────────
-router.get('/', forward(''));
+// Media only stores our numeric user ids — resolve author names locally so
+// cards don't all read "Anonymous", and translate ?username= into user_id.
+function _nameUsers(rows) {
+ for (const p of rows || []) {
+ if (p && p.user_id != null && !p.username) {
+ const u = db.getUserById(p.user_id);
+ if (u) { p.username = u.username; p.display_name = u.display_name; p.profile_color = u.profile_color; p.avatar_url = u.avatar_url; }
+ }
+ }
+ return rows;
+}
+function forwardEnriched(subPath, pick) {
+ return async (req, res) => {
+ try {
+ const query = { ...req.query };
+ if (query.username && query.username !== 'all') {
+ const u = db.getUserByUsername(query.username);
+ delete query.username;
+ if (!u) return res.json({ pastes: [], total: 0, limit: 0, offset: 0, hasMore: false });
+ query.user_id = u.id;
+ }
+ const p = typeof subPath === 'function' ? subPath(req) : subPath;
+ const out = await media.request('GET', `/pastes${p}`, { query, userToken: media.userTokenFrom(req) });
+ _nameUsers(pick(out));
+ res.json(out);
+ } catch (err) {
+ if (err && err.name === 'MediaApiError' && err.status) return res.status(err.status).json(err.body || { error: err.message });
+ console.warn('[Pastes proxy]', err.message);
+ res.status(502).json({ error: 'Media service unavailable' });
+ }
+ };
+}
+router.get('/', forwardEnriched('', (o) => o?.pastes));
router.post('/', forward(''));
router.post('/', forward(''));
router.get('/config', forward('/config'));
router.get('/config', forward('/config'));
router.get('/admin/stats', requireAdmin, forwardAsApp('/admin/stats'));
router.get('/admin/stats', requireAdmin, forwardAsApp('/admin/stats'));
@@ -124,13 +156,13 @@ router.get('/by-user/:username', async (req, res) => {
@@ -124,13 +156,13 @@ router.get('/by-user/:username', async (req, res) => {
.filter(p => !seen.has(p.slug) && seen.add(p.slug))
.filter(p => !seen.has(p.slug) && seen.add(p.slug))
.sort((a, b) => (sort === 'oldest' ? 1 : -1) * (new Date(a.created_at) - new Date(b.created_at)))
.sort((a, b) => (sort === 'oldest' ? 1 : -1) * (new Date(a.created_at) - new Date(b.created_at)))
.slice(0, limit);
.slice(0, limit);
- res.json({ pastes, total: pastes.length, username: user.username });
+ res.json({ pastes: _nameUsers(pastes), total: pastes.length, username: user.username });
} catch (err) {
} catch (err) {
console.warn('[Pastes proxy] by-user:', err.message);
console.warn('[Pastes proxy] by-user:', err.message);
res.status(502).json({ error: 'Media service unavailable' });
res.status(502).json({ error: 'Media service unavailable' });
}
}
});
});
-router.get('/:slug', forward(slugPath()));
+router.get('/:slug', forwardEnriched(slugPath(), (o) => (o?.paste ? [o.paste] : [o])));
router.put('/:slug', requireAuth, forward(slugPath()));
router.put('/:slug', requireAuth, forward(slugPath()));
router.delete('/:slug', requireAuth, forward(slugPath()));
router.delete('/:slug', requireAuth, forward(slugPath()));
// TODO(contract): paste admin tools (censor, admin/stats, admin/forks, bulk)
// TODO(contract): paste admin tools (censor, admin/stats, admin/forks, bulk)
@@ -158,7 +190,7 @@ router.post('/:slug/fork', forward(slugPath('/fork')));
@@ -158,7 +190,7 @@ router.post('/:slug/fork', forward(slugPath('/fork')));
router.get('/:slug/raw', (req, res) => res.redirect(302, media.pasteRawUrl(req.params.slug)));
router.get('/:slug/raw', (req, res) => res.redirect(302, media.pasteRawUrl(req.params.slug)));
router.post('/:slug/like', requireAuth, forward(slugPath('/like')));
router.post('/:slug/like', requireAuth, forward(slugPath('/like')));
router.post('/:slug/copy', forward(slugPath('/copy')));
router.post('/:slug/copy', forward(slugPath('/copy')));
-router.get('/:slug/comments', forward(slugPath('/comments')));
+router.get('/:slug/comments', forwardEnriched(slugPath('/comments'), (o) => o?.comments));
router.post('/:slug/comments', forward(slugPath('/comments')));
router.post('/:slug/comments', forward(slugPath('/comments')));
router.delete('/:slug/comments/:commentId', forward((req) => `/${encodeURIComponent(req.params.slug)}/comments/${encodeURIComponent(req.params.commentId)}`));
router.delete('/:slug/comments/:commentId', forward((req) => `/${encodeURIComponent(req.params.slug)}/comments/${encodeURIComponent(req.params.commentId)}`));
@@ -97,6 +97,13 @@ function withUserFields(row) {
@@ -97,6 +97,13 @@ function withUserFields(row) {
// so the SPA doesn't resolve them against Live's origin.
// so the SPA doesn't resolve them against Live's origin.
row.playback_url = media.publicUrl(row.playback_url) || media.vodPlaybackUrl(row.id);
row.playback_url = media.publicUrl(row.playback_url) || media.vodPlaybackUrl(row.id);
if (row.thumbnail_url) row.thumbnail_url = media.publicUrl(row.thumbnail_url);
if (row.thumbnail_url) row.thumbnail_url = media.publicUrl(row.thumbnail_url);
+ // Short AI overview is Live-owned (vod_ai_state) — overlay for the cards.
+ if (row.id != null && !row.ai_overview_short) {
+ try {
+ const s = db.get('SELECT ai_overview_short FROM vod_ai_state WHERE vod_id = ?', [row.id]);
+ if (s && s.ai_overview_short) row.ai_overview_short = s.ai_overview_short;
+ } catch { /* best-effort */ }
+ }
return row;
return row;
}
}
@@ -330,6 +330,19 @@ router.get('/channel/:username', optionalAuth, async (req, res) => {
@@ -330,6 +330,19 @@ router.get('/channel/:username', optionalAuth, async (req, res) => {
};
};
clips.forEach(nameClip);
clips.forEach(nameClip);
clipsOfStreams.forEach(nameClip);
clipsOfStreams.forEach(nameClip);
+ // Short AI overviews are Live-owned (vod_ai_state / clip_ai_state).
+ const aiShort = (rows, table, col) => {
+ for (const r of rows) {
+ if (!r || r.id == null || r.ai_overview_short) continue;
+ try {
+ const s = db.get(`SELECT ai_overview_short FROM ${table} WHERE ${col} = ?`, [r.id]);
+ if (s && s.ai_overview_short) r.ai_overview_short = s.ai_overview_short;
+ } catch { /* best-effort */ }
+ }
+ };
+ aiShort(vods, 'vod_ai_state', 'vod_id');
+ aiShort(clips, 'clip_ai_state', 'clip_id');
+ aiShort(clipsOfStreams, 'clip_ai_state', 'clip_id');
}
}
const followerCount = db.getFollowerCount(channel.user_id);
const followerCount = db.getFollowerCount(channel.user_id);
const isFollowing = req.user ? db.isFollowing(req.user.id, channel.user_id) : false;
const isFollowing = req.user ? db.isFollowing(req.user.id, channel.user_id) : false;
@@ -1171,6 +1184,18 @@ router.get('/recent-vods', async (req, res) => {
@@ -1171,6 +1184,18 @@ router.get('/recent-vods', async (req, res) => {
const media = require('../media-client');
const media = require('../media-client');
const r = await media.listVods({ limit, offset }).catch(() => null);
const r = await media.listVods({ limit, offset }).catch(() => null);
const vods = r?.vods || (Array.isArray(r) ? r : []);
const vods = r?.vods || (Array.isArray(r) ? r : []);
+ for (const v of vods) {
+ if (v && v.user_id != null && !v.username) {
+ const u = db.getUserById(v.user_id);
+ if (u) { v.username = u.username; v.display_name = u.display_name; v.avatar_url = u.avatar_url; }
+ }
+ if (v && v.id != null && !v.ai_overview_short) {
+ try {
+ const s = db.get('SELECT ai_overview_short FROM vod_ai_state WHERE vod_id = ?', [v.id]);
+ if (s && s.ai_overview_short) v.ai_overview_short = s.ai_overview_short;
+ } catch { /* */ }
+ }
+ }
const total = r?.total ?? vods.length;
const total = r?.total ?? vods.length;
res.json({ vods, total, limit, offset, hasMore: offset + vods.length < total });
res.json({ vods, total, limit, offset, hasMore: offset + vods.length < total });
} catch (err) {
} catch (err) {
@@ -3,12 +3,17 @@
@@ -3,12 +3,17 @@
// Consistent top bar across all services with logo, navigation,
// Consistent top bar across all services with logo, navigation,
// notification bell, account switcher, and theme-aware styling.
// notification bell, account switcher, and theme-aware styling.
// Usage: OpenVibeNavbar.init({ service, token, user, apiBase })
// Usage: OpenVibeNavbar.init({ service, token, user, apiBase })
+// Optional: loginUrl (override the Sign In href), sessionUrl (same-origin
+// endpoint returning { user } — used only when no ov_token exists),
+// onLogin/onLogout callbacks. When no `user` is passed the navbar resolves
+// it itself: ov_token cookie → localStorage → token opt → sessionUrl, then
+// GET {apiBase}/api/auth/me with `Authorization: Bearer <token>`.
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
(function (root) {
(function (root) {
'use strict';
'use strict';
- let _config = { service: 'network', token: null, user: null, apiBase: 'https://openvibe.network', onLogin: null, onLogout: null };
+ let _config = { service: 'network', token: null, user: null, apiBase: 'https://openvibe.network', onLogin: null, onLogout: null, loginUrl: null, sessionUrl: null };
let _navEl = null;
let _navEl = null;
function injectStyles() {
function injectStyles() {
@@ -362,6 +367,9 @@
@@ -362,6 +367,9 @@
],
],
media: [
media: [
{ label: 'Home', href: '/' },
{ label: 'Home', href: '/' },
+ { label: 'Network', href: 'https://openvibe.network' },
+ { label: 'Live', href: 'https://openvibe.live' },
+ { label: 'Tools', href: 'https://openvibe.tools' },
],
],
network: [
network: [
{ label: 'Home', href: '/' },
{ label: 'Home', href: '/' },
@@ -429,6 +437,200 @@
@@ -429,6 +437,200 @@
try { return JSON.parse(localStorage.getItem('openvibe_accounts') || '[]'); } catch { return []; }
try { return JSON.parse(localStorage.getItem('openvibe_accounts') || '[]'); } catch { return []; }
}
}
+ // ─── SSO auth resolution ───────────────────────────────────
+ // Cookie policy (CONTRACTS.md): ov_token is host-only on openvibe.network,
+ // and Domain=.openvibe.tools on the tools apex + every tool subdomain.
+ // The tools gateway (apex) owns the OAuth client: /auth/login, /auth/logout,
+ // /auth/me, POST /auth/refresh. Satellite subdomains (maps., audio., text.,
+ // img., yt., docs., food., …) do NOT mount /auth/* — they only read the
+ // shared cookie — so sign-in/out always goes through the gateway apex.
+ // Detection order: ov_token cookie → localStorage → page-provided token →
+ // app-specific session endpoint (opt-in via config.sessionUrl).
+ const TOOLS_APEX = 'openvibe.tools';
+ const TOKEN_KEY = 'ov_token';
+
+ function currentHost() {
+ return (typeof location !== 'undefined' && location.hostname) || '';
+ }
+
+ function onToolsDomain(host) {
+ const h = host !== undefined ? host : currentHost();
+ return h === TOOLS_APEX || h.endsWith('.' + TOOLS_APEX);
+ }
+
+ function onNetworkDomain(host) {
+ const h = host !== undefined ? host : currentHost();
+ try { return h !== '' && h === new URL(_config.apiBase).hostname; } catch { return false; }
+ }
+
+ function parseCookieToken(cookieStr) {
+ const m = String(cookieStr || '').match(/(?:^|;\s*)ov_token=([^;]*)/);
+ if (!m || !m[1]) return null;
+ try { return decodeURIComponent(m[1]); } catch { return m[1]; }
+ }
+
+ function getCookieToken() {
+ if (typeof document === 'undefined') return null;
+ return parseCookieToken(document.cookie);
+ }
+
+ function getStoredToken() {
+ try { return localStorage.getItem(TOKEN_KEY) || null; } catch { return null; }
+ }
+
+ function resolveToken() {
+ return getCookieToken() || getStoredToken() || _config.token || null;
+ }
+
+ /** Where "Sign In" goes on this host. */
+ function resolveLoginHref(host, returnUrl) {
+ if (_config.loginUrl) return _config.loginUrl;
+ if (onToolsDomain(host)) {
+ // Every *.openvibe.tools host signs in through the gateway apex: it
+ // holds the OAuth state cookie + redirect_uri (both apex-host-only)
+ // and sets ov_token with Domain=.openvibe.tools, so the session
+ // reaches all satellites. A host-local /auth/login would 404 on
+ // satellites and break the OAuth state check on gateway subdomains.
+ return `https://${TOOLS_APEX}/auth/login?next=${encodeURIComponent(returnUrl)}`;
+ }
+ if (onNetworkDomain(host)) return `/login?return=${encodeURIComponent(returnUrl)}`;
+ return `${_config.apiBase}/login?return=${encodeURIComponent(returnUrl)}`;
+ }
+
+ function setAuthCookie(token) {
+ if (typeof document === 'undefined') return;
+ const maxAge = 24 * 60 * 60; // matches the 24h access JWT
+ if (onToolsDomain()) {
+ document.cookie = `ov_token=${token};path=/;max-age=${maxAge};domain=.${TOOLS_APEX};SameSite=Lax;Secure`;
+ } else {
+ const secure = (typeof location !== 'undefined' && location.protocol === 'https:') ? ';Secure' : '';
+ document.cookie = `ov_token=${token};path=/;max-age=${maxAge};SameSite=Lax${secure}`;
+ }
+ }
+
+ function clearAuthState() {
+ if (typeof document !== 'undefined') {
+ document.cookie = 'ov_token=;path=/;max-age=0;SameSite=Lax';
+ if (onToolsDomain()) {
+ document.cookie = `ov_token=;path=/;max-age=0;domain=.${TOOLS_APEX};SameSite=Lax`;
+ }
+ }
+ try {
+ localStorage.removeItem(TOKEN_KEY);
+ localStorage.removeItem('openvibe_anon_token');
+ localStorage.removeItem('openvibe_active_account');
+ } catch { /* storage unavailable */ }
+ }
+
+ function persistToken(token) {
+ try { localStorage.setItem(TOKEN_KEY, token); } catch { /* storage unavailable */ }
+ setAuthCookie(token);
+ }
+
+ /** GET {apiBase}/api/auth/me with the Bearer token. */
+ async function fetchMe(token) {
+ try {
+ const res = await fetch(`${_config.apiBase}/api/auth/me`, {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ if (res.ok) {
+ const data = await res.json();
+ return { ok: true, user: (data && (data.user || data)) || null };
+ }
+ return { ok: false, unauthorized: res.status === 401 || res.status === 403 };
+ } catch {
+ return { ok: false, unauthorized: false };
+ }
+ }
+
+ /** Same-origin session refresh — mounted by the tools gateway (POST /auth/refresh). */
+ async function gatewayRefresh() {
+ try {
+ const res = await fetch('/auth/refresh', { method: 'POST', credentials: 'same-origin' });
+ if (!res.ok) return null;
+ const data = await res.json();
+ return data && data.token ? data : null;
+ } catch { return null; }
+ }
+
+ /** Network-side refresh — accepts tokens expired up to 7 days ago (grace). */
+ async function networkRefresh(token) {
+ try {
+ const res = await fetch(`${_config.apiBase}/api/auth/refresh`, {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ if (!res.ok) return null;
+ const data = await res.json();
+ return data && data.token ? data : null;
+ } catch { return null; }
+ }
+
+ /**
+ * Resolve the signed-in user from the shared SSO state.
+ * With a token: validate it against the Network. If it is rejected, make
+ * ONE refresh attempt (same-origin gateway refresh first on tools hosts,
+ * then the Network's grace refresh) before giving up — never leave a
+ * stale "Sign In" when a recoverable session exists.
+ */
+ async function resolveSessionUser() {
+ const token = resolveToken();
+ if (token) {
+ const me = await fetchMe(token);
+ if (me.ok && me.user) return { user: me.user, token };
+ if (me.unauthorized) {
+ if (onToolsDomain()) {
+ const r = await gatewayRefresh();
+ if (r) {
+ const user = r.user || (await fetchMe(r.token)).user;
+ if (user) { persistToken(r.token); return { user, token: r.token }; }
+ }
+ }
+ const n = await networkRefresh(token);
+ if (n) {
+ const user = n.user || (await fetchMe(n.token)).user;
+ if (user) { persistToken(n.token); return { user, token: n.token }; }
+ }
+ }
+ return null;
+ }
+ // No token anywhere — ask the page's own session endpoint if it has one
+ if (_config.sessionUrl) {
+ try {
+ const res = await fetch(_config.sessionUrl, { credentials: 'include' });
+ if (res.ok) {
+ const data = await res.json();
+ const user = (data && (data.user || data)) || null;
+ if (user && (user.username || user.id)) return { user, token: null };
+ }
+ } catch { /* signed out */ }
+ }
+ return null;
+ }
+
+ let _authInFlight = null;
+
+ function refreshAuthState() {
+ if (_authInFlight) return _authInFlight;
+ _authInFlight = resolveSessionUser()
+ .then((session) => {
+ _authInFlight = null;
+ if (session && session.user) {
+ _config.user = session.user;
+ if (session.token) _config.token = session.token;
+ render();
+ }
+ try {
+ document.dispatchEvent(new CustomEvent('openvibe-navbar-auth', {
+ detail: { user: session ? session.user : null, token: session ? session.token : null },
+ }));
+ } catch { /* non-DOM environment */ }
+ return session ? session.user : null;
+ })
+ .catch(() => { _authInFlight = null; return null; });
+ return _authInFlight;
+ }
+
function escapeAttr(value) {
function escapeAttr(value) {
return String(value ?? '').replace(/&/g, '&').replace(/"/g, '"');
return String(value ?? '').replace(/&/g, '&').replace(/"/g, '"');
}
}
@@ -497,8 +699,8 @@
@@ -497,8 +699,8 @@
const u = _config.user;
const u = _config.user;
const accounts = getAccounts();
const accounts = getAccounts();
const isAnon = u && u.is_anon;
const isAnon = u && u.is_anon;
- const loginHref = `https://openvibe.network/login?return=${encodeURIComponent(window.location.href)}`;
- const addAccountHref = `https://openvibe.network/login?add_account=1&return=${encodeURIComponent(window.location.href)}`;
+ const loginHref = resolveLoginHref(currentHost(), window.location.href);
+ const addAccountHref = `${_config.apiBase}/login?add_account=1&return=${encodeURIComponent(window.location.href)}`;
nav.innerHTML = `
nav.innerHTML = `
<a class="openvibe-navbar-brand" href="${brandHref}">
<a class="openvibe-navbar-brand" href="${brandHref}">
@@ -588,12 +790,16 @@
@@ -588,12 +790,16 @@
dropdown.classList.remove('open');
dropdown.classList.remove('open');
if (_config.onLogout) _config.onLogout();
if (_config.onLogout) _config.onLogout();
else {
else {
- document.cookie = 'ov_token=;path=/;max-age=0';
- document.cookie = 'ov_token=;path=/;max-age=0;domain=.openvibe.tools';
- localStorage.removeItem('ov_token');
- localStorage.removeItem('openvibe_anon_token');
- localStorage.removeItem('openvibe_active_account');
- window.location.reload();
+ clearAuthState();
+ _config.user = null;
+ _config.token = null;
+ if (onToolsDomain()) {
+ // The gateway also clears the Domain=.openvibe.tools cookie
+ // and the httpOnly refresh cookie, then sends us back here.
+ window.location.href = `https://${TOOLS_APEX}/auth/logout?next=${encodeURIComponent(window.location.href)}`;
+ } else {
+ window.location.reload();
+ }
}
}
});
});
} else {
} else {
@@ -621,9 +827,19 @@
@@ -621,9 +827,19 @@
init(opts = {}) {
init(opts = {}) {
Object.assign(_config, opts);
Object.assign(_config, opts);
injectStyles();
injectStyles();
- return render();
+ const el = render();
+ // Pages that hand us a resolved user (openvibe.network, the tools
+ // gateway hub) keep full control. Everyone else — pages that pass
+ // only a token, or nothing at all — gets the user resolved from
+ // the shared SSO state (ov_token cookie / localStorage / optional
+ // sessionUrl) and the navbar re-renders when it arrives.
+ if (!_config.user) refreshAuthState();
+ return el;
},
},
+ /** Re-resolve the signed-in user from the shared SSO state. */
+ refreshAuth() { return refreshAuthState(); },
+
/** Update user (after account switch). */
/** Update user (after account switch). */
setUser(user) {
setUser(user) {
_config.user = user;
_config.user = user;
@@ -643,6 +859,18 @@
@@ -643,6 +859,18 @@
_navEl?.remove();
_navEl?.remove();
_navEl = null;
_navEl = null;
},
},
+
+ /** Internal auth helpers — exposed for tests, not a public API. */
+ _auth: {
+ parseCookieToken,
+ onToolsDomain,
+ onNetworkDomain,
+ resolveLoginHref,
+ resolveToken,
+ resolveSessionUser,
+ clearAuthState,
+ persistToken,
+ },
};
};
if (typeof module !== 'undefined' && module.exports) module.exports = OpenVibeNavbar;
if (typeof module !== 'undefined' && module.exports) module.exports = OpenVibeNavbar;