openvibers/openvibe.live Commits
Internal: analytics summary and budget-gated site-copy endpoint for the Network's shared chrome
Changes
@@@ -573,6 +573,18 @@ app.use('/data/arena', express.static(path.resolve(process.env.ARENA_IMAGE_PATHapp.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'));@@@ -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();
@@ -573,6 +573,18 @@ app.use('/data/arena', express.static(path.resolve(process.env.ARENA_IMAGE_PATH
@@ -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'));
app.post('/internal/media-webhook', require('./media-proxy/webhook'));
// Internal (server-to-server) routes — allow openvibe.network to call into this service
// 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'));
app.use('/internal', require('./internal/routes'));
@@ -15,6 +15,35 @@ function requireInternalKey(req, res, next) {
@@ -15,6 +15,35 @@ function requireInternalKey(req, res, next) {
router.use(requireInternalKey);
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) => {
router.post('/url-registry/refresh', async (req, res) => {
try {
try {
await config.refreshRegistry();
await config.refreshRegistry();