openvibers/openvibe.network Commits

Network: the retired 'campfire' theme becomes ember (59 people got a 404 on every page) (#210)

OP OpenVibers committed 29 days ago · 93ea978a54 · 5 files, +24 −2

59 people's saved theme was `campfire`, which came from the August import of Live's theme system and was never a Network theme. So every page they opened asked `GET /api/themes/campfire`, got a 404 and logged a console error. A signed-in browser check of Live found it on the probe account.

- **Migration `0024_retired_theme_campfire`** (`migrate` phase, idempotent): moves those preferences to `ember`, the closest built-in (warm, dark), only when `ember` exists. `updated_at` uses `ov_now()`.
- **`RETIRED_THEMES` in `server/themes/routes.js`:** `GET /api/themes/campfire` answers a 301 to `/api/themes/ember`, for pages and caches that still have the old name. An unknown name is still a 404.

**Tests:**
- `themes-review.test.js` checks the 301 and its Location, that following it gives the ember theme, and that an unknown slug is a 404.
- `agent-schema` and `user-owned-trust` list the new migration among those they apply.
- The 14 files `ov test` maps pass.

Changes

ddiff --git a/migrations/0024_retired_theme_campfire.sql b/migrations/0024_retired_theme_campfire.sql
nnew file mode 100644
iindex 00000000..a935fc66
---- /dev/null
++++ b/migrations/0024_retired_theme_campfire.sql
@@@ -0,0 +1,7 @@
++-- phase: migrate
++-- Preferences that name a theme which does not exist: 'campfire' came from the August import of Live's theme system and
++-- was never a Network theme, so 59 people's pages asked GET /api/themes/campfire and got a 404 on every page
++-- (2026-10-15). They get the closest built-in, 'ember' (warm, dark). GET /api/themes/campfire also redirects there
++-- (server/themes/routes.js RETIRED_THEMES) for pages that still have the old name cached. Idempotent.
++UPDATE user_preferences SET theme_id = 'ember', updated_at = ov_now()
++ WHERE theme_id = 'campfire' AND EXISTS (SELECT 1 FROM themes WHERE slug = 'ember');
ddiff --git a/server/themes/routes.js b/server/themes/routes.js
iindex 73e9db27..cf9819a6 100644
---- a/server/themes/routes.js
++++ b/server/themes/routes.js
@@@ -133,10 +133,16 @@ router.get('/me/submissions', requireAuth, async (req, res) => {
res.set('Cache-Control', cache.htmlHeaders({ private: true })).json({ themes });
});
++// Themes that no longer exist but are still named in old preferences or cached pages, and the built-in each became.
++// 'campfire' came from the August import of Live's theme system and was never a Network theme (59 people had it,
++// 2026-10-15; migrations/0024 moves their preferences): a request for it lands on 'ember' instead of a 404 on every page.
++const RETIRED_THEMES = Object.freeze({ campfire: 'ember' });
++
// ── Get Theme by ID or Slug (approved themes; your own at any status) ──
router.get('/:idOrSlug', optionalAuth, async (req, res) => {
const db = await getDb(req);
ensureReview(db);
++ if (Object.hasOwn(RETIRED_THEMES, req.params.idOrSlug)) return res.redirect(301, `${req.baseUrl}/${RETIRED_THEMES[req.params.idOrSlug]}`);
const theme = await db.prepare('SELECT * FROM themes WHERE id = ? OR slug = ?').get(req.params.idOrSlug, req.params.idOrSlug);
if (!visibleTo(theme, req.user && (req.user.sub || req.user.id))) return res.status(404).json({ error: 'Theme not found' });
res.json({ theme: parseTheme(theme) });
ddiff --git a/test/agent-schema.test.js b/test/agent-schema.test.js
iindex 05ce6628..bdf2f9d1 100644
---- a/test/agent-schema.test.js
++++ b/test/agent-schema.test.js
@@@ -44,7 +44,7 @@ const quiet = { log() {}, warn() {}, error() {} };
// The real directory: 0016 applies on top of 0015, once.
const first = await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }));
-- assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0016', 'agents', 'expand'], ['0017', 'agent_grants', 'expand'], ['0018', 'confirmations', 'expand'], ['0019', 'agent_budgets', 'expand'], ['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate'], ['0022', 'public_profiles', 'expand'], ['0023', 'project_secrets', 'expand']]);
++ assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0016', 'agents', 'expand'], ['0017', 'agent_grants', 'expand'], ['0018', 'confirmations', 'expand'], ['0019', 'agent_budgets', 'expand'], ['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate'], ['0022', 'public_profiles', 'expand'], ['0023', 'project_secrets', 'expand'], ['0024', 'retired_theme_campfire', 'migrate']]);
assert.ok(await hasAgents());
assert.deepStrictEqual((await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');
// Expand only, IF NOT EXISTS throughout: running the file again changes nothing and fails nothing.
ddiff --git a/test/themes-review.test.js b/test/themes-review.test.js
iindex 52dc601b..8f25fb98 100644
---- a/test/themes-review.test.js
++++ b/test/themes-review.test.js
@@@ -92,6 +92,15 @@ const token = async (id) => signToken(await db.prepare('SELECT * FROM users WHER
for (let i = 0; i < 5; i++) await call('POST', '/api/themes', { as: 2, body: { name: `Try ${i}`, slug: `try-${i}`, variables: good } });
assert.strictEqual((await call('POST', '/api/themes', { as: 2, body: { name: 'One more', slug: 'one-more', variables: good } })).status, 429, 'at most five waiting');
++ // A retired theme name (Live's imported 'campfire', never a Network theme) answers with the built-in it became, not a
++ // 404 on every page of the people whose preferences still name it.
++ await db.prepare("INSERT INTO themes (id, name, slug, mode, variables, is_builtin, is_public) VALUES ('ember', 'Ember', 'ember', 'dark', '{\"--accent\":\"#f97316\"}', 1, 1) ON CONFLICT DO NOTHING").run();
++ const old = await fetch(`${base}/api/themes/campfire`, { redirect: 'manual' });
++ assert.deepStrictEqual([old.status, old.headers.get('location')], [301, '/api/themes/ember']);
++ r = await call('GET', '/api/themes/campfire');
++ assert.deepStrictEqual([r.status, r.body.theme.slug], [200, 'ember'], 'followed, it is the ember theme');
++ assert.strictEqual((await call('GET', '/api/themes/no-such-theme')).status, 404, 'an unknown name is still a 404');
++
server.close();
fs.rmSync(dir, { recursive: true, force: true });
console.log('themes review: all checks passed');
ddiff --git a/test/user-owned-trust.test.js b/test/user-owned-trust.test.js
iindex 82e03f3c..d05a6cb0 100644
---- a/test/user-owned-trust.test.js
++++ b/test/user-owned-trust.test.js
@@@ -52,7 +52,7 @@ const quiet = { log() {}, warn() {}, error() {} };
// ── 0020 applies on top of 0019, once, and rewrites nothing.
const first = await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }));
-- assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate'], ['0022', 'public_profiles', 'expand'], ['0023', 'project_secrets', 'expand']]);
++ assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate'], ['0022', 'public_profiles', 'expand'], ['0023', 'project_secrets', 'expand'], ['0024', 'retired_theme_campfire', 'migrate']]);
assert.deepStrictEqual((await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');
await asOwner((o) => o.exec(fs.readFileSync(path.join(MIGRATIONS, '0020_user_owned_trust.sql'), 'utf8'))); // safe to run twice
assert.deepStrictEqual(await rows(), seeded, 'no existing row changes');