openvibers/openvibe.network Commits
GET /api/frame/pulse: the network right now for the shared footer's live line (streams live, people watching, open sites, last week's page views) (#345)
Changes
@@@ -3,6 +3,7 @@// OpenVibe Frame data: what the navbar and footer of every site show.//// GET /api/frame?host=<hostname> public, CORS *, max-age=600, ETag++// GET /api/frame/pulse public, CORS *, max-age=30: { live_now, watching, sites, views_7d, updated }// → { 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@@@ -184,6 +185,34 @@ async function createFrameService(db, config, analytics, { privateKey = null, isres.send(e.body);});++ // GET /api/frame/pulse — the network right now, for the footer's live line (openvibe-shared footer.js; owner,++ // 2026-10-27: "update and improve the footer so it feels more alive and has some stats"): streams live on++ // OpenVibe.Live and the people watching them (Live's public list), the open sites, and last week's page views across++ // them (the ranking's totals). Numbers only; public, CORS *, cached 30 s. A part Live does not answer is null.++ let pulse = null;++ let pulseAt = 0;++ let pulseInflight = null;++ async function readPulse() {++ const d = await getJson(`${svcUrl('live', 'http://127.0.0.1:3000')}/api/streams`);++ const streams = d && Array.isArray(d.streams) ? d.streams : null;++ const views = Object.entries(rank.scores || {}).filter(([k]) => !k.includes(':')).reduce((a, [, n]) => a + (Number(n) || 0), 0);++ return {++ live_now: streams ? streams.length : null,++ watching: streams ? streams.reduce((a, st) => a + Math.max(0, Number(st.total_viewer_count != null ? st.total_viewer_count : st.viewer_count) || 0), 0) : null,++ sites: orderedOpen().length,++ views_7d: views > 0 ? Math.round(views) : null,++ updated: new Date().toISOString(),++ };++ }++ router.get('/pulse', async (req, res) => {++ if (!pulse || Date.now() - pulseAt > 30_000) {++ pulseInflight = pulseInflight || readPulse().then((p) => { pulse = p; pulseAt = Date.now(); }).catch(() => {}).finally(() => { pulseInflight = null; });++ await pulseInflight;++ }++ res.set({ 'Access-Control-Allow-Origin': '*', 'Cross-Origin-Resource-Policy': 'cross-origin', 'Cache-Control': 'public, max-age=30, stale-while-revalidate=120', 'Content-Type': 'application/json; charset=utf-8' });++ res.send(JSON.stringify(pulse || { live_now: null, watching: null, sites: orderedOpen().length, views_7d: null, updated: null }));++ });++// POST /api/frame/hit — one anonymous count for the calling page's host. The host comes from the// browser-set Origin header (never the body) and must be one of ours or a registered tool domain.const recent = new Map(); // ip → [count, windowStart]: 40/min is plenty for a person, useless for stuffing@@@ -108,6 +108,29 @@ for (let i = 0; i < 6; i++) await ins.run(1, 'tools', 'dns');assert.equal(r.headers.get('access-control-allow-origin'), '*');srv.close();}++ // The footer's live line: streams live and people watching (Live's public list), open sites, last week's views.++ {++ const liveList = http.createServer((req, res) => {++ res.setHeader('content-type', 'application/json');++ res.end(JSON.stringify(req.url === '/api/streams' ? { streams: [{ id: 1, viewer_count: 3, total_viewer_count: 5 }, { id: 2, viewer_count: 2 }] } : {}));++ });++ await new Promise(r => liveList.listen(0, '127.0.0.1', r));++ const pulseSvc = await createFrameService(db, { services: { live: { internalUrl: `http://127.0.0.1:${liveList.address().port}` } } }, null);++ const express = require('express');++ const app = express(); app.use('/api/frame', pulseSvc.router);++ const srv = await new Promise(r => { const s2 = app.listen(0, '127.0.0.1', () => r(s2)); });++ const r = await fetch(`http://127.0.0.1:${srv.address().port}/api/frame/pulse`);++ assert.equal(r.status, 200);++ assert.equal(r.headers.get('access-control-allow-origin'), '*');++ assert.match(r.headers.get('cache-control'), /max-age=30/);++ const j = await r.json();++ assert.deepStrictEqual([j.live_now, j.watching, j.sites], [2, 7, pulseSvc.ranking().order.length]);++ assert.ok(j.views_7d > 0 && j.updated, 'last week\'s views from the ranking');++ liveList.close();++ const down = await (await fetch(`http://127.0.0.1:${srv.address().port}/api/frame/pulse`)).json();++ assert.equal(down.live_now, 2, 'answered from the last read for 30 s');++ srv.close();++ }live.close(); ai.close();console.log('frame service: all checks passed');})().catch(e => { console.error(e); process.exit(1); });
@@ -3,6 +3,7 @@
@@ -3,6 +3,7 @@
// OpenVibe Frame data: what the navbar and footer of every site show.
// OpenVibe Frame data: what the navbar and footer of every site show.
//
//
// GET /api/frame?host=<hostname> public, CORS *, max-age=600, ETag
// GET /api/frame?host=<hostname> public, CORS *, max-age=600, ETag
+// GET /api/frame/pulse public, CORS *, max-age=30: { live_now, watching, sites, views_7d, updated }
// → { updated, nav: [{ id, name, url, icon, tagline, group, of? }], open sites, most used first (of: a product
// → { 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)
// on its own domain, served by that site)
// groups: [{ id, name }], the launcher's sections, in order
// groups: [{ id, name }], the launcher's sections, in order
@@ -184,6 +185,34 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -184,6 +185,34 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
res.send(e.body);
res.send(e.body);
});
});
+ // GET /api/frame/pulse — the network right now, for the footer's live line (openvibe-shared footer.js; owner,
+ // 2026-10-27: "update and improve the footer so it feels more alive and has some stats"): streams live on
+ // OpenVibe.Live and the people watching them (Live's public list), the open sites, and last week's page views across
+ // them (the ranking's totals). Numbers only; public, CORS *, cached 30 s. A part Live does not answer is null.
+ let pulse = null;
+ let pulseAt = 0;
+ let pulseInflight = null;
+ async function readPulse() {
+ const d = await getJson(`${svcUrl('live', 'http://127.0.0.1:3000')}/api/streams`);
+ const streams = d && Array.isArray(d.streams) ? d.streams : null;
+ const views = Object.entries(rank.scores || {}).filter(([k]) => !k.includes(':')).reduce((a, [, n]) => a + (Number(n) || 0), 0);
+ return {
+ live_now: streams ? streams.length : null,
+ watching: streams ? streams.reduce((a, st) => a + Math.max(0, Number(st.total_viewer_count != null ? st.total_viewer_count : st.viewer_count) || 0), 0) : null,
+ sites: orderedOpen().length,
+ views_7d: views > 0 ? Math.round(views) : null,
+ updated: new Date().toISOString(),
+ };
+ }
+ router.get('/pulse', async (req, res) => {
+ if (!pulse || Date.now() - pulseAt > 30_000) {
+ pulseInflight = pulseInflight || readPulse().then((p) => { pulse = p; pulseAt = Date.now(); }).catch(() => {}).finally(() => { pulseInflight = null; });
+ await pulseInflight;
+ }
+ res.set({ 'Access-Control-Allow-Origin': '*', 'Cross-Origin-Resource-Policy': 'cross-origin', 'Cache-Control': 'public, max-age=30, stale-while-revalidate=120', 'Content-Type': 'application/json; charset=utf-8' });
+ res.send(JSON.stringify(pulse || { live_now: null, watching: null, sites: orderedOpen().length, views_7d: null, updated: null }));
+ });
+
// POST /api/frame/hit — one anonymous count for the calling page's host. The host comes from the
// POST /api/frame/hit — one anonymous count for the calling page's host. The host comes from the
// browser-set Origin header (never the body) and must be one of ours or a registered tool domain.
// browser-set Origin header (never the body) and must be one of ours or a registered tool domain.
const recent = new Map(); // ip → [count, windowStart]: 40/min is plenty for a person, useless for stuffing
const recent = new Map(); // ip → [count, windowStart]: 40/min is plenty for a person, useless for stuffing
@@ -108,6 +108,29 @@ for (let i = 0; i < 6; i++) await ins.run(1, 'tools', 'dns');
@@ -108,6 +108,29 @@ for (let i = 0; i < 6; i++) await ins.run(1, 'tools', 'dns');
assert.equal(r.headers.get('access-control-allow-origin'), '*');
assert.equal(r.headers.get('access-control-allow-origin'), '*');
srv.close();
srv.close();
}
}
+ // The footer's live line: streams live and people watching (Live's public list), open sites, last week's views.
+ {
+ const liveList = http.createServer((req, res) => {
+ res.setHeader('content-type', 'application/json');
+ res.end(JSON.stringify(req.url === '/api/streams' ? { streams: [{ id: 1, viewer_count: 3, total_viewer_count: 5 }, { id: 2, viewer_count: 2 }] } : {}));
+ });
+ await new Promise(r => liveList.listen(0, '127.0.0.1', r));
+ const pulseSvc = await createFrameService(db, { services: { live: { internalUrl: `http://127.0.0.1:${liveList.address().port}` } } }, null);
+ const express = require('express');
+ const app = express(); app.use('/api/frame', pulseSvc.router);
+ const srv = await new Promise(r => { const s2 = app.listen(0, '127.0.0.1', () => r(s2)); });
+ const r = await fetch(`http://127.0.0.1:${srv.address().port}/api/frame/pulse`);
+ assert.equal(r.status, 200);
+ assert.equal(r.headers.get('access-control-allow-origin'), '*');
+ assert.match(r.headers.get('cache-control'), /max-age=30/);
+ const j = await r.json();
+ assert.deepStrictEqual([j.live_now, j.watching, j.sites], [2, 7, pulseSvc.ranking().order.length]);
+ assert.ok(j.views_7d > 0 && j.updated, 'last week\'s views from the ranking');
+ liveList.close();
+ const down = await (await fetch(`http://127.0.0.1:${srv.address().port}/api/frame/pulse`)).json();
+ assert.equal(down.live_now, 2, 'answered from the last read for 30 s');
+ srv.close();
+ }
live.close(); ai.close();
live.close(); ai.close();
console.log('frame service: all checks passed');
console.log('frame service: all checks passed');
})().catch(e => { console.error(e); process.exit(1); });
})().catch(e => { console.error(e); process.exit(1); });