Skip to content
-
Server-rendered channel, VOD and clip pages for every client (roadmap 32.1/32.2): the SEO middleware no longer depends on the Accept header (a crawler or tool sending */* or none got the home page's title and canonical for /@finditfixit), and each page gets its own head and a real body. /@user renders for an account with no channel row too, names the channel's own spelling as canonical, lists its videos with ?page=N pages (rel prev/next, self-canonical; past the end is noindex with the canonical on page 1), the clips people took and then the AI Moments, labelled; it is cached a minute since it says who is live. /vod/:id lists its clips, people's then the AI's, carries them as schema.org Clip parts (hasPart, /vod/<id>?t=<s>) and breadcrumbs through the channel; /clip/:id links the moment in the full stream and the channel; names come from Live's accounts (Media rows carry ids only). Without JavaScript the body is shown with the site's links and the empty app shell hidden; with it the route boots and hydrates as before. The SPA fallback's shell comes from seo.shellHtml: a 404 is 'Page not found', noindex, with no canonical, OG or JSON-LD from the home page, and no other page claims the home canonical (a channel slot names /@user). Item fetches wait at most 2 s and share one upstream budget with page-status (req.ovLookupDeadlineAt), and what a fresh fetch learned primes page-status, so a missing or private item still costs one Media call; lists wait 1.5 s and a page rendered without one is cached 30 s. The home rails load in parallel under the same limit. test/seo-ssr.test.js; test/page-status.test.js uses the same shell as server/index.js and checks the 404 head.OpenVibers · 24 Sept 2026
517fbcc2
-
Go Live needs an account, and a guest is told so: anonymous /broadcast shows a sign-in gate (Sign in with OpenVibe, back to /broadcast) in place of the slot workspace, and loadBroadcastPage returns before any slot, restream or RobotStreamer request. The page used to render the workspace for a guest, swallow the 401s and offer "Create stream slot"; the create-slot entry point now refuses a guest too. test/broadcast-signin-gate.test.js runs the page's own functions as a guest (only the gate and showStreamManager are reached) and signed in, checks the markup and styles, and checks the server still answers 401 to slot and stream creation without a session and writes nothing; the browser smoke checks the gate at 412px, direct and by SPA navigation, with no writes sentOpenVibers · 24 Sept 2026
ae484288
-
One canonical page per paste (roadmap 32.2): the PASTES_ON_COMMUNITY /p/<slug> handover moves to server/web/paste-handover.js and is mounted before the SEO middleware, so crawlers and browsers get the same 301 to openvibe.community (302 through its silent sign-in when signed in) instead of Live's own self-canonical copy for every HTML client. The SEO middleware also skips /p/ while pastes are on Community, in case the mount order ever changes. Live's sitemap lists no /p/ URL (Community's sitemap holds them), and the paste links Live renders on its home, /pastes and /content snapshots point straight at Community. test/paste-canonical.test.jsOpenVibers · 24 Sept 2026
57a83a27
-
RobotStreamer is per stream slot only: a stream uses its slot's robotstreamer_integrations row and nothing else. getRobotStreamerIntegrationForStream no longer falls back to the account-level row (which sent every slot without its own row to the same robot); an unmigrated account-level row is logged once per user and skipped, never used and never a crash. validate, login, save and remove answer 400 without managed_stream_id (validate no longer borrows the account token), upserts refuse to write an account-level row, refreshIntegration has no account path, and GET without a slot answers only the server passthrough capability with exists:false. The broadcast page's RobotStreamer settings and viewer poll work on the page's slot, a stream without its slot's config starts no browser publish, and the slot workspace no longer offers to reuse an account token. Run scripts/rs-integrations-to-slots.js on production before deploying this. test/rs-slot-only.test.jsOpenVibers · 24 Sept 2026
155f68ef
-
scripts/rs-integrations-to-slots.js: bind account-level RobotStreamer rows to stream slots, for the lead to run on production before Live stops reading them (5 of 8 rows have no slot). Dry run by default. A row goes to the user's only slot, or to the one slot with RobotStreamer on it (its own row for the same robot, or mirrored RobotStreamer chat on its streams while it had no row); a row whose slot already has its own row is superseded and left alone (--drop-superseded deletes it); anything else is reported as ambiguous with each slot's last live time and RobotStreamer chat count, and left alone until --assign <row>=<slot> decides it. --apply requires --backup <file> (an online SQLite backup) and writes <file>.journal.json before changing anything, in one transaction; --rollback <journal> undoes exactly what it recorded. Tokens are never printed. test/rs-integrations-to-slots.test.js covers the plan, apply, refusals and rollbackOpenVibers · 24 Sept 2026
3d990d2a
-
AI clips are never filed as something a person made (roadmap 33.4/33.6/33.7). The auto-clip job cuts in the streamer's name, so every 'clips by user X' list showed the AI's clips as the streamer's own: the clip page now says 'AI clip · from <streamer>'s stream' with what made it (and 'Clipped by <clipper>' only for a person's clip, built with DOM nodes); GET /api/clips/mine asks Media for auto_generated=0 whatever the query says and drops AI rows an old Media still returns; /api/clips/my-stream takes ?auto_generated=0|1 and labels AI rows; the channel endpoint returns people's clips (by and of the streamer) apart from a new aiClips list (aiClipsTotal/Limit/Offset, ai_label, the streamer as source) and Clips Taken asks for people's clips. The channel Clips tab lists people's clips, then an AI Moments section with 'AI clip' cards and its own pagination; the dashboard's Clips of My Stream shows people's clips and a new AI Moments sub-tab lists the auto-clips of my stream with a count. test/clip-attribution.test.jsOpenVibers · 24 Sept 2026
95a21d82
-
AI Moments pages are labelled, credited to no person and kept out of search (roadmap 33.4/33.8): an auto-clip (Media auto_generated), an AI moment paste (Community origin ai) and an AI-written recap render noindex,follow with the canonical on the source VOD at the moment (/vod/<id>?t=<s>) when that VOD is known and public, og:url on the item itself, JSON-LD without author/creator/Person (keywords AI-generated, isBasedOn the source), an 'AI clip · from X's stream' / 'AI note · from <stream>' / 'AI recap' byline, a workflow disclosure and a link to the moment, and breadcrumbs under AI Moments. People's clips and pastes keep index,follow and their author (a clip's clipper now resolves from Live's accounts; Media rows carry ids only). Template recaps stay indexable without a Person author. The sitemap asks Media for auto_generated=0 and Community for origin=user and drops any AI row an upstream still returns. test/seo-moments.test.jsOpenVibers · 24 Sept 2026
30b1234e
-
Restream control is slot-only: a stream on a slot auto-starts and resumes only that slot's destinations, and a legacy stream with no slot only the owner's unbound ones. _getDestinationsForStream merged every destination the account owned into a slotted stream, so a streamer with auto-start destinations on one slot (user 1: slot 1 of slots 1/60/85) would start them when going live on another. The display paths follow the same rule: GET /api/restream/destinations without a slot lists only unbound rows (?all=1 is the explicit whole-account listing), GET /api/restream/viewer-counts takes ?managed_stream_id or counts the slots live now, getExternalViewerCountsForUser(user, null) counts only unbound destinations, a manual start of an unbound destination refuses a slotted stream, and the broadcast restream panel loads and polls its own slot. test/restream-slot-scope.test.js covers auto-start and resume per slot, the slot-less case and the routesOpenVibers · 24 Sept 2026
d378f4f6
-
Channel links carry the @ everywhere: Live's own go-live push (golive-notify.js) and the AI 'caught live' pastes (stream-memory-job.js) still built https://openvibe.live/<username>, which is a 404 by rule. test/channel-links.test.js scans server/ and public/js for a username placed straight after an origin or base-URL expression without the @ (it catches the shapes Live used; the four allowed lines are PowerChat's own tip pages and relay API paths, each with its reason)OpenVibers · 24 Sept 2026
4585f613
-
openvibe-shared v1.5.1 (Track R release manifest), openvibe-contracts v0.33.0, openvibe-sdk v0.5.0OpenVibers · 23 Sept 2026
7ae1b24c
-
gitleaks: allow RFC 6455's sample Sec-WebSocket-Key ('the sample nonce') in test/drill-mode.test.js, matched on path and value; the generic-api-key rule took it for a keyOpenVibers · 23 Sept 2026
2767603d
-
Restore drills: LIVE_DRILL=1 makes Live a read-only instance on a restored copy, so `ovhost drill live` can boot one next to production. It refuses to start unless DB_PATH and DATA_DIR are set and outside the checkout and /opt/openvibe.live, HOST is loopback, PORT is not 3000 and no socket was handed over by systemd, before any file is opened. It then starts only its HTTP server (no jobs, WebSocket servers, RTMP, SFU, JSMPEG, WHIP, TURN credentials, restream or relay resume, AI jobs, Media reconcilers, Events outbox, chat bridge, identity sync, deploy notice, star job or registry refresh), answers 403 to every method but GET/HEAD/OPTIONS and to every WebSocket upgrade, and guards net/fetch/child_process/dgram/listen so nothing leaves the process (other services look down; only git runs). Every file location now comes from server/paths.js (DATA_DIR, default ./data), analytics.db included; module-level timers stay off in a drill. test/drill-mode.test.js boots the real server that way and checks each pointOpenVibers · 23 Sept 2026
8a58aea2
-
Deploy notice: with Live's Events outbox on, OpenVibe.Chat learns a deploy from live.release.deployed alone and the chat-remote deployNotice bridge hop is skipped (compatibility register C-84: the 22:14 UTC deploy produced one card, the event folding onto the bridge's by head commit). The bridge stays only as the fallback while Events publishing is offOpenVibers · 23 Sept 2026
5324f403
-
Merge Content and Moments (feeds at /content and /moments) with the home page polish: home rows link to /content?type=… and /moments in the section-action styleOpenVibers · 23 Sept 2026
32a05fa1
-
Content and Moments: VODs, clips and pastes are one human-made feed at /content (filters All, VODs, Clips, Pastes; New, or Top by views over a week, a month or all time), and everything the AI makes is a separate feed at /moments (auto-clips, AI moment pastes, from VODs and caught live, and AI-written after-show recaps), every card badged AI. /vods, /clips and /pastes are /content with that filter (the address becomes /content?type=…; /pastes?edit=<slug> still opens the editor). The navbar shows Content and Moments instead of VODs, Clips and Pastes; the home page's AI Moments header links to /moments, its Recent VODs/Clips/Pastes "View All" to the filtered feed, and its Recent Clips and Pastes rails list people's onlyOpenVibers · 23 Sept 2026
88f7b97f
-
The daily secret only listens while it is on screen on the home page: its document-wide keydown listener stayed wired after leaving home and took every arrow key (blocking arrow-key scrolling on every page and fighting players' shortcuts), letters and Backspace anywhere outside a text field. Arrows still type into it while its panel is open or focused; letters start it when it is visibleOpenVibers · 23 Sept 2026
e31367c0
-
Home polish from phone feedback: one family of Star card actions, a themed card rail for AI Moments, no tap flash, and 360/400px fixesOpenVibers · 23 Sept 2026
6a301126
-
openvibe-contracts v0.32.0: live.lineage.resolve is written inline in the guard, so the CI contract check enforces it (6 capabilities now); the lineage resolver test requires lineage.resolution@1 and validates every answer; live.stream.started/ended and live.release.deployed payloads are validated against their new contracts (and the started event's channel link is /@<username>). Stale comments saying these were not in the registry are goneOpenVibers · 23 Sept 2026
ca7cc876
-
No redirect from /<username>: a channel is only ever /@<username>, so usernames and page routes never compete for the same paths (a user named like a route, or a route added later, can never change where a link goes). A bare username path is a 404 again. The 677 stored Network notifications that linked to /<username> were rewritten to /@<username> at the source instead (backup taken first)OpenVibers · 23 Sept 2026
2789db40
-
Channel links carry the @: live.stream.started/ended's channel.url (what Network's go-live notifications link to), the Discord go-live post, new-follower and payment notifications, the broadcast page's chat popout and viewer preview (which also linked to /watch/<id>, a page that never existed) and the chat user list (whose click handler called showPage with a path) all pointed at /<username>, which showed the home page. A path that names an existing user without the @ (/<username>[/<slot>], query kept) now answers 301 to /@<username>, so links already sent in notifications and Discord land on the channel; route names are never taken for usersOpenVibers · 23 Sept 2026
4c703326
-
DM notifications link to /?dm=<conversation id>, which the messenger opens once someone is signed in (and drops from the address bar). They used to link to /dm/<id>, a page Live never had: it showed the home page and, since real 404s, Page not found. Links already sent redirect from /dm/<id>OpenVibers · 23 Sept 2026
62261ca8
-
Chat settings: timestamps, compact mode, font size and badges now also live in the OpenVibe.Network user module chat.preferences, through OpenVibe.Chat's /api/chat/preferences. After Live's own copy loads, the Network record wins for those four; with no record yet the browser's values seed it; every save sends them too. The other chat settings stay in /api/auth/preferences until the module schema has fields for themOpenVibers · 23 Sept 2026
49fe04f0
-
Identity: Live asks Network for the subject of every linked account it has no token subject for yet (backfillSubjects, POST /internal/identity/resolve-batch by Network user id, 500 per call), right after the daily legacy-map sync. Only 5 of 328 production links had one, because subject ids were only learned from tokens people presented since Wave 1; module migrations (chat.preferences), stream events, the follower list and the lineage resolver all key on it. An answer is stored only when it names the same Network user id with a well-formed usr_ subject, and never over a subject Live already has. test/identity-sync.test.js covers batching, the skipped rows, mismatched and malformed answers, the rerun and a Network without the endpointOpenVibers · 23 Sept 2026
cff1daa1
-
Soft 404s end: a page path that names nothing answers 404 with the SPA shell instead of 200 (requirement ledger D43-D46). The fallback in server/index.js is now server/web/page-status.js spaFallback, which asks statusFor(): / and /vods /clips /pastes /updates /documentation /settings /admin /themes (exact path), /dashboard/* /broadcast/* /chat/* /arena/* (any depth, they route sub-paths themselves), /@user and /@user/<slot> for a user that exists (the client's name rule, case-insensitive), /vod/:id and /clip/:id for a Media item this visitor may see (a private one is a 404 to everyone but its owners and staff, by the same rule in server/media-proxy/access.js that already answers the API), /p/:slug for a paste that exists, and /recap/:id and /stream/:id for a stream that exists stay 200; everything else is 404 (typos, /search, /login, /vods/extra, /@nobody, /vod/999, /p/missing, /wp-login.php). Server routes (API, docs, overlays, popouts, legal pages, static files, the SEO-rendered pages) answer before it and are unchanged; /api/* keeps its JSON 404. Cost: the fixed routes are a table lookup and never wait; users and streams are one SQLite read; Media and paste lookups are cached per id (a minute when found, 30 s when missing, 10 s after an upstream error), shared by concurrent requests, and wait at most 2.5 s; when Media or Community cannot answer the page is 200, never a false 404. The cache keeps only the fields the visibility rule reads. The SEO renderer now remembers a Media/Community 404 too, so a dead link is not a round trip per request. Client: an unknown path renders a Page not found view (showNotFound() in public/js/app.js, filling a new empty #page-not-found shell in index.html) with links home, to VODs, clips and chat, instead of silently showing the home page; an invalid /@name does the same. Links that pointed at pages that never existed: the pastes limit notice's Log in (navigate('/login')) now signs in, the IRL Pro tip names /overlay/chat/<username> instead of /embed/chat/<id>, and the home page's WebSite JSON-LD drops its SearchAction for a /search page that does not exist. docs/architecture.md (Unknown pages) and AGENTS.md (new top-level routes go in routeFromURL() and page-status.js) say so. Tests: test/page-status.test.js runs the SEO middleware and the real fallback over a temp database with Media and Community stubbed: every real route 200 with and without Accept: text/html, unknown paths 404, private VOD/clip 404 to strangers and 200 to owners and staff, the 404 body is the shell and carries no private title, HEAD matches GET, one Media fetch per id, the home page never waits on a lookup, Media down or slow gives 200 within the deadline, index.js mounts this fallback last, and every client route is known to the server. test/browser/smoke.js adds /no-such-page (renders page-not-found at every width). Size budgets unchanged in practice (eager JS 1108.6 / 1150 KB raw, 251.3 / 260 KB brotli)OpenVibers · 23 Sept 2026
39355a0c
-
Canonical channel/owner resolver (roadmap D20, openvibe-contracts lineage.resolve-request@1 -> lineage.resolution@1): server/lineage/resolver.js answers which channel, and whose, a slug, <channel>/<slot> or parent slug, channel id, stream, slot, VOD, clip, Media object (med_ or legacy:live:<kind>:<id>), owner subject or legacy id (Live user id, Network user id) belongs to, with the stream/VOD/clip lineage it walked, resolved_by, rule, confidence (exact|derived|legacy_map), via and what every input came to. Precedence is the roadmap's order (explicit slug, nested/parent slug, channel id, stream lookup, the record's own lineage, Media lineage, owner subject, legacy maps): the first input that resolves decides, any other that names a different channel makes the answer a conflict, one Media could not answer makes it source_unavailable, one that names nothing does not block. Inside a record the first link that answers wins: a clip goes to its stream, then its VOD's lineage, then the channel Media recorded on it, never to the clipper; a VOD to its stream, then its slot when the stream row is gone, then its recorded owner; a Media object to the record its legacy_ref names, then its owner subject, then its owner user. A display name is never read: alone it is answered display_name_only. Nothing is written: an owner without a channels row is not_found. VODs, clips and objects come from Media (server/lineage/media-source.js, API v1 plus v2 objects), never from the frozen tables.OpenVibers · 23 Sept 2026
21528ba0
-
AGENTS.md: the legacy vods/clips/pastes tables are never read or written (the test enforces both); Media outcomes arrive over EventsOpenVibers · 23 Sept 2026
0981a8a6
-
docs/vods-and-clips.md: what used to read the frozen tables and what answers now, the Media and Community API additions that would make the moved lookups exact (batch by stream ids, created_after, owner-subject and metadata filters for pastes, a public paste count), and C-73 step 3, the procedure to drop vods, clips, pastes, paste_likes and paste_comments 30 days after this deploy: ship the code that stops creating them, back up (online .backup plus a .dump of the five tables, checksummed), reconcile read-only against media.db and community.db (every missing count 0, nothing else references them), drop children first with foreign keys off in one transaction, verify through a restart, rollback from the dump. docs/architecture.md points thereOpenVibers · 23 Sept 2026
bc2f2a62
-
test/frozen-tables.test.js refuses reads as well as writes: SQL in server/ and scripts/ (.js and .sql) that selects from or joins vods, clips, pastes, paste_likes or paste_comments, also split over lines, or a table registry naming one (`table: 'vods'`), fails unless it is on an allow-list whose entries each need a reason and must still match; the list is empty. paste_likes and paste_comments are guarded for writes too. The scanner checks itself on the shapes Live used (89 reads in the code before C-73 step 2) and leaves schema upkeep (CREATE/ALTER/PRAGMA) aloneOpenVibers · 23 Sept 2026
9b3be330
-
C-73 step 2: nothing in Live reads the frozen vods/clips/pastes tables any more; every reader asks OpenVibe.Media (VODs, clips) or OpenVibe.Community (pastes) through the new server/media-proxy/lookups.js. Recently ended streams get their public VOD from Media per stream (cached 5 min, 4 s budget; never a private or unlisted id or thumbnail); the workspace history gets each session's VOD from Media by slot (owner only, private included); the channel page's paste and clips-taken badges come from Community and Media (hidden ones only for the owner, asked as them, or staff, vouched with X-OV-Staff, which pastes-client now sends); the offline screen's week/month/all-time top VOD and clip are cut from Media's newest 100 plus its most-viewed; the AI timeline's session links are public Media VODs and a timeline built while Media is down is not cached; the transcript view, the AI moments and auto-clip jobs lose their local fallbacks (clip start times come from Media); avatar history and the setup hub's paste count come from Community; home hero, kiosk and SEO archive counters come from Media /stats with pastes from Community; admin VOD totals and the AI explorer read Media and Community; stream analytics' clips_created is filled from Media right after the stream ends; arena portraits use Media VOD thumbnails; the streamer overview job needs stream memories. Response shapes are unchanged (null where the upstream does not answer). database.js loses the 45 uncalled VOD/clip/paste/paste-comment helpers, the 11 whose callers moved, the boot-time migrations that still rewrote frozen vods/clips rows, and the indexes that served only the removed listings. Tests: test/media-lookups.test.js (every moved reader and route against in-process Media and Community stand-ins, including who is asked for hidden items), media-privacy's recently-ended check now runs against MediaOpenVibers · 23 Sept 2026
9b80d2b3
-
Analytics comes from openvibe-shared/analytics (v1.4.0; retires C-80): Live's hand-copied ADR-021 module in server/analytics/ is deleted, server/index.js requires the package (retention: false kept; the jobs.js analytics-prune job still runs retention.pruneRawEvents), scripts/analytics-prune.js is a thin wrapper over openvibe-shared/analytics/prune-cli (default $ANALYTICS_DB, else data/analytics.db; backups are now owner-only), test/analytics-privacy.test.js tests the package. New with v1.4.0: a request with Sec-GPC: 1 or DNT: 1 is not recorded at all (no raw row, visitor hash, session id or rate counter), noted in README and docs/architecture.md; SECURITY_AUDIT.md's analytics row points at the package (roll-ups still synchronous; the prune is 30 days in bounded batches)OpenVibers · 23 Sept 2026
c893c6ca
More commits