openvibers/openvibe.live Commits

Internal: analytics summary and budget-gated site-copy endpoint for the Network's shared chrome

OP OpenVibers committed 18 Sept 2026 · 0cd9ef378b · 2 files, +41 −0


Changes

ddiff --git a/server/index.js b/server/index.js
iindex 8c84bfe9..7156cb36 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -573,6 +573,18 @@ app.use('/data/arena', express.static(path.resolve(process.env.ARENA_IMAGE_PATH
app.post('/internal/media-webhook', require('./media-proxy/webhook'));
// Internal (server-to-server) routes — allow openvibe.network to call into this service
++// Summary numbers for the Network's navigation service (ordering sites by real use).
++// Internal key only; returns totals, never rows.
++app.get('/internal/analytics-summary', (req, res) => {
++ const key = req.headers['x-internal-key'];
++ if (!config.internalApiKey || !key || key !== config.internalApiKey) return res.status(401).json({ ok: false });
++ try {
++ const days = Math.min(parseInt(req.query.days, 10) || 7, 90);
++ const st = analytics.getStats({ days }) || {};
++ res.json({ ok: true, summary: st.summary || {} });
++ } catch (err) { res.status(500).json({ ok: false, error: err.message }); }
++});
++
app.use('/internal', require('./internal/routes'));
ddiff --git a/server/internal/routes.js b/server/internal/routes.js
iindex a0166117..e9b147e1 100644
---- a/server/internal/routes.js
++++ b/server/internal/routes.js
@@@ -15,6 +15,35 @@ function requireInternalKey(req, res, next) {
router.use(requireInternalKey);
++// ── Site copy for the shared footer (OpenVibe.Network → here, once a day) ─────────────
++// The Network sends facts about each site plus an allow-list of link ids; the model writes a
++// short blurb per site and PICKS ids from that list. It never returns URLs or markup, so nothing
++// it says can become a link or a tag on another site. Gated by the shared AI budget.
++let _siteCopyBusy = false;
++router.post('/ai/site-copy', async (req, res) => {
++ if (_siteCopyBusy) return res.status(429).json({ ok: false, error: 'busy' });
++ const sites = Array.isArray(req.body?.sites) ? req.body.sites.slice(0, 24) : [];
++ const links = Array.isArray(req.body?.links) ? req.body.links.slice(0, 80) : [];
++ if (!sites.length) return res.status(400).json({ ok: false, error: 'sites required' });
++ const clean = (v, n) => String(v == null ? '' : v).replace(/[<>]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, n);
++ const facts = sites.map(x => ({ id: clean(x.id, 40), name: clean(x.name, 60), what: clean(x.what, 400), popular: (Array.isArray(x.popular) ? x.popular : []).slice(0, 8).map(v => clean(v, 60)) }));
++ const linkList = links.map(l => ({ id: clean(l.id, 60), name: clean(l.name, 60), about: clean(l.about, 120) }));
++ _siteCopyBusy = true;
++ try {
++ const llm = require('../ai/llm');
++ const out = await llm.complete({
++ role: 'summary', kind: 'site_copy', source: 'network-footer', maxTokens: 2200, temperature: 0.7, timeoutMs: 60000,
++ system: 'You write footer copy for OpenVibe, an open source, community-run network of sites (live streaming, online tools, community pastes, games, media). Voice: plain, confident, specific, a little playful. Never claim anything is free, costs $0, or has no ads. No hype words, no emoji, no markup, no URLs. For each site write one blurb of at most 150 characters saying what a visitor can do there right now, and choose 4 link ids from the provided list that a visitor of that site would most likely want next (prefer other sites and popular tools; never the site itself).',
++ user: JSON.stringify({ sites: facts, links: linkList }),
++ json: { name: 'site_copy', strict: true, schema: { type: 'object', additionalProperties: false, required: ['sites'], properties: { sites: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['id', 'blurb', 'picks'], properties: { id: { type: 'string' }, blurb: { type: 'string' }, picks: { type: 'array', items: { type: 'string' } } } } } } } },
++ });
++ if (!out || !out.json || !Array.isArray(out.json.sites)) return res.status(503).json({ ok: false, error: 'AI unavailable or over budget' });
++ res.json({ ok: true, model: out.model, sites: out.json.sites });
++ } catch (err) {
++ res.status(502).json({ ok: false, error: err.message });
++ } finally { _siteCopyBusy = false; }
++});
++
router.post('/url-registry/refresh', async (req, res) => {
try {
await config.refreshRegistry();