openvibers/openvibe.network Commits
Theme API: GET /api/themes/seasons (what stands in for the default theme when, and now) and each seasonal theme in the catalog names its season (#325)
Changes
@@@ -79,14 +79,32 @@ router.get('/', async (req, res) => {params.push(Math.min(parseInt(limit, 10) || 100, 500));const themes = await db.prepare(sql).all(...params);++ const seasonOf = new Map(seasonList().map((s) => [s.theme, s]));for (const t of themes) {try { t.variables = JSON.parse(t.variables); } catch { t.variables = {}; }try { t.preview_colors = JSON.parse(t.preview_colors); } catch { t.preview_colors = null; }try { t.tags = JSON.parse(t.tags); } catch { t.tags = []; }++ const s = seasonOf.get(t.id) || seasonOf.get(t.slug);++ if (s) t.season = { id: s.id, name: s.name, from: s.from, to: s.to };}res.json({ themes });});++// ── Seasons: which theme stands in for the default theme when (openvibe-shared builtin-themes SEASONS) ──++// GET /api/themes/seasons[?date=YYYY-MM-DD] → { seasons: [{ id, name, theme, from, to }], now: { id, name, theme } | null }++// from/to are MM-DD, both days included, in the visitor's own calendar (the browser decides; `now` here is for the date++// given, else today in UTC). Anyone on the default theme who has not turned seasonal themes off (display `seasonal`) sees++// the season's theme, and every OpenVibe mark wears the season (theme-loader.js, ov-mark.js).++function seasonList() { return (require('openvibe-shared/builtin-themes').SEASONS || []).map((s) => ({ id: s.id, name: s.name, theme: s.theme, from: s.from, to: s.to })); }++function seasonOn(md, list) { return list.find((s) => (s.from <= s.to ? md >= s.from && md <= s.to : md >= s.from || md <= s.to)) || null; }++router.get('/seasons', (req, res) => {++ const list = seasonList();++ const asked = /^\d{4}-(\d{2})-(\d{2})$/.exec(String(req.query.date || ''));++ const md = asked ? `${asked[1]}-${asked[2]}` : new Date().toISOString().slice(5, 10);++ const now = seasonOn(md, list);++ res.set('Cache-Control', 'public, max-age=3600').json({ seasons: list, now: now ? { id: now.id, name: now.name, theme: now.theme } : null });++});++// ── Get User's Active Theme ──────────────────────────────────// A request with no credentials at all is a guest asking "do I have a theme?": answer "no" with// a 200 so every page view by a guest does not log a failed request in the console.@@@ -109,6 +109,17 @@ const token = async (id) => signToken(await db.prepare('SELECT * FROM users WHERr = await call('PUT', '/api/themes/me/display', { as: 2, body: { seasonal: 'maybe' } });assert.ok(!('seasonal' in (r.body.display || {})), 'only on or off');++ // ── Seasons: what stands in for the default theme when, for apps and the SDK ──++ r = await call('GET', '/api/themes/seasons?date=2026-10-25');++ assert.deepStrictEqual(r.body.seasons.map((x) => [x.id, x.theme, x.from, x.to]), [['halloween', 'halloween', '10-01', '10-31'], ['winter', 'frost', '12-01', '12-31']]);++ assert.deepStrictEqual(r.body.now, { id: 'halloween', name: 'Halloween', theme: 'halloween' });++ assert.strictEqual((await call('GET', '/api/themes/seasons?date=2026-11-05')).body.now, null);++ assert.strictEqual((await call('GET', '/api/themes/seasons?date=2026-12-24')).body.now.id, 'winter');++ const cat = (await call('GET', '/api/themes')).body.themes;++ const pumpkin = cat.find((t) => t.id === 'halloween');++ assert.ok(!pumpkin || (pumpkin.season && pumpkin.season.id === 'halloween'), 'a seasonal theme in the catalog says its season');++ assert.ok(cat.filter((t) => t.season).every((t) => ['halloween', 'frost'].includes(t.id)), 'and only those');++server.close();fs.rmSync(dir, { recursive: true, force: true });console.log('themes review: all checks passed');
@@ -79,14 +79,32 @@ router.get('/', async (req, res) => {
@@ -79,14 +79,32 @@ router.get('/', async (req, res) => {
params.push(Math.min(parseInt(limit, 10) || 100, 500));
params.push(Math.min(parseInt(limit, 10) || 100, 500));
const themes = await db.prepare(sql).all(...params);
const themes = await db.prepare(sql).all(...params);
+ const seasonOf = new Map(seasonList().map((s) => [s.theme, s]));
for (const t of themes) {
for (const t of themes) {
try { t.variables = JSON.parse(t.variables); } catch { t.variables = {}; }
try { t.variables = JSON.parse(t.variables); } catch { t.variables = {}; }
try { t.preview_colors = JSON.parse(t.preview_colors); } catch { t.preview_colors = null; }
try { t.preview_colors = JSON.parse(t.preview_colors); } catch { t.preview_colors = null; }
try { t.tags = JSON.parse(t.tags); } catch { t.tags = []; }
try { t.tags = JSON.parse(t.tags); } catch { t.tags = []; }
+ const s = seasonOf.get(t.id) || seasonOf.get(t.slug);
+ if (s) t.season = { id: s.id, name: s.name, from: s.from, to: s.to };
}
}
res.json({ themes });
res.json({ themes });
});
});
+// ── Seasons: which theme stands in for the default theme when (openvibe-shared builtin-themes SEASONS) ──
+// GET /api/themes/seasons[?date=YYYY-MM-DD] → { seasons: [{ id, name, theme, from, to }], now: { id, name, theme } | null }
+// from/to are MM-DD, both days included, in the visitor's own calendar (the browser decides; `now` here is for the date
+// given, else today in UTC). Anyone on the default theme who has not turned seasonal themes off (display `seasonal`) sees
+// the season's theme, and every OpenVibe mark wears the season (theme-loader.js, ov-mark.js).
+function seasonList() { return (require('openvibe-shared/builtin-themes').SEASONS || []).map((s) => ({ id: s.id, name: s.name, theme: s.theme, from: s.from, to: s.to })); }
+function seasonOn(md, list) { return list.find((s) => (s.from <= s.to ? md >= s.from && md <= s.to : md >= s.from || md <= s.to)) || null; }
+router.get('/seasons', (req, res) => {
+ const list = seasonList();
+ const asked = /^\d{4}-(\d{2})-(\d{2})$/.exec(String(req.query.date || ''));
+ const md = asked ? `${asked[1]}-${asked[2]}` : new Date().toISOString().slice(5, 10);
+ const now = seasonOn(md, list);
+ res.set('Cache-Control', 'public, max-age=3600').json({ seasons: list, now: now ? { id: now.id, name: now.name, theme: now.theme } : null });
+});
+
// ── Get User's Active Theme ──────────────────────────────────
// ── Get User's Active Theme ──────────────────────────────────
// A request with no credentials at all is a guest asking "do I have a theme?": answer "no" with
// A request with no credentials at all is a guest asking "do I have a theme?": answer "no" with
// a 200 so every page view by a guest does not log a failed request in the console.
// a 200 so every page view by a guest does not log a failed request in the console.
@@ -109,6 +109,17 @@ const token = async (id) => signToken(await db.prepare('SELECT * FROM users WHER
@@ -109,6 +109,17 @@ const token = async (id) => signToken(await db.prepare('SELECT * FROM users WHER
r = await call('PUT', '/api/themes/me/display', { as: 2, body: { seasonal: 'maybe' } });
r = await call('PUT', '/api/themes/me/display', { as: 2, body: { seasonal: 'maybe' } });
assert.ok(!('seasonal' in (r.body.display || {})), 'only on or off');
assert.ok(!('seasonal' in (r.body.display || {})), 'only on or off');
+ // ── Seasons: what stands in for the default theme when, for apps and the SDK ──
+ r = await call('GET', '/api/themes/seasons?date=2026-10-25');
+ assert.deepStrictEqual(r.body.seasons.map((x) => [x.id, x.theme, x.from, x.to]), [['halloween', 'halloween', '10-01', '10-31'], ['winter', 'frost', '12-01', '12-31']]);
+ assert.deepStrictEqual(r.body.now, { id: 'halloween', name: 'Halloween', theme: 'halloween' });
+ assert.strictEqual((await call('GET', '/api/themes/seasons?date=2026-11-05')).body.now, null);
+ assert.strictEqual((await call('GET', '/api/themes/seasons?date=2026-12-24')).body.now.id, 'winter');
+ const cat = (await call('GET', '/api/themes')).body.themes;
+ const pumpkin = cat.find((t) => t.id === 'halloween');
+ assert.ok(!pumpkin || (pumpkin.season && pumpkin.season.id === 'halloween'), 'a seasonal theme in the catalog says its season');
+ assert.ok(cat.filter((t) => t.season).every((t) => ['halloween', 'frost'].includes(t.id)), 'and only those');
+
server.close();
server.close();
fs.rmSync(dir, { recursive: true, force: true });
fs.rmSync(dir, { recursive: true, force: true });
console.log('themes review: all checks passed');
console.log('themes review: all checks passed');