openvibers/openvibe.network Commits
Registry flip: chat is live on openvibe.chat (exposure live/service); the Frame's site switcher opens it (its host now comes from the manifest's publicOrigin)
Changes
@@@ -11,7 +11,7 @@ const { exposureOf } = require('../registry/exposure');// Network's exposure overlay (server/registry/exposure.js); every other site is 'soon' and says why// (state: internal = runs on loopback only, placeholder = planned). The two lists cannot disagree.// host comes from the service's manifest (openvibe-contracts publicOrigin); an entry names its host only--// while its manifest has no public origin (chat). TODO(plan §5.1/§5.3): name, icon, tagline, what and the++// while its manifest has no public origin. TODO(plan §5.1/§5.3): name, icon, tagline, what and the// legal profile are presentation with no manifest field yet; they move when the manifest schema has them.const RAW_SITES = [{ id: 'live', name: 'Live', icon: 'live', service: 'live', profile: 'streaming', tagline: 'Streams, clips and chat', what: 'Live streaming from a browser, OBS or a phone, with restreaming, clips, VODs, chat games and channel points.' },@@@ -20,7 +20,7 @@ const RAW_SITES = [{ id: 'games', name: 'Games', icon: 'games', service: 'games', profile: 'games', tagline: 'Browser games', what: 'Scraplandia, a shared pixel canvas and other games that run in a tab and know your OpenVibe account.' },{ id: 'media', name: 'Media', icon: 'media', service: 'media', profile: 'hosting', tagline: 'VODs, clips, files', what: 'The public media library behind every site: recorded streams, clips, images and uploads.' },{ id: 'network', name: 'Network', icon: 'network', service: 'network', profile: 'account', tagline: 'Account and themes', what: 'The account behind every site: one sign-in, themes, notifications, history and linked services.' },-- { id: 'chat', name: 'Chat', host: 'openvibe.chat', icon: 'chat', service: 'chat', profile: 'ugc', tagline: 'One chat for the network', what: 'Rooms, stream chat, direct messages, calls and text-to-speech under one identity.' },++ { id: 'chat', name: 'Chat', icon: 'chat', service: 'chat', profile: 'ugc', tagline: 'One chat for the network', what: 'Rooms, stream chat, direct messages, calls and text-to-speech under one identity.' },{ id: 'codes', name: 'Codes', icon: 'codes', service: 'codes', profile: 'ugc', tagline: 'Build on OpenVibe', what: 'The developer portal: API and SDK docs, the service and capability registry, event schemas, credentials, playgrounds and mod publishing.' },{ id: 'blog', name: 'Blog', icon: 'blog', service: 'blog', profile: 'info', tagline: 'Long-form, yours to keep', what: 'The official OpenVibe blog and a blog for every member: drafts, scheduling, media, comments and feeds.' },{ id: 'wiki', name: 'Wiki', icon: 'wiki', service: 'wiki', profile: 'ugc', tagline: 'Knowledge, with sources', what: 'Wiki spaces with page trees, revision history, citations and discussion, editable together.' },@@@ -82,7 +82,7 @@ const EXPOSURE = {openre: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },search: { state: 'live', public_site: 'service', note: 'public search page and query API at search.openvibe.network (since 2026-09-23)' },sources: { state: 'internal', public_site: null, note: 'internal ingestion service; sources.openvibe.network says so (its API is loopback-only)' },-- chat: { state: 'internal', public_site: null, note: 'serves Live\'s chat through Live; openvibe.chat serves the OpenVibe.Sites placeholder page' },++ chat: { state: 'live', public_site: 'service', note: 'openvibe.chat since 2026-09-24 (global chat, messages, settings); also serves Live\'s chat paths' },ai: { state: 'internal', public_site: null, note: 'called by other services at 127.0.0.1:4700; ai.openvibe.network serves the OpenVibe.Sites placeholder page' },sdk: library('openvibe-sdk', 'OpenVibe.SDK'),@@@ -14,10 +14,10 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');assert.equal(siteForHost('evil.example'), null);assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');let p = svc.payloadFor('openvibe.media');--assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'community', 'games', 'media', 'network', 'codes', 'blog', 'wiki'], 'cold start follows the base order');++assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'codes', 'blog', 'wiki'], 'cold start follows the base order');assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');--assert.ok(p.soon.length >= 10);++assert.ok(p.soon.length >= 9, 'every planned site still listed as soon (chat launched 2026-09-24)');// Real use reorders: community gets the history, so it rises above the cold-start order.const ins = db.prepare("INSERT INTO user_history (user_id, service, sub, type, title, url) VALUES (?, ?, ?, 'page', 't', 'u')");@@@ -18,7 +18,7 @@ const PINNED = {network: 'live', live: 'live', tools: 'live', media: 'live', games: 'live', community: 'live', events: 'live',billing: 'live', codes: 'live', blog: 'live', wiki: 'live', sites: 'live',news: 'internal', reviews: 'internal', deals: 'internal', coupons: 'internal', trade: 'internal', host: 'internal',-- tips: 'internal', vip: 'internal', openre: 'internal', search: 'live', sources: 'internal', chat: 'internal', ai: 'internal',++ tips: 'internal', vip: 'internal', openre: 'internal', search: 'live', sources: 'internal', chat: 'live', ai: 'internal',sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',examples: 'repository',realtime: 'placeholder',@@@ -101,11 +101,11 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], chassert.strictEqual(sBy.live.label, 'up');assert.strictEqual(sBy.live.origin, 'https://openvibe.live');assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);-- assert.deepStrictEqual(st.exposure_summary, { live: 13, internal: 12, library: 4, repository: 1, placeholder: 1 });++ assert.deepStrictEqual(st.exposure_summary, { live: 14, internal: 11, library: 4, repository: 1, placeholder: 1 });// The page: no row for a service whose domain serves a placeholder shows a bare "Up".const html = await fetch(base + '/status').then(x => x.text());-- for (const id of PLACEHOLDER_DOMAINS.concat(['chat', 'ai'])) {++ for (const id of PLACEHOLDER_DOMAINS.concat(['ai'])) {const row = html.match(new RegExp(`<tr id="svc-${id}">[\\s\\S]*?</tr>`))[0];assert.ok(!/>Up<\/span>/.test(row), `${id} row must not read a bare Up`);assert.ok(row.includes('Up · loopback only'), `${id} row says loopback only`);@@@ -116,12 +116,12 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.const open = SITES.filter(s => s.status === 'open').map(s => s.id);-- assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'codes', 'blog', 'wiki']);++ assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'codes', 'blog', 'wiki']);for (const s of SITES) {assert.ok(s.service && PINNED[s.service], `${s.id} names its service`);assert.strictEqual(s.status === 'open', PINNED[s.service] === 'live', `${s.id} nav status follows exposure`);}-- for (const id of ['news', 'reviews', 'deals', 'coupons', 'trade', 'host', 'tips', 'vip', 'stream', 'chat']) {++ for (const id of ['news', 'reviews', 'deals', 'coupons', 'trade', 'host', 'tips', 'vip', 'stream']) {const s = SITES.find(x => x.id === id);assert.strictEqual(s.status, 'soon'); assert.strictEqual(s.state, 'internal');}
@@ -11,7 +11,7 @@ const { exposureOf } = require('../registry/exposure');
@@ -11,7 +11,7 @@ const { exposureOf } = require('../registry/exposure');
// Network's exposure overlay (server/registry/exposure.js); every other site is 'soon' and says why
// Network's exposure overlay (server/registry/exposure.js); every other site is 'soon' and says why
// (state: internal = runs on loopback only, placeholder = planned). The two lists cannot disagree.
// (state: internal = runs on loopback only, placeholder = planned). The two lists cannot disagree.
// host comes from the service's manifest (openvibe-contracts publicOrigin); an entry names its host only
// host comes from the service's manifest (openvibe-contracts publicOrigin); an entry names its host only
-// while its manifest has no public origin (chat). TODO(plan §5.1/§5.3): name, icon, tagline, what and the
+// while its manifest has no public origin. TODO(plan §5.1/§5.3): name, icon, tagline, what and the
// legal profile are presentation with no manifest field yet; they move when the manifest schema has them.
// legal profile are presentation with no manifest field yet; they move when the manifest schema has them.
const RAW_SITES = [
const RAW_SITES = [
{ id: 'live', name: 'Live', icon: 'live', service: 'live', profile: 'streaming', tagline: 'Streams, clips and chat', what: 'Live streaming from a browser, OBS or a phone, with restreaming, clips, VODs, chat games and channel points.' },
{ id: 'live', name: 'Live', icon: 'live', service: 'live', profile: 'streaming', tagline: 'Streams, clips and chat', what: 'Live streaming from a browser, OBS or a phone, with restreaming, clips, VODs, chat games and channel points.' },
@@ -20,7 +20,7 @@ const RAW_SITES = [
@@ -20,7 +20,7 @@ const RAW_SITES = [
{ id: 'games', name: 'Games', icon: 'games', service: 'games', profile: 'games', tagline: 'Browser games', what: 'Scraplandia, a shared pixel canvas and other games that run in a tab and know your OpenVibe account.' },
{ id: 'games', name: 'Games', icon: 'games', service: 'games', profile: 'games', tagline: 'Browser games', what: 'Scraplandia, a shared pixel canvas and other games that run in a tab and know your OpenVibe account.' },
{ id: 'media', name: 'Media', icon: 'media', service: 'media', profile: 'hosting', tagline: 'VODs, clips, files', what: 'The public media library behind every site: recorded streams, clips, images and uploads.' },
{ id: 'media', name: 'Media', icon: 'media', service: 'media', profile: 'hosting', tagline: 'VODs, clips, files', what: 'The public media library behind every site: recorded streams, clips, images and uploads.' },
{ id: 'network', name: 'Network', icon: 'network', service: 'network', profile: 'account', tagline: 'Account and themes', what: 'The account behind every site: one sign-in, themes, notifications, history and linked services.' },
{ id: 'network', name: 'Network', icon: 'network', service: 'network', profile: 'account', tagline: 'Account and themes', what: 'The account behind every site: one sign-in, themes, notifications, history and linked services.' },
- { id: 'chat', name: 'Chat', host: 'openvibe.chat', icon: 'chat', service: 'chat', profile: 'ugc', tagline: 'One chat for the network', what: 'Rooms, stream chat, direct messages, calls and text-to-speech under one identity.' },
+ { id: 'chat', name: 'Chat', icon: 'chat', service: 'chat', profile: 'ugc', tagline: 'One chat for the network', what: 'Rooms, stream chat, direct messages, calls and text-to-speech under one identity.' },
{ id: 'codes', name: 'Codes', icon: 'codes', service: 'codes', profile: 'ugc', tagline: 'Build on OpenVibe', what: 'The developer portal: API and SDK docs, the service and capability registry, event schemas, credentials, playgrounds and mod publishing.' },
{ id: 'codes', name: 'Codes', icon: 'codes', service: 'codes', profile: 'ugc', tagline: 'Build on OpenVibe', what: 'The developer portal: API and SDK docs, the service and capability registry, event schemas, credentials, playgrounds and mod publishing.' },
{ id: 'blog', name: 'Blog', icon: 'blog', service: 'blog', profile: 'info', tagline: 'Long-form, yours to keep', what: 'The official OpenVibe blog and a blog for every member: drafts, scheduling, media, comments and feeds.' },
{ id: 'blog', name: 'Blog', icon: 'blog', service: 'blog', profile: 'info', tagline: 'Long-form, yours to keep', what: 'The official OpenVibe blog and a blog for every member: drafts, scheduling, media, comments and feeds.' },
{ id: 'wiki', name: 'Wiki', icon: 'wiki', service: 'wiki', profile: 'ugc', tagline: 'Knowledge, with sources', what: 'Wiki spaces with page trees, revision history, citations and discussion, editable together.' },
{ id: 'wiki', name: 'Wiki', icon: 'wiki', service: 'wiki', profile: 'ugc', tagline: 'Knowledge, with sources', what: 'Wiki spaces with page trees, revision history, citations and discussion, editable together.' },
@@ -82,7 +82,7 @@ const EXPOSURE = {
@@ -82,7 +82,7 @@ const EXPOSURE = {
openre: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },
openre: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },
search: { state: 'live', public_site: 'service', note: 'public search page and query API at search.openvibe.network (since 2026-09-23)' },
search: { state: 'live', public_site: 'service', note: 'public search page and query API at search.openvibe.network (since 2026-09-23)' },
sources: { state: 'internal', public_site: null, note: 'internal ingestion service; sources.openvibe.network says so (its API is loopback-only)' },
sources: { state: 'internal', public_site: null, note: 'internal ingestion service; sources.openvibe.network says so (its API is loopback-only)' },
- chat: { state: 'internal', public_site: null, note: 'serves Live\'s chat through Live; openvibe.chat serves the OpenVibe.Sites placeholder page' },
+ chat: { state: 'live', public_site: 'service', note: 'openvibe.chat since 2026-09-24 (global chat, messages, settings); also serves Live\'s chat paths' },
ai: { state: 'internal', public_site: null, note: 'called by other services at 127.0.0.1:4700; ai.openvibe.network serves the OpenVibe.Sites placeholder page' },
ai: { state: 'internal', public_site: null, note: 'called by other services at 127.0.0.1:4700; ai.openvibe.network serves the OpenVibe.Sites placeholder page' },
sdk: library('openvibe-sdk', 'OpenVibe.SDK'),
sdk: library('openvibe-sdk', 'OpenVibe.SDK'),
@@ -14,10 +14,10 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
@@ -14,10 +14,10 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
assert.equal(siteForHost('evil.example'), null);
assert.equal(siteForHost('evil.example'), null);
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
let p = svc.payloadFor('openvibe.media');
let p = svc.payloadFor('openvibe.media');
-assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'community', 'games', 'media', 'network', 'codes', 'blog', 'wiki'], 'cold start follows the base order');
+assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'codes', 'blog', 'wiki'], 'cold start follows the base order');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');
assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');
-assert.ok(p.soon.length >= 10);
+assert.ok(p.soon.length >= 9, 'every planned site still listed as soon (chat launched 2026-09-24)');
// Real use reorders: community gets the history, so it rises above the cold-start order.
// Real use reorders: community gets the history, so it rises above the cold-start order.
const ins = db.prepare("INSERT INTO user_history (user_id, service, sub, type, title, url) VALUES (?, ?, ?, 'page', 't', 'u')");
const ins = db.prepare("INSERT INTO user_history (user_id, service, sub, type, title, url) VALUES (?, ?, ?, 'page', 't', 'u')");
@@ -18,7 +18,7 @@ const PINNED = {
@@ -18,7 +18,7 @@ const PINNED = {
network: 'live', live: 'live', tools: 'live', media: 'live', games: 'live', community: 'live', events: 'live',
network: 'live', live: 'live', tools: 'live', media: 'live', games: 'live', community: 'live', events: 'live',
billing: 'live', codes: 'live', blog: 'live', wiki: 'live', sites: 'live',
billing: 'live', codes: 'live', blog: 'live', wiki: 'live', sites: 'live',
news: 'internal', reviews: 'internal', deals: 'internal', coupons: 'internal', trade: 'internal', host: 'internal',
news: 'internal', reviews: 'internal', deals: 'internal', coupons: 'internal', trade: 'internal', host: 'internal',
- tips: 'internal', vip: 'internal', openre: 'internal', search: 'live', sources: 'internal', chat: 'internal', ai: 'internal',
+ tips: 'internal', vip: 'internal', openre: 'internal', search: 'live', sources: 'internal', chat: 'live', ai: 'internal',
sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',
sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',
examples: 'repository',
examples: 'repository',
realtime: 'placeholder',
realtime: 'placeholder',
@@ -101,11 +101,11 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -101,11 +101,11 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
assert.strictEqual(sBy.live.label, 'up');
assert.strictEqual(sBy.live.label, 'up');
assert.strictEqual(sBy.live.origin, 'https://openvibe.live');
assert.strictEqual(sBy.live.origin, 'https://openvibe.live');
assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);
assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);
- assert.deepStrictEqual(st.exposure_summary, { live: 13, internal: 12, library: 4, repository: 1, placeholder: 1 });
+ assert.deepStrictEqual(st.exposure_summary, { live: 14, internal: 11, library: 4, repository: 1, placeholder: 1 });
// The page: no row for a service whose domain serves a placeholder shows a bare "Up".
// The page: no row for a service whose domain serves a placeholder shows a bare "Up".
const html = await fetch(base + '/status').then(x => x.text());
const html = await fetch(base + '/status').then(x => x.text());
- for (const id of PLACEHOLDER_DOMAINS.concat(['chat', 'ai'])) {
+ for (const id of PLACEHOLDER_DOMAINS.concat(['ai'])) {
const row = html.match(new RegExp(`<tr id="svc-${id}">[\\s\\S]*?</tr>`))[0];
const row = html.match(new RegExp(`<tr id="svc-${id}">[\\s\\S]*?</tr>`))[0];
assert.ok(!/>Up<\/span>/.test(row), `${id} row must not read a bare Up`);
assert.ok(!/>Up<\/span>/.test(row), `${id} row must not read a bare Up`);
assert.ok(row.includes('Up · loopback only'), `${id} row says loopback only`);
assert.ok(row.includes('Up · loopback only'), `${id} row says loopback only`);
@@ -116,12 +116,12 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -116,12 +116,12 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.
// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
- assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'codes', 'blog', 'wiki']);
+ assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'codes', 'blog', 'wiki']);
for (const s of SITES) {
for (const s of SITES) {
assert.ok(s.service && PINNED[s.service], `${s.id} names its service`);
assert.ok(s.service && PINNED[s.service], `${s.id} names its service`);
assert.strictEqual(s.status === 'open', PINNED[s.service] === 'live', `${s.id} nav status follows exposure`);
assert.strictEqual(s.status === 'open', PINNED[s.service] === 'live', `${s.id} nav status follows exposure`);
}
}
- for (const id of ['news', 'reviews', 'deals', 'coupons', 'trade', 'host', 'tips', 'vip', 'stream', 'chat']) {
+ for (const id of ['news', 'reviews', 'deals', 'coupons', 'trade', 'host', 'tips', 'vip', 'stream']) {
const s = SITES.find(x => x.id === id);
const s = SITES.find(x => x.id === id);
assert.strictEqual(s.status, 'soon'); assert.strictEqual(s.state, 'internal');
assert.strictEqual(s.status, 'soon'); assert.strictEqual(s.state, 'internal');
}
}