openvibers/openvibe.network Commits

Frame: every site gets a launcher group (Watch & stream, Create & play, Talk & learn, Shop & everyday, Build & automate, Your OpenVibe), Pics/Download/Homes join the list after the site that serves them, and Video draws the video icon (#273)

OP OpenVibers committed 27 days ago · 0b7596c746 · 3 files, +41 −6

Changes

ddiff --git a/server/frame/service.js b/server/frame/service.js
iindex 793c2e7c..97c455c2 100644
---- a/server/frame/service.js
++++ b/server/frame/service.js
@@@ -3,7 +3,9 @@
// OpenVibe Frame data: what the navbar and footer of every site show.
//
// GET /api/frame?host=<hostname> public, CORS *, max-age=600, ETag
--// → { updated, nav: [{ id, name, url, icon, tagline }], open sites, most used first
++// → { updated, nav: [{ id, name, url, icon, tagline, group, of? }], open sites, most used first (of: a product
++// on its own domain, served by that site)
++// groups: [{ id, name }], the launcher's sections, in order
// soon: [{ id, name, url, icon, state }], state: internal | placeholder
// footer: { blurb, discover: [{ name, url }], popular: [{ name, url }], legal: { terms, privacy, dmca } } }
//
@@@ -17,7 +19,7 @@
// ═══════════════════════════════════════════════════════════════
const express = require('express');
const crypto = require('crypto');
--const { SITES, siteForHost } = require('./sites');
++const { SITES, siteForHost, GROUPS, groupOf, PRODUCTS, ICON_ALIAS } = require('./sites');
const toolsCatalog = require('../domains/catalog');
const RANK_MS = 30 * 60_000, COPY_MS = 24 * 60 * 60_000;
@@@ -161,7 +163,9 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
const legalBase = `https://${site.host}`;
return {
updated: new Date(version).toISOString(), site: { id: site.id, name: site.name, host: site.host, status: site.status, state: site.state },
-- nav: open.map(s => ({ id: s.id, name: s.name, url: `https://${s.host}/`, icon: s.icon, tagline: s.tagline })),
++ // 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 } : {}) })),
++ 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 })),
footer: { blurb: (ai && ai.blurb) || site.what, ai: !!ai, discover, popular: popularTools(8), legal: { terms: legalBase + '/terms', privacy: legalBase + '/privacy', dmca: legalBase + '/dmca' } },
ddiff --git a/server/frame/sites.js b/server/frame/sites.js
iindex 504dca26..a106df9e 100644
---- a/server/frame/sites.js
++++ b/server/frame/sites.js
@@@ -29,6 +29,28 @@ const SITES = RAW_SITES.map((s) => {
});
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.
++const GROUPS = [
++ { id: 'watch', name: 'Watch & stream', sites: ['live', 'media-hub', 'pics', 'stream', 'tips', 'vip'] },
++ { id: 'create', name: 'Create & play', sites: ['games', 'play', 'space', 'blog'] },
++ { id: 'talk', name: 'Talk & learn', sites: ['chat', 'community', 'wiki', 'news'] },
++ { id: 'everyday', name: 'Shop & everyday', sites: ['deals', 'coupons', 'reviews', 'food', 'rent', 'homes', 'trade', 'work', 'host'] },
++ { id: 'build', name: 'Build & automate', sites: ['tools', 'codes', 'run', 'actor', 'ai', 'bot', 'services', 'events', 'watch', 'zone', 'media', 'download'] },
++ { id: 'you', name: 'Your OpenVibe', sites: ['network', 'search', 'help', 'quest', 'inventory'] },
++];
++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
++// while that service's site is open, right after it. They are not sites of their own (no ranking, no legal profile).
++const PRODUCTS = [
++ { id: 'pics', name: 'Pics', host: 'openvibe.pics', icon: 'pics', of: 'media-hub', tagline: 'Share pictures, fast and open' },
++ { id: 'download', name: 'Download', host: 'openvibe.download', icon: 'download', of: 'media-hub', tagline: 'Your files, shared on your terms' },
++ { id: 'homes', name: 'Homes', host: 'openvibe.homes', icon: 'homes', of: 'rent', tagline: 'Every home-for-sale site, one search' },
++].filter((p) => { const s = SITES.find(x => x.id === p.of); const m = s && contracts.services.get(s.service); return m && (m.domains || []).includes(p.host); });
++// Manifest icon names the shared icon set draws differently.
++const ICON_ALIAS = { 'media-hub': 'video' };
++
/** The site a hostname belongs to (subdomains included), or null. */
function siteForHost(hostname) {
const h = String(hostname || '').toLowerCase().replace(/:\d+$/, '').replace(/^www\./, '');
@@@ -37,4 +59,4 @@ function siteForHost(hostname) {
return null;
}
--module.exports = { SITES, siteForHost };
++module.exports = { SITES, siteForHost, GROUPS, groupOf, PRODUCTS, ICON_ALIAS };
ddiff --git a/test/frame.test.js b/test/frame.test.js
iindex b3d1a71c..509d9c83 100644
---- a/test/frame.test.js
++++ b/test/frame.test.js
@@@ -14,7 +14,16 @@ 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', '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');
++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), ['watch', 'create', 'talk', 'everyday', 'build', 'you']);
++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', 'build'], 'Video draws the video icon (there is no media-hub icon)');
++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');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
@@@ -33,7 +42,7 @@ for (let i = 0; i < 6; i++) await ins.run(1, 'tools', 'dns');
await svc.refreshRank(); global.fetch = realFetch;
p = svc.payloadFor('openvibe.network');
assert.equal(p.nav[0].id, 'games', 'most viewed site comes first');
-- assert.deepEqual(svc.ranking().order, p.nav.map(n => n.id), 'ranking() is the navigation order (the registry features by it)');
++ assert.deepEqual(svc.ranking().order, p.nav.filter(n => !n.of).map(n => n.id), 'ranking() is the navigation order (the registry features by it)');
assert.ok(Date.now() - svc.ranking().at < 5000, 'and says when it was counted');
assert.ok(p.nav.findIndex(n => n.id === 'network') > 0, 'the Network is never first on shared-service traffic alone');
assert.ok(BANNED.test('Totally free tools') && BANNED.test('see https://x.y') && BANNED.test('<b>hi</b>') && !BANNED.test('Go live from a browser in seconds.'), 'AI copy screen');