From 3b3d7780c96bf98204161d4a315310b804bc9847 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 25 Aug 2026 18:49:58 +0200 Subject: [PATCH] docs: complete housekeeping migration matrix --- package.json | 3 +- scripts/verify-housekeeping-matrix.ts | 18 + .../housekeeping/migration/matrix.test.ts | 29 ++ src/features/housekeeping/migration/matrix.ts | 15 + .../housekeeping/migration/system.test.ts | 128 +++++ src/features/housekeeping/migration/system.ts | 442 ++++++++++++++++++ 6 files changed, 634 insertions(+), 1 deletion(-) create mode 100644 scripts/verify-housekeeping-matrix.ts create mode 100644 src/features/housekeeping/migration/matrix.test.ts create mode 100644 src/features/housekeeping/migration/matrix.ts create mode 100644 src/features/housekeeping/migration/system.test.ts create mode 100644 src/features/housekeeping/migration/system.ts diff --git a/package.json b/package.json index bdc4c8fa..c3d1ab1c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "db:generate": "drizzle-kit generate", "db:migrate": "tsx scripts/apply-migrations.ts", "db:migrate:status": "tsx scripts/apply-migrations.ts --status", - "db:studio": "drizzle-kit studio" + "db:studio": "drizzle-kit studio", + "hk:matrix:check": "tsx scripts/verify-housekeeping-matrix.ts" }, "lint-staged": { "*.{js,ts,jsx,tsx,json}": "biome check --write --no-errors-on-unmatched" diff --git a/scripts/verify-housekeeping-matrix.ts b/scripts/verify-housekeeping-matrix.ts new file mode 100644 index 00000000..05352751 --- /dev/null +++ b/scripts/verify-housekeeping-matrix.ts @@ -0,0 +1,18 @@ +import { discoverLegacyPages } from "../src/features/housekeeping/migration/discover-legacy-pages"; +import { HOUSEKEEPING_MIGRATION_MATRIX } from "../src/features/housekeeping/migration/matrix"; +import { validateMigrationEntries } from "../src/features/housekeeping/migration/validate-matrix"; + +const discovered = discoverLegacyPages(); +const issues = validateMigrationEntries( + discovered, + HOUSEKEEPING_MIGRATION_MATRIX, +); + +if (issues.length > 0) { + for (const issue of issues) console.error(issue); + process.exitCode = 1; +} else { + console.log( + `Housekeeping migration matrix: ${HOUSEKEEPING_MIGRATION_MATRIX.length}/${discovered.length} valid`, + ); +} diff --git a/src/features/housekeeping/migration/matrix.test.ts b/src/features/housekeeping/migration/matrix.test.ts new file mode 100644 index 00000000..c4a9cf50 --- /dev/null +++ b/src/features/housekeeping/migration/matrix.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { discoverLegacyPages } from "./discover-legacy-pages"; +import { HOUSEKEEPING_MIGRATION_MATRIX } from "./matrix"; +import { validateMigrationEntries } from "./validate-matrix"; + +describe("HOUSEKEEPING_MIGRATION_MATRIX", () => { + it("covers all 137 legacy pages exactly once", () => { + const discovered = discoverLegacyPages(); + + expect(HOUSEKEEPING_MIGRATION_MATRIX).toHaveLength(137); + expect( + validateMigrationEntries(discovered, HOUSEKEEPING_MIGRATION_MATRIX), + ).toEqual([]); + }); + + it("contains no undecided evidence markers", () => { + expect(JSON.stringify(HOUSEKEEPING_MIGRATION_MATRIX)).not.toMatch( + /TBD|TODO|FIXME|UNCLASSIFIED/, + ); + }); + + it("is sorted by legacy path", () => { + expect(HOUSEKEEPING_MIGRATION_MATRIX.map((row) => row.legacyPath)).toEqual( + [...HOUSEKEEPING_MIGRATION_MATRIX.map((row) => row.legacyPath)].sort( + (a, b) => a.localeCompare(b), + ), + ); + }); +}); diff --git a/src/features/housekeeping/migration/matrix.ts b/src/features/housekeeping/migration/matrix.ts new file mode 100644 index 00000000..05e7e3e3 --- /dev/null +++ b/src/features/housekeeping/migration/matrix.ts @@ -0,0 +1,15 @@ +import { contentMigrationEntries } from "./content"; +import { economyMigrationEntries } from "./economy"; +import { hotelMigrationEntries } from "./hotel"; +import { operationsMigrationEntries } from "./operations"; +import { peopleMigrationEntries } from "./people"; +import { systemMigrationEntries } from "./system"; + +export const HOUSEKEEPING_MIGRATION_MATRIX = [ + ...operationsMigrationEntries, + ...peopleMigrationEntries, + ...contentMigrationEntries, + ...economyMigrationEntries, + ...hotelMigrationEntries, + ...systemMigrationEntries, +].sort((a, b) => a.legacyPath.localeCompare(b.legacyPath)); diff --git a/src/features/housekeeping/migration/system.test.ts b/src/features/housekeeping/migration/system.test.ts new file mode 100644 index 00000000..da8b3244 --- /dev/null +++ b/src/features/housekeeping/migration/system.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it } from "vitest"; +import { PERMS } from "@/lib/permission-slugs"; +import { ownedLegacyPages } from "./discover-legacy-pages"; +import { systemMigrationEntries } from "./system"; +import { validateMigrationEntries } from "./validate-matrix"; + +const SYSTEM_PREFIXES = [ + "/admin/alerts", + "/admin/analytics", + "/admin/commandocentrum", + "/admin/devops", + "/admin/emulator", + "/admin/housekeeping", + "/admin/logs", + "/admin/maintenance", + "/admin/menu", + "/admin/permissions", + "/admin/settings", +] as const; + +describe("systemMigrationEntries", () => { + it("covers all 19 System pages exactly once", () => { + expect(systemMigrationEntries).toHaveLength(19); + expect( + validateMigrationEntries( + ownedLegacyPages(SYSTEM_PREFIXES), + systemMigrationEntries, + ), + ).toEqual([]); + }); + + it("removes legacy foundation-owned pages", () => { + expect(systemMigrationEntries).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + legacyPath: "/admin/housekeeping", + decision: "REMOVE", + targetPath: null, + }), + expect.objectContaining({ + legacyPath: "/admin/menu", + decision: "REMOVE", + targetPath: null, + }), + ]), + ); + }); + + it("keeps live permissions as a System workflow", () => { + expect(systemMigrationEntries).toContainEqual( + expect.objectContaining({ + legacyPath: "/admin/permissions", + targetPath: "/admin/system/access/permissions", + decision: expect.not.stringMatching("REMOVE"), + }), + ); + }); + + it("retains the read-only legacy housekeeping value in removal notes", () => { + const housekeeping = systemMigrationEntries.find( + (row) => row.legacyPath === "/admin/housekeeping", + ); + + expect(housekeeping?.dependencies.queries).toEqual( + expect.arrayContaining([ + "exportPermissions", + "getAuditLogs", + "legacy ACL comparison", + ]), + ); + expect(housekeeping?.notes.join(" ")).toMatch( + /read-only.*export.*comparison/i, + ); + }); + + it("uses only the approved System target roots", () => { + const allowedRoots = [ + "/admin/system/access", + "/admin/system/configuration", + "/admin/system/observability", + "/admin/system/operations", + ]; + + expect( + systemMigrationEntries.every( + (row) => + row.targetPath === null || + allowedRoots.some( + (root) => + row.targetPath === root || row.targetPath?.startsWith(`${root}/`), + ), + ), + ).toBe(true); + }); + + it("keeps non-removal workflows planned without parity claims", () => { + expect( + systemMigrationEntries + .filter((row) => row.decision !== "REMOVE") + .every( + (row) => row.status === "PLANNED" && row.parityEvidence.length === 0, + ), + ).toBe(true); + }); + + it("records authoritative capabilities for privileged System mutations", () => { + expect(systemMigrationEntries).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + legacyPath: "/admin/commandocentrum", + capabilities: { + read: [PERMS.RCON_EXECUTE], + mutate: [PERMS.RCON_EXECUTE], + }, + auditRequirement: "PRIVILEGED_MUTATION", + }), + expect.objectContaining({ + legacyPath: "/admin/permissions/:id", + capabilities: { + read: [PERMS.PERMISSIONS_MANAGE], + mutate: [PERMS.PERMISSIONS_MANAGE], + }, + auditRequirement: "PRIVILEGED_MUTATION", + }), + ]), + ); + }); +}); diff --git a/src/features/housekeeping/migration/system.ts b/src/features/housekeeping/migration/system.ts new file mode 100644 index 00000000..d990c1d6 --- /dev/null +++ b/src/features/housekeeping/migration/system.ts @@ -0,0 +1,442 @@ +import { PERMS } from "@/lib/permission-slugs"; +import type { MigrationEntry } from "./types"; + +type PlannedSystemEntry = Omit< + MigrationEntry, + "targetDomain" | "requiredTests" | "parityEvidence" | "status" | "notes" +> & { + requiredTests?: MigrationEntry["requiredTests"]; + notes?: MigrationEntry["notes"]; +}; + +function plannedSystemEntry(entry: PlannedSystemEntry): MigrationEntry { + return { + ...entry, + targetDomain: "system", + requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"], + parityEvidence: [], + status: "PLANNED", + notes: entry.notes ?? [], + }; +} + +export const systemMigrationEntries: readonly MigrationEntry[] = [ + // Operator alerts and read-only analytics. + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/alerts", + sourceFile: "src/app/admin/alerts/page.tsx", + targetPath: "/admin/system/operations/alerts", + decision: "REBUILD", + capabilities: { + read: [PERMS.NOTIFICATIONS_VIEW], + mutate: [PERMS.NOTIFICATIONS_EDIT, PERMS.NOTIFICATIONS_VIEW], + }, + dependencies: { + queries: ["AlertLogs"], + mutations: [ + "markAllAlertsRead", + "rcon.send hotelalert", + "sendHotelAlert", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "Marking alerts read and broadcasting a hotel alert use different current permission slugs", + ], + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/analytics", + sourceFile: "src/app/admin/analytics/page.tsx", + targetPath: "/admin/system/observability/analytics", + decision: "REBUILD", + capabilities: { read: [PERMS.ANALYTICS_VIEW], mutate: [] }, + dependencies: { + queries: [ + "Ban", + "Rooms", + "RoomTradeLog", + "User", + "chatlogs_room raw SQL", + "redisCache", + ], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/analytics/activity", + sourceFile: "src/app/admin/analytics/activity/page.tsx", + targetPath: "/admin/system/observability/analytics/activity", + decision: "REBUILD", + capabilities: { read: [PERMS.ANALYTICS_VIEW], mutate: [] }, + dependencies: { + queries: [ + "Ban", + "User", + "bans raw SQL", + "chatlogs_room raw SQL", + "commandlogs raw SQL", + "redisCache", + "users raw SQL", + ], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/analytics/economy", + sourceFile: "src/app/admin/analytics/economy/page.tsx", + targetPath: "/admin/system/observability/analytics/economy", + decision: "REBUILD", + capabilities: { read: [PERMS.ANALYTICS_VIEW], mutate: [] }, + dependencies: { + queries: [ + "LogsShopPurchases", + "MarketplaceItems", + "RoomTradeLog", + "User", + "logs_shop_purchases raw SQL", + "redisCache", + ], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + + // Runtime operations, health, and emulator configuration. + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/commandocentrum", + sourceFile: "src/app/admin/commandocentrum/page.tsx", + targetPath: "/admin/system/operations/command-center", + decision: "REBUILD", + capabilities: { + read: [PERMS.RCON_EXECUTE], + mutate: [PERMS.RCON_EXECUTE], + }, + dependencies: { + queries: [ + "EmulatorErrors", + "StaffActivities", + "User", + "fetchOpsHealth", + "fetchOpsOnlineUsers", + "siteSettings.getBool", + ], + mutations: [ + "alertUser", + "disconnectUser", + "executeCommand", + "forwardUser", + "giveBadge", + "giveCredits", + "giveDiamonds", + "giveDuckets", + "hotelAlert", + "sendGift", + "setMotto", + "setRank", + "updateCatalog", + "updateNavigator", + "updateWordFilter", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "The current set-rank action combines RCON with a database update and rank-relative authorization", + ], + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/devops", + sourceFile: "src/app/admin/devops/page.tsx", + targetPath: "/admin/system/observability/devops", + decision: "REBUILD", + capabilities: { read: [PERMS.DEVOPS_VIEW], mutate: [] }, + dependencies: { + queries: [ + "EmulatorErrors", + "EmulatorSettings", + "GET /api/admin/devops/health", + "fetchOpsHealth", + ], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Health checks probe the database, Redis, RCON, and online users"], + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/devops/errors", + sourceFile: "src/app/admin/devops/errors/page.tsx", + targetPath: "/admin/system/observability/devops/errors", + decision: "REBUILD", + capabilities: { read: [PERMS.DEVOPS_VIEW], mutate: [] }, + dependencies: { queries: ["EmulatorErrors"], mutations: [] }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/emulator", + sourceFile: "src/app/admin/emulator/page.tsx", + targetPath: "/admin/system/configuration/emulator", + decision: "REBUILD", + capabilities: { + read: [PERMS.SETTINGS_VIEW], + mutate: [PERMS.SETTINGS_EDIT], + }, + dependencies: { + queries: ["EmulatorSettings", "EmulatorTexts"], + mutations: ["updateEmulatorSetting", "updateEmulatorText"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["The current actions upsert emulator-owned settings and texts"], + }), + + // Foundation-owned legacy pages are retired, not migrated as workflows. + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/housekeeping", + sourceFile: "src/app/admin/housekeeping/page.tsx", + targetPath: null, + decision: "REMOVE", + capabilities: { read: [PERMS.SETTINGS_VIEW], mutate: [] }, + dependencies: { + queries: [ + "AclModelPermission", + "AclPermission", + "AclRole", + "WebsiteHousekeepingPermissions", + "exportPermissions", + "getAuditLogs", + "legacy ACL comparison", + ], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + requiredTests: ["integration", "e2e"], + notes: [ + "Remove the legacy foundation surface while preserving its read-only export and ACL comparison value in the live access workflow", + "Legacy housekeeping writes already redirect to live Permissions", + ], + }), + + // Staff, audit, chat, command, and trade observability. + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/logs", + sourceFile: "src/app/admin/logs/page.tsx", + targetPath: "/admin/system/observability/logs/staff", + decision: "REBUILD", + capabilities: { read: [PERMS.LOGS_VIEW], mutate: [] }, + dependencies: { queries: ["StaffActivities", "User"], mutations: [] }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/logs/audit", + sourceFile: "src/app/admin/logs/audit/page.tsx", + targetPath: "/admin/system/observability/logs/audit", + decision: "REBUILD", + capabilities: { read: [PERMS.LOGS_VIEW], mutate: [] }, + dependencies: { queries: ["getAuditLogs"], mutations: [] }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/logs/chat", + sourceFile: "src/app/admin/logs/chat/page.tsx", + targetPath: "/admin/system/observability/logs/chat", + decision: "REBUILD", + capabilities: { read: [PERMS.LOGS_VIEW], mutate: [] }, + dependencies: { + queries: ["chatlogs_room raw SQL", "loadChatLogList", "users raw SQL"], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/logs/commands", + sourceFile: "src/app/admin/logs/commands/page.tsx", + targetPath: "/admin/system/observability/logs/commands", + decision: "REBUILD", + capabilities: { read: [PERMS.LOGS_VIEW], mutate: [] }, + dependencies: { + queries: ["commandlogs raw SQL", "loadCommandLogList", "users raw SQL"], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/logs/trades", + sourceFile: "src/app/admin/logs/trades/page.tsx", + targetPath: "/admin/system/observability/logs/trades", + decision: "REBUILD", + capabilities: { read: [PERMS.LOGS_VIEW], mutate: [] }, + dependencies: { + queries: ["loadTradeLogList", "room_trade_log raw SQL", "users raw SQL"], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + + // Global configuration and access control. + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/maintenance", + sourceFile: "src/app/admin/maintenance/page.tsx", + targetPath: "/admin/system/operations/maintenance", + decision: "REBUILD", + capabilities: { + read: [PERMS.SETTINGS_VIEW], + mutate: [PERMS.SETTINGS_EDIT], + }, + dependencies: { + queries: ["WebsiteSetting"], + mutations: ["saveMaintenance", "siteSettings.reload"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Maintenance changes global login availability and splash content"], + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/menu", + sourceFile: "src/app/admin/menu/page.tsx", + targetPath: null, + decision: "REMOVE", + capabilities: { + read: [PERMS.SETTINGS_VIEW], + mutate: [PERMS.SETTINGS_EDIT], + }, + dependencies: { + queries: ["ADMIN_NAV_GROUPS", "siteSettings.get"], + mutations: ["saveAdminNavConfig", "siteSettings.update"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + requiredTests: ["integration", "e2e"], + notes: [ + "Remove the manual legacy menu editor because foundation manifests own navigation", + ], + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/permissions", + sourceFile: "src/app/admin/permissions/page.tsx", + targetPath: "/admin/system/access/permissions", + decision: "REBUILD", + capabilities: { + read: [PERMS.PERMISSIONS_MANAGE], + mutate: [PERMS.PERMISSIONS_MANAGE], + }, + dependencies: { + queries: ["fetchEmulatorRankSummaries", "users grouped by rank"], + mutations: [ + "createRank", + "deleteRank", + "repairAdminNavAclGrants", + "rcon.send updatepermissions", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Keep this live ACL and emulator-rank workflow in System access"], + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/permissions/:id", + sourceFile: "src/app/admin/permissions/[id]/page.tsx", + targetPath: "/admin/system/access/permissions/:id", + decision: "REBUILD", + capabilities: { + read: [PERMS.PERMISSIONS_MANAGE], + mutate: [PERMS.PERMISSIONS_MANAGE], + }, + dependencies: { + queries: [ + "AclModelPermission", + "AclPermission", + "AclRole", + "User", + "fetchEmulatorRankForEdit", + ], + mutations: [ + "AclRole load-time synchronization", + "rcon.send updatepermissions", + "saveRank", + "setCmsPermissions", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "The current route can create or retitle a CMS role while loading rank detail", + ], + }), + plannedSystemEntry({ + surface: "admin", + legacyPath: "/admin/settings", + sourceFile: "src/app/admin/settings/page.tsx", + targetPath: "/admin/system/configuration/settings", + decision: "REBUILD", + capabilities: { + read: [PERMS.SETTINGS_VIEW], + mutate: [PERMS.SETTINGS_EDIT], + }, + dependencies: { + queries: ["WebsiteSetting"], + mutations: [ + "createSetting", + "deleteSetting", + "saveManagedSettings", + "siteSettings.reload", + "updateSetting", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "Global setting changes can also invalidate official gamedata and badge caches", + ], + }), +];