diff --git a/README.md b/README.md index 3b6b48d9..5a349d40 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,9 @@ an explicitly owned new host seat and initial AI players. [Configure lobby](docs/ADMIN_LOBBY_CONFIGURATION.md) manages supported rules, AI seats and host assignment without private seat access. [Remove or restore](docs/ADMIN_ROOM_REMOVAL.md) recoverably removes selected rooms; -use the Directory filter to find them for restoration. Close/end, archive, +use the Directory filter to find them for restoration. +[Close or reopen](docs/ADMIN_ROOM_CLOSURE.md) stops further play while preserving +readable tables and history, then resumes the same saved position. Archive, permanent deletion, participant support and backup tools are still being built. ## Saved seats and reconnecting diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 2194f1c2..555d8b69 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -11,6 +11,7 @@ import { FACTIONS } from '@/game/catalog'; import { AdminRoomControls } from '@/components/admin-room-controls'; import { AdminRoomCreation } from '@/components/admin-room-creation'; import { AdminLobbyControls } from '@/components/admin-lobby-controls'; +import { AdminRoomClosure } from '@/components/admin-room-closure'; import { AdminRoomRemoval } from '@/components/admin-room-removal'; import './admin.css'; @@ -31,6 +32,7 @@ export default function Administration() { const [confirmAll, setConfirmAll] = useState(false); const [selectedRoom, setSelectedRoom] = useState(null); const [selectedLobby, setSelectedLobby] = useState(null); + const [selectedClosure, setSelectedClosure] = useState(null); const [selectedRemoval, setSelectedRemoval] = useState(null); const [creatingRoom, setCreatingRoom] = useState(false); const [operationBusy, setOperationBusy] = useState(false); @@ -39,7 +41,7 @@ export default function Administration() { const clearSession = useCallback(() => { epoch.current++; setAccount(null); setDirectory(null); setConfirmAll(false); - setBusy(false); setSelectedRoom(null); setSelectedLobby(null); setSelectedRemoval(null); setCreatingRoom(false); + setBusy(false); setSelectedRoom(null); setSelectedLobby(null); setSelectedRemoval(null); setSelectedClosure(null); setCreatingRoom(false); }, []); const accessChanged = useCallback(() => { clearSession(); @@ -115,7 +117,7 @@ export default function Administration() { : <>

Rooms

Public room details. Cards, sealed choices and private discussion stay hidden.

-
{account.role !== 'viewer' && } +
{account.role !== 'viewer' && }
{creatingRoom && account.role !== 'viewer' && setCreatingRoom(false)} onDenied={accessChanged} onBusyChange={setOperationBusy} @@ -132,7 +134,7 @@ export default function Administration() { + +
: pending ?
+

Saved request: {pending.closed ? 'Close' : 'Reopen'} room {code}.

Reason: {pending.reason}

+

The exact request stays in this tab across refresh and reopening these controls. Retrying confirms a completed change without applying it again. Current availability can differ if another administrator changed the room afterward.

+

After refreshing the page or signing in again, choose All rooms in the Directory filter to find this room and reopen these controls.

+ +
: room?.removed ?

Restore this removed room before changing its closure. Any saved close/reopen request can still be confirmed above.

: room &&
{ event.preventDefault(); void submit(); }}> + +

Use a short operational reason. Do not include credentials or private game information.

+
+ {room.closed ? <> +

Reopen play in room {code}. Saved progress and seats are retained, and current credentials and access revocations remain in effect.

+

Reopening preserves the previous pause and joining settings. {room.paused ? 'This room will remain paused.' : 'This room will resume with the existing AI pace.'} New joins will remain {room.joinLocked ? 'locked' : 'open'}.

+ : <> +

Close room {code} to further play. This stops joining, game decisions, AI, new messages and seat security changes. Existing players can still read their own table and discussion history.

+

Warn active players before interrupting their game. Closing preserves the exact pending game and seats. It does not award a winner. An administrator can reopen it later.

+ + } + + +
+
} + } +
; +} diff --git a/components/admin-room-controls.tsx b/components/admin-room-controls.tsx index ed5e1f55..a1fea3f1 100644 --- a/components/admin-room-controls.tsx +++ b/components/admin-room-controls.tsx @@ -133,7 +133,7 @@ export function AdminRoomControls({ accountId, code, canManage, onClose, onUpdat

Room {code}

{notice && {notice}} {loading ? Loading current room settings… : !control ? : <> -

Current status: {control.paused ? 'Paused' : 'Running'} · New joins {control.joinLocked ? 'locked' : 'open'}.

+

Current status: {control.closed ? 'Closed' : control.paused ? 'Paused' : 'Running'} · New joins {control.joinLocked ? 'locked' : 'open'}.

Pause stops player decisions, automatic effects and AI play. The pending game stays saved. Discussion, seat recovery and taking back control remain available.

A joining lock blocks new players. Existing players can reconnect or recover their seats.

@@ -146,7 +146,7 @@ export function AdminRoomControls({ accountId, code, canManage, onClose, onUpdat

Reason: {pending.reason}

The exact request is kept in this tab across refresh. Retrying confirms an already completed operation without applying it again.

- :
{ event.preventDefault(); void submit(); }}> + : control.closed ?

Reopen this room before changing pause or joining settings. Existing settings are preserved while closed.

: { event.preventDefault(); void submit(); }}> diff --git a/components/admin-room-removal.tsx b/components/admin-room-removal.tsx index b087e03b..96629f96 100644 --- a/components/admin-room-removal.tsx +++ b/components/admin-room-removal.tsx @@ -132,7 +132,7 @@ export function AdminRoomRemoval({ accountId, code, canManage, onClose, onUpdate
{room.removed ? <>

Restore ordinary access to room {code}. Saved progress and seats are retained, and current credentials and access revocations remain in effect.

-

Restoration preserves the previous pause and joining settings. {room.paused ? 'This room will remain paused.' : 'This room will resume with the existing AI pace.'} New joins will remain {room.joinLocked ? 'locked' : 'open'}.

+

Restoration preserves the previous pause and joining settings. {room.closed && 'This room is also closed: restoration keeps it closed to further play until reopened.'} {room.closed ? 'AI remains stopped while closed.' : room.paused ? 'This room will remain paused.' : 'This room will resume with the existing AI pace.'} New joins will remain {room.joinLocked ? 'locked' : 'open'}.

: <>

Remove room {code} from ordinary access. This interrupts every player: private room access, joining, game actions, AI play, seat security changes and messages stop until restoration.

Warn active players before interrupting their game. This recoverable removal keeps the saved game and seats for restoration.

diff --git a/components/game-table.tsx b/components/game-table.tsx index aef008cf..18f0f31f 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -286,7 +286,7 @@ export function GameTable({ : [], ); const busy = - transportBusy || !!g.roomControl?.paused || !!me.autopilot || g.automaticContinuationPending; + transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot || g.automaticContinuationPending; const traitorBattle = g.battle; const traitorBeneficiary = traitorBattle && @@ -1575,14 +1575,14 @@ export function GameTable({ - - - + + + - - - + + + {!g.nexusCards?.waiting.length && } @@ -1633,6 +1633,7 @@ export function GameTable({ Your faction