openvibers/openvibe.live Commits

Home: the paste chart reads Community's series (#84)

OP OpenVibers committed 10 Oct 2026 · adef2108f6 · 2 files, +98 −3

The home "Pastes" chart (stats-for-nerds, `GET /api/home/stats/series/pastes`) now reads OpenVibe.Community's daily series (`/api/pastes/admin/stats/series`, OpenVibe.Community#50) as staff. It used to read Media's, which comes from Media's frozen legacy `pastes` table: that stopped at the move to Community on 2026-09-22, so the chart has been flat since. The paste total on the same card already came from Community.

VODs, clips and hours still come from Media. A Community that does not answer is a 503 ("Paste stats are unavailable right now"), as Media's are.

This also unblocks plan T4's Media cleanup: Media's `pastes` table is no longer read for Live.

New `test/home-series.test.js`: pastes come from Community with the window and staff act; Community down gives a 503; VODs still come from Media.

Changes

ddiff --git a/server/home/routes.js b/server/home/routes.js
iindex 78caadc6..cf388ebf 100644
---- a/server/home/routes.js
++++ b/server/home/routes.js
@@@ -164,8 +164,9 @@ async function heroSlogans() {
const lookups = require('../media-proxy/lookups');
// GET /api/home/stats/series/:metric?days=30 — daily values behind a hero stat.
--// VODs, clips, pastes and archived hours live in OpenVibe.Media; the local tables stopped at the split.
--const MEDIA_SERIES = new Set(['vods', 'clips', 'pastes', 'hours']);
++// VODs, clips and archived hours live in OpenVibe.Media; pastes in OpenVibe.Community (Media's copy stopped
++// at the move); the local tables stopped at the split.
++const MEDIA_SERIES = new Set(['vods', 'clips', 'hours']);
const _seriesCache = new Map(); // `${metric}:${days}` → { at, data }
router.get('/stats/series/:metric', async (req, res) => {
const metric = String(req.params.metric);
@@@ -181,10 +182,16 @@ router.get('/stats/series/:metric', async (req, res) => {
if (m && Array.isArray(m.points)) series = { ...m, kind: 'count', peak: Math.max(0, ...m.points.map(p => p.value)), source: 'media' };
} catch (err) { console.warn('[Home] Media series unavailable:', err.message); }
if (!series) return res.status(503).json({ error: 'Media stats are unavailable right now' });
++ } else if (metric === 'pastes') {
++ try {
++ const c = await require('../pastes-client').request('GET', '/admin/stats/series', { query: { days }, act: { staff: true }, timeoutMs: 5000 });
++ if (c && Array.isArray(c.points)) series = { ...c, kind: 'count', peak: Math.max(0, ...c.points.map(p => p.value)), source: 'community' };
++ } catch (err) { console.warn('[Home] Community paste series unavailable:', err.message); }
++ if (!series) return res.status(503).json({ error: 'Paste stats are unavailable right now' });
} else {
series = await db.getReadingSeries(metric, days) || await db.getHomeStatSeries(metric, days);
}
-- if (!series) return res.status(404).json({ error: 'Unknown metric', metrics: [...db.HOME_SERIES_KEYS, ...MEDIA_SERIES] });
++ if (!series) return res.status(404).json({ error: 'Unknown metric', metrics: [...db.HOME_SERIES_KEYS, ...MEDIA_SERIES, 'pastes'] });
if (_seriesCache.size > 200) _seriesCache.clear();
_seriesCache.set(key, { at: Date.now(), data: series });
res.json(series);
ddiff --git a/test/home-series.test.js b/test/home-series.test.js
nnew file mode 100644
iindex 00000000..72a8a6be
---- /dev/null
++++ b/test/home-series.test.js
@@@ -0,0 +1,88 @@
++/**
++ * GET /api/home/stats/series/:metric — where each hero chart's daily values come from:
++ * - vods, clips and hours from OpenVibe.Media;
++ * - pastes from OpenVibe.Community (/api/pastes/admin/stats/series, as staff): Media's legacy copy
++ * stopped at the move, so its chart was flat;
++ * - a source that does not answer is a 503, never a made-up flat line.
++ *
++ * node test/home-series.test.js
++ */
++'use strict';
++const assert = require('assert');
++const http = require('http');
++
++process.env.NODE_ENV = 'test';
++const quiet = console.log;
++console.log = (...a) => { if (!/^\[/.test(String(a[0]))) quiet(...a); };
++console.warn = () => {};
++
++const db = require('../server/db/database');
++
++const POINTS = [{ day: '2026-10-09', value: 2 }, { day: '2026-10-10', value: 5 }];
++const calls = { media: [], community: [] };
++let communityDown = false;
++const media = require('../server/media-client');
++media.request = async (method, p) => { calls.media.push(p); return { metric: 'vods', days: 2, points: POINTS, total: 7 }; };
++const pastesClient = require('../server/pastes-client');
++pastesClient.request = async (method, p, opts = {}) => {
++ calls.community.push({ method, p, opts });
++ if (communityDown) throw new Error('down');
++ return { metric: 'pastes', days: Number(opts.query && opts.query.days), points: POINTS, total: 7, before: 40, prev_total: 3 };
++};
++
++const express = require('express');
++const app = express();
++app.use('/api/home', require('../server/home/routes'));
++
++function get(p) {
++ return new Promise((resolve, reject) => {
++ http.get(base + p, (res) => {
++ let body = '';
++ res.on('data', (c) => { body += c; });
++ res.on('end', () => resolve({ status: res.statusCode, json: (() => { try { return JSON.parse(body); } catch { return null; } })() }));
++ }).on('error', reject);
++ });
++}
++
++let base;
++let failures = 0;
++async function check(name, fn) {
++ try { await fn(); quiet(` ✓ ${name}`); } catch (err) { failures++; quiet(` ✗ ${name}\n${err.stack}`); }
++}
++
++(async () => {
++ await db.initDb();
++ const server = app.listen(0, '127.0.0.1');
++ await new Promise((r) => server.once('listening', r));
++ base = `http://127.0.0.1:${server.address().port}`;
++
++ await check('pastes come from Community as staff, with the requested window', async () => {
++ const r = await get('/api/home/stats/series/pastes?days=14');
++ assert.strictEqual(r.status, 200);
++ assert.deepStrictEqual([r.json.metric, r.json.source, r.json.kind, r.json.peak, r.json.total], ['pastes', 'community', 'count', 5, 7]);
++ const c = calls.community.pop();
++ assert.deepStrictEqual([c.method, c.p, c.opts.query, c.opts.act], ['GET', '/admin/stats/series', { days: 14 }, { staff: true }]);
++ assert.ok(!calls.media.some((p) => p.includes('/pastes')), 'Media is not asked for pastes');
++ });
++
++ await check('a Community that does not answer is a 503', async () => {
++ communityDown = true;
++ try {
++ const r = await get('/api/home/stats/series/pastes?days=15');
++ assert.strictEqual(r.status, 503);
++ } finally { communityDown = false; }
++ });
++
++ await check('VODs still come from Media', async () => {
++ const r = await get('/api/home/stats/series/vods?days=2');
++ assert.strictEqual(r.status, 200);
++ assert.strictEqual(r.json.source, 'media');
++ assert.ok(calls.media.includes('/stats/series/vods?days=2'));
++ });
++
++ server.close();
++ await db.close().catch(() => {});
++ if (failures) { quiet(`home-series: ${failures} failed`); process.exit(1); }
++ quiet('home-series: passed');
++ process.exit(0);
++})().catch((err) => { quiet(err.stack); process.exit(1); });