From 09850c807c550207bc1715def618b46bb854de5e Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 24 Aug 2026 21:54:10 +0200 Subject: [PATCH] docs: audit housekeeping content workflows --- .../housekeeping/migration/content.test.ts | 96 +++ .../housekeeping/migration/content.ts | 572 ++++++++++++++++++ 2 files changed, 668 insertions(+) create mode 100644 src/features/housekeeping/migration/content.test.ts create mode 100644 src/features/housekeeping/migration/content.ts diff --git a/src/features/housekeeping/migration/content.test.ts b/src/features/housekeeping/migration/content.test.ts new file mode 100644 index 0000000000..69c56fab8e --- /dev/null +++ b/src/features/housekeeping/migration/content.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from "vitest"; +import { PERMS } from "@/lib/permission-slugs"; +import { contentMigrationEntries } from "./content"; +import { ownedLegacyPages } from "./discover-legacy-pages"; +import { validateMigrationEntries } from "./validate-matrix"; + +const CONTENT_PREFIXES = [ + "/admin/articles", + "/admin/photos", + "/admin/media", + "/admin/banners", + "/admin/ads", + "/admin/events", + "/admin/polls", + "/admin/help-questions", + "/admin/tags", + "/admin/prefixes", + "/admin/writeable-boxes", + "/admin/email-templates", + "/admin/theme", + "/admin/favicon", + "/admin/translations", +] as const; + +describe("contentMigrationEntries", () => { + it("covers every content page without legacy-domain targets", () => { + const expected = ownedLegacyPages(CONTENT_PREFIXES); + expect(validateMigrationEntries(expected, contentMigrationEntries)).toEqual( + [], + ); + expect( + contentMigrationEntries.every( + (row) => + row.targetPath === null || + row.targetPath.startsWith("/admin/content/"), + ), + ).toBe(true); + }); + + it("preserves dynamic ids in the consolidated workflow targets", () => { + expect( + contentMigrationEntries + .filter((row) => row.legacyPath.endsWith(":id")) + .map(({ legacyPath, targetPath }) => ({ legacyPath, targetPath })), + ).toEqual([ + { + legacyPath: "/admin/articles/:id", + targetPath: "/admin/content/editorial/articles/:id", + }, + { + legacyPath: "/admin/ads/:id", + targetPath: "/admin/content/media/ads/:id", + }, + { + legacyPath: "/admin/events/:id", + targetPath: "/admin/content/engagement/events/:id", + }, + { + legacyPath: "/admin/polls/:id", + targetPath: "/admin/content/engagement/polls/:id", + }, + { + legacyPath: "/admin/help-questions/:id", + targetPath: "/admin/content/help/questions/:id", + }, + ]); + }); + + it("keeps every audited workflow planned without parity claims", () => { + expect( + contentMigrationEntries.every( + (row) => row.status === "PLANNED" && row.parityEvidence.length === 0, + ), + ).toBe(true); + }); + + it("requires privileged settings edits for global runtime configuration", () => { + for (const legacyPath of [ + "/admin/theme", + "/admin/favicon", + "/admin/translations/cms", + "/admin/translations/client", + "/admin/translations/emulator", + ]) { + expect(contentMigrationEntries).toContainEqual( + expect.objectContaining({ + legacyPath, + capabilities: expect.objectContaining({ + mutate: [PERMS.SETTINGS_EDIT], + }), + auditRequirement: "PRIVILEGED_MUTATION", + }), + ); + } + }); +}); diff --git a/src/features/housekeeping/migration/content.ts b/src/features/housekeeping/migration/content.ts new file mode 100644 index 0000000000..693743b8a3 --- /dev/null +++ b/src/features/housekeeping/migration/content.ts @@ -0,0 +1,572 @@ +import { PERMS } from "@/lib/permission-slugs"; +import type { MigrationEntry } from "./types"; + +type PlannedContentEntry = Omit< + MigrationEntry, + "targetDomain" | "requiredTests" | "parityEvidence" | "status" | "notes" +> & { + requiredTests?: MigrationEntry["requiredTests"]; + notes?: MigrationEntry["notes"]; +}; + +function plannedContentEntry(entry: PlannedContentEntry): MigrationEntry { + return { + ...entry, + targetDomain: "content", + requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"], + parityEvidence: [], + status: "PLANNED", + notes: entry.notes ?? [], + }; +} + +export const contentMigrationEntries: readonly MigrationEntry[] = [ + // Batch 1: editorial publishing and media management. + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/articles", + sourceFile: "src/app/admin/articles/page.tsx", + targetPath: "/admin/content/editorial/articles", + decision: "REBUILD", + capabilities: { + read: [PERMS.NEWS_VIEW], + mutate: [PERMS.NEWS_EDIT], + }, + dependencies: { + queries: ["User", "WebsiteArticles"], + mutations: ["deleteArticle"], + }, + auditRequirement: "MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/articles/new", + sourceFile: "src/app/admin/articles/new/page.tsx", + targetPath: "/admin/content/editorial/articles/new", + decision: "REBUILD", + capabilities: { + read: [PERMS.NEWS_EDIT], + mutate: [PERMS.NEWS_EDIT], + }, + dependencies: { + queries: ["GET /api/media"], + mutations: ["createArticle", "uploadMediaAndReturn"], + }, + auditRequirement: "MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/articles/:id", + sourceFile: "src/app/admin/articles/[id]/page.tsx", + targetPath: "/admin/content/editorial/articles/:id", + decision: "REBUILD", + capabilities: { + read: [PERMS.NEWS_VIEW], + mutate: [PERMS.NEWS_EDIT], + }, + dependencies: { + queries: ["GET /api/media", "WebsiteArticles"], + mutations: ["deleteArticle", "updateArticle", "uploadMediaAndReturn"], + }, + auditRequirement: "MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/photos", + sourceFile: "src/app/admin/photos/page.tsx", + targetPath: "/admin/content/media/photos", + decision: "REBUILD", + capabilities: { + read: [PERMS.PAGES_VIEW], + mutate: [PERMS.PAGES_EDIT], + }, + dependencies: { + queries: ["CameraWeb", "User"], + mutations: ["deletePhoto"], + }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/media", + sourceFile: "src/app/admin/media/page.tsx", + targetPath: "/admin/content/media/library", + decision: "REBUILD", + capabilities: { + read: [PERMS.PAGES_VIEW], + mutate: [PERMS.PAGES_EDIT], + }, + dependencies: { + queries: ["GET /api/media"], + mutations: ["deleteMedia", "uploadMedia"], + }, + auditRequirement: "MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/banners", + sourceFile: "src/app/admin/banners/page.tsx", + targetPath: "/admin/content/media/banners", + decision: "REBUILD", + capabilities: { + read: [PERMS.BANNERS_VIEW], + mutate: [PERMS.BANNERS_EDIT], + }, + dependencies: { + queries: ["WebsiteBanner"], + mutations: ["createBanner", "deleteBanner", "updateBanner"], + }, + auditRequirement: "MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/ads", + sourceFile: "src/app/admin/ads/page.tsx", + targetPath: "/admin/content/media/ads", + decision: "REBUILD", + capabilities: { + read: [PERMS.PAGES_VIEW], + mutate: [PERMS.PAGES_EDIT], + }, + dependencies: { + queries: ["WebsiteAds"], + mutations: ["deleteAd"], + }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/ads/new", + sourceFile: "src/app/admin/ads/new/page.tsx", + targetPath: "/admin/content/media/ads/new", + decision: "REBUILD", + capabilities: { + read: [PERMS.PAGES_EDIT], + mutate: [PERMS.PAGES_EDIT], + }, + dependencies: { queries: [], mutations: ["createAd"] }, + auditRequirement: "MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/ads/:id", + sourceFile: "src/app/admin/ads/[id]/page.tsx", + targetPath: "/admin/content/media/ads/:id", + decision: "REBUILD", + capabilities: { + read: [PERMS.PAGES_VIEW], + mutate: [PERMS.PAGES_EDIT], + }, + dependencies: { + queries: ["WebsiteAds"], + mutations: ["deleteAd", "updateAd"], + }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + + // Batch 2: events, polls, help content, and supporting taxonomies. + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/events", + sourceFile: "src/app/admin/events/page.tsx", + targetPath: "/admin/content/engagement/events", + decision: "REBUILD", + capabilities: { + read: [PERMS.EVENTS_VIEW], + mutate: [PERMS.EVENTS_EDIT], + }, + dependencies: { + queries: ["fetchAdminList", "WebsiteEvent", "WebsiteEventType"], + mutations: ["deleteEvent"], + }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/events/create", + sourceFile: "src/app/admin/events/create/page.tsx", + targetPath: "/admin/content/engagement/events/create", + decision: "REBUILD", + capabilities: { + read: [PERMS.EVENTS_EDIT], + mutate: [PERMS.EVENTS_EDIT], + }, + dependencies: { + queries: ["WebsiteEventType"], + mutations: ["createEvent"], + }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/events/types", + sourceFile: "src/app/admin/events/types/page.tsx", + targetPath: "/admin/content/engagement/events/types", + decision: "REBUILD", + capabilities: { + read: [PERMS.EVENTS_EDIT], + mutate: [PERMS.EVENTS_EDIT], + }, + dependencies: { + queries: ["WebsiteEventType"], + mutations: ["createEventType", "deleteEventType", "updateEventType"], + }, + auditRequirement: "MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/events/:id", + sourceFile: "src/app/admin/events/[id]/page.tsx", + targetPath: "/admin/content/engagement/events/:id", + decision: "REBUILD", + capabilities: { + read: [PERMS.EVENTS_EDIT], + mutate: [PERMS.EVENTS_EDIT], + }, + dependencies: { + queries: ["User", "WebsiteEvent", "WebsiteEventType"], + mutations: [ + "addEventPrize", + "addEventWinner", + "deleteEventPrize", + "updateEvent", + ], + }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/polls", + sourceFile: "src/app/admin/polls/page.tsx", + targetPath: "/admin/content/engagement/polls", + decision: "REBUILD", + capabilities: { + read: [PERMS.POLLS_VIEW], + mutate: [PERMS.POLLS_EDIT], + }, + dependencies: { + queries: ["fetchAdminList", "WebsitePoll"], + mutations: ["deletePoll"], + }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/polls/create", + sourceFile: "src/app/admin/polls/create/page.tsx", + targetPath: "/admin/content/engagement/polls/create", + decision: "REBUILD", + capabilities: { + read: [PERMS.POLLS_EDIT], + mutate: [PERMS.POLLS_EDIT], + }, + dependencies: { queries: [], mutations: ["createPoll"] }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/polls/:id", + sourceFile: "src/app/admin/polls/[id]/page.tsx", + targetPath: "/admin/content/engagement/polls/:id", + decision: "REBUILD", + capabilities: { + read: [PERMS.POLLS_EDIT], + mutate: [PERMS.POLLS_EDIT], + }, + dependencies: { + queries: ["WebsitePoll"], + mutations: [ + "addPollQuestion", + "deletePollQuestion", + "updatePoll", + "updatePollQuestion", + ], + }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/help-questions", + sourceFile: "src/app/admin/help-questions/page.tsx", + targetPath: "/admin/content/help/questions", + decision: "REBUILD", + capabilities: { read: [PERMS.PAGES_VIEW], mutate: [] }, + dependencies: { + queries: ["WebsiteHelpCenterCategories"], + mutations: [], + }, + auditRequirement: "NONE", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/help-questions/new", + sourceFile: "src/app/admin/help-questions/new/page.tsx", + targetPath: "/admin/content/help/questions/new", + decision: "REBUILD", + capabilities: { + read: [PERMS.PAGES_EDIT], + mutate: [PERMS.PAGES_EDIT], + }, + dependencies: { queries: [], mutations: ["createHelpQuestion"] }, + auditRequirement: "MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/help-questions/:id", + sourceFile: "src/app/admin/help-questions/[id]/page.tsx", + targetPath: "/admin/content/help/questions/:id", + decision: "REBUILD", + capabilities: { + read: [PERMS.PAGES_VIEW], + mutate: [PERMS.PAGES_EDIT], + }, + dependencies: { + queries: ["WebsiteHelpCenterCategories"], + mutations: ["deleteHelpQuestion", "updateHelpQuestion"], + }, + auditRequirement: "MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/tags", + sourceFile: "src/app/admin/tags/page.tsx", + targetPath: "/admin/content/editorial/tags", + decision: "REBUILD", + capabilities: { + read: [PERMS.PAGES_VIEW], + mutate: [PERMS.PAGES_EDIT], + }, + dependencies: { + queries: ["Taggables", "Tags"], + mutations: ["createTag", "deleteTag", "updateTag"], + }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/prefixes", + sourceFile: "src/app/admin/prefixes/page.tsx", + targetPath: "/admin/content/engagement/prefixes", + decision: "REBUILD", + capabilities: { + read: [PERMS.PREFIXES_VIEW], + mutate: [PERMS.PREFIXES_EDIT], + }, + dependencies: { + queries: [ + "GET /api/admin/prefixes", + "GET /api/admin/prefixes/blacklist", + "GET /api/admin/prefixes/settings", + ], + mutations: [ + "addBlacklistWord", + "createPrefix", + "deletePrefix", + "removeBlacklistWord", + "updatePrefix", + "updatePrefixSettings", + ], + }, + auditRequirement: "MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/writeable-boxes", + sourceFile: "src/app/admin/writeable-boxes/page.tsx", + targetPath: "/admin/content/editorial/writeable-boxes", + decision: "REBUILD", + capabilities: { + read: [PERMS.PAGES_VIEW], + mutate: [PERMS.PAGES_EDIT], + }, + dependencies: { + queries: ["WebsiteWriteableBoxes"], + mutations: ["createBox", "deleteBox", "toggleBox", "updateBox"], + }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/email-templates", + sourceFile: "src/app/admin/email-templates/page.tsx", + targetPath: "/admin/content/help/email-templates", + decision: "REBUILD", + capabilities: { + read: [PERMS.PAGES_VIEW], + mutate: [PERMS.PAGES_EDIT], + }, + dependencies: { + queries: ["EmailTemplates"], + mutations: [ + "createEmailTemplate", + "deleteEmailTemplate", + "updateEmailTemplate", + ], + }, + auditRequirement: "MUTATION", + localization: "COMPLETE", + accessibility: "PARTIAL", + }), + + // Batch 3: brand presentation and global localization configuration. + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/theme", + sourceFile: "src/app/admin/theme/page.tsx", + targetPath: "/admin/content/brand/theme", + decision: "REBUILD", + capabilities: { + read: [PERMS.SETTINGS_VIEW], + mutate: [PERMS.SETTINGS_EDIT], + }, + dependencies: { + queries: ["listCustomThemes", "siteSettings.get"], + mutations: [ + "applyCustomTheme", + "applyPreset", + "deleteCustomTheme", + "renameCustomTheme", + "saveCustomTheme", + "saveTheme", + ], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/favicon", + sourceFile: "src/app/admin/favicon/page.tsx", + targetPath: "/admin/content/brand/favicon", + decision: "REBUILD", + capabilities: { + read: [PERMS.SETTINGS_VIEW], + mutate: [PERMS.SETTINGS_EDIT], + }, + dependencies: { + queries: ["WebsiteSetting"], + mutations: ["deleteFavicon", "saveFavicon"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: [ + "Current favicon actions do not enforce an edit permission or emit an audit record", + ], + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/translations", + sourceFile: "src/app/admin/translations/page.tsx", + targetPath: "/admin/content/localization", + decision: "MERGE", + capabilities: { read: [PERMS.SETTINGS_VIEW], mutate: [] }, + dependencies: { queries: [], mutations: [] }, + auditRequirement: "NONE", + localization: "COMPLETE", + accessibility: "COMPLETE", + requiredTests: ["e2e"], + notes: ["Redirect-only landing page for the localization workflow"], + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/translations/client", + sourceFile: "src/app/admin/translations/client/page.tsx", + targetPath: "/admin/content/localization/client", + decision: "REBUILD", + capabilities: { + read: [PERMS.SETTINGS_VIEW], + mutate: [PERMS.SETTINGS_EDIT], + }, + dependencies: { + queries: ["CLIENT_TRANSLATION_FILES", "fs.readFile"], + mutations: ["saveClientTranslations"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Current file mutation does not emit an audit record"], + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/translations/cms", + sourceFile: "src/app/admin/translations/cms/page.tsx", + targetPath: "/admin/content/localization/cms", + decision: "REBUILD", + capabilities: { + read: [PERMS.SETTINGS_VIEW], + mutate: [PERMS.SETTINGS_EDIT], + }, + dependencies: { + queries: ["fs.readFile"], + mutations: ["saveTranslations"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + notes: ["Current file mutation does not emit an audit record"], + }), + plannedContentEntry({ + surface: "admin", + legacyPath: "/admin/translations/emulator", + sourceFile: "src/app/admin/translations/emulator/page.tsx", + targetPath: "/admin/content/localization/emulator", + decision: "REBUILD", + capabilities: { + read: [PERMS.SETTINGS_VIEW], + mutate: [PERMS.SETTINGS_EDIT], + }, + dependencies: { + queries: ["EmulatorSettings"], + mutations: ["saveEmulatorSettings"], + }, + auditRequirement: "PRIVILEGED_MUTATION", + localization: "PARTIAL", + accessibility: "PARTIAL", + }), +];