Prototype administrative discussion mute controls

Let owners/operators mute or unmute new public/private sends from a human seat while retaining history, incoming messages, gameplay and credentials. Fence metadata and durable audit writes with live authority, membership and versions. Preserve exact admin/message retries and inherit restrictions through seat recovery.

Validation: types/lint, 5431 offline tests, build, 52 HTTP integration tests, 24 administrator HTTP groups, independent SQL/privacy/UI review and desktop/mobile refresh acceptance. All 2230 prior games and 4746 seats preserved. Additive migration 0016. Verified runtime fingerprint 588967fbb9c2e3b6abe2990c4fce96ee69e2256fb7e3ee416314b87593cebba9; documentation-only grammar/button-label review gives b5e57bdd26812debede91c588e38f247f1fa895ad23e7ae30bc8550eedcd90ec. New-revision deployed acceptance remains open; full admin/rules scope is unfinished.
This commit is contained in:
Quintine committed 2026-09-26 14:30:52 +10:00
1 parent 2cddf03932
commit 160fdc1f30
26 files changed
+3008 -33

No files matched your search

+26
View File
@@ -363,6 +363,32 @@ export const roomEntryReceipts = sqliteTable('room_entry_receipts', {
playerId: text('player_id').notNull(),
});
// Discussion moderation follows the logical seat across recovery and handover.
export const seatDiscussionControls = sqliteTable('seat_discussion_controls', {
roomCode: text('room_code').notNull().references(() => rooms.code, { onDelete: 'cascade' }),
playerId: text('player_id').notNull(),
muted: integer('muted').notNull().default(0),
revision: integer('revision').notNull().default(0),
updatedAt: integer('updated_at').notNull(),
}, table => [primaryKey({ columns: [table.roomCode, table.playerId] }),
check('seat_discussion_muted', sql`${table.muted} IN (0, 1)`),
check('seat_discussion_revision', sql`${table.revision} >= 0`)]);
// Keep audit receipts independent of room/account deletion, like other admin operations.
export const adminDiscussionOperations = sqliteTable('admin_discussion_operations', {
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(),
appliedRevision: integer('applied_revision').notNull(),
target: text('target').notNull(),
muted: integer('muted').notNull(),
reason: text('reason').notNull(),
createdAt: integer('created_at').notNull(),
}, table => [index('admin_discussion_room').on(table.roomCode, table.createdAt)]);
export const roomMessages = sqliteTable(
'room_messages',
{