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)

OP OpenVibers committed 17 days ago · c2fe49fee4 · 2 files, +52 −0

Changes

ddiff --git a/server/frame/service.js b/server/frame/service.js
iindex dd41f760..1c7b643e 100644
---- a/server/frame/service.js
++++ b/server/frame/service.js
@@@ -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, is
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
// 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
ddiff --git a/test/frame.test.js b/test/frame.test.js
iindex c9169e6f..b46d2c5d 100644
---- a/test/frame.test.js
++++ b/test/frame.test.js
@@@ -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); });