openvibers/openvibe.network Commits

The all-sites menu, regrouped by what people came to do (Watch & stream first, Games, Build with AI, Everyday, Your account, For developers) with short menu labels that fit a phone (#334)

OP OpenVibers committed 19 days ago · 0c0a364154 · 4 files, +37 −15

Changes

ddiff --git a/server/frame/service.js b/server/frame/service.js
iindex 97c455c2..dd41f760 100644
---- a/server/frame/service.js
++++ b/server/frame/service.js
@@@ -19,7 +19,7 @@
// ═══════════════════════════════════════════════════════════════
const express = require('express');
const crypto = require('crypto');
--const { SITES, siteForHost, GROUPS, groupOf, PRODUCTS, ICON_ALIAS } = require('./sites');
++const { SITES, siteForHost, GROUPS, groupOf, PRODUCTS, ICON_ALIAS, MENU_LINE } = require('./sites');
const toolsCatalog = require('../domains/catalog');
const RANK_MS = 30 * 60_000, COPY_MS = 24 * 60 * 60_000;
@@@ -164,7 +164,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
return {
updated: new Date(version).toISOString(), site: { id: site.id, name: site.name, host: site.host, status: site.status, state: site.state },
// Every open site, most used first, each followed by the products it serves on their own domains (`of`).
-- nav: open.flatMap(s => [s, ...PRODUCTS.filter(p => p.of === s.id)]).map(s => ({ id: s.id, name: s.name, url: `https://${s.host}/`, icon: ICON_ALIAS[s.icon] || s.icon, tagline: s.tagline, group: groupOf(s.id), ...(s.of ? { of: s.of } : {}) })),
++ nav: open.flatMap(s => [s, ...PRODUCTS.filter(p => p.of === s.id)]).map(s => ({ id: s.id, name: s.name, url: `https://${s.host}/`, icon: ICON_ALIAS[s.icon] || s.icon, tagline: MENU_LINE[s.id] || s.tagline, group: groupOf(s.id), ...(s.of ? { of: s.of } : {}) })),
groups: GROUPS.map(g => ({ id: g.id, name: g.name })),
// state: 'internal' (runs on loopback, the domain still shows a placeholder) or 'placeholder' (planned).
soon: SITES.filter(s => s.status === 'soon').map(s => ({ id: s.id, name: s.name, url: `https://${s.host}/`, icon: s.icon, state: s.state })),
ddiff --git a/server/frame/sites.js b/server/frame/sites.js
iindex 899556a6..a0fc427f 100644
---- a/server/frame/sites.js
++++ b/server/frame/sites.js
@@@ -31,17 +31,36 @@ const byHost = new Map(SITES.map(s => [s.host, s]));
// The launcher (openvibe-shared navbar) lists every open site under one of these groups, most used first within
// each. A site missing here lands in the last group, so a new manifest is never left out of the menu.
--// Plan track K (the owner, 2026-10-24: consumer-friendly products, the developer side on Services): what people use
--// first, agents and robots as their own group, and the building blocks last under "For developers".
++// Grouped by what a person came to do (the owner, 2026-10-25: "reorganize this whole menu and make it more clean and
++// clear and organized and easier to understand"): watching first (Live is the flagship), then talking, playing,
++// building with AI, everyday errands, their own account and items, and the building blocks last "For developers"
++// (plan track K). Alerts (watch) is an everyday tool; Download sits with Video and Pics; Inventory with the account,
++// so a person's items are one tap away on every site.
const GROUPS = [
-- { id: 'talk', name: 'Chat & community', sites: ['chat', 'community', 'wiki', 'news', 'blog'] },
-- { id: 'watch', name: 'Watch & stream', sites: ['live', 'media-hub', 'pics', 'stream', 'tips', 'vip'] },
-- { id: 'agents', name: 'Agents & robots', sites: ['actor', 'bot', 'codes', 'watch'] },
-- { id: 'everyday', name: 'Shop & everyday', sites: ['deals', 'coupons', 'reviews', 'food', 'rent', 'homes', 'work', 'trade', 'host'] },
-- { id: 'create', name: 'Create & play', sites: ['games', 'space', 'tools', 'download', 'quest'] },
-- { id: 'you', name: 'Your OpenVibe', sites: ['network', 'search', 'help', 'inventory'] },
++ { id: 'watch', name: 'Watch & stream', sites: ['live', 'media-hub', 'stream', 'pics', 'download', 'tips', 'vip'] },
++ { id: 'talk', name: 'Chat & community', sites: ['chat', 'community', 'blog', 'news', 'wiki'] },
++ { id: 'play', name: 'Games', sites: ['games', 'quest'] },
++ { id: 'make', name: 'Build with AI', sites: ['codes', 'actor', 'space', 'tools', 'bot'] },
++ { id: 'everyday', name: 'Everyday', sites: ['deals', 'coupons', 'reviews', 'work', 'rent', 'homes', 'food', 'help', 'trade', 'watch', 'search', 'host'] },
++ { id: 'you', name: 'Your account', sites: ['network', 'inventory'] },
{ id: 'build', name: 'For developers', sites: ['services', 'ai', 'run', 'zone', 'events', 'media', 'play'] },
];
++// 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
++// its tagline.
++const MENU_LINE = {
++ live: 'Go live, watch streams', 'media-hub': 'Clips and past streams', stream: 'Stream everywhere', pics: 'Share pictures',
++ download: 'Your files', tips: 'Support streamers', vip: 'Back your creators',
++ chat: 'Chat with anyone', community: 'Posts and pastes', blog: 'Write long-form', news: 'Sourced, not spun', wiki: 'Facts with sources',
++ games: 'Play in your browser', quest: 'Earn badges',
++ codes: 'Build apps with AI', actor: 'Agents do it for you', space: 'Host a site or app', tools: 'Every online tool', bot: 'Drive a robot',
++ deals: 'Deals people share', coupons: 'Codes that work', reviews: 'Honest reviews', work: 'Find a job', rent: 'Places to rent',
++ homes: 'Homes for sale', food: 'Food near you', help: 'Help nearby', trade: 'Watch the markets', watch: 'Know when it changes',
++ search: 'Search everything', host: 'Compare hosting',
++ network: 'Your account', inventory: 'Your items',
++ services: 'APIs and SDK', ai: 'AI models API', run: 'Run jobs anywhere', zone: 'S3 storage', events: 'Event streams', media: 'Media API', play: 'Game services',
++};
++
const groupOf = (id) => (GROUPS.find(g => g.sites.includes(id)) || GROUPS[GROUPS.length - 1]).id;
// Products with a domain of their own that another service serves (its manifest lists the domain): in the menu
@@@ -62,4 +81,4 @@ function siteForHost(hostname) {
return null;
}
--module.exports = { SITES, siteForHost, GROUPS, groupOf, PRODUCTS, ICON_ALIAS };
++module.exports = { SITES, siteForHost, GROUPS, groupOf, PRODUCTS, ICON_ALIAS, MENU_LINE };
ddiff --git a/test/frame.test.js b/test/frame.test.js
iindex 4f65bf23..c9169e6f 100644
---- a/test/frame.test.js
++++ b/test/frame.test.js
@@@ -17,14 +17,17 @@ let p = svc.payloadFor('openvibe.media');
assert.deepEqual(p.nav.filter(n => !n.of).map(n => n.id), ['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', 'ai', 'search', 'run', 'play', 'zone'], 'cold start follows the base order');
// 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.id), ['talk', 'watch', 'agents', 'everyday', 'create', 'you', 'build'], 'what people use first, the building blocks last (plan track K)');
++assert.deepStrictEqual(p.groups.map(g => g.name), ['Watch & stream', 'Chat & community', 'Games', 'Build with AI', 'Everyday', 'Your account', 'For developers'], 'by what a person came to do, the building blocks last (the owner, 2026-10-25)');
assert.strictEqual(p.groups.find(g => g.id === 'build').name, 'For developers');
assert.ok(p.nav.every(n => p.groups.some(g => g.id === n.group)), 'every entry has a group');
assert.deepStrictEqual(p.nav.filter(n => n.of).map(n => [n.id, n.of, n.url]), [['pics', 'media-hub', 'https://openvibe.pics/'], ['download', 'media-hub', 'https://openvibe.download/'], ['homes', 'rent', 'https://openvibe.homes/']]);
const at = (id) => p.nav.findIndex(n => n.id === id);
assert.ok(at('pics') === at('media-hub') + 1 && at('homes') === at('rent') + 1, 'a product follows its site');
--assert.deepStrictEqual([p.nav[at('media-hub')].icon, p.nav[at('media-hub')].group, p.nav[at('homes')].group, p.nav[at('tools')].group], ['video', 'watch', 'everyday', 'create'], 'Video draws the video icon (there is no media-hub icon)');
--assert.deepStrictEqual(['actor', 'bot', 'run', 'zone'].map((id) => p.nav[at(id)].group), ['agents', 'agents', 'build', 'build'], 'agents and robots for people, compute and storage for developers');
++assert.deepStrictEqual([p.nav[at('media-hub')].icon, p.nav[at('media-hub')].group, p.nav[at('homes')].group, p.nav[at('tools')].group], ['video', 'watch', 'everyday', 'make'], 'Video draws the video icon (there is no media-hub icon)');
++assert.deepStrictEqual(['actor', 'bot', 'run', 'zone'].map((id) => p.nav[at(id)].group), ['make', 'make', 'build', 'build'], 'agents and robots for people, compute and storage for developers');
++// Games together, items with the account, Alerts as an everyday tool, files beside Video (each when its site is open).
++[['games', 'play'], ['quest', 'play'], ['inventory', 'you'], ['watch', 'everyday'], ['download', 'watch']]
++ .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, 40);
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-front-door.test.js b/test/home-front-door.test.js
iindex 7aa2b7eb..26958030 100644
---- a/test/home-front-door.test.js
++++ b/test/home-front-door.test.js
@@@ -107,7 +107,7 @@ assert.ok(faqLd.mainEntity.length >= 5, 'the FAQ has its questions');
faqLd.mainEntity.forEach((q) => assert.ok(html.includes(`<h3>${q.name.replace(/'/g, '&#39;')}</h3>`), `the FAQ shows "${q.name}"`));
assert.ok(faqLd.mainEntity.some((q) => /separate account/.test(q.name)), "Contracts' network FAQ is included");
// The service map groups the sites the way the launcher does; numbers say only what was read.
--assert.ok(/<h3 class="home-group">Watch &amp; stream<\/h3>/.test(html) && /<h3 class="home-group">For developers<\/h3>/.test(html) && /<h3 class="home-group">Agents &amp; robots<\/h3>/.test(html), 'the sites are grouped');
++assert.ok(/<h3 class="home-group">Watch &amp; stream<\/h3>/.test(html) && /<h3 class="home-group">For developers<\/h3>/.test(html) && /<h3 class="home-group">Build with AI<\/h3>/.test(html), 'the sites are grouped');
assert.ok(/<section class="net-num"/.test(html) && /sites, one account/.test(html), 'the numbers panel renders');
assert.ok(!/services up/.test(html) && !/accounts<\/span>/.test(html), 'numbers nobody read are left out');
assert.ok(!/Latest recordings|New jobs|Fresh coupon codes/.test(html), 'an empty column is left out');