Add administrative room close and reopen controls

Preserve readable private tables, discussion and pending games while closure blocks new play, joins, messages, AI and seat mutations. Add live-authorized atomic closure audits, room-version/revision fences, dedicated controls and exact saved retries. Reopen with the saved pause/join settings and current credentials; restoring a removed but closed room keeps its AI stopped.

Verified types, lint, 5356 offline tests, 52 HTTP tests and build. Seventeen dedicated authenticated HTTP groups and independent privacy/persistence review passed. Desktop/mobile browser QA confirmed one close after a lost response and refresh, persistent readable notices, retained private draft, mobile reopening and saved-seat refresh. All 2,001 baseline rooms, 4,243 seats and related records preserved. Positive production admin acceptance awaits authorized QA access; full admin and rules gates remain open.
This commit is contained in:
Quintine committed 2026-09-22 12:31:02 +10:00
1 parent 238de3860d
commit 98b1b3261b
49 files changed
+4255 -86

No files matched your search

+45
View File
@@ -91,6 +91,51 @@ export const adminRoomRemovals = sqliteTable(
],
);
// Closure freezes mutations while retaining private read access, independent of saved game state and pause settings.
export const roomClosures = sqliteTable(
'room_closures',
{
roomCode: text('room_code')
.primaryKey()
.references(() => rooms.code, { onDelete: 'cascade' }),
closed: integer('closed').notNull().default(0),
revision: integer('revision').notNull().default(0),
closedAt: integer('closed_at'),
updatedAt: integer('updated_at').notNull(),
},
(table) => [
check('room_closures_closed', sql`${table.closed} IN (0, 1)`),
check('room_closures_revision', sql`${table.revision} >= 0`),
check(
'room_closures_time',
sql`(${table.closed} = 1 AND ${table.closedAt} IS NOT NULL) OR (${table.closed} = 0 AND ${table.closedAt} IS NULL)`,
),
],
);
// Exact operation receipts also retain safe audit evidence independently of rooms/accounts.
export const adminRoomClosures = sqliteTable(
'admin_room_closures',
{
operationId: text('operation_id').primaryKey(),
actorAdminId: text('actor_admin_id').notNull(),
roomCode: text('room_code').notNull(),
requestHash: text('request_hash').notNull(),
expectedVersion: integer('expected_version').notNull(),
expectedRevision: integer('expected_revision').notNull(),
appliedVersion: integer('applied_version').notNull(),
appliedRevision: integer('applied_revision').notNull(),
closed: integer('closed').notNull(),
reason: text('reason').notNull(),
beforeMetadata: text('before_metadata').notNull(),
afterMetadata: text('after_metadata').notNull(),
createdAt: integer('created_at').notNull(),
},
(table) => [
index('admin_room_closures_room').on(table.roomCode, table.createdAt),
],
);
// Durable receipts are also the room operations audit. They intentionally survive
// account/room removal, and contain no credentials, game state or private cards.
export const adminRoomAudit = sqliteTable(