openvibers/openvibe.network Commits
Front door: the most visited things to do lead, with this week's visits (L43) (#473)
Changes
@@@ -109,7 +109,7 @@ a{color:inherit}.intents{list-style:none;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:14px}.intent{display:flex;gap:12px;align-items:flex-start;padding:11px 12px;border-radius:12px;border:1px solid var(--border);background:color-mix(in srgb,var(--bg-secondary) 80%,transparent);text-decoration:none;color:inherit;transition:border-color .15s,background .15s,opacity .15s,transform .15s;height:100%}.intent>i{width:34px;height:34px;flex:none;display:grid;place-items:center;border-radius:10px;font-size:15px;color:var(--accent-light);background:color-mix(in srgb,var(--accent) 14%,transparent)}--.intent b{display:block;font-size:14.5px;letter-spacing:-.1px}.intent small{display:block;color:var(--text-secondary);font-size:12.5px;line-height:1.4;margin-top:1px}++.intent b{display:block;font-size:14.5px;letter-spacing:-.1px}.intent-n{display:inline-block;margin-top:5px;padding:1px 7px;border-radius:999px;font-style:normal;font-size:11px;font-weight:650;color:var(--accent-light);background:color-mix(in srgb,var(--accent) 14%,transparent)}.intent small{display:block;color:var(--text-secondary);font-size:12.5px;line-height:1.4;margin-top:1px}.intent:hover,.intent:focus-visible{border-color:var(--accent);background:var(--bg-hover);outline:0;transform:translateY(-1px)}.intent.match{border-color:var(--accent);background:var(--bg-hover)}.intent.dim{opacity:.38}@@@ -22,7 +22,7 @@ const SOURCES = {};const PUBLIC = { live: 'https://openvibe.live', video: 'https://openvibe.video', community: 'https://openvibe.community', blog: 'https://openvibe.blog', work: 'https://openvibe.work', coupons: 'https://openvibe.coupons' };--const EMPTY = { live: [], liveCount: 0, recordings: [], pastes: [], posts: [], jobs: [], codes: [], counts: {} };++const EMPTY = { live: [], liveCount: 0, recordings: [], pastes: [], posts: [], jobs: [], codes: [], counts: {}, popularity: null };let state = { at: 0, version: 0, ...EMPTY };let inflight = null;// Tests switch the reads off; the page then shows the "nothing live" state.@@@ -182,7 +182,7 @@ async function refresh({ fetch: fetchImpl = globalThis.fetch } = {}) {if (inflight) return inflight;inflight = (async () => {const q = (owner, type) => `${SOURCES.search}/api/v1/search?owner=${owner}&type=${type}&limit=12`;-- const [live, vods, pastes, posts, work, codes, accounts, services] = await Promise.all([++ const [live, vods, pastes, posts, work, codes, accounts, services, popularity] = await Promise.all([getJson(`${SOURCES.live}/api/streams`, fetchImpl),getJson(`${SOURCES.live}/api/vods?limit=40`, fetchImpl),getJson(`${SOURCES.community}/api/pastes?limit=12`, fetchImpl),@@@ -191,6 +191,7 @@ async function refresh({ fetch: fetchImpl = globalThis.fetch } = {}) {getJson(q('coupons', 'coupon'), fetchImpl),readCount(readers.accounts),readCount(readers.services),++ readCount(readers.popularity),]);const counts = { ...state.counts };if (Number.isFinite(Number(accounts)) && Number(accounts) > 0) counts.accounts = Number(accounts);@@@ -206,6 +207,8 @@ async function refresh({ fetch: fetchImpl = globalThis.fetch } = {}) {jobs: work ? jobs(work) : state.jobs,codes: codes ? coupons(codes) : state.codes,counts,++ // Visits per site this week (Network's open stats): the front door's intents are ordered by them.++ popularity: popularity && typeof popularity === 'object' ? popularity : state.popularity,};return state;})().finally(() => { inflight = null; });@@@ -220,7 +223,8 @@ function peek() {module.exports = {peek, refresh, liveStreams, recordings, publicPastes, blogPosts, jobs, coupons, runtime, ADULT, LOOKS,-- /** server/index.js: Network's own numbers, read on each refresh ({ accounts: async () => n, services: () => ({ up, live }) }). */++ /** server/index.js: Network's own numbers, read on each refresh ({ accounts: async () => n, services: () => ({ up, live }),++ * popularity: async () => ({ siteId: visits this week }) }). */configure(r) { readers = { ...readers, ...(r || {}) }; },/** Tests: switch the background reads on or off. */setEnabled(on) { enabled = Boolean(on); },@@@ -75,9 +75,23 @@ const INTENTS = [['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'],];--function intents(catalog) {++// The site whose visits rank an intent (the owner's plan L43: "the most popular things to do now, from the numbers").++// One site counts once: Ask Vibe and Chat are both openvibe.chat, so Chat carries its number; pictures and files are++// openvibe.video's products, so Video does. An intent with no site of its own keeps its place after the ranked ones.++const INTENT_SITE = { vibe: null, paste: 'community', video: 'media-hub', pics: null, 'media-hub': null };++const siteOfIntent = (id) => (Object.prototype.hasOwnProperty.call(INTENT_SITE, id) ? INTENT_SITE[id] : id);++const fmtVisits = (n) => (n >= 10000 ? `${Math.round(n / 1000)}k` : n >= 1000 ? `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k` : String(n));++/** The intents, most visited first this week; the first five say how many visits. Unknown numbers keep the order. */++function rankedIntents(popularity) {++ const visits = (id) => { const s = siteOfIntent(id); const n = s && popularity ? Number(popularity[s]) : NaN; return Number.isFinite(n) && n > 0 ? n : 0; };++ if (!popularity) return INTENTS.map((it) => ({ it, n: 0 }));++ return INTENTS.map((it, i) => ({ it, n: visits(it[0]), i })).sort((a, b) => (b.n - a.n) || (a.i - b.i));++}++++function intents(catalog, popularity = null) {const toolsLine = catalog.tools.length ? `Convert, compress, look up or download: ${catalog.tools.length} tools that just open.` : 'Convert, compress, look up or download: free tools that just open.';-- return `<ul class="intents" aria-label="What you can do here">${INTENTS.map(([id, fa, verb, line, href, k]) => `<li><a class="intent" href="${esc(href)}" data-id="${esc(id)}" data-k="${esc(k)}"><i class="fa-solid ${esc(fa)}" aria-hidden="true"></i><span><b>${esc(verb)}</b><small>${esc(line || toolsLine)}</small></span></a></li>`).join('')}</ul>`;++ const ranked = rankedIntents(popularity);++ return `<ul class="intents" aria-label="What you can do here${popularity ? ', most visited this week first' : ''}">${ranked.map(({ it: [id, fa, verb, line, href, k], n }, pos) => `<li><a class="intent" href="${esc(href)}" data-id="${esc(id)}" data-k="${esc(k)}"><i class="fa-solid ${esc(fa)}" aria-hidden="true"></i><span><b>${esc(verb)}</b><small>${esc(line || toolsLine)}</small>${pos < 5 && n >= 10 ? `<em class="intent-n">${esc(fmtVisits(n))} visits this week</em>` : ''}</span></a></li>`).join('')}</ul>`;}// The network as an orbit (the owner, 2026-10-28: the hero was "ugly af, boring… not easy to understand… nice animated@@@ -421,7 +435,7 @@ function render({ releaseId = '' } = {}) {})) + seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what })))) + seo.jsonLdTag(seo.jsonLd.faq(faqPairs()));const card = social.url('network', 'https://openvibe.network/shared') || 'https://openvibe.network/assets/logo-512.png';const summary = summaryOf(catalog);-- const html = lookBoot.apply(fs.readFileSync(SHELL, 'utf8')).replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + summary.body + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog)).replace('<!--OV:CONSTELLATION-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(OPEN.length + 1)).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);++ const html = lookBoot.apply(fs.readFileSync(SHELL, 'utf8')).replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + summary.body + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog, act.popularity)).replace('<!--OV:CONSTELLATION-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(OPEN.length + 1)).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);pageCache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };return pageCache;}@@@ -818,6 +818,12 @@ app.get(['/login.html', '/admin.html'], (req, res) => {// accounts (people, not guests or banned ones) and how many public services answered their last health check (a// count is left out until at least half of them have been checked once).require('./home/activity').configure({++ // Visits per site this week, from the open stats (page views the shared navbar counts): the front door's++ // intents are ordered by them (plan L43).++ popularity: async () => {++ const d = await openStats.collect(7);++ return d && Array.isArray(d.sites) ? Object.fromEntries(d.sites.map((s) => [s.id, Number(s.pageviews) || 0])) : null;++ },accounts: async () => (await db.prepare('SELECT COUNT(*) AS cnt FROM users WHERE COALESCE(is_anon, 0) = 0 AND COALESCE(is_banned, 0) = 0').get()).cnt,services: () => {const live = require('./status/routes').rows(ecosystem).filter((r) => r.exposure && r.exposure.state === 'live');@@@ -152,4 +152,22 @@ assert.ok(html.includes('href="https://openvibe.video/v/9"') && html.includes('<assert.ok(html.includes('.more [data-more-part]:target{display:block}'), 'a link to a section opens it');}assert.ok(/<b>1,234<\/b><span>accounts/.test(html) && /<b>37<\/b><span>services up <small>of 38 checked/.test(html) && /<b>1<\/b><span>stream live now/.test(html), 'the numbers read render');++// The most visited things to do lead the intents, with this week's visits on the first five (plan L43). One site counts++// once (Ask Vibe and Chat are both openvibe.chat: Chat carries the number), and no numbers keep the written order.++{++ activity._set({ live: [], pastes: [], posts: [], popularity: { live: 59680, 'media-hub': 1615, tools: 232, games: 151, community: 89, chat: 40, work: 3 } });++ const page = home.render().html;++ const order = [...page.matchAll(/<a class="intent" href="[^"]*" data-id="([a-z-]+)"/g)].map((m) => m[1]);++ assert.deepStrictEqual(order.slice(0, 6), ['live', 'video', 'tools', 'games', 'paste', 'chat'], `most visited first: ${order.slice(0, 8).join(', ')}`);++ assert.match(page, /data-id="live"[\s\S]*?<em class="intent-n">60k visits this week<\/em>/, 'the count says what it counts');++ assert.match(page, /data-id="video"[\s\S]*?<em class="intent-n">1.6k visits this week<\/em>/);++ assert.strictEqual([...page.matchAll(/class="intent-n"/g)].length, 5, 'only the first five say a number');++ assert.ok(order.indexOf('vibe') > order.indexOf('chat'), 'Ask Vibe does not borrow Chat\'s number');++ assert.match(page, /aria-label="What you can do here, most visited this week first"/);++ activity._set({ live: [], pastes: [], posts: [] });++ const plain = home.render().html;++ const plainOrder = [...plain.matchAll(/<a class="intent" href="[^"]*" data-id="([a-z-]+)"/g)].map((m) => m[1]);++ assert.deepStrictEqual(plainOrder.slice(0, 3), ['vibe', 'live', 'tools'], 'without numbers, the written order');++ assert.ok(!plain.includes('class="intent-n"'), 'and no counts');++}console.log('home front door: all checks passed');
@@ -109,7 +109,7 @@ a{color:inherit}
@@ -109,7 +109,7 @@ a{color:inherit}
.intents{list-style:none;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:14px}
.intents{list-style:none;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:14px}
.intent{display:flex;gap:12px;align-items:flex-start;padding:11px 12px;border-radius:12px;border:1px solid var(--border);background:color-mix(in srgb,var(--bg-secondary) 80%,transparent);text-decoration:none;color:inherit;transition:border-color .15s,background .15s,opacity .15s,transform .15s;height:100%}
.intent{display:flex;gap:12px;align-items:flex-start;padding:11px 12px;border-radius:12px;border:1px solid var(--border);background:color-mix(in srgb,var(--bg-secondary) 80%,transparent);text-decoration:none;color:inherit;transition:border-color .15s,background .15s,opacity .15s,transform .15s;height:100%}
.intent>i{width:34px;height:34px;flex:none;display:grid;place-items:center;border-radius:10px;font-size:15px;color:var(--accent-light);background:color-mix(in srgb,var(--accent) 14%,transparent)}
.intent>i{width:34px;height:34px;flex:none;display:grid;place-items:center;border-radius:10px;font-size:15px;color:var(--accent-light);background:color-mix(in srgb,var(--accent) 14%,transparent)}
-.intent b{display:block;font-size:14.5px;letter-spacing:-.1px}.intent small{display:block;color:var(--text-secondary);font-size:12.5px;line-height:1.4;margin-top:1px}
+.intent b{display:block;font-size:14.5px;letter-spacing:-.1px}.intent-n{display:inline-block;margin-top:5px;padding:1px 7px;border-radius:999px;font-style:normal;font-size:11px;font-weight:650;color:var(--accent-light);background:color-mix(in srgb,var(--accent) 14%,transparent)}.intent small{display:block;color:var(--text-secondary);font-size:12.5px;line-height:1.4;margin-top:1px}
.intent:hover,.intent:focus-visible{border-color:var(--accent);background:var(--bg-hover);outline:0;transform:translateY(-1px)}
.intent:hover,.intent:focus-visible{border-color:var(--accent);background:var(--bg-hover);outline:0;transform:translateY(-1px)}
.intent.match{border-color:var(--accent);background:var(--bg-hover)}
.intent.match{border-color:var(--accent);background:var(--bg-hover)}
.intent.dim{opacity:.38}
.intent.dim{opacity:.38}
@@ -22,7 +22,7 @@ const SOURCES = {
@@ -22,7 +22,7 @@ const SOURCES = {
};
};
const PUBLIC = { live: 'https://openvibe.live', video: 'https://openvibe.video', community: 'https://openvibe.community', blog: 'https://openvibe.blog', work: 'https://openvibe.work', coupons: 'https://openvibe.coupons' };
const PUBLIC = { live: 'https://openvibe.live', video: 'https://openvibe.video', community: 'https://openvibe.community', blog: 'https://openvibe.blog', work: 'https://openvibe.work', coupons: 'https://openvibe.coupons' };
-const EMPTY = { live: [], liveCount: 0, recordings: [], pastes: [], posts: [], jobs: [], codes: [], counts: {} };
+const EMPTY = { live: [], liveCount: 0, recordings: [], pastes: [], posts: [], jobs: [], codes: [], counts: {}, popularity: null };
let state = { at: 0, version: 0, ...EMPTY };
let state = { at: 0, version: 0, ...EMPTY };
let inflight = null;
let inflight = null;
// Tests switch the reads off; the page then shows the "nothing live" state.
// Tests switch the reads off; the page then shows the "nothing live" state.
@@ -182,7 +182,7 @@ async function refresh({ fetch: fetchImpl = globalThis.fetch } = {}) {
@@ -182,7 +182,7 @@ async function refresh({ fetch: fetchImpl = globalThis.fetch } = {}) {
if (inflight) return inflight;
if (inflight) return inflight;
inflight = (async () => {
inflight = (async () => {
const q = (owner, type) => `${SOURCES.search}/api/v1/search?owner=${owner}&type=${type}&limit=12`;
const q = (owner, type) => `${SOURCES.search}/api/v1/search?owner=${owner}&type=${type}&limit=12`;
- const [live, vods, pastes, posts, work, codes, accounts, services] = await Promise.all([
+ const [live, vods, pastes, posts, work, codes, accounts, services, popularity] = await Promise.all([
getJson(`${SOURCES.live}/api/streams`, fetchImpl),
getJson(`${SOURCES.live}/api/streams`, fetchImpl),
getJson(`${SOURCES.live}/api/vods?limit=40`, fetchImpl),
getJson(`${SOURCES.live}/api/vods?limit=40`, fetchImpl),
getJson(`${SOURCES.community}/api/pastes?limit=12`, fetchImpl),
getJson(`${SOURCES.community}/api/pastes?limit=12`, fetchImpl),
@@ -191,6 +191,7 @@ async function refresh({ fetch: fetchImpl = globalThis.fetch } = {}) {
@@ -191,6 +191,7 @@ async function refresh({ fetch: fetchImpl = globalThis.fetch } = {}) {
getJson(q('coupons', 'coupon'), fetchImpl),
getJson(q('coupons', 'coupon'), fetchImpl),
readCount(readers.accounts),
readCount(readers.accounts),
readCount(readers.services),
readCount(readers.services),
+ readCount(readers.popularity),
]);
]);
const counts = { ...state.counts };
const counts = { ...state.counts };
if (Number.isFinite(Number(accounts)) && Number(accounts) > 0) counts.accounts = Number(accounts);
if (Number.isFinite(Number(accounts)) && Number(accounts) > 0) counts.accounts = Number(accounts);
@@ -206,6 +207,8 @@ async function refresh({ fetch: fetchImpl = globalThis.fetch } = {}) {
@@ -206,6 +207,8 @@ async function refresh({ fetch: fetchImpl = globalThis.fetch } = {}) {
jobs: work ? jobs(work) : state.jobs,
jobs: work ? jobs(work) : state.jobs,
codes: codes ? coupons(codes) : state.codes,
codes: codes ? coupons(codes) : state.codes,
counts,
counts,
+ // Visits per site this week (Network's open stats): the front door's intents are ordered by them.
+ popularity: popularity && typeof popularity === 'object' ? popularity : state.popularity,
};
};
return state;
return state;
})().finally(() => { inflight = null; });
})().finally(() => { inflight = null; });
@@ -220,7 +223,8 @@ function peek() {
@@ -220,7 +223,8 @@ function peek() {
module.exports = {
module.exports = {
peek, refresh, liveStreams, recordings, publicPastes, blogPosts, jobs, coupons, runtime, ADULT, LOOKS,
peek, refresh, liveStreams, recordings, publicPastes, blogPosts, jobs, coupons, runtime, ADULT, LOOKS,
- /** server/index.js: Network's own numbers, read on each refresh ({ accounts: async () => n, services: () => ({ up, live }) }). */
+ /** server/index.js: Network's own numbers, read on each refresh ({ accounts: async () => n, services: () => ({ up, live }),
+ * popularity: async () => ({ siteId: visits this week }) }). */
configure(r) { readers = { ...readers, ...(r || {}) }; },
configure(r) { readers = { ...readers, ...(r || {}) }; },
/** Tests: switch the background reads on or off. */
/** Tests: switch the background reads on or off. */
setEnabled(on) { enabled = Boolean(on); },
setEnabled(on) { enabled = Boolean(on); },
@@ -75,9 +75,23 @@ const INTENTS = [
@@ -75,9 +75,23 @@ const INTENTS = [
['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'],
['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'],
];
];
-function intents(catalog) {
+// The site whose visits rank an intent (the owner's plan L43: "the most popular things to do now, from the numbers").
+// One site counts once: Ask Vibe and Chat are both openvibe.chat, so Chat carries its number; pictures and files are
+// openvibe.video's products, so Video does. An intent with no site of its own keeps its place after the ranked ones.
+const INTENT_SITE = { vibe: null, paste: 'community', video: 'media-hub', pics: null, 'media-hub': null };
+const siteOfIntent = (id) => (Object.prototype.hasOwnProperty.call(INTENT_SITE, id) ? INTENT_SITE[id] : id);
+const fmtVisits = (n) => (n >= 10000 ? `${Math.round(n / 1000)}k` : n >= 1000 ? `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k` : String(n));
+/** The intents, most visited first this week; the first five say how many visits. Unknown numbers keep the order. */
+function rankedIntents(popularity) {
+ const visits = (id) => { const s = siteOfIntent(id); const n = s && popularity ? Number(popularity[s]) : NaN; return Number.isFinite(n) && n > 0 ? n : 0; };
+ if (!popularity) return INTENTS.map((it) => ({ it, n: 0 }));
+ return INTENTS.map((it, i) => ({ it, n: visits(it[0]), i })).sort((a, b) => (b.n - a.n) || (a.i - b.i));
+}
+
+function intents(catalog, popularity = null) {
const toolsLine = catalog.tools.length ? `Convert, compress, look up or download: ${catalog.tools.length} tools that just open.` : 'Convert, compress, look up or download: free tools that just open.';
const toolsLine = catalog.tools.length ? `Convert, compress, look up or download: ${catalog.tools.length} tools that just open.` : 'Convert, compress, look up or download: free tools that just open.';
- return `<ul class="intents" aria-label="What you can do here">${INTENTS.map(([id, fa, verb, line, href, k]) => `<li><a class="intent" href="${esc(href)}" data-id="${esc(id)}" data-k="${esc(k)}"><i class="fa-solid ${esc(fa)}" aria-hidden="true"></i><span><b>${esc(verb)}</b><small>${esc(line || toolsLine)}</small></span></a></li>`).join('')}</ul>`;
+ const ranked = rankedIntents(popularity);
+ return `<ul class="intents" aria-label="What you can do here${popularity ? ', most visited this week first' : ''}">${ranked.map(({ it: [id, fa, verb, line, href, k], n }, pos) => `<li><a class="intent" href="${esc(href)}" data-id="${esc(id)}" data-k="${esc(k)}"><i class="fa-solid ${esc(fa)}" aria-hidden="true"></i><span><b>${esc(verb)}</b><small>${esc(line || toolsLine)}</small>${pos < 5 && n >= 10 ? `<em class="intent-n">${esc(fmtVisits(n))} visits this week</em>` : ''}</span></a></li>`).join('')}</ul>`;
}
}
// The network as an orbit (the owner, 2026-10-28: the hero was "ugly af, boring… not easy to understand… nice animated
// The network as an orbit (the owner, 2026-10-28: the hero was "ugly af, boring… not easy to understand… nice animated
@@ -421,7 +435,7 @@ function render({ releaseId = '' } = {}) {
@@ -421,7 +435,7 @@ function render({ releaseId = '' } = {}) {
})) + seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what })))) + seo.jsonLdTag(seo.jsonLd.faq(faqPairs()));
})) + seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what })))) + seo.jsonLdTag(seo.jsonLd.faq(faqPairs()));
const card = social.url('network', 'https://openvibe.network/shared') || 'https://openvibe.network/assets/logo-512.png';
const card = social.url('network', 'https://openvibe.network/shared') || 'https://openvibe.network/assets/logo-512.png';
const summary = summaryOf(catalog);
const summary = summaryOf(catalog);
- const html = lookBoot.apply(fs.readFileSync(SHELL, 'utf8')).replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + summary.body + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog)).replace('<!--OV:CONSTELLATION-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(OPEN.length + 1)).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);
+ const html = lookBoot.apply(fs.readFileSync(SHELL, 'utf8')).replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + summary.body + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog, act.popularity)).replace('<!--OV:CONSTELLATION-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(OPEN.length + 1)).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);
pageCache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };
pageCache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };
return pageCache;
return pageCache;
}
}
@@ -818,6 +818,12 @@ app.get(['/login.html', '/admin.html'], (req, res) => {
@@ -818,6 +818,12 @@ app.get(['/login.html', '/admin.html'], (req, res) => {
// accounts (people, not guests or banned ones) and how many public services answered their last health check (a
// accounts (people, not guests or banned ones) and how many public services answered their last health check (a
// count is left out until at least half of them have been checked once).
// count is left out until at least half of them have been checked once).
require('./home/activity').configure({
require('./home/activity').configure({
+ // Visits per site this week, from the open stats (page views the shared navbar counts): the front door's
+ // intents are ordered by them (plan L43).
+ popularity: async () => {
+ const d = await openStats.collect(7);
+ return d && Array.isArray(d.sites) ? Object.fromEntries(d.sites.map((s) => [s.id, Number(s.pageviews) || 0])) : null;
+ },
accounts: async () => (await db.prepare('SELECT COUNT(*) AS cnt FROM users WHERE COALESCE(is_anon, 0) = 0 AND COALESCE(is_banned, 0) = 0').get()).cnt,
accounts: async () => (await db.prepare('SELECT COUNT(*) AS cnt FROM users WHERE COALESCE(is_anon, 0) = 0 AND COALESCE(is_banned, 0) = 0').get()).cnt,
services: () => {
services: () => {
const live = require('./status/routes').rows(ecosystem).filter((r) => r.exposure && r.exposure.state === 'live');
const live = require('./status/routes').rows(ecosystem).filter((r) => r.exposure && r.exposure.state === 'live');
@@ -152,4 +152,22 @@ assert.ok(html.includes('href="https://openvibe.video/v/9"') && html.includes('<
@@ -152,4 +152,22 @@ assert.ok(html.includes('href="https://openvibe.video/v/9"') && html.includes('<
assert.ok(html.includes('.more [data-more-part]:target{display:block}'), 'a link to a section opens it');
assert.ok(html.includes('.more [data-more-part]:target{display:block}'), 'a link to a section opens it');
}
}
assert.ok(/<b>1,234<\/b><span>accounts/.test(html) && /<b>37<\/b><span>services up <small>of 38 checked/.test(html) && /<b>1<\/b><span>stream live now/.test(html), 'the numbers read render');
assert.ok(/<b>1,234<\/b><span>accounts/.test(html) && /<b>37<\/b><span>services up <small>of 38 checked/.test(html) && /<b>1<\/b><span>stream live now/.test(html), 'the numbers read render');
+// The most visited things to do lead the intents, with this week's visits on the first five (plan L43). One site counts
+// once (Ask Vibe and Chat are both openvibe.chat: Chat carries the number), and no numbers keep the written order.
+{
+ activity._set({ live: [], pastes: [], posts: [], popularity: { live: 59680, 'media-hub': 1615, tools: 232, games: 151, community: 89, chat: 40, work: 3 } });
+ const page = home.render().html;
+ const order = [...page.matchAll(/<a class="intent" href="[^"]*" data-id="([a-z-]+)"/g)].map((m) => m[1]);
+ assert.deepStrictEqual(order.slice(0, 6), ['live', 'video', 'tools', 'games', 'paste', 'chat'], `most visited first: ${order.slice(0, 8).join(', ')}`);
+ assert.match(page, /data-id="live"[\s\S]*?<em class="intent-n">60k visits this week<\/em>/, 'the count says what it counts');
+ assert.match(page, /data-id="video"[\s\S]*?<em class="intent-n">1.6k visits this week<\/em>/);
+ assert.strictEqual([...page.matchAll(/class="intent-n"/g)].length, 5, 'only the first five say a number');
+ assert.ok(order.indexOf('vibe') > order.indexOf('chat'), 'Ask Vibe does not borrow Chat\'s number');
+ assert.match(page, /aria-label="What you can do here, most visited this week first"/);
+ activity._set({ live: [], pastes: [], posts: [] });
+ const plain = home.render().html;
+ const plainOrder = [...plain.matchAll(/<a class="intent" href="[^"]*" data-id="([a-z-]+)"/g)].map((m) => m[1]);
+ assert.deepStrictEqual(plainOrder.slice(0, 3), ['vibe', 'live', 'tools'], 'without numbers, the written order');
+ assert.ok(!plain.includes('class="intent-n"'), 'and no counts');
+}
console.log('home front door: all checks passed');
console.log('home front door: all checks passed');