openvibers/openvibe.live Commits

The Arena's tables go (contract 0015, after 0008); arena_opt_outs stays for the Rundown (#334)

OP OpenVibers committed 21 days ago · 930af13f4f · 7 files, +74 −9

* The Arena's tables go (contract 0015, after 0008): nine tables and 22 dead settings rows; arena_opt_outs stays for the Rundown

Live#264 deleted server/arena on 10-19 and AI#130 retired its workflows; nothing names these since
(test/arena-tables-gone.test.js). account-data's person-column list and the site-config job-state filter
lose their Arena entries. 0008 was applied 10-16 18:34 UTC, so the migrator runs this after 10-23 18:34.

* The boot banner stops listing /ws/chat (OpenVibe.Chat serves it; Live mounts no chat server)

Changes

ddiff --git a/docs/rundown.md b/docs/rundown.md
iindex 424e5c01..d4cbf15f 100644
---- a/docs/rundown.md
++++ b/docs/rundown.md
@@@ -90,7 +90,7 @@ Migration `0010_rundown.sql`:
- `rundown_progress`: how far the desk has read each stream.
- `rundown_reactions`: readers' reactions.
--The Arena's tables are no longer written. Its pages, styles and readers are gone (the recap, the home digest, the star pick and the VOD stats read The Rundown); its server code and `/api/arena` go once no open tab of an earlier release calls them, and its tables after that.
++The Arena's tables are no longer written. Its pages, styles and readers are gone (the recap, the home digest, the star pick and the VOD stats read The Rundown); its server code and `/api/arena` went on 2026-10-19 (Live#264), and its tables with migration `0015_drop_arena` (applied after 2026-10-23 18:34 UTC). `arena_opt_outs` stays: it is The Rundown's list of streamers who keep themselves out.
## Chat
ddiff --git a/migrations/0015_drop_arena.sql b/migrations/0015_drop_arena.sql
nnew file mode 100644
iindex 00000000..9840397b
---- /dev/null
++++ b/migrations/0015_drop_arena.sql
@@@ -0,0 +1,24 @@
++-- phase: contract
++-- after: 0008
++-- The Arena's tables (Arena cleanup, step 3). The Rundown replaced the Arena on 2026-10-19; Live#264 (deployed that
++-- day) deleted server/arena and AI#130 retired the live.arena.* workflows, so nothing in Live or any other service
++-- reads or writes these since (test/arena-tables-gone.test.js). 0008 (arena_drama, arena_drama_reactions,
++-- arena_clout_weeks) was applied 2026-10-16 18:34 UTC, so the migrator runs this after 2026-10-23 18:34 UTC.
++--
++-- arena_opt_outs stays: it is the Rundown's list of streamers who keep themselves out (server/rundown/read.js,
++-- desk.js), named for the Arena. arena_voice_daily_max stays a Rundown setting.
++-- Production on 2026-10-23: 59 drama stories, 519 mic moments, 38 profiles, 884 XP log rows (1.6 MB with the rest);
++-- the job cursors (arena_backfill_cursor_*) and the Arena's own settings (arena_bot_usernames,
++-- arena_discover_window_min) are 22 site_settings rows nothing reads. Rollback: the operator's pg_dump of these
++-- tables, taken before the deploy.
++DROP TABLE IF EXISTS arena_drama_reactions;
++DROP TABLE IF EXISTS arena_drama;
++DROP TABLE IF EXISTS arena_clout_weeks;
++DROP TABLE IF EXISTS arena_beef_hype;
++DROP TABLE IF EXISTS arena_beefs;
++DROP TABLE IF EXISTS arena_mic_moments;
++DROP TABLE IF EXISTS arena_profiles;
++DROP TABLE IF EXISTS arena_trash_levels;
++DROP TABLE IF EXISTS arena_xp_log;
++
++DELETE FROM site_settings WHERE key LIKE 'arena\_backfill\_cursor\_%' OR key IN ('arena_bot_usernames', 'arena_discover_window_min');
ddiff --git a/server/admin/site-config.js b/server/admin/site-config.js
iindex d5ea611e..2a3a6b55 100644
---- a/server/admin/site-config.js
++++ b/server/admin/site-config.js
@@@ -20,7 +20,7 @@
const config = require('openvibe-shared/config');
const db = require('../db/database');
--const NOT_CONFIG = /^(arena_backfill_cursor_|star_streamer$|deploy_last_announced$|relay_users_backfilled$|bucks_bits_migration_done$|stats_vibes_reset_at$|storage_tier\.|money_writes_frozen)/;
++const NOT_CONFIG = /^(star_streamer$|deploy_last_announced$|relay_users_backfilled$|bucks_bits_migration_done$|stats_vibes_reset_at$|storage_tier\.|money_writes_frozen)/;
// A webhook URL is its own credential (anyone holding it can post), so it is secret-class too.
const SECRET = /(api[_-]?key|secret|token|password|client_secret|service_account|private[_-]?key|salt|access[_-]?key|webhook)/i;
const PUBLIC_KEYS = new Set(['site_name', 'site_description', 'motd', 'registration_open', 'require_email', 'nsfw_enabled']);
ddiff --git a/server/auth/account-data.js b/server/auth/account-data.js
iindex 710eef7a..76a9853a 100644
---- a/server/auth/account-data.js
++++ b/server/auth/account-data.js
@@@ -41,7 +41,7 @@ const OVERRIDE_DELETE = new Set(['ip_log.user_id', 'vpn_approvals.user_id']);
// Copies of the person's name kept beside a person column: cleared wherever the row stays.
const NAME_COLS = ['username', 'display_name', 'user_name', 'sender_name', 'sender_username', 'author_name', 'author_username', 'avatar_url', 'user_avatar'];
// Person columns the schema declares without a foreign key.
--const EXTRA = [['channel_points', 'streamer_id'], ['arena_mic_moments', 'target_user_id'], ['arena_drama', 'other_user_id'], ['arena_topics', 'created_by'], ['arena_topics', 'target_user_id']];
++const EXTRA = [['channel_points', 'streamer_id']];
const SECRET_COL = /(^|_)(token|tokens|secret|hash|password|code|key|keys|p256dh|auth|cookie|cookies|credentials)($|_)/i;
const ROW_LIMIT = 2000;
const PART_BUDGET = 18 * 1024 * 1024;
ddiff --git a/server/index.js b/server/index.js
iindex 0dec9c3a..3b81f1f9 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -1400,7 +1400,6 @@ async function start() {
server.listen(socketActivated ? { fd: 3 } : { port: config.port, host: config.listenHost }, async () => {
console.log('');
console.log(`[Server] HTTP server: http://${config.host}:${config.port}`);
-- console.log(`[Server] WebSocket: ws://${config.host}:${config.port}/ws/chat`);
console.log(`[Server] WebSocket: ws://${config.host}:${config.port}/ws/broadcast`);
console.log(`[Server] Environment: ${config.nodeEnv}`);
console.log(`[Server] BASE_URL: ${config.baseUrl}`);
ddiff --git a/test/arena-tables-gone.test.js b/test/arena-tables-gone.test.js
nnew file mode 100644
iindex 00000000..b91920bb
---- /dev/null
++++ b/test/arena-tables-gone.test.js
@@@ -0,0 +1,42 @@
++'use strict';
++
++// Arena cleanup, step 3: the Rundown replaced the Arena on 2026-10-19 (Live#264 deleted server/arena, AI#130 retired its
++// workflows). Nothing under server/ or scripts/ names the Arena's tables, so the contract migration that drops them
++// breaks nothing. arena_opt_outs stays: it is the Rundown's list of streamers who keep themselves out.
++
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++const ROOT = path.join(__dirname, '..');
++const DROPPED = ['arena_beef_hype', 'arena_beefs', 'arena_clout_weeks', 'arena_drama', 'arena_drama_reactions', 'arena_mic_moments', 'arena_profiles', 'arena_trash_levels', 'arena_xp_log'];
++const NAMES = new RegExp(`\\b(${DROPPED.join('|')}|arena_topics|arena_backfill_cursor_\\w*|arena_bot_usernames|arena_discover_window_min)\\b`);
++const isCommentLine = (line) => /^\s*(\/\/|\*|\/\*|--)/.test(line);
++
++function walk(dir, out = []) {
++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
++ const p = path.join(dir, e.name);
++ if (e.isDirectory()) { if (e.name !== 'node_modules') walk(p, out); } else if (e.name.endsWith('.js')) out.push(p);
++ }
++ return out;
++}
++
++const offenders = [];
++for (const file of [...walk(path.join(ROOT, 'server')), ...walk(path.join(ROOT, 'scripts'))]) {
++ fs.readFileSync(file, 'utf8').split('\n').forEach((line, i) => {
++ if (!isCommentLine(line) && NAMES.test(line)) offenders.push(`${path.relative(ROOT, file)}:${i + 1}: ${line.trim().slice(0, 120)}`);
++ });
++}
++assert.deepStrictEqual(offenders, [], 'the Arena is gone (the Rundown replaced it): nothing reads its tables or settings');
++assert.ok(!fs.existsSync(path.join(ROOT, 'server/arena')), 'server/arena is gone');
++// The Rundown still reads the opt-out list.
++assert.match(fs.readFileSync(path.join(ROOT, 'server/rundown/read.js'), 'utf8'), /\barena_opt_outs\b/);
++
++// The contract migration drops exactly these, after 0008's N-1 window, and keeps arena_opt_outs.
++const drop = fs.readFileSync(path.join(ROOT, 'migrations', '0015_drop_arena.sql'), 'utf8');
++assert.match(drop, /^-- phase: contract$/m);
++assert.match(drop, /^-- after: 0008$/m);
++const dropped = [...drop.matchAll(/^DROP TABLE IF EXISTS (\w+);$/gm)].map((m) => m[1]).sort();
++assert.deepStrictEqual(dropped, DROPPED, '0015 drops exactly the nine');
++assert.ok(!/arena_opt_outs/.test(drop.split('\n').filter((l) => !l.startsWith('--')).join('\n')), 'arena_opt_outs is kept');
++console.log('arena tables: nothing under server/ or scripts/ names them, 0015 drops the nine and keeps arena_opt_outs');
ddiff --git a/test/site-config.test.js b/test/site-config.test.js
iindex b967763a..e77cf3b9 100644
---- a/test/site-config.test.js
++++ b/test/site-config.test.js
@@@ -45,7 +45,7 @@ permissions.isOwner = (u) => !!u && Number(u.id) === 1;
// What production looks like: configuration, a secret, job state and the money freeze in one table.
for (const [k, v, t] of [['motd', 'hello', 'string'], ['max_clip_duration', '60', 'number'], ['stripe_secret_key', 'sk_live_verysecret', 'string'],
-- ['star_streamer', 'alice', 'string'], ['arena_backfill_cursor_3', '100', 'string'], ['money_writes_frozen', 'false', 'boolean']]) {
++ ['star_streamer', 'alice', 'string'], ['storage_tier.cursor_3', '100', 'string'], ['money_writes_frozen', 'false', 'boolean']]) {
await raw.prepare('INSERT INTO site_settings (key, value, type) VALUES (?, ?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value, type = excluded.type').run(k, v, t);
}
@@@ -67,7 +67,7 @@ const call = async (method, p, body, user = 1) => {
assert.strictEqual(store.revision(), 1);
const keys = Object.keys(store.get());
for (const k of ['max_clip_duration', 'motd', 'stripe_secret_key']) assert.ok(keys.includes(k), `${k} is configuration`);
-- for (const k of ['star_streamer', 'arena_backfill_cursor_3', 'money_writes_frozen']) assert.ok(!keys.includes(k), `${k} is not`);
++ for (const k of ['star_streamer', 'storage_tier.cursor_3', 'money_writes_frozen']) assert.ok(!keys.includes(k), `${k} is not`);
assert.strictEqual(await db.getSetting('max_clip_duration'), 60, 'readers read the rows, typed as before');
// ── An admin change: one revision, written to the rows ──
@@@ -107,10 +107,10 @@ const call = async (method, p, body, user = 1) => {
assert.strictEqual(r.status, 200);
assert.strictEqual(await db.getSetting('motd'), null);
assert.ok(!('motd' in store.get()));
-- r = await call('PUT', '/api/admin/settings', { settings: { arena_backfill_cursor_3: '200' } }, 2);
++ r = await call('PUT', '/api/admin/settings', { settings: { 'storage_tier.cursor_3': '200' } }, 2);
assert.strictEqual(r.status, 200);
-- assert.strictEqual(await db.getSetting('arena_backfill_cursor_3'), '200');
-- assert.ok(!('arena_backfill_cursor_3' in store.get()), 'job state stays out of the journal');
++ assert.strictEqual(await db.getSetting('storage_tier.cursor_3'), '200');
++ assert.ok(!('storage_tier.cursor_3' in store.get()), 'job state stays out of the journal');
assert.strictEqual(await db.getSetting('money_writes_frozen'), false);
await assert.rejects(siteConfig.change({ set: { money_writes_frozen: 'true' } }), /not configuration/);