openvibers/openvibe.network Commits

Play is part of Games: stop listing it as its own site (#659)

OP OpenVibers committed 5 days ago · 8c547c31a2 · 7 files, +25 −13

* Stop listing OpenVibe.Play as its own site: it is part of OpenVibe.Games now

* Registry exposure: Play is still live, but no longer a nav site

* Merge remote-tracking branch 'origin/main' into network-play-site-retire

# Conflicts:
#	server/frame/sites.js
#	test/frame.test.js
#	test/registry-exposure.test.js

Changes

ddiff --git a/server/frame/sites.js b/server/frame/sites.js
iindex 0fcc56fa..84b89055 100644
---- a/server/frame/sites.js
++++ b/server/frame/sites.js
@@@ -24,13 +24,21 @@ function manifestHost(service) {
const m = contracts.services.get(service);
try { return m && m.publicOrigin ? new URL(m.publicOrigin).hostname : null; } catch { return null; }
}
--const SITES = RAW_SITES.map((s) => {
++// Play is the game-services API behind OpenVibe.Games, not a destination of its own: its pages moved to
++// openvibe.games (achievements, leaderboards, developers) and the menu lists Games once. The service stays live and
++// exposed in the registry (so a game can still call it); the frame, footer, home, llms.txt and JSON-LD drop the entry.
++// The owner, 2026-11-08: "it should just be part of openvibe.games".
++const HIDDEN_SITE = new Set(['play']);
++const ALL_SITES = RAW_SITES.map((s) => {
const e = exposureOf(s.service);
const host = manifestHost(s.service) || s.host;
if (!host) throw new Error(`site ${s.id}: no host (its manifest has no publicOrigin and the entry names none)`);
return { ...s, host, status: e.state === 'live' && e.public_site === 'service' ? 'open' : 'soon', state: e.state };
});
--const byHost = new Map(SITES.map(s => [s.host, s]));
++// The menu, footer, home and llms.txt list SITES; siteForHost still knows Play so a Play-host page (or a token
++// consent screen) keeps its own name, without the site being offered as a destination.
++const SITES = ALL_SITES.filter((s) => !HIDDEN_SITE.has(s.id));
++const byHost = new Map(ALL_SITES.map(s => [s.host, s]));
/** A site's full name: its own when it is a brand already (OpenRestream), otherwise OpenVibe.<name> (OpenVibe.Live).
* "OpenVibe.OpenRestream" read wrong on the home page's cards (the owner, 2026-10-28). */
@@@ -51,7 +59,7 @@ const GROUPS = [
{ id: 'people', name: 'People & you', sites: ['chat', 'mail', 'community', 'tips', 'vip', 'help', 'network', 'inventory'] },
// Actor is the engine behind every agent (Codes' Auto, Vibe, the Dock call it): a developer service, not a second
// place to chat with an agent (plan L39, 2026-10-31).
-- { id: 'build', name: 'For developers', sites: ['services', 'exchange', 'actor', 'run', 'zone', 'events', 'media', 'watch', 'play'] },
++ { id: 'build', name: 'For developers', sites: ['services', 'exchange', 'actor', 'run', 'zone', 'events', 'media', 'watch'] },
];
// What each site is, in a few words, for the menus (the launcher's rows are two to a line on a phone: about 22
// characters fit). The manifests' taglines stay the sites' own sentences everywhere else; a site missing here shows
@@@ -67,7 +75,7 @@ const MENU_LINE = {
search: 'Search everything', host: 'Compare hosting', report: 'Your numbers, honest', vision: 'Ask a picture',
exchange: 'AI models, one account',
network: 'Your account', inventory: 'Your items',
-- services: 'APIs, SDK and keys', bench: 'Which AI is best', run: 'Run jobs anywhere', zone: 'S3 storage', events: 'Event streams', media: 'Media API', play: 'Game services',
++ services: 'APIs, SDK and keys', bench: 'Which AI is best', run: 'Run jobs anywhere', zone: 'S3 storage', events: 'Event streams', media: 'Media API',
};
// A site's name in the menus, where the rows are short words (Pics, Download, VIP, Tips): the product keeps its full name
ddiff --git a/server/home/render.js b/server/home/render.js
iindex 891d57ba..b231acaa 100644
---- a/server/home/render.js
++++ b/server/home/render.js
@@@ -75,7 +75,7 @@ const INTENTS = [
['pics', 'fa-image', 'Share pictures', 'Upload a picture and get links to share or embed it anywhere, in any size.', 'https://openvibe.pics/', 'picture pictures image images photo photos imgur upload host embed gif album albums alt text'],
['media-hub', 'fa-cloud-arrow-up', 'Keep and share files', 'A 25 GB drive with share links and folder backups.', 'https://openvibe.download/', 'download files drive upload share link storage backup sync cloud send mediafire mega'],
['zone', 'fa-box-archive', 'Store files in buckets', 'Private S3-compatible storage zones for your projects, with keys you scope and quotas you set.', 'https://openvibe.zone/', 'storage s3 bucket buckets object storage files backup backups rclone aws zone data upload'],
-- ['play', 'fa-trophy', 'Climb the leaderboards', 'This week\'s leaders and the achievements you have unlocked in every OpenVibe game.', 'https://play.openvibe.network/', 'leaderboard leaderboards achievement achievements score scores ranking rank high score trophy games play'],
++ ['play', 'fa-trophy', 'Climb the leaderboards', 'This week\'s leaders and the achievements you have unlocked in every OpenVibe game, now part of OpenVibe.Games.', 'https://openvibe.games/leaderboards', 'leaderboard leaderboards achievement achievements score scores ranking rank high score trophy games play'],
['inventory', 'fa-shirt', 'Wear what you earn', 'One inventory for the items you earn across OpenVibe, worn on every site.', 'https://inventory.openvibe.network/', 'inventory items cosmetics name effect particles hat voice badge collect equip wear profile'],
['watch', 'fa-bell', 'Get told when it changes', 'Watch a page, a feed or an API and hear when your value moves.', 'https://openvibe.watch/', 'watch monitor alert alerts notify change changes price drop feed rss tracker uptime'],
['help', 'fa-life-ring', 'Get help', 'Every OpenVibe site\'s questions answered, and a person when they are not.', 'https://openvibe.help/', 'help support faq question questions ticket contact problem how'],
ddiff --git a/server/profile/public.js b/server/profile/public.js
iindex ef76f956..54208450 100644
---- a/server/profile/public.js
++++ b/server/profile/public.js
@@@ -27,8 +27,8 @@ const esc = seo.esc;
const SITE = 'https://openvibe.network';
const INVENTORY_SITE = 'https://inventory.openvibe.network';
const QUEST_SITE = 'https://openvibe.quest';
--const PLAY_SITE = 'https://play.openvibe.network';
--const RECENT_ACHIEVEMENTS = 6; // per game on the page; the rest is on the game's page on OpenVibe.Play
++const GAMES_ACHIEVEMENTS = 'https://openvibe.games/achievements#game-';
++const RECENT_ACHIEVEMENTS = 6; // per game on the page; the rest is on the game's row on OpenVibe.Games
const AUDIENCE = 'openvibe.inventory';
const ITEMS_TTL_MS = 60_000;
const KINDS_TTL_MS = 10 * 60_000;
@@@ -128,7 +128,7 @@ function createPublicProfiles({ db, selfToken = () => null, inventoryUrl = 'http
const body = await r.json();
const games = (Array.isArray(body.titles) ? body.titles : []).filter((t) => t && typeof t.title === 'string' && Array.isArray(t.achievements)).map((t) => ({
title: t.title.slice(0, 64), name: String(t.name || t.title).slice(0, 80), count: t.achievements.length,
-- url: `${PLAY_SITE}/t/${encodeURIComponent(t.title.slice(0, 64))}`,
++ url: `${GAMES_ACHIEVEMENTS}${encodeURIComponent(t.title.slice(0, 64))}`,
recent: t.achievements.slice(0, RECENT_ACHIEVEMENTS).filter((a) => a && typeof a.name === 'string').map((a) => ({ name: a.name.slice(0, 80), description: String(a.description || '').slice(0, 200), unlocked_at: typeof a.unlocked_at === 'string' ? a.unlocked_at : null })),
}));
value = { total: games.reduce((n, g) => n + g.count, 0), games };
@@@ -291,7 +291,7 @@ function badgesHtml(inv) {
}).join('')}</ul></section>`;
}
--/** Games: each game the person played with its achievements (newest first), Steam-style; links to the game on OpenVibe.Play. */
++/** Games: each game the person played with its achievements (newest first), Steam-style; links to the game's row on OpenVibe.Games. */
function gamesHtml(inv) {
const a = inv.achievements;
if (!a || !a.games.length) return '';
ddiff --git a/test/frame.test.js b/test/frame.test.js
iindex c156bb00..ad67342b 100644
---- a/test/frame.test.js
++++ b/test/frame.test.js
@@@ -38,9 +38,10 @@ assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the
let p = svc.payloadFor('openvibe.media');
// Email (OpenVibe.Mail) joins the cold order after Chat once its manifest is in the pinned Contracts release; until then
// the test still follows what the installed manifests say.
--const COLD_ORDER = ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect'];
++const COLD_ORDER = ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'search', 'run', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect'];
if (HAS_MAIL) COLD_ORDER.splice(COLD_ORDER.indexOf('chat') + 1, 0, 'mail');
assert.deepEqual(p.nav.filter(n => !n.of).map(n => n.id), COLD_ORDER, 'cold start follows the base order');
++assert.ok(!p.nav.some(n => n.id === 'play'), 'Play is part of Games, not a menu entry of its own');
// The launcher: every open site in one of the groups, and the products other services serve on their own domains
// right after their site (Pics and Download after Video, Homes after Rent).
assert.deepStrictEqual(p.groups.map(g => g.name), ['Streams & places', 'Games & laughs', 'News & answers', 'Make & build', 'Shop & swap', 'People & you', 'For developers'], 'the six neighbourhoods (plan 2026-10-28 §3.1), the building blocks last');
@@@ -55,7 +56,7 @@ assert.deepStrictEqual(['actor', 'bot', 'run', 'zone', 'codes'].map((id) => p.na
// Games together, items with the account, Alerts with the building blocks, files beside Video (each when its site is open).
[['games', 'games'], ['connect', 'make'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['mail', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games'], ['vision', 'answers'], ['exchange', 'build'], ['fun', 'games'], ['cam', 'streams'], ['bench', 'answers'], ['casino', 'games'], ['tours', 'streams'], ['shoes', 'shop']]
.filter(([id]) => at(id) >= 0).forEach(([id, group]) => assert.strictEqual(p.nav[at(id)].group, group, `${id} is in ${group}`));
--assert.strictEqual(p.nav.length, 54 + (HAS_MAIL ? 1 : 0)); // Connect since contracts 0.279.0, Tours and Shoes since contracts 0.265.0, Casino since contracts 0.264.0, Domains since contracts 0.226.0, Report since 0.227.0, Design since 0.228.0, Today since 0.230.0, Surf since 0.231.0, Meme since 0.234.0, Vision and Exchange since 0.235.0; AI is no longer a site of its own (its pages are Exchange now), Fun since 0.236.0, Cam since 0.237.0, Bench since 0.238.0; Email joins after Chat with the Contracts release that adds its manifest
++assert.strictEqual(p.nav.length, 53 + (HAS_MAIL ? 1 : 0)); // Connect since contracts 0.279.0, Tours and Shoes since contracts 0.265.0, Casino since contracts 0.264.0, Domains since contracts 0.226.0, Report since 0.227.0, Design since 0.228.0, Today since 0.230.0, Surf since 0.231.0, Meme since 0.234.0, Vision and Exchange since 0.235.0; AI is no longer a site of its own (its pages are Exchange now), Fun since 0.236.0, Cam since 0.237.0, Bench since 0.238.0; Email joins after Chat with the Contracts release that adds its manifest
assert.deepStrictEqual([p.nav[at('stream')].name, p.nav[at('pics')].name], ['Restream', 'Pics'], 'OpenRestream is "Restream" among the menu\'s short names (owner, 2026-10-28)');
assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'Space keeps its own home (the forum returned to Community, 0.118.0)');
assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community: the forum, pastes and Pulse');
ddiff --git a/test/home-intent-routing.test.js b/test/home-intent-routing.test.js
iindex f2002f3f..2a060544 100644
---- a/test/home-intent-routing.test.js
++++ b/test/home-intent-routing.test.js
@@@ -31,6 +31,7 @@ const cases = [
['how do I go live?', 'https://openvibe.live/broadcast'],
['convert mp4 to mp3', 'tool search'],
['merge pdf', 'tool search'],
++ ['climb the leaderboards', 'https://openvibe.games/leaderboards'],
['find me a cheap apartment in austin', 'https://openvibe.rent/'],
['get help with my account', 'https://openvibe.help/'],
['what is the weather in paris?', vibe('what is the weather in paris?')],
ddiff --git a/test/public-profile.test.js b/test/public-profile.test.js
iindex 06419cf4..81badbcf 100644
---- a/test/public-profile.test.js
++++ b/test/public-profile.test.js
@@@ -101,7 +101,7 @@ const INSTANCES = [
assert.strictEqual(calls.find((c) => c.path.endsWith('/achievements')).auth, '', 'Play\'s read is public: no token');
assert.ok(r.text.includes('id="pf-games"') && r.text.includes('Salvage &lt;Rush&gt;') && !r.text.includes('Salvage <Rush>'), 'games, escaped');
assert.ok(/pf-game-n">2 achievements<[\s\S]*Big Haul[\s\S]*First Haul[\s\S]*Vibe Heroes[\s\S]*1 achievement</.test(r.text), 'each game with its achievements, newest first');
-- assert.ok(r.text.includes('href="https://play.openvibe.network/t/salvage-rush"'), 'a game links to its page on Play');
++ assert.ok(r.text.includes('href="https://openvibe.games/achievements#game-salvage-rush"'), 'a game links to its row on OpenVibe.Games');
assert.ok(/<span class="pf-stat-k">Achievements<\/span><span class="pf-stat-v">3<\/span>/.test(r.text));
assert.strictEqual(calls.find((c) => c.path.endsWith('/badges')).auth, 'Bearer tok:openvibe.quest:quest.badge.read', 'and for Quest');
assert.ok(r.text.includes('id="pf-badges"') && /Thread &lt;Starter&gt;[\s\S]*First Follow/.test(r.text), 'badges, newest first, escaped');
ddiff --git a/test/registry-exposure.test.js b/test/registry-exposure.test.js
iindex 9f889b7d..50885d23 100644
---- a/test/registry-exposure.test.js
++++ b/test/registry-exposure.test.js
@@@ -142,8 +142,10 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
assert.ok(/<tr id="svc-ai">[\s\S]*?>Up<\/span>/.test(html), 'AI serves its own public origin since contracts 0.110.0');
// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.
++ // Play stays live in the exposure overlay (the API serves) but is part of Games now, so it is not a nav site.
++ assert.ok(!SITES.some(s => s.id === 'play'), 'Play is part of Games, not a menu entry');
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
-- const EXPECTED_OPEN = ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect']; // by site position; Email joins after Chat once its manifest is pinned
++ const EXPECTED_OPEN = ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'search', 'run', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect']; // by site position; Email joins after Chat once its manifest is pinned
if (HAS_MAIL) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('chat') + 1, 0, 'mail');
assert.deepStrictEqual(open, EXPECTED_OPEN);
for (const s of SITES) {