Add reversible administrator room archiving
Organize closed rooms with archive filters, live-authorized atomic receipts and exact saved retries. Preserve saved games, timestamps, credentials, discussion and AI deadlines. Unarchive leaves play closed; reopen requires unarchive, and removal/restoration preserves both flags. Verified types, lint, 5379 offline tests, 52 HTTP tests and build. Twenty dedicated administrator HTTP groups and independent privacy/persistence review passed. Desktop/mobile browser QA confirmed a single archive after a lost response and refresh, readable recovery text, mobile unarchive and separate reopening, and saved-seat restoration. All 2,049 baseline rooms, 4,348 seats and related records preserved. Authenticated production admin acceptance awaits authorized QA access; full admin and rules gates remain open.
This commit is contained in:
1 parent
98b1b3261b
commit
e918e2e0c0
35 files changed
+3624
-38
No files matched your search
@@ -0,0 +1,64 @@
|
||||
export type AdminArchiveInput = {
|
||||
operationId: string;
|
||||
expectedVersion: number;
|
||||
expectedRevision: number;
|
||||
archived: boolean;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export type AdminArchiveView = {
|
||||
closed: boolean;
|
||||
removed: boolean;
|
||||
code: string;
|
||||
version: number;
|
||||
archived: boolean;
|
||||
revision: number;
|
||||
archivedAt: number | null;
|
||||
updatedAt: number | null;
|
||||
paused: boolean;
|
||||
joinLocked: boolean;
|
||||
};
|
||||
|
||||
export type AdminArchiveResult = {
|
||||
operationId: string;
|
||||
replayed: boolean;
|
||||
appliedRevision: number;
|
||||
appliedVersion: number;
|
||||
room: AdminArchiveView;
|
||||
};
|
||||
|
||||
export function validAdminArchiveInput(
|
||||
value: unknown,
|
||||
): value is AdminArchiveInput {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
||||
const input = value as Record<string, unknown>;
|
||||
const fields = [
|
||||
'operationId',
|
||||
'expectedVersion',
|
||||
'expectedRevision',
|
||||
'archived',
|
||||
'reason',
|
||||
];
|
||||
return (
|
||||
Object.keys(input).length === fields.length &&
|
||||
fields.every((field) => Object.hasOwn(input, field)) &&
|
||||
typeof input.operationId === 'string' &&
|
||||
input.operationId.length === 36 &&
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
|
||||
input.operationId,
|
||||
) &&
|
||||
[input.expectedVersion, input.expectedRevision].every(
|
||||
(version) =>
|
||||
Number.isSafeInteger(version) &&
|
||||
Number(version) >= 0 &&
|
||||
Number(version) < Number.MAX_SAFE_INTEGER,
|
||||
) &&
|
||||
typeof input.archived === 'boolean' &&
|
||||
typeof input.reason === 'string' &&
|
||||
!!input.reason.trim() &&
|
||||
input.reason.length <= 300 &&
|
||||
!input.reason
|
||||
.split('')
|
||||
.some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127)
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user