openvibers/openvibe.network Commits
Network: the retired 'campfire' theme becomes ember (59 people got a 404 on every page) (#210)
Changes
nnew file mode 100644@@@ -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');@@@ -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) });@@@ -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.@@@ -92,6 +92,15 @@ const token = async (id) => signToken(await db.prepare('SELECT * FROM users WHERfor (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');@@@ -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 twiceassert.deepStrictEqual(await rows(), seeded, 'no existing row changes');
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -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');
@@ -133,10 +133,16 @@ router.get('/me/submissions', requireAuth, async (req, res) => {
@@ -133,10 +133,16 @@ router.get('/me/submissions', requireAuth, async (req, res) => {
res.set('Cache-Control', cache.htmlHeaders({ private: true })).json({ themes });
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) ──
// ── Get Theme by ID or Slug (approved themes; your own at any status) ──
router.get('/:idOrSlug', optionalAuth, async (req, res) => {
router.get('/:idOrSlug', optionalAuth, async (req, res) => {
const db = await getDb(req);
const db = await getDb(req);
ensureReview(db);
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);
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' });
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) });
res.json({ theme: parseTheme(theme) });
@@ -44,7 +44,7 @@ const quiet = { log() {}, warn() {}, error() {} };
@@ -44,7 +44,7 @@ const quiet = { log() {}, warn() {}, error() {} };
// The real directory: 0016 applies on top of 0015, once.
// The real directory: 0016 applies on top of 0015, once.
const first = await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }));
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.ok(await hasAgents());
assert.deepStrictEqual((await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');
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.
// Expand only, IF NOT EXISTS throughout: running the file again changes nothing and fails nothing.
@@ -92,6 +92,15 @@ const token = async (id) => signToken(await db.prepare('SELECT * FROM users WHER
@@ -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 } });
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');
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();
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');
@@ -52,7 +52,7 @@ const quiet = { log() {}, warn() {}, error() {} };
@@ -52,7 +52,7 @@ const quiet = { log() {}, warn() {}, error() {} };
// ── 0020 applies on top of 0019, once, and rewrites nothing.
// ── 0020 applies on top of 0019, once, and rewrites nothing.
const first = await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }));
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');
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
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');
assert.deepStrictEqual(await rows(), seeded, 'no existing row changes');