diff --git a/src/features/housekeeping/migration/hotel.test.ts b/src/features/housekeeping/migration/hotel.test.ts new file mode 100644 index 00000000..898916a8 --- /dev/null +++ b/src/features/housekeeping/migration/hotel.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from "vitest"; +import { PERMS } from "@/lib/permission-slugs"; +import { ownedLegacyPages } from "./discover-legacy-pages"; +import { hotelMigrationEntries } from "./hotel"; +import { validateMigrationEntries } from "./validate-matrix"; + +const HOTEL_PREFIXES = [ + "/admin/rooms", + "/admin/navigation", + "/admin/radio", + "/admin/studio", +] as const; + +describe("hotelMigrationEntries", () => { + it("maps all hotel tools below the hotel target root", () => { + expect( + validateMigrationEntries( + ownedLegacyPages(HOTEL_PREFIXES), + hotelMigrationEntries, + ), + ).toEqual([]); + expect( + hotelMigrationEntries.every( + (row) => + row.targetPath === null || row.targetPath.startsWith("/admin/hotel/"), + ), + ).toBe(true); + }); + + it("preserves room ids while merging only the detail aliases", () => { + expect( + hotelMigrationEntries + .filter((row) => row.legacyPath.startsWith("/admin/rooms")) + .map(({ legacyPath, targetPath, decision }) => ({ + legacyPath, + targetPath, + decision, + })), + ).toEqual([ + { + legacyPath: "/admin/rooms", + targetPath: "/admin/hotel/rooms", + decision: "REBUILD", + }, + { + legacyPath: "/admin/rooms/:id", + targetPath: "/admin/hotel/rooms/:id", + decision: "MERGE", + }, + { + legacyPath: "/admin/rooms/:id/furni", + targetPath: "/admin/hotel/rooms/:id/furni", + decision: "REBUILD", + }, + { + legacyPath: "/admin/rooms/edit/:id", + targetPath: "/admin/hotel/rooms/:id", + decision: "MERGE", + }, + { + legacyPath: "/admin/rooms/show/:id", + targetPath: "/admin/hotel/rooms/:id", + decision: "MERGE", + }, + ]); + }); + + it("keeps every audited workflow planned without parity claims", () => { + expect( + hotelMigrationEntries.every( + (row) => row.status === "PLANNED" && row.parityEvidence.length === 0, + ), + ).toBe(true); + }); + + it("requires privileged audit and an explicit capability for every mutation", () => { + const mutationRows = hotelMigrationEntries.filter( + (row) => row.dependencies.mutations.length > 0, + ); + + expect(mutationRows.length).toBeGreaterThan(0); + expect( + mutationRows.every( + (row) => + row.capabilities.mutate.length > 0 && + row.auditRequirement === "PRIVILEGED_MUTATION", + ), + ).toBe(true); + }); + + it("records the actual permissions for room, radio, and Studio mutations", () => { + expect(hotelMigrationEntries).toContainEqual( + expect.objectContaining({ + legacyPath: "/admin/rooms/edit/:id", + capabilities: { + read: [PERMS.ROOMS_EDIT], + mutate: [PERMS.ROOMS_EDIT, PERMS.ROOMS_DELETE], + }, + }), + ); + expect(hotelMigrationEntries).toContainEqual( + expect.objectContaining({ + legacyPath: "/admin/radio/api-keys", + capabilities: { + read: [PERMS.RADIO_VIEW], + mutate: [PERMS.RADIO_EDIT], + }, + }), + ); + expect(hotelMigrationEntries).toContainEqual( + expect.objectContaining({ + legacyPath: "/admin/studio/sync", + capabilities: { + read: [PERMS.ASSETS_IMPORT], + mutate: [PERMS.ASSETS_IMPORT], + }, + }), + ); + }); +}); diff --git a/src/features/housekeeping/migration/hotel.ts b/src/features/housekeeping/migration/hotel.ts new file mode 100644 index 00000000..095f9888 --- /dev/null +++ b/src/features/housekeeping/migration/hotel.ts @@ -0,0 +1,659 @@ +import { PERMS } from "@/lib/permission-slugs"; +import type { MigrationEntry } from "./types"; + +type PlannedHotelEntry = Omit< + MigrationEntry, + "targetDomain" | "requiredTests" | "parityEvidence" | "status" | "notes" +> & { + requiredTests?: MigrationEntry["requiredTests"]; + notes?: MigrationEntry["notes"]; +}; + +function plannedHotelEntry(entry: PlannedHotelEntry): MigrationEntry { + return { + ...entry, + targetDomain: "hotel", + requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"], + parityEvidence: [], + status: "PLANNED", + notes: entry.notes ?? [], + }; +} + +export const hotelMigrationEntries: readonly MigrationEntry[] = [ + // Batch 1: room directory, consolidated room detail, furniture, and navigator. + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/rooms", + sourceFile: "src/app/admin/rooms/page.tsx", + targetPath: "/admin/hotel/rooms", + decision: "REBUILD", + capabilities: { read: [PERMS.ROOMS_VIEW], mutate: [] }, + dependencies: { + queries: ["fetchAdminList", "Rooms", "GET /api/admin/export?type=rooms"], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/rooms/:id", + sourceFile: "src/app/admin/rooms/[id]/page.tsx", + targetPath: "/admin/hotel/rooms/:id", + decision: "MERGE", + capabilities: { read: [PERMS.ROOMS_VIEW], mutate: [] }, + dependencies: { queries: [], mutations: [] }, + auditRequirement: "NONE", + localization: "COMPLETE", + accessibility: "COMPLETE", + requiredTests: ["e2e"], + notes: ["Redirect-only alias for the room detail workflow"], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/rooms/:id/furni", + sourceFile: "src/app/admin/rooms/[id]/furni/page.tsx", + targetPath: "/admin/hotel/rooms/:id/furni", + decision: "REBUILD", + capabilities: { + read: [PERMS.ROOMS_VIEW], + mutate: [PERMS.ROOMS_EDIT], + }, + dependencies: { + queries: ["Items", "ItemsBase", "Rooms", "User", "loadRoom"], + mutations: [ + "bulkDeleteRoomItems", + "deleteRoomItem", + "RCON kickall", + "RCON reloadroom", + "roomRconAction", + "updateRoomItem", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Preserve the furniture suffix and room id in the target route"], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/rooms/edit/:id", + sourceFile: "src/app/admin/rooms/edit/[id]/page.tsx", + targetPath: "/admin/hotel/rooms/:id", + decision: "MERGE", + capabilities: { + read: [PERMS.ROOMS_EDIT], + mutate: [PERMS.ROOMS_EDIT, PERMS.ROOMS_DELETE], + }, + dependencies: { + queries: ["Rooms", "User", "loadRoom"], + mutations: [ + "deleteRoom", + "RCON kickall", + "RCON reloadroom", + "RCON updateroom", + "roomRconAction", + "updateRoom", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Merge editing and RCON controls into the room detail workflow"], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/rooms/show/:id", + sourceFile: "src/app/admin/rooms/show/[id]/page.tsx", + targetPath: "/admin/hotel/rooms/:id", + decision: "MERGE", + capabilities: { read: [PERMS.ROOMS_VIEW], mutate: [] }, + dependencies: { queries: ["Rooms", "User", "loadRoom"], mutations: [] }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "Merge the canonical read-only detail alias without changing its id", + ], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/navigation", + sourceFile: "src/app/admin/navigation/page.tsx", + targetPath: "/admin/hotel/navigation", + decision: "REBUILD", + capabilities: { read: [PERMS.PAGES_VIEW], mutate: [] }, + dependencies: { + queries: [ + "NavigatorFlatcats", + "NavigatorPubliccats", + "navigator_publics raw SQL", + "Rooms", + ], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + + // Batch 2: radio overview, monitoring, moderation, and runtime configuration. + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/radio", + sourceFile: "src/app/admin/radio/page.tsx", + targetPath: "/admin/hotel/radio/overview", + decision: "REBUILD", + capabilities: { + read: [PERMS.RADIO_VIEW], + mutate: [PERMS.RADIO_EDIT], + }, + dependencies: { + queries: ["RadioApplications", "RadioSchedules", "RadioShouts"], + mutations: ["deleteShout"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/radio/api-keys", + sourceFile: "src/app/admin/radio/api-keys/page.tsx", + targetPath: "/admin/hotel/radio/api-keys", + decision: "REBUILD", + capabilities: { + read: [PERMS.RADIO_VIEW], + mutate: [PERMS.RADIO_EDIT], + }, + dependencies: { + queries: ["RadioApiKeys"], + mutations: ["createApiKey", "deleteApiKey", "toggleApiKey"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "API-key generation and lifecycle changes are credential mutations", + ], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/radio/autodj", + sourceFile: "src/app/admin/radio/autodj/page.tsx", + targetPath: "/admin/hotel/radio/autodj", + decision: "REBUILD", + capabilities: { + read: [PERMS.RADIO_VIEW], + mutate: [PERMS.RADIO_EDIT], + }, + dependencies: { + queries: ["RadioAutoDjPlaylist"], + mutations: ["createTrack", "deleteTrack", "toggleTrack"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/radio/banners", + sourceFile: "src/app/admin/radio/banners/page.tsx", + targetPath: "/admin/hotel/radio/banners", + decision: "REBUILD", + capabilities: { + read: [PERMS.RADIO_VIEW], + mutate: [PERMS.RADIO_EDIT], + }, + dependencies: { + queries: ["RadioBanners"], + mutations: [ + "createRadioBanner", + "deleteRadioBanner", + "updateRadioBanner", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/radio/embed", + sourceFile: "src/app/admin/radio/embed/page.tsx", + targetPath: "/admin/hotel/radio/embed", + decision: "REBUILD", + capabilities: { read: [PERMS.RADIO_VIEW], mutate: [] }, + dependencies: { + queries: ["resolveHotelName", "siteSettings.get"], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/radio/history", + sourceFile: "src/app/admin/radio/history/page.tsx", + targetPath: "/admin/hotel/radio/history", + decision: "REBUILD", + capabilities: { read: [PERMS.RADIO_VIEW], mutate: [] }, + dependencies: { queries: ["RadioHistory", "User"], mutations: [] }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/radio/moderation", + sourceFile: "src/app/admin/radio/moderation/page.tsx", + targetPath: "/admin/hotel/radio/moderation", + decision: "REBUILD", + capabilities: { + read: [PERMS.RADIO_VIEW], + mutate: [PERMS.RADIO_EDIT], + }, + dependencies: { + queries: ["RadioShouts"], + mutations: ["deleteShout"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Shout deletion is a radio moderation mutation"], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/radio/monitoring", + sourceFile: "src/app/admin/radio/monitoring/page.tsx", + targetPath: "/admin/hotel/radio/monitoring", + decision: "REBUILD", + capabilities: { read: [PERMS.RADIO_VIEW], mutate: [] }, + dependencies: { + queries: [ + "fetch radio_listeners_api_url", + "fetch radio_now_playing_api_url", + "siteSettings.get", + ], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Monitoring probes operator-configured external HTTP endpoints"], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/radio/points", + sourceFile: "src/app/admin/radio/points/page.tsx", + targetPath: "/admin/hotel/radio/points", + decision: "REBUILD", + capabilities: { + read: [PERMS.RADIO_VIEW], + mutate: [PERMS.RADIO_EDIT], + }, + dependencies: { + queries: ["siteSettings.get", "WebsiteSetting"], + mutations: ["savePoints", "siteSettings.reload"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/radio/ranks", + sourceFile: "src/app/admin/radio/ranks/page.tsx", + targetPath: "/admin/hotel/radio/ranks", + decision: "REBUILD", + capabilities: { + read: [PERMS.RADIO_VIEW], + mutate: [PERMS.RADIO_EDIT], + }, + dependencies: { + queries: ["RadioRanks"], + mutations: ["createRadioRank", "deleteRadioRank", "updateRadioRank"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/radio/settings", + sourceFile: "src/app/admin/radio/settings/page.tsx", + targetPath: "/admin/hotel/radio/settings", + decision: "REBUILD", + capabilities: { + read: [PERMS.RADIO_VIEW], + mutate: [PERMS.RADIO_EDIT], + }, + dependencies: { + queries: ["WebsiteSetting"], + mutations: [ + "saveRadioSetting", + "saveRadioSettings", + "siteSettings.reload", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "Settings include external-service credentials, runtime flags, webhooks, and custom code", + ], + }), + + // Batch 3: Studio audit and asset import workflows. + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio", + sourceFile: "src/app/admin/studio/page.tsx", + targetPath: "/admin/hotel/studio/furni", + decision: "MERGE", + capabilities: { read: [PERMS.ASSETS_IMPORT], mutate: [] }, + dependencies: { queries: [], mutations: [] }, + auditRequirement: "NONE", + localization: "COMPLETE", + accessibility: "COMPLETE", + requiredTests: ["e2e"], + notes: ["Redirect-only entry into the Studio furniture workflow"], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio/audit", + sourceFile: "src/app/admin/studio/audit/page.tsx", + targetPath: "/admin/hotel/studio/audit", + decision: "REBUILD", + capabilities: { read: [PERMS.ASSETS_IMPORT], mutate: [] }, + dependencies: { + queries: ["POST /api/admin/import/audit", "runCatalogAudit"], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "The POST endpoint performs a read-only database and filesystem audit", + ], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio/badges", + sourceFile: "src/app/admin/studio/badges/page.tsx", + targetPath: "/admin/hotel/studio/badges", + decision: "REBUILD", + capabilities: { + read: [PERMS.ASSETS_IMPORT], + mutate: [PERMS.ASSETS_IMPORT, PERMS.CATALOG_EDIT], + }, + dependencies: { + queries: [ + "GET /api/admin/import/badges", + "getBadgeData", + "searchBadges", + "WebsiteBadges", + ], + mutations: [ + "importBadgeSynced", + "POST /api/admin/import/badges", + "updateBadge", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "Imports external badge assets into the configured gamedata filesystem", + ], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio/clone", + sourceFile: "src/app/admin/studio/clone/page.tsx", + targetPath: "/admin/hotel/studio/clone", + decision: "REBUILD", + capabilities: { + read: [PERMS.ASSETS_IMPORT], + mutate: [PERMS.ASSETS_IMPORT], + }, + dependencies: { + queries: [ + "GET /api/admin/import/clone", + "GET /api/admin/import/clone/icon", + "getCloneList", + "listSources", + ], + mutations: [ + "DELETE /api/admin/import/clone", + "POST /api/admin/import/clone", + "POST /api/admin/import/clone/batch", + "cloneSingleFurni", + "rcon.updateCatalog", + "rcon.updateItems", + "syncAssetsToGamedataBundle", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "Clone imports combine external HTTP, database, filesystem, and RCON mutations", + ], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio/clothing", + sourceFile: "src/app/admin/studio/clothing/page.tsx", + targetPath: "/admin/hotel/studio/clothing", + decision: "REBUILD", + capabilities: { + read: [PERMS.ASSETS_IMPORT], + mutate: [PERMS.ASSETS_IMPORT], + }, + dependencies: { + queries: [ + "GET /api/admin/import/clothing", + "GET /api/admin/import/clothing/sets", + "getClothingSetList", + "getFigureList", + ], + mutations: [ + "DELETE /api/admin/import/clothing", + "POST /api/admin/import/clothing/batch", + "POST /api/admin/import/clothing/sets/batch", + "importClothingSet", + "importSingleFigure", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Clothing imports download and delete asset files"], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio/effects", + sourceFile: "src/app/admin/studio/effects/page.tsx", + targetPath: "/admin/hotel/studio/effects", + decision: "REBUILD", + capabilities: { + read: [PERMS.ASSETS_IMPORT], + mutate: [PERMS.ASSETS_IMPORT], + }, + dependencies: { + queries: ["GET /api/admin/import/effects", "getEffectList"], + mutations: [ + "DELETE /api/admin/import/effects", + "importSingleEffect", + "POST /api/admin/import/effects/batch", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Effect imports download and delete asset files"], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio/furni", + sourceFile: "src/app/admin/studio/furni/page.tsx", + targetPath: "/admin/hotel/studio/furni", + decision: "REBUILD", + capabilities: { + read: [PERMS.ASSETS_IMPORT], + mutate: [PERMS.ASSETS_IMPORT], + }, + dependencies: { + queries: [ + "GET /api/admin/import/clone", + "GET /api/admin/import/furni", + "getHabboGamedataHotel", + "getTreeFlat", + "previewAutoCatalog", + "siteSettings.getBool", + ], + mutations: [ + "deleteImportedFurni", + "PATCH /api/admin/import/furni", + "POST /api/admin/import/furni", + "POST /api/admin/import/furni/batch", + "PUT /api/admin/import/furni/nitro-editor", + "rcon.updateCatalog", + "rcon.updateItems", + "setFurnidataTranslateEnabled", + "syncAssetsToGamedataBundle", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "Furniture imports combine official Habbo HTTP sources, catalog and item tables, asset files, and RCON reloads", + ], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio/maintenance", + sourceFile: "src/app/admin/studio/maintenance/page.tsx", + targetPath: "/admin/hotel/studio/maintenance", + decision: "REBUILD", + capabilities: { + read: [PERMS.CATALOG_EDIT], + mutate: [PERMS.CATALOG_EDIT], + }, + dependencies: { + queries: ["getFurniHealthAction", "previewAlignIdsAction"], + mutations: [ + "applyAlignIdsAction", + "fixCatalogOffersAction", + "fixEverythingAction", + "fixSpriteIdsAction", + "reconcileIdsAction", + "removeDuplicateItemsBaseAction", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["The current page relies on action-level catalog-edit enforcement"], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio/pets", + sourceFile: "src/app/admin/studio/pets/page.tsx", + targetPath: "/admin/hotel/studio/pets", + decision: "REBUILD", + capabilities: { + read: [PERMS.ASSETS_IMPORT], + mutate: [PERMS.ASSETS_IMPORT], + }, + dependencies: { + queries: [ + "GET /api/admin/import/pets", + "GET /api/admin/import/pets/icon", + "getPetList", + ], + mutations: [ + "DELETE /api/admin/import/pets", + "importSinglePet", + "POST /api/admin/import/pets/batch", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Pet imports download and delete asset files"], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio/repair-icons", + sourceFile: "src/app/admin/studio/repair-icons/page.tsx", + targetPath: "/admin/hotel/studio/repair-icons", + decision: "REBUILD", + capabilities: { + read: [PERMS.ASSETS_IMPORT], + mutate: [PERMS.ASSETS_IMPORT], + }, + dependencies: { + queries: [], + mutations: [ + "POST /api/admin/import/furni/repair-icons", + "repairMissingIcons", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "Repairs icon files from local bundles or configured external sources", + ], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio/sync", + sourceFile: "src/app/admin/studio/sync/page.tsx", + targetPath: "/admin/hotel/studio/sync", + decision: "REBUILD", + capabilities: { + read: [PERMS.ASSETS_IMPORT], + mutate: [PERMS.ASSETS_IMPORT], + }, + dependencies: { + queries: ["ItemsBase", "listSources"], + mutations: [ + "appendFurniEntriesBatch", + "cloneSingleFurni", + "POST /api/admin/import/clone/sync-all", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "Full source sync mutates item data and asset files from external clone sources", + ], + }), + plannedHotelEntry({ + surface: "admin", + legacyPath: "/admin/studio/upload", + sourceFile: "src/app/admin/studio/upload/page.tsx", + targetPath: "/admin/hotel/studio/upload", + decision: "REBUILD", + capabilities: { + read: [PERMS.ASSETS_IMPORT], + mutate: [PERMS.ASSETS_IMPORT], + }, + dependencies: { + queries: [], + mutations: ["POST /api/admin/import/furni/upload", "uploadSingleFurni"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Uploads mutate database, catalog, FurnitureData, and asset files"], + }), +];