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

+5 -2
View File
@@ -18,6 +18,7 @@ import {
/** Remount on room/seat changes. Messages belong to seats, never browser identities. */
export function TableTalk({ game }: { game: GameView }) {
const field = useId();
const closed = !!game.roomControl?.closed;
const [open, setOpen] = useState(false);
const [recipient, setRecipient] = useState('');
const [text, setText] = useState('');
@@ -151,7 +152,7 @@ export function TableTalk({ game }: { game: GameView }) {
}
}
async function submit() {
if (sending.current || !ready) return;
if (sending.current || !ready || (closed && !record)) return;
const draft = record ?? {
id: randomId(),
recipientId: recipient || null,
@@ -314,6 +315,7 @@ export function TableTalk({ game }: { game: GameView }) {
{shownPage.messages.length === 0 && (
<p className="muted">No messages loaded in this conversation.</p>
)}
{closed && <p className="notice">This room is closed. Discussion history remains available; an exact saved message can still be confirmed.</p>}
<form
onSubmit={(event) => {
event.preventDefault();
@@ -330,7 +332,7 @@ export function TableTalk({ game }: { game: GameView }) {
rows={3}
maxLength={TALK_LIMIT}
value={text}
disabled={busy || !!record}
disabled={busy || !!record || closed}
onChange={(event) => setText(event.target.value)}
aria-describedby={`${field}-count`}
/>
@@ -341,6 +343,7 @@ export function TableTalk({ game }: { game: GameView }) {
type="submit"
disabled={
!ready ||
(closed && !record) ||
busy ||
!text.trim() ||
(!record && !!recipient && !target)