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

+14 -13
View File
@@ -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({
<PrivateBattlePlan game={g} />
<NexusCards game={g} act={act} busy={busy} />
<Recruits game={g} act={act} busy={busy} />
<NexusTraitors game={g} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<DiscoveryPanel game={g} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<LeaderSkillsPanel skills={g.leaderSkills} leaders={g.allLeaders} players={g.players} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<NexusTraitors game={g} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
<DiscoveryPanel game={g} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
<LeaderSkillsPanel skills={g.leaderSkills} leaders={g.allLeaders} players={g.players} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
<RihaniHistory game={g} />
<MentatHistory game={g} />
<NexusChoamTrade game={g} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<NexusTleilaxu game={g} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<NexusSuboids game={g} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<NexusChoamTrade game={g} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
<NexusTleilaxu game={g} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
<NexusSuboids game={g} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
<NexusAdvisors game={g} act={act} busy={busy} />
<NexusSardaukar game={g} act={act} busy={busy} />
{!g.nexusCards?.waiting.length && <TupileIntelligence game={g} act={act} busy={busy} />}
@@ -1633,6 +1633,7 @@ export function GameTable({
Your faction
<select
value={me.faction}
disabled={busy}
onChange={(e) =>
act({ type: 'faction', faction: e.target.value })
}
@@ -3037,9 +3038,9 @@ export function GameTable({
))}
</>
) : g.decision.kind === 'sukRescue' ? (
<SukGraduatePanel key={g.decision.event} decision={g.decision} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<SukGraduatePanel key={g.decision.event} decision={g.decision} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
) : g.decision.kind === 'rihani' ? (
<RihaniChoice key={g.decision.event} game={g} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<RihaniChoice key={g.decision.event} game={g} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
) : g.decision.kind === 'battleLosses' ? (
<>
<p className="muted">
@@ -3144,13 +3145,13 @@ export function GameTable({
})}
</>
) : g.decision.kind === 'discoveryEntry' ? (
<DiscoveryEntryDecision game={g} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<DiscoveryEntryDecision game={g} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
) : g.decision.kind === 'ecologicalStorm' ? (
<DiscoveryStormDecision game={g} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<DiscoveryStormDecision game={g} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
) : g.decision.kind === 'discoveryDiscard' ? (
<DiscoveryDiscardDecision game={g} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<DiscoveryDiscardDecision game={g} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
) : g.decision.kind === 'greatMakerVote' || g.decision.kind === 'greatMakerRide' ? (
<GreatMakerDecision game={g} act={act} busy={transportBusy || !!g.roomControl?.paused || !!me.autopilot} />
<GreatMakerDecision game={g} act={act} busy={transportBusy || (!!g.roomControl?.paused || !!g.roomControl?.closed) || !!me.autopilot} />
) : g.decision.kind === 'wormProtection' ? (
<>
<p className="muted">