diff --git a/.env.example b/.env.example index 14d9448f..bdd1d9e7 100644 --- a/.env.example +++ b/.env.example @@ -17,9 +17,6 @@ NODE_ENV=production PORT=3002 NEXT_TELEMETRY_DISABLED=1 UV_THREADPOOL_SIZE=16 -# Production requires this kill switch plus housekeeping.preview.access. -HOUSEKEEPING_NEXT_PREVIEW_ENABLED=false - # --- HOTEL & URLS --- HOTEL_NAME=EPIC WEB CONTROL APP_URL=http://localhost:3002 @@ -77,6 +74,9 @@ LOG_LEVEL=error # --- FLARESOLVERR (Cloudflare bypass for clone sources) --- FLARESOLVERR_URL=http://localhost:8191 +# Gated Housekeeping preview; never enabled in production +HOUSEKEEPING_NEXT_PREVIEW_ENABLED=false + # Catalog Studio export: dedicated clean clone on Beta-3 with Git push credentials. CATALOG_GIT_CHECKOUT= # Persistent directory shared by CMS and worker, outside the catalog clone. diff --git a/.husky/pre-commit b/.husky/pre-commit index 5ee7abd8..f07372ad 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,3 @@ +#!/usr/bin/env sh + pnpm exec lint-staged diff --git a/.husky/pre-push b/.husky/pre-push index 42267220..8da45aae 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,2 +1,4 @@ +#!/usr/bin/env sh + pnpm typecheck pnpm test diff --git a/.superpowers/sdd/2026-08-26-housekeeping-completion/task-10-report.md b/.superpowers/sdd/2026-08-26-housekeeping-completion/task-10-report.md new file mode 100644 index 00000000..a0105134 --- /dev/null +++ b/.superpowers/sdd/2026-08-26-housekeeping-completion/task-10-report.md @@ -0,0 +1,148 @@ +# Task 10 report: System vertical + +## Outcome + +Delivered the real System access, configuration, observability, and operations vertical from base `0117b45d74450510187f8f860ee927a193c45a3c` on `codex/housekeeping-complete`. + +- Registered the exact 17 System route IDs, canonical `/ase/system/*` hrefs, labels, and read capabilities from `migration/system.ts`. +- Added injected access, configuration, observability, and operations queries with forbidden, partial, and dependency-unavailable results. +- Extracted redirect-free, server-only, capability-guarded mutation services from the six legacy action modules while retaining their existing permission checks and `/admin` revalidation behavior. +- Registered 29 sensitive System commands through deterministic bootstrap, dispatcher authorization, confirmation, rate limiting, and audit. Reasons are mandatory for ACL/permission changes, global settings, alert broadcast, every RCON operation, and maintenance/global availability. +- Added four query-backed server workflow page modules with loading, empty, partial, error, forbidden, and ready states. +- Added the exact 17 System handlers to the global aggregate. The manifest contains real routes and deliberately keeps providers, search, inbox, and widgets empty for Task 19. + +No database operation, deployment, push, pull request update, Task 11 work, `/admin` or `/mod` cutover, rank-threshold authorization, or placeholder workflow was performed. `.remember/remember.md` remained untracked and untouched. + +## TDD evidence + +All Vitest commands used `--coverage.enabled=false` so each RED/GREEN cycle exercised only the named boundary. + +| Phase | Exact command | RED | GREEN | +| --- | --- | --- | --- | +| Routes | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/routes.test.ts` | 1 file failed before tests: missing `./routes`. | 1 file, 3 tests passed. | +| Injected queries | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/queries/system-queries.test.ts` | 1 file failed before tests: missing `./access`. | 1 file, 6 tests passed. | +| Commands and guarded service | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/commands/system-commands.test.ts` | 1 file failed before tests: missing `../services/mutations`. | 1 file, 6 tests passed at the first command boundary. | +| Legacy alert and maintenance wrappers | `pnpm exec vitest run --coverage.enabled=false src/actions/admin-alerts.test.ts src/actions/admin-maintenance.test.ts` | 1 of 7 tests failed because the existing alert mock granted `notifications.edit` instead of the canonical `admin.notifications.edit`. | 2 files, 7 tests passed after correcting only the stale mock permission. | +| ACL wrapper extraction | `pnpm exec vitest run --coverage.enabled=false src/lib/admin/acl-management-contract.test.ts` | 1 of 2 tests failed before `access.permissions.update` was present. | 1 file, 2 tests passed after the wrapper delegated to the guarded service. | +| Workflow pages | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/pages/system-pages.test.tsx` | 1 file failed before tests: missing `./access`. | 1 file, 8 tests passed. | +| Handler/bootstrap integration | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/route-handlers.test.ts src/features/housekeeping/foundation/commands/bootstrap.test.ts` | 2 tests failed: System had 0 handlers instead of 17 and no 29-command bootstrap registration. | 2 files, 3 tests passed. | +| Review regressions | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/commands/system-commands.test.ts src/features/housekeeping/domains/system/services/mutations-production.test.ts src/lib/admin/acl-management-contract.test.ts` | 3 files failed; 11 tests failed and 6 passed. Failures proved missing alert reason, six whitespace-only inputs accepted, three alert dependency failures swallowed, and the public/non-strict production boundary. | 3 files, 17 tests passed after the minimal corrections. | + +The first integrated target run passed 17 files and 179 tests. `pnpm typecheck` then exposed four integration-only type errors (a heterogeneous test tuple, a bigint alert identifier, and result-union narrowing); the corrected run passed. The first `pnpm test:housekeeping` exposed exactly three obsolete foundation assertions (40 files/372 tests otherwise passed). The three directly obsolete contracts were updated with strict positive System assertions without relaxing another domain; the focused rerun passed 2 files/38 tests and the then-current full suite passed 42 files/376 tests. + +## Final verification + +- `pnpm exec vitest run --coverage.enabled=false src/actions/admin-alerts.test.ts src/actions/admin-maintenance.test.ts src/lib/admin/acl-management-contract.test.ts src/features/housekeeping/domains/system/routes.test.ts src/features/housekeeping/domains/system/queries/system-queries.test.ts src/features/housekeeping/domains/system/commands/system-commands.test.ts src/features/housekeeping/domains/system/services/mutations-production.test.ts src/features/housekeeping/domains/system/pages/system-pages.test.tsx src/features/housekeeping/migration/system.test.ts src/features/housekeeping/route-handlers.test.ts src/features/housekeeping/foundation/commands/bootstrap.test.ts src/features/housekeeping/foundation/commands/registry.test.ts src/features/housekeeping/foundation/commands/dispatcher.test.ts src/features/housekeeping/foundation/commands/confirmation.test.ts src/features/housekeeping/foundation/commands/audit-envelope.test.ts src/features/housekeeping/foundation/foundation-source-contract.test.ts src/features/housekeeping/foundation/registry.test.ts`  17 files, 185 tests passed. +- `pnpm exec vitest run --coverage.enabled=false src/lib/admin-operations-contract.test.ts src/lib/staff-smoke-contract.test.ts src/lib/admin/authorization-contract.test.ts`  3 files, 97 tests passed. +- `pnpm test:housekeeping`  43 files, 385 tests passed. +- `pnpm typecheck`  passed (`tsc --noEmit`). +- `pnpm exec biome check --formatter-enabled=false src/actions/admin-alerts.test.ts src/actions/admin-alerts.ts src/actions/admin-emulator.ts src/actions/admin-maintenance.ts src/actions/admin-settings.ts src/actions/commandocentrum.ts src/actions/permissions.ts src/features/housekeeping/domains/system src/features/housekeeping/foundation/commands/bootstrap.test.ts src/features/housekeeping/foundation/commands/bootstrap.ts src/features/housekeeping/foundation/foundation-source-contract.test.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/route-handlers.test.ts src/features/housekeeping/route-handlers.ts src/lib/admin/acl-management-contract.test.ts`  checked 32 files; no fixes applied. +- `git diff --check`  exit 0; only expected Git autocrlf warnings. +- `git diff --cached --check`  exit 0 before staging and rerun after exact staging. +- Independent read-only re-review  0 Critical, 0 Important, 0 Minor; ready verdict. + +Node/pnpm emitted this non-blocking warning during pnpm gates: + +```text +[WARN] Unsupported engine: wanted: {"node":">=26.8.1 <27"} (current: {"node":"v26.7.0","pnpm":"11.24.0"}) +``` + +## Architectural decisions + +- The migration matrix remains the single source of route truth. The System route array is materialized from its exact identifiers and values, and tests assert ordered route/handler equality rather than set-only coverage. +- Query factories accept narrow adapters; production adapters reuse existing ACL, settings, emulator, health, online-user, analytics, log, alert, and maintenance services. The fix round added only a strict online-roster helper beside the unchanged tolerant legacy API in `ops-online-users.ts`, so System can report a database outage truthfully. +- `systemMutationService` is the only public production mutation boundary. It is server-only and repeats capability enforcement even when called by an already-guarded legacy action or an authorized dispatcher. The unguarded production adapter is module-private. +- Adapter exceptions and unsuccessful RCON sends become typed `DEPENDENCY_UNAVAILABLE` failures. Rank-delete conflict metadata travels in the standard `fieldErrors` shape; the legacy wrapper reconstructs the prior human-readable `ActionError`, keeping the dispatcher result schema strict. +- Command string schemas use a non-transforming `\S` check to reject whitespace-only values; normalization and trimming remain at the guarded service boundary. This preserves the foundation registry rule that command schemas contain no executable transforms. +- System navigation labels use stable `pages.housekeeping.routes.system.*` keys. Complete source strings are present in the tested English and Italian catalogs; repository fallback remains responsible for other locales. +- Foundation source-boundary changes are a narrow source-to-import allowlist for the new System integration edges. Existing forbidden directions for every other domain remain asserted. + +## Changed files + +- `.superpowers/sdd/2026-08-26-housekeeping-completion/task-10-report.md` +- `src/actions/admin-alerts.test.ts` +- `src/actions/admin-alerts.ts` +- `src/actions/admin-emulator.ts` +- `src/actions/admin-maintenance.ts` +- `src/actions/admin-settings.ts` +- `src/actions/commandocentrum.ts` +- `src/actions/permissions.ts` +- `src/features/housekeeping/domains/system/commands/system-commands.test.ts` +- `src/features/housekeeping/domains/system/commands/system-commands.ts` +- `src/features/housekeeping/domains/system/manifest.ts` +- `src/features/housekeeping/domains/system/pages/access.tsx` +- `src/features/housekeeping/domains/system/pages/configuration.tsx` +- `src/features/housekeeping/domains/system/pages/observability.tsx` +- `src/features/housekeeping/domains/system/pages/operations.tsx` +- `src/features/housekeeping/domains/system/pages/system-pages.test.tsx` +- `src/features/housekeeping/domains/system/queries/access.ts` +- `src/features/housekeeping/domains/system/queries/configuration.ts` +- `src/features/housekeeping/domains/system/queries/observability.ts` +- `src/features/housekeeping/domains/system/queries/operations.ts` +- `src/features/housekeeping/domains/system/queries/system-queries.test.ts` +- `src/features/housekeeping/domains/system/route-handlers.ts` +- `src/features/housekeeping/domains/system/routes.test.ts` +- `src/features/housekeeping/domains/system/routes.ts` +- `src/features/housekeeping/domains/system/services/mutations-production.test.ts` +- `src/features/housekeeping/domains/system/services/mutations.ts` +- `src/features/housekeeping/foundation/commands/bootstrap.test.ts` +- `src/features/housekeeping/foundation/commands/bootstrap.ts` +- `src/features/housekeeping/foundation/foundation-source-contract.test.ts` +- `src/features/housekeeping/foundation/registry.test.ts` +- `src/features/housekeeping/route-handlers.test.ts` +- `src/features/housekeeping/route-handlers.ts` +- `src/lib/admin/acl-management-contract.test.ts` + +## Official review fix round 1 + +The official review was addressed on exact base `3788ecd9f1a4e32e68abac5ee2dae3418cdebfb2`. The three parked Minor findings were deliberately left unchanged. + +### Findings resolved + +1. **Housekeeping label namespace:** all 17 System routes used legacy `pages.admin.*` keys, which the Task 9 preview layout correctly rejected. Routes now use 17 stable `pages.housekeeping.routes.system.*` keys, EN/IT provide non-empty source strings, and a preview-contract test builds and translates the real System navigation without broadening layout validation. +2. **Truthful partial/outage states:** access, configuration, and observability previously rendered empty before considering failed dependencies. Partial now takes precedence whenever any dependency failed. System online-user queries use a strict helper that exposes database failure; the existing tolerant `fetchOpsOnlineUsers` API and legacy behavior remain intact. +3. **Rank synchronization failures:** create, delete, and update no longer report success when `updatepermissions` returns false, and set-rank no longer reports success when RCON committed but database persistence failed. Both paths return the existing strict `DEPENDENCY_UNAVAILABLE` envelope with stable message keys and explicit `fieldErrors` describing `operation`, `completed`, and `pending` effects. Dispatcher audit records `intent` then `failure`, never `success`. +4. **Legacy permission error parity:** known rank-in-use and role-not-found conflicts retain their established `ActionError` text. Unknown infrastructure failures now cross the real `adminAction` boundary as ordinary errors and are sanitized to `Internal server error`; internal Housekeeping message keys are not exposed to the legacy UI. + +### Fix-round TDD evidence + +| Cycle | Exact command | RED | GREEN | +| --- | --- | --- | --- | +| Labels and runtime navigation | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/routes.test.ts src/features/housekeeping/foundation/localization-contract.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts` | 3 files failed; 4 tests failed and 66 passed. The route labels mismatched, EN/IT lacked the routes subtree, and preview layout rejected `pages.admin.hubs.tabs.permissions`. | 3 files, 70 tests passed. | +| Partial precedence and online-user outage | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/pages/system-pages.test.tsx src/features/housekeeping/domains/system/queries/operations-production.test.ts` | 2 files failed; 4 tests failed and 9 passed. Three pages rendered empty, and the production adapter resolved a false ready zero-user state on database failure. | 2 files, 13 tests passed. | +| Rank synchronization | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/services/rank-mutations-production.test.ts` | 1 file failed; 4 tests failed. Create/delete/update returned success after failed permission synchronization, and set-rank lacked explicit partial-completion metadata. | 1 file, 4 tests passed. The first post-production run had 3 passed and 1 test-only audit expectation failure; aligning it with the established `intent` then `failure` envelope produced the final GREEN without a further production change. | +| Legacy permission parity | `pnpm exec vitest run --coverage.enabled=false src/actions/permissions.test.ts` | 1 file failed; 1 test failed and 2 passed. The generic infrastructure case leaked `errors.housekeeping.dependencyUnavailable`; both known business conflicts already retained their prior text. | 1 file, 3 tests passed. | + +The combined focused rerun passed 7 files and 90 tests. The first fix-round `pnpm typecheck` found two test-only narrowing errors in the new rank test; after the minimal annotations, its focused test remained green and `tsc --noEmit` passed. + +### Fix-round verification + +- Full affected Task 10 System, action, audit, authorization, bootstrap, localization, and preview suite: 22 files, 264 tests passed. +- Legacy operations, staff smoke, and authorization suite: 3 files, 97 tests passed. +- `pnpm test:housekeeping`: 45 files, 395 tests passed. +- `pnpm typecheck`: passed (`tsc --noEmit`). +- Exact changed-file `pnpm exec biome check --formatter-enabled=false ...`: checked 17 code, test, and locale files; no fixes applied after the one mechanical import-order correction. +- UTF-8 source verification confirmed the Italian `Analisi attività` label contains U+00E0, followed by a 3-file/70-test route-localization-preview GREEN rerun. +- `git diff --check` and the pre-stage `git diff --cached --check`: exit 0; only expected Git autocrlf warnings. +- Independent read-only re-review: 0 Critical, 0 Important, 0 new Minor; all four official Important findings resolved, all three parked Minors unchanged, ready-to-merge verdict. + +### Fix-round changed files + +- `.superpowers/sdd/2026-08-26-housekeeping-completion/task-10-report.md` +- `src/actions/permissions.test.ts` +- `src/actions/permissions.ts` +- `src/features/housekeeping/domains/system/pages/access.tsx` +- `src/features/housekeeping/domains/system/pages/configuration.tsx` +- `src/features/housekeeping/domains/system/pages/observability.tsx` +- `src/features/housekeeping/domains/system/pages/system-pages.test.tsx` +- `src/features/housekeeping/domains/system/queries/operations-production.test.ts` +- `src/features/housekeeping/domains/system/queries/operations.ts` +- `src/features/housekeeping/domains/system/routes.test.ts` +- `src/features/housekeeping/domains/system/routes.ts` +- `src/features/housekeeping/domains/system/services/mutations.ts` +- `src/features/housekeeping/domains/system/services/rank-mutations-production.test.ts` +- `src/features/housekeeping/foundation/localization-contract.test.ts` +- `src/features/housekeeping/foundation/preview-route-contract.test.ts` +- `src/lib/admin/ops-online-users.ts` +- `src/messages/en.json` +- `src/messages/it.json` diff --git a/.superpowers/sdd/2026-08-26-housekeeping-completion/task-11-report.md b/.superpowers/sdd/2026-08-26-housekeeping-completion/task-11-report.md new file mode 100644 index 00000000..065f2541 --- /dev/null +++ b/.superpowers/sdd/2026-08-26-housekeeping-completion/task-11-report.md @@ -0,0 +1,349 @@ +# Task 11 — People workflow read models + +Status: DONE — fix round 2 + +## Delivered scope + +- Added the exact 24-route People catalog covering the 39 migration-matrix entries across users, multi-account review, online/community, guilds, staff applications/teams, support tickets/help tickets, CFH, moderation overview, bans, IP rules, VPN settings, and word filter workflows. +- Added canonical, JSON-serializable People DTOs, `/ase/people` link builders, bounded list normalization, and stable sorting with numeric-ID tie breaking. +- Added injected query factories and narrow server-only production adapters for user/detail, community/guild, staff/applications/teams, support queues/tickets/help/CFH, and moderation/bans/sanctions sources. +- Reused foundation `HousekeepingResult`, error codes, capability context, authorization, and canonical href contracts. People-local `ListInput` and `Page` were added because no shared foundation equivalents exist in this checkout. +- Kept the People manifest, global handlers, pages, mutations, providers, widgets, search, and inbox unchanged for Task 12. + +## Security and behavior decisions + +- User mail and current IP remain independently nullable fields. Each is projected only when the capability context contains the existing `PERMS.USERS_VIEW`; `PERMS.MOD_USERS_VIEW` alone receives the safe base projection with both values set to `null`, and a context with neither permission is forbidden. +- No new ACL slug or rank threshold was introduced. Staff filtering reuses the existing `getMinStaffRank()` source. +- The production user selection is explicit and excludes passwords, authentication tickets, secrets, and two-factor material. VPN settings intentionally exclude `vpn_api_key`. +- Adapters fail closed. Malformed driver envelopes, invalid identifiers, corrupt links, non-serializable DTO values, and count failures map to `DEPENDENCY_UNAVAILABLE`. Primary/entity identifiers remain positive safe integers; zero is accepted only for the schema-declared guild `userId`/`roomId` and CFH `senderId`/`reportedId`/`roomId`/`moderatorId` sentinels. Missing valid detail entities map to `NOT_FOUND`; invalid request identifiers map to `VALIDATION`. +- Pagination clamps page size to 100 and offset to 10,000. Deterministic primary sorting, numeric-ID tie breaking, and `LIMIT`/`OFFSET` now execute in the database; no list query fetches a prefix for locale re-sorting or second slicing. +- Raw production adapters and `buildPeopleUserSelection` are module-private. Runtime exports expose only context-authorized query factories and singleton query surfaces. +- Multi-account clusters use one bounded CTE/window page query plus one independent matching-cluster count query, cap accounts per cluster at 100, and never issue one query per IP cluster. +- User detail/edit now includes the operator's watched state and canonical permission-rank data. Support ticket reads use the existing unified inbox through a strict, fail-closed, database-paged mode that includes CMS and help-center rows while leaving the legacy tolerant mode unchanged. +- The unified `/support/tickets` inbox still merges CMS and help-center rows. The ticket desk now has its own strict CMS-only page loader preserving priority, category, assignee, and message count; help summaries include reply count. Support desk/detail DTOs explicitly include queue counts, bounded staff, and the relevant active ban. +- Active bans are filtered before sorting. Expiry `0` remains the permanent-active sentinel; expired rows cannot hide permanent or future-active bans in lists or details. + +## Official fix round 1 findings + +1. **Authorization boundary:** fixed by making all raw production adapters and the user selection builder module-private and testing only guarded public query surfaces plus source/runtime export contracts. +2. **Pagination and sorting:** fixed by moving declared sort fields, deterministic tie ordering, and bounded `LIMIT`/`OFFSET` to production adapters. The exact `user10`/`user2` and support `status`/`updatedAt` page regressions are covered. +3. **Resource bounds:** fixed by replacing multi-account prefix loading plus per-row `Promise.all` with one bounded batched CTE/window query. No million-row prefix and no N+1 cluster query remain. +4. **Fail-closed database validation:** fixed across People models and community/support/moderation query boundaries. Invalid driver shapes and invalid identifiers cannot become empty lists, `NOT_FOUND`, ID `0`, or corrupt canonical links. +5. **Canonical dependencies:** fixed watched and permission-rank data for user detail/edit; `/support/tickets` now calls strict `fetchUnifiedTicketInbox`; support queue/staff/active-ban data is explicit in canonical query DTOs. +6. **Moderation capability equality:** fixed after direct user authorization. `moderationQuery.capability` now exactly equals the existing eleven-slug overview union already used by the route and `run`; no route, mutation, rank threshold, or new slug changed. +7. **Active bans:** fixed list/detail selection so permanent `ban_expire = 0` and future-active bans are deterministic and expired rows cannot hide them. + +The two official Minor findings remain parked and unchanged as instructed. + +## Official fix round 2 findings + +1. **Entity-aware sentinels:** canonical guild DTOs now use the real schema names `userId` and `roomId`. Serialization permits zero only on those two guild fields and the four named CFH fields when the containing DTO has the matching canonical entity href. Generic `*Id` zero values, negatives, unsafe integers, and primary ID zero remain unavailable failures. +2. **Support source fidelity:** `people.support.tickets` remains on strict `fetchUnifiedTicketInbox`. `people.support.ticket-desk` now dispatches to a distinct strict `website_tickets` loader with message aggregation and no help-center source. Real priority/category/assignee/message count and help reply count are present in canonical DTOs; malformed driver rows fail closed. +3. **Multi-account total:** the page CTE and matching-cluster count run as two bounded parallel queries. Empty pages retain the correct total without prefix loading or N+1 queries. + +## Strict TDD evidence + +### Cycle 1 — exact route catalog + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/routes.test.ts +Test Files 1 failed +Error: Cannot find module './routes' +``` + +GREEN: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/routes.test.ts +Test Files 1 passed (1) +Tests 3 passed (3) +``` + +### Cycle 2 — canonical models and normalizers + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts +Test Files 1 failed +Error: Cannot find module './models' +``` + +GREEN: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts +Test Files 1 passed (1) +Tests 5 passed (5) +``` + +### Cycle 3 — injected-adapter read queries + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts +Test Files 1 failed +Error: Cannot find module './community' +``` + +GREEN: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts +Test Files 1 passed (1) +Tests 10 passed (10) +``` + +Production-source contract RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts +Test Files 1 failed (1) +Tests 2 failed (2) +Reason: raw production adapters and buildPeopleUserSelection were exported as bypassable runtime internals. +``` + +Pagination regression RED after adding the production contract fixture: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts +Test Files 1 failed (1) +Tests 1 failed | 2 passed (3) +Expected ["203.0.113.1", "203.0.113.2"], received ["203.0.113.2"]. +``` + +GREEN for the original baseline implementation: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts +Test Files 1 passed (1) +Tests 3 passed (3) +``` + +The query tests cover adversarial page size/offset/search, stable tie sorting, empty/missing entities, adapter and count failures, PII capability combinations, serializable DTOs, explicit source projection, and production pagination. + +## Fix round 1 strict behavioral TDD evidence + +### Authorization boundary + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts +Test Files 1 failed (1) +Tests 2 failed (2) +Observed runtime exports: buildPeopleUserSelection and peopleUsersAdapters. +``` + +GREEN: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts +Test Files 1 passed (1) +Tests 3 passed (3) +``` + +### Database pagination and declared sorting + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts src/features/housekeeping/domains/people/queries/people-queries.test.ts +Test Files 2 failed (2) +Tests 4 failed | 14 passed (18) +Failures: offset 999999 was not capped; DB pages were sliced a second time for user10/user2 and support status/updatedAt. +``` + +GREEN: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts src/features/housekeeping/domains/people/queries/people-queries.test.ts +Test Files 2 passed (2) +Tests 18 passed (18) +``` + +### Multi-account resource bounds + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts +Test Files 1 failed (1) +Tests 1 failed | 2 passed (3) +Observed prefix result plus one query per IP cluster. +``` + +GREEN: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts +Test Files 1 passed (1) +Tests 3 passed (3) +``` + +### Fail-closed validation + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts src/features/housekeeping/domains/people/queries/people-queries.test.ts src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts +Test Files 3 failed (3) +Tests 5 failed | 21 passed (26) +Failures covered ID 0, corrupt links/dates, corrupt detail shapes, and malformed driver envelopes. +``` + +GREEN: + +```text +same command +Test Files 3 passed (3) +Tests 26 passed (26) +``` + +### Canonical dependencies and unified support inbox + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts -t "watched state|hydrates desk|strict unified" +Test Files 2 failed (2) +Tests 3 failed | 22 skipped (25) +``` + +GREEN: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/models.test.ts src/features/housekeeping/domains/people/queries/people-queries.test.ts src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts -t "watched state|hydrates desk|strict unified|normalizes BigInt" +Test Files 3 passed (3) +Tests 4 passed | 27 skipped (31) +``` + +### Moderation capability equality + +RED captured before direct authorization: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts -t "eleven-permission" +Test Files 1 failed (1) +Tests 1 failed | 18 skipped (19) +Expected the route/run eleven-slug union; query metadata still contains six slugs. +``` + +GREEN after the user directly authorized only this isolated metadata hunk: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts -t "eleven-permission" +Test Files 1 passed (1) +Tests 1 passed | 18 skipped (19) +``` + +### Active-ban semantics + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts -t "permanent" +Test Files 1 failed (1) +Tests 1 failed | 4 skipped (5) +Expected permanent expiresAt 0; received null. +``` + +GREEN: + +```text +same command +Test Files 1 passed (1) +Tests 1 passed | 4 skipped (5) +``` + +## Fix round 2 strict behavioral TDD evidence + +### Entity-aware schema sentinels + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts -t "zero sentinels" +Test Files 1 failed (1) +Tests 2 failed | 19 skipped (21) +Valid guild userId/roomId zero and CFH senderId/reportedId/moderatorId/roomId zero were rejected by generic identifier validation. +``` + +GREEN: + +```text +same command +Test Files 1 passed (1) +Tests 2 passed | 19 skipped (21) +``` + +### CMS-only ticket desk and help reply counts + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-queries.test.ts src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts -t "CMS-only context loader|reply counts" +Test Files 2 failed (2) +Tests 2 failed | 28 skipped (30) +The desk received a help row with synthetic normal priority; help summary omitted replyCount. +``` + +GREEN: + +```text +same command +Test Files 2 passed (2) +Tests 2 passed | 28 skipped (30) +``` + +### Independent multi-account total + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts -t "beyond the last page" +Test Files 1 failed (1) +Tests 1 failed | 8 skipped (9) +Expected total 4 on the empty page; received 0. +``` + +GREEN: + +```text +same command +Test Files 1 passed (1) +Tests 1 passed | 8 skipped (9) +``` + +## Verification + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/routes.test.ts src/features/housekeeping/domains/people/models.test.ts src/features/housekeeping/domains/people/queries/people-queries.test.ts src/features/housekeeping/domains/people/queries/people-adapters-production.test.ts +Test Files 4 passed (4) +Tests 40 passed (40) + +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people src/features/housekeeping/foundation/foundation-source-contract.test.ts src/features/housekeeping/foundation/authorization.test.ts src/features/housekeeping/foundation/capability-context.test.ts src/features/housekeeping/foundation/server-capability-context.test.ts src/features/housekeeping/foundation/contracts/contracts.test.ts +Test Files 9 passed (9) +Tests 86 passed (86) + +pnpm test:housekeeping +Test Files 49 passed (49) +Tests 435 passed (435) + +pnpm typecheck +tsc --noEmit +Exit 0 + +pnpm exec biome check --formatter-enabled=false <7 exact changed Task 11 TypeScript files> +Checked 7 files. No fixes applied. + +git diff --check +Exit 0 +``` + +Both `pnpm test:housekeeping` and `pnpm typecheck` emitted the environment warning: the repository requires Node `>=26.8.1 <27`, while this host runs Node `v26.7.0` with pnpm `11.24.0`. Tests and typecheck still exited successfully. + +No database operation, deployment, push, or pull-request update was performed. diff --git a/.superpowers/sdd/2026-08-26-housekeeping-completion/task-12-report.md b/.superpowers/sdd/2026-08-26-housekeeping-completion/task-12-report.md new file mode 100644 index 00000000..6a993373 --- /dev/null +++ b/.superpowers/sdd/2026-08-26-housekeeping-completion/task-12-report.md @@ -0,0 +1,404 @@ +# Task 12 — People users, community, and staff workflows + +Status: DONE + +## Delivered scope + +- Registered exactly the nine approved real People routes: users list/edit/multi-account/detail, community online/guilds/guild detail, and staff applications/teams. The remaining support and moderation routes stay catalogued in `routes.ts` but unregistered for Task 13. +- Added query-backed People pages with explicit loading, empty, partial, dependency-error, forbidden, and ready states. Links are canonical `/ase/people/*` links; optional mail/IP fields and mutation affordances remain absent unless their exact capability is present. +- Added the exact fourteen user command IDs plus the six stable People-owned IDs `people.guild.disband`, `people.application.decide`, `people.team.change`, `people.ip.action`, `people.vpn.configure`, and `people.word-filter.update`. +- Added bounded Zod command schemas, stable rate limits, dispatcher capability rechecks, confirmation metadata, a redirect-free server-only mutation service, and deterministic bootstrap registration. +- Extracted shared mutation behavior behind the existing actions while preserving the legacy action exports, exact ACLs, `/admin` revalidation, VPN redirect/fail-soft behavior, word-filter `ActionResult` shapes, already-gone delete semantics, and failure propagation where legacy persistence errors previously propagated. +- Added neutral EN/IT labels only for the nine runtime routes. + +## Security and behavior decisions + +- No ACL slug or route authorization rank threshold was added. Exact legacy capabilities remain authoritative: single ban/unban use `USERS_BAN`, reset-password uses `USERS_RESET_PASSWORD`, bulk/user/community/team/application operations use `USERS_EDIT`, IP/VPN use `SETTINGS_EDIT`, and word filter uses `WORDFILTER_EDIT`. +- The existing target hierarchy safeguard remains for legacy user mutations that previously used `guardRank`; alert remains capability-authorized without a new target-rank rule. +- Ordinary user edit and alert remain reason-free because their existing semantics are non-destructive. Sanctions, destructive operations, global/security changes, currency delivery, and bulk mutations require a nonblank dispatcher reason. Ban and bulk-ban operational reasons are also persisted with the mutation audit evidence. +- Every public service call rechecks the exact capability before production work. The production adapter is module-private; server-only placement is not treated as authorization. +- Successful mutations emit before/after audit evidence and a stable correlation ID. Audit persistence failure is fail-closed and maps to `DEPENDENCY_UNAVAILABLE`. User mutation audit snapshots omit mail because it is unnecessary PII; page projection continues to follow Task 11 exactly. +- User pages consume only Task 11 guarded read models, preserving bounded pagination, deterministic sorting, fail-closed DTO validation, serialization, zero-sentinel rules, watched state, permission context, and PII projection. +- Legacy wrappers remain on `/admin` behavior until Task 25. No `/admin`, `/mod`, API, redirect, database schema, deployment, or cutover behavior was changed. + +## Strict TDD evidence + +### Initial command/page/route RED + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/commands/user-commands.test.ts src/features/housekeeping/domains/people/commands/community-commands.test.ts src/features/housekeeping/domains/people/pages/people-primary-pages.test.tsx +Test Files 3 failed (3) +Tests 0 +Missing modules: community-commands, ../services/mutations, ../route-handlers +``` + +Initial focused GREEN: + +```text +Command tests: 2 files passed, 22 tests passed +Primary page/route tests: 3 files passed, 12 tests passed +Bootstrap tests: 1 file passed, 2 tests passed +``` + +### Foundation integration RED/GREEN + +RED after enabling People: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people src/features/housekeeping/foundation +Test Files 3 failed | 31 passed +Tests 4 failed | 376 passed +Failures: stale System-only registry assertions, stale preview expectation, and an unapproved People vertical runtime edge. +``` + +GREEN after updating the explicit runtime-edge and registry contracts: + +```text +Focused foundation contracts: 3 files passed, 40 tests passed +People + foundation: 34 files passed, 380 tests passed +``` + +### Audited sanction reason + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/services/mutations-reason-production.test.ts +Test Files 1 failed (1) +Tests 1 failed (1) +The ban audit after-snapshot did not contain the nonblank sanction reason. +``` + +GREEN: + +```text +Production mutation contracts: 2 files passed, 4 tests passed +The audited snapshot includes the reason and excludes mail. +``` + +### Legacy wrapper failure parity + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/actions/people-wrapper-errors.test.ts +Test Files 1 failed (1) +Tests 3 failed (3) +Persistence failures were swallowed and already-gone word-filter deletion was not idempotent. +``` + +GREEN: + +```text +Test Files 1 passed (1) +Tests 3 passed (3) +``` + +### Single authorization check for positive bulk adjustment + +RED: + +```text +pnpm exec vitest run --coverage.enabled=false src/actions/bulk-adjust-wrapper.test.ts +Test Files 1 failed (1) +Tests 1 failed (1) +Expected one requirePermission call; received two. +``` + +GREEN: + +```text +Test Files 1 passed (1) +Tests 1 passed (1) +``` + +### Static gates during implementation + +```text +pnpm typecheck +RED: one unused `describe` import in admin-ip.test.ts +GREEN: tsc --noEmit, exit 0 + +pnpm exec biome check --formatter-enabled=false +RED: 14 import-order assists +GREEN: checked 44 files, no fixes applied +``` + +## Final verification + +```text +Focused wrapper/command/page/service/route/authorization/audit matrix +Test Files 22 passed (22) +Tests 129 passed (129) + +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people src/features/housekeeping/foundation +Test Files 35 passed (35) +Tests 381 passed (381) + +pnpm test:housekeeping +Test Files 54 passed (54) +Tests 469 passed (469) + +pnpm typecheck +tsc --noEmit +Exit 0 + +pnpm exec biome check --formatter-enabled=false <44 exact changed Task 12 src files> +Checked 44 files. No fixes applied. + +git diff --check +Exit 0 +``` + +The Node engine warning remains the approved non-blocker: the repository requests Node `>=26.8.1 <27`, while this host runs Node `v26.7.0` with pnpm `11.24.0`. All test and type gates exited successfully. + +No database operation, deployment, push, or pull-request update was performed. + +## Official review fix round 1 + +The official review reported 0 Critical and 5 Important findings. This round addresses the five findings without widening the Task 12 route catalog or changing legacy redirects, safe-action response shapes, or cutover behavior. + +### RED evidence + +```text +Production server authority: auth resolver was called 0 times at the public service boundary (1 failing regression). +Canonical external audit: 3 failing regressions for missing durable intent/outcome behavior. +Transactional audit: expected one transaction and observed zero (1 failing regression). +Legacy/production workflows: new production matrix initially exposed bulk truncation/deduplication, trade-lock hierarchy/state, missing StaffActivities, and missing word-filter refresh behavior. +Primary workflows: page suite started at 7 passed / 2 failed (no executable command form and no bounded URL parser); preview contract started at 61 passed / 3 failed (searchParams/loading propagation). +Import boundary after real forms: test:housekeeping reached 481 passed / 1 failed, then the focused boundary exposed one exact page-state -> People models edge (22 passed / 1 failed). +``` + +### GREEN implementation + +- Production People services now rehydrate `getHousekeepingCapabilityContext()` on every public mutation. Invocation data can carry correlation and an expected actor only; it cannot synthesize permissions. The production adapter stays private, while test factories inject an authority resolver. +- Pure database mutations write their canonical before/after audit evidence in the same transaction. Mixed database/RCON/cache work writes sanitized intent first and a correlated success, failure, or partial outcome afterward. Audit-outcome persistence errors retain truthful completed/partial state, and legacy wrappers preserve their observable behavior. +- Ban/unban use observed active-ban state; trade-lock uses observed sanction/settings state. Passwords, hashes, API keys, and secrets are excluded from canonical evidence. +- Shared legacy bulk paths preserve original order, duplicates, totals, and iteration with no service-side 100-item cap. The <=100 bound remains in command schemas. Trade lock has no invented hierarchy gate and its missing-user wrapper message remains exactly `User not found`. +- Original `StaffActivities` side effects are retained for bulk ban/unban/currency/badge, guild disband, VPN, and trade lock. Missing word-filter deletion still reloads local cache, sends RCON refresh, and returns legacy success. +- The nine registered pages now expose capability-gated, accessible command forms backed by `executeHousekeepingCommand`; no inert command spans remain. Edit submits a mutation, list inputs come from bounded URL search parameters, and the dynamic preview route passes them through. Atomic Task 11 queries keep their fail-closed contracts; the impossible synthetic partial state was removed. A real Next loading route was added. +- The foundation contract allows only the exact same-domain edges required here: each People page to the shared People command form, the form to the single housekeeping command action, and page-state to the People `ListInput` model. No wildcard or prefix relaxation was introduced. + +### Final verification after review fixes + +```text +Focused wrapper/command/page/service/route/auth/audit/staff-smoke matrix +Test Files 24 passed (24) +Tests 187 passed (187) + +pnpm test:housekeeping +Test Files 58 passed (58) +Tests 482 passed (482) + +pnpm test +Test Files 206 passed | 3 skipped (209) +Tests 1321 passed | 5 skipped (1326) + +pnpm typecheck +tsc --noEmit +Exit 0 + +pnpm exec biome check --formatter-enabled=false <40 exact changed Task 12 source files> +Checked 40 files. No fixes applied. + +git diff --check e1b31ff7738eb5cc59e7765c8ed7290d62130972 -- +Exit 0 +``` + +The approved Node engine warning remains: the repository requests Node `>=26.8.1 <27`, while the host runs Node `v26.7.0` with pnpm `11.24.0`. No database operation, deployment, push, or pull-request update was performed. +## Official review fix round 2 + +The round-1 re-review reported 0 Critical, 7 Important, and no Minor findings. This round addresses all seven findings without changing the nine-route Task 12 manifest or exposing any raw production adapter. + +### RED evidence + +```text +Typed partial/result contract: 5 failed / 8 passed before completion metadata and audit-outcome handling were added. +Observed active-ban and absent-settings snapshots: 2 focused failures before deterministic active reads and null-preserving trade snapshots. +BIGINT preservation: 8 focused failures across application, team, IP, and wordfilter before decimal string/BigInt boundaries. +Real form/reset workflow: 2 primary-page failures before the actual action adapter and one-time credential result were added. +Production operation closure: 2 failed / 10 passed before unban observed-after and bulk false-RCON partial truth. +Post-commit notification/legacy parity: 2 failed / 12 passed before update/reset transactional intent and legacy throw/false mapping. +Cumulative gate exposed one unsupported custom Zod schema, one stale direct-alert expectation, and one stale numeric audit-ID expectation; each received a minimal regression-preserving fix. +``` + +### GREEN implementation + +- Mixed database/external operations now commit sanitized intent with the mutation and return correlated typed `partial` completion when RCON, cache, notification, or final audit persistence fails afterward. Pre-mutation external failure and intent persistence failure remain blocking. The dispatcher and public server action preserve one serializable partial result and emit no contradictory generic failure evidence. +- Ban and unban reuse the permanent-or-unexpired Task 11 filter, deterministic timestamp/ID ordering, and observed before/after reads. An absent `UsersSettings` row remains null before and after a no-op trade settings update. +- Legacy alert calls RCON without a target query or hierarchy guard. Legacy wrappers retain their prior false/throw behavior while new Housekeeping commands report synchronization false as partial truth. +- Application, team, IP, and wordfilter identifiers remain canonical decimal strings/`BigInt` through wrappers and Drizzle, including values above `Number.MAX_SAFE_INTEGER`. The cloneable command regex accepts the full unsigned BIGINT range and rejects overflow without a Zod custom refinement. +- Reset-password returns the generated credential once in the current authorized form result. It is rendered through an accessible `output`, excluded from durable service evidence, and recursively redacted by the canonical audit sanitizer. +- Production tests execute all twenty Task 12 operation IDs with meaningful database/RCON/audit assertions. The primary-page test invokes the actual form action adapter, and the unused multi-accounts-to-command-form boundary exception was removed. + +### Final verification after review fix round 2 + +```text +Focused People + foundation + wrappers + audit + action + staff-smoke matrix +Test Files 45 passed (45) +Tests 459 passed (459) + +pnpm test:housekeeping +Test Files 58 passed (58) +Tests 502 passed (502) + +pnpm test +Test Files 206 passed | 3 skipped (209) +Tests 1345 passed | 5 skipped (1350) + +pnpm typecheck +tsc --noEmit +Exit 0 + +pnpm exec biome check --formatter-enabled=false <28 exact changed TypeScript/TSX files> +Checked 28 files. No fixes applied. +``` + +The approved Node engine warning remains: the repository requests Node `>=26.8.1 <27`, while the host runs Node `v26.7.0` with pnpm `11.24.0`. No database operation, deployment, push, or pull-request update was performed. + +## Official review fix round 3 + +The round-2 re-review reported 0 Critical, 2 Important, and 2 adjacent Minor findings. This round addresses all four findings without changing the nine-route manifest, the public command IDs, or legacy external call ordering and permissions. + +### RED evidence + +```text +Focused external-audit, bulk-production, and dispatcher matrix +Test Files 2 failed (2) +Tests 15 failed | 54 passed (69) + +The ten external-only false/throw cases persisted optimistic desired after-state instead of confirmed unchanged or unknown delivery evidence. Four bulk currency/badge false/throw cases reported completed=0 and Database error after a committed database write. The dispatcher accepted one ok:false result carrying impossible completion metadata. +``` + +### GREEN implementation + +- External-only alert, disconnect, mute, unmute, and send-currency keep desired state in intent/success evidence. Confirmed RCON `false` now writes a dedicated unchanged/no-delivery failure snapshot; an exception writes unknown delivery with a null after-state. Correlation and failure outcome stay identical across intent/outcome records. +- Bulk currency and badge count a successful database write before RCON. RCON false/throw is additive `externalSyncFailures` sync debt, never a database failure; `failedIds` remains reserved for database/business failures and the result is typed partial with an explicit no-automatic-retry warning. +- Webhook notification remains explicit fire-and-forget best effort (`void notify(...)`) and no longer participates in mutation completion. The impossible promise-rejection test was replaced with the real void contract. +- The dispatcher runtime schema now accepts `completion` only for `ok: true`, matching the TypeScript `HousekeepingResult` contract; failure envelopes containing it are rejected as malformed. + +### Final verification after review fix round 3 + +```text +Focused external audit + production bulk + dispatcher +Test Files 3 passed (3) +Tests 73 passed (73) + +People + foundation + legacy wrappers + staff-smoke matrix +Test Files 48 passed (48) +Tests 470 passed (470) + +pnpm test:housekeeping +Test Files 58 passed (58) +Tests 516 passed (516) + +pnpm test +Test Files 206 passed | 3 skipped (209) +Tests 1359 passed | 5 skipped (1364) + +pnpm typecheck +tsc --noEmit +Exit 0 + +pnpm exec biome check --formatter-enabled=false <4 exact changed source/test files> +Checked 4 files. No fixes applied. + +git diff --check +Exit 0 +``` + +The approved Node engine warning remains: the repository requests Node `>=26.8.1 <27`, while the host runs Node `v26.7.0` with pnpm `11.24.0`. No database operation, deployment, push, or pull-request update was performed. + +## Official review fix round 4 + +The round-3 re-review reported 0 Critical, 2 Important, and 0 Minor findings. This round restores the pre-cutover legacy bulk result contract at the wrapper boundary and makes committed-database/emulator-sync debt explicit in the successful partial operator result. Canonical Housekeeping accounting, audit evidence, routes, commands, and ACLs remain unchanged. + +### Pre-Task12 parity evidence + +`git show e1b31ff7^:src/actions/bulk-users.ts` confirms that currency and badge wrappers awaited RCON inside the same `try`: an RCON exception entered the catch, did not increment `given`, and appended `{ userId, reason: "Database error" }`; an RCON `false` return did not throw and therefore remained a legacy success. Positive bulk adjustment delegated to the same currency wrapper and had the same result semantics. + +### RED evidence + +```text +pnpm exec vitest run --coverage.enabled=false src/actions/bulk-users.test.ts src/actions/bulk-adjust-wrapper.test.ts +Test Files 2 failed (2) +Tests 3 failed | 3 passed (6) +Currency, badge, and positive-adjust wrappers returned given/adjusted=1 with no failedIds for the canonical external-sync debt produced by a thrown RCON call; historical results require 0 plus Database error. + +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/pages/people-primary-pages.test.tsx +Test Files 1 failed (1) +Tests 2 failed | 12 passed (14) +The operator result rendered only Partially completed and exposed neither an alert/do-not-retry instruction nor the typed user sync debt returned by the real form adapter. +``` + +### GREEN implementation + +- `src/actions/bulk-users.ts` translates only `externalSyncFailures` at the legacy wrapper boundary into historical `Database error` failures and subtracts those entries from `given`/positive `adjusted`. Canonical completed counts and sync-debt evidence are untouched; the existing legacy `false` path still produces no external-sync entry and remains successful. Failure entries are restored in input order, including duplicate IDs. +- `src/features/housekeeping/domains/people/pages/people-command-form.tsx` reads only a successful typed partial result with failed external completion and a bounded `after.externalSyncFailures` array. It renders an alert, explicit do-not-retry instruction, and safe user/reason debt entries. Unknown payload fields and malformed entries are never rendered, and the generated reset password path remains one-time and unchanged. + +Focused GREEN: + +```text +pnpm exec vitest run --coverage.enabled=false src/actions/bulk-users.test.ts src/actions/bulk-adjust-wrapper.test.ts +Test Files 2 passed (2) +Tests 6 passed (6) + +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/people/pages/people-primary-pages.test.tsx +Test Files 1 passed (1) +Tests 14 passed (14) + +pnpm exec vitest run --coverage.enabled=false src/actions/bulk-users.test.ts src/actions/bulk-adjust-wrapper.test.ts src/features/housekeeping/domains/people/pages/people-primary-pages.test.tsx src/features/housekeeping/domains/people/services/mutations-production-workflows.test.ts +Test Files 4 passed (4) +Tests 48 passed (48) +``` + +### Cumulative verification + +```text +People + foundation + Task12 legacy wrappers + route/audit/staff-smoke matrix +Test Files 52 passed (52) +Tests 491 passed (491) + +pnpm test:housekeeping +Test Files 58 passed (58) +Tests 518 passed (518) + +pnpm test +Test Files 206 passed | 3 skipped (209) +Tests 1364 passed | 5 skipped (1369) + +pnpm typecheck +tsc --noEmit +Exit 0 + +pnpm exec biome check --formatter-enabled=false src/actions/bulk-users.ts src/actions/bulk-users.test.ts src/actions/bulk-adjust-wrapper.test.ts src/features/housekeeping/domains/people/pages/people-command-form.tsx src/features/housekeeping/domains/people/pages/people-primary-pages.test.tsx +Checked 5 files. No fixes applied. + +git diff --check +Exit 0 +``` + +The only warning is the approved Node engine mismatch: the repository requests Node `>=26.8.1 <27`, while the host runs Node `v26.7.0` with pnpm `11.24.0`. + +### Exact tracked paths + +- `.superpowers/sdd/2026-08-26-housekeeping-completion/task-12-report.md` +- `src/actions/bulk-adjust-wrapper.test.ts` +- `src/actions/bulk-users.test.ts` +- `src/actions/bulk-users.ts` +- `src/features/housekeeping/domains/people/pages/people-command-form.tsx` +- `src/features/housekeeping/domains/people/pages/people-primary-pages.test.tsx` + +The required controller lines were appended to the git-ignored `.superpowers/sdd/2026-08-26-housekeeping-completion/progress.md`; it is excluded from the commit. `.remember/` remains untouched. + +### Self-review + +- Scope and compatibility: the production mutation service, canonical audit/accounting, manifest, route, command, ACL, database, redirect, and cutover behavior are unchanged. The adapter applies only to legacy currency/badge results and their historical positive-adjust delegate. +- Security: the operator surface requires an `ok: true` partial/external-failed envelope, accepts at most 100 positive safe-integer user IDs, renders only the canonical safe reason, and does not inspect or serialize arbitrary result payloads. Reset-password display and audit redaction tests remain green. +- Test quality: legacy tests exercise the real exported wrappers against a complete canonical partial response and fail on either wrong count or missing historical failure; UI tests render the real component, invoke the real form adapter, and prove malformed/extra payload is not displayed. + +### Commit + +Single local commit message: `fix(housekeeping): restore people partial compatibility`. The final SHA of the commit containing this report is returned to the controller after creation. + +No database operation, deployment, push, pull, or pull-request update was performed. diff --git a/.superpowers/sdd/2026-08-26-housekeeping-completion/task-9-report.md b/.superpowers/sdd/2026-08-26-housekeeping-completion/task-9-report.md new file mode 100644 index 00000000..941916d4 --- /dev/null +++ b/.superpowers/sdd/2026-08-26-housekeeping-completion/task-9-report.md @@ -0,0 +1,234 @@ +# Task 9 report — canonical route dispatch + +## Status + +- DONE: matcher, registry collision guard, empty handler aggregate, preview root routing, and catch-all dispatch are implemented. +- Base verified before edits: `2970dff56378cf5259fd125794bf0d89bec25b25` on `codex/housekeeping-complete`. +- Commit message: `feat(housekeeping): dispatch canonical domain routes`. +- `.remember/` remained untouched and untracked. +- No pull, push, PR/MR update, deployment, database operation, Task 10 work, or worktree was performed. + +## TDD evidence + +### RED — tests written before production + +Exact command: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/foundation/routing/match-route.test.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/route-handlers.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts +``` + +Observed exit 1: + +```text +Test Files 4 failed (4) +Tests 1 failed | 14 passed (15) + +Cannot find module './match-route' +Cannot find module './route-handlers' +Cannot find package '@/app/ase-next/[domain]/[[...segments]]/page' + +registry > rejects duplicate dynamic route shapes regardless of parameter name +AssertionError: expected [Function] to throw an error +``` + +This proved the three missing production boundaries and the existing registry's acceptance of equivalent `:id` / `:username` route shapes. + +### First targeted GREEN + +The same exact command after the minimum implementation exited 0: + +```text +Test Files 4 passed (4) +Tests 94 passed (94) +``` + +An intermediate run had 92/94 passing because two import-boundary fixtures still used the old route file's relative depth. The fixture imports were moved one directory higher for the new catch-all location; the forbidden-module assertions were unchanged. + +### Full-suite contract correction + +The first full housekeeping run correctly exposed one obsolete Task 1 expectation: + +```text +Test Files 1 failed | 37 passed (38) +Tests 1 failed | 348 passed (349) +Expected NEXT_REDIRECT:/ase-next/operations +Received NEXT_NOT_FOUND +``` + +`server-capability-context.test.ts` was updated to the Task 9 ruling: with the real registered route set still empty, `/ase-next` calls `notFound()` and must not redirect to an empty Operations placeholder. Its request-scoped context isolation assertions remain intact. + +## Implemented behavior + +- `matchHousekeepingRoute` compares decoded path segments without constructing a regular expression from route text. +- Static routes win over same-depth dynamic routes; dynamic and nested parameters are returned in a frozen readonly record. +- Unknown, cross-domain, malformed, repeated-separator, trailing-separator, query/fragment, invalid-percent, encoded-separator, dot-segment, and backslash paths fail closed. +- Registry construction rejects duplicate dynamic shapes even when parameter names differ. +- `HousekeepingPageInput` is exactly the readonly `{ context, match }` pair. +- The global route-handler aggregate is empty and its test proves one-to-one equality with the currently empty manifest route set; no placeholder handlers were added. +- `/ase-next` searches registered routes in manifest order, requires both domain and route capability, redirects to the first permitted route, and calls `notFound()` when none exists. +- `/ase-next//` derives the domain's canonical `/ase` path, matches it, finds the exact handler, reacquires the cached request-scoped context, rechecks domain and route capability, and invokes the handler with that same context and match. +- Unknown routes fail before context loading; inaccessible matched routes load one context and never invoke a handler. + +## Verification evidence + +Targeted routing/registry/handler/preview tests: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/foundation/routing/match-route.test.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/route-handlers.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts +Test Files 4 passed (4) +Tests 94 passed (94) +``` + +Directly affected context contract: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/foundation/server-capability-context.test.ts +Test Files 1 passed (1) +Tests 2 passed (2) +``` + +Full housekeeping suite: + +```text +pnpm test:housekeeping +Test Files 38 passed (38) +Tests 349 passed (349) +``` + +TypeScript: + +```text +pnpm typecheck +$ tsc --noEmit +exit 0 +``` + +The only output note was the existing engine warning: local Node `26.7.0` is below the package request `>=26.8.1 <27`. + +Targeted Biome with formatting disabled: + +```text +pnpm exec biome check --formatter-enabled=false <11 changed Task 9 source/test files> +Checked 11 files in 47ms. No fixes applied. +``` + +`git diff --check` exited 0 before staging. Cached-diff and committed-tree checks are run as the final staging/commit gates. + +## Exact Task 9 files + +```text +src/app/ase-next/[domain]/[[...segments]]/page.tsx +src/app/ase-next/[domain]/layout.tsx +src/app/ase-next/[domain]/page.tsx (deleted) +src/app/ase-next/page.tsx +src/features/housekeeping/foundation/preview-route-contract.test.ts +src/features/housekeeping/foundation/registry.test.ts +src/features/housekeeping/foundation/registry.ts +src/features/housekeeping/foundation/routing/match-route.test.ts +src/features/housekeeping/foundation/routing/match-route.ts +src/features/housekeeping/foundation/server-capability-context.test.ts +src/features/housekeeping/route-handlers.test.ts +src/features/housekeeping/route-handlers.ts +.superpowers/sdd/2026-08-26-housekeeping-completion/task-9-report.md +``` + +## Self-review and tooling + +- Mutation check: dynamic-name normalization removal, regex-style static matching, static-priority removal, decoded-separator acceptance, domain mismatch acceptance, skipped route ACL, context reload inside the handler, missing handler lookup, placeholder handler addition, and empty-domain redirect each break a focused test. +- The catch-all route imports only housekeeping foundation/manifests/handlers and retains the existing forbidden database/auth/permissions/actions/legacy-page boundary audit. +- `apply_patch` created all new files, but the Windows sandbox helper repeatedly failed to read existing files with `apply deny-read ACLs`. Existing-file edits therefore used controller-approved exact-anchor/full-file fallbacks only after resolving absolute paths and validating every target under `E:\Users\simol\Desktop\EpicNext-cms`. + +## Fix Round 1 — deterministic encoded route matching + +### Status and scope + +- Fix base: `e5c230ba35aec2b4c471608d32b8a16ecc1ee382`. +- Only the two Important matcher blockers were addressed. +- The three parked Minor findings remain unchanged; no changed line required an adjustment to them. +- Commit message: `fix(housekeeping): make route matching deterministic`. +- `.remember/` remained untouched. No worktree, push, PR/MR, database operation, deployment, or Task 10 work was performed. + +### Root-cause evidence + +1. The catch-all receives decoded Next segments and re-encodes them with `encodeURIComponent`. The matcher decoded the request path into `decodedSegments` but compared static route text against `rawSegments`. Therefore literal `a+b[1]` did not equal `a%2Bb%5B1%5D`; the competing `:id` route captured the request and could change the selected capability/handler. +2. Candidate sorting used only total dynamic-segment count. Intersecting patterns `/:kind/settings` and `/users/:id` have the same count, so stable sort preserved manifest order and allowed registration order to decide dispatch. + +### RED + +Exact command after test setup was validated: + +```text +pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/foundation/routing/match-route.test.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/route-handlers.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts +``` + +Observed exit 1: + +```text +Test Files 2 failed | 2 passed (4) +Tests 2 failed | 95 passed (97) + +catch-all encoded literal: +Expected routeId people.literal-tool with params {} +Received routeId people.tool-detail with params { id: "a+b[1]" } + +equal-count specificity: +Expected routeId people.user-detail with params { id: "settings" } +Received routeId people.kind-settings with params { kind: "users" } +``` + +The registration-order table exercises both orders. Before production changes the general-first order failed while the reverse order passed, proving that order was the deciding variable. + +An earlier RED attempt exposed a test-table setup error (`manifest.routes is not iterable`); the table was changed from spread array rows to named `{ routes }` rows, then rerun to obtain the behavioral RED above before any production edit. + +### Fix + +- A single `decodeCanonicalSegment` boundary now normalizes request segments and static route-pattern segments exactly once. +- Invalid percent encoding, empty values, decoded `/` or `\`, and decoded `.` / `..` remain fail closed. +- Dynamic markers retain their parameter names and receive the already-decoded request segment. +- Candidate specificity is compared left-to-right. At the earliest static/dynamic difference, the static segment wins; manifest order no longer selects among intersecting patterns. +- Existing static-over-dynamic behavior and registry duplicate-shape rejection remain unchanged. + +### GREEN and pre-commit verification + +Targeted command above, exit 0: + +```text +Test Files 4 passed (4) +Tests 97 passed (97) +``` + +Full housekeeping suite, exit 0: + +```text +pnpm test:housekeeping +Test Files 38 passed (38) +Tests 352 passed (352) +``` + +TypeScript, exit 0: + +```text +pnpm typecheck +$ tsc --noEmit +``` + +The only output note remained the existing Node warning: local `26.7.0`, package request `>=26.8.1 <27`. + +Exact changed-file Biome, exit 0: + +```text +pnpm exec biome check --formatter-enabled=false src/features/housekeeping/foundation/routing/match-route.ts src/features/housekeeping/foundation/routing/match-route.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts +Checked 3 files in 25ms. No fixes applied. +``` + +`git diff --check` exited 0 before the report update. Cached and committed-tree checks are final staging/commit gates. + +### Exact fix files + +```text +src/features/housekeeping/foundation/routing/match-route.ts +src/features/housekeeping/foundation/routing/match-route.test.ts +src/features/housekeeping/foundation/preview-route-contract.test.ts +.superpowers/sdd/2026-08-26-housekeeping-completion/task-9-report.md +``` \ No newline at end of file diff --git a/docs/superpowers/evidence/2026-08-26-housekeeping-pre-cutover.md b/docs/superpowers/evidence/2026-08-26-housekeeping-pre-cutover.md new file mode 100644 index 00000000..cdabde6f --- /dev/null +++ b/docs/superpowers/evidence/2026-08-26-housekeeping-pre-cutover.md @@ -0,0 +1,74 @@ +# Housekeeping pre-cutover verification + +Verified on 2026-08-30 at 19:50 CEST against branch commit `65a62867`. + +## Outcome + +The pre-cutover gate passed. The replacement Housekeeping workspace has complete route coverage, passes the automated suite and production build, and rendered successfully across the required desktop, tablet, and mobile viewports. The remaining environmental limitations are recorded below and do not hide a failed dependency or a failed state. + +## Environment + +- Windows and PowerShell, local non-production environment. +- Repository system Node.js was `26.7.0`, which does not match `.nvmrc`. Because the protected NVM installation could not be updated without administrator rights, every recorded gate used the official portable Node.js `26.8.1` distribution with pnpm `11.24.0`. +- The database was used read-only for the browser verification. No mutation command or database write was executed. +- Redis was not configured. RCON was unavailable during the preview and the workspace represented that dependency as a partial provider warning. +- The temporary `AUTH_SECRET` used by the build and local preview was restored or removed after each command. + +## Automated gates + +| Gate | Result | Evidence | +| --- | --- | --- | +| Toolchain | Pass | `pnpm toolchain:check` reported Node.js `26.8.1` aligned with `.nvmrc`; pnpm was `11.24.0`. | +| Migration matrix and runtime parity | Pass | `137/137` valid; discovered, mapped, and verified routes were all `137`; `2` legacy removals were accounted for; no gaps. | +| Housekeeping suite | Pass | `109` test files, `841` tests. | +| Full suite | Pass | `266` test files passed and `3` skipped; `1,741` tests passed and `5` skipped; statement coverage `24.55%` (`7,737/31,510`). | +| TypeScript | Pass | `pnpm typecheck` exited successfully. | +| Cumulative Biome | Pass | `450` changed JavaScript, TypeScript, JSON, and JSONC files checked in `12` batches; no fixes remained. | +| Patch hygiene | Pass | `git diff --check` exited successfully. | +| Production build | Pass | Next.js `16.3.3` compiled, typechecked, and generated `239/239` static pages. `AUTH_SECRET` restoration was confirmed. | + +The production build emitted two non-blocking environmental warnings: `REDIS_URL` is unset, and Turbopack traced a dynamic translation-file path in `mutation-runtime-external.ts`. Both are explicit in the build output and must be considered for the deployment environment. + +## Runtime boundary correction + +The first real browser run found a React Server Components boundary failure because provider definitions containing a `load` function were passed into a client component. A failing projection test was added first. The workspace now projects definitions to serializable widget options before crossing the client boundary, while preserving the domain import contract by locating the projection in the shared preferences foundation. + +The correction is covered by commits `cb77b2d3` and `1ae59bcc`. Cumulative Biome corrections are isolated in `b9c47aa8` and `65a62867`. The corrected browser matrix below rendered without the error boundary. + +## Browser and responsive matrix + +The canonical route for each domain was tested at `1440x900`, `1024x768`, `390x844`, and `320x568` with an authorized rank-7 fixture. + +| Domain | Canonical route | Heading | Viewports | Runtime result | +| --- | --- | --- | --- | --- | +| Operations | `/ase-next` | Operations workspace | 4/4 | HTTP 200, Housekeeping root present, no error boundary or horizontal overflow. | +| People | `/ase-next/people/users` | Users | 4/4 | HTTP 200, Housekeeping root present, no error boundary or horizontal overflow. | +| Content | `/ase-next/content/editorial/articles` | Editorial content | 4/4 | HTTP 200, Housekeeping root present, no error boundary or horizontal overflow. | +| Economy | `/ase-next/economy/catalog` | Catalog | 4/4 | HTTP 200, Housekeeping root present, no error boundary or horizontal overflow. | +| Hotel | `/ase-next/hotel/rooms` | Rooms | 4/4 | HTTP 200, Housekeeping root present, no error boundary or horizontal overflow. | +| System | `/ase-next/system/access/permissions` | Access control | 4/4 | HTTP 200, Housekeeping root present, no error boundary or horizontal overflow. | + +All `24/24` canonical route/viewport combinations passed with zero page errors and zero horizontal overflow. The screenshots are retained outside the repository at `C:\Users\simol\.codex\visualizations\2026\08\24\01a03498-f8e9-70d2-9180-2ef86d73ebb6\housekeeping-task24`. + +An initial exploratory pass used the non-canonical domain roots `/ase-next/{domain}` and correctly received 404 responses. Those invalid routes were excluded and replaced by the canonical routes shown above. + +## Access, states, and command safety + +| Scenario | Result | +| --- | --- | +| Anonymous access | Redirected to `/login`; no Housekeeping root rendered. | +| Authenticated rank-1 access | Failed closed with `Page not found`; no Housekeeping root rendered. | +| Authenticated rank-7 access | All 24 browser combinations rendered successfully. | +| Real partial provider state | RCON outage surfaced as `Unable to load Housekeeping`; sibling workspace content remained usable. | +| Real empty state | Operations recent work rendered `Nothing available`. | +| Loading, error, forbidden, partial, empty, and ready UI states | Covered by the targeted smoke suite. | +| Safe and sensitive command dispatch | Covered in tests, including intent, preflight, success, and failure paths; no real mutation was submitted. | +| Provider timeout isolation | Covered at the two-second abort boundary with sibling preservation. | +| Preferences | Schema, upsert, corruption recovery, and reconciliation covered. | +| Studio | All ten kinds, authorization, outage, audit route, lifecycle ordering, and page states covered. | + +The targeted state and safety run passed `11` files and `98` tests. + +## Cutover readiness + +This evidence verifies the preview implementation only. It does not claim a production deployment or live service health. With the recorded limitations accepted, the branch is ready for the route cutover from `/ase-next` to `/ase` and removal of the legacy `/admin` and `/mod` page trees. diff --git a/docs/superpowers/evidence/2026-08-30-housekeeping-final.md b/docs/superpowers/evidence/2026-08-30-housekeeping-final.md new file mode 100644 index 00000000..eefcb6fe --- /dev/null +++ b/docs/superpowers/evidence/2026-08-30-housekeeping-final.md @@ -0,0 +1,66 @@ +# Housekeeping final cutover verification + +Verified on 2026-08-30 CEST on branch `codex/housekeeping-complete` after production commit `222535e1`. + +## Outcome + +The Housekeeping replacement is complete and the administration UI has been cut over atomically to `/ase`. The former `/admin`, `/mod`, and `/ase-next` UI trees are absent and do not redirect. Internal `/api/admin/*` endpoints remain intentionally available behind their existing permission gates. + +This report verifies the branch and local production build. It does not claim that the branch is merged, deployed, or healthy in production. + +## Delivered cutover + +- `2b8f73a9` moved the canonical workspace to `src/app/ase`, removed the three legacy UI roots, removed the preview gate, and deleted the superseded UI and dependency surface. +- `222535e1` closed the final authorization findings: logo writes require `admin.settings.edit`; generic media deletion cannot traverse into nested asset namespaces; hierarchy bypasses use `isSuperAdmin`; bulk ban/unban require `admin.users.ban`; every bulk target is checked before mutation; configured rank identifiers are no longer capped at 7 and must exist in `permission_ranks`. +- The historical migration matrix remains as an auditable 137-row record while the physical legacy-root scanner reports zero retained UI pages. + +## Final automated gates + +| Gate | Result | Evidence | +| --- | --- | --- | +| Toolchain | Pass | `pnpm toolchain:check`: Node.js `26.8.1` aligned with `.nvmrc`. | +| Migration and runtime parity | Pass | `137/137` historical rows valid; legacy UI pages present `0`; runtime discovered/mapped/verified `137/137/137`; removals `2`. | +| Housekeeping suite | Pass | `109` test files and `843` tests passed. | +| Security regression set | Pass | The focused People production workflow passed `60/60` tests after the review-driven coverage additions. The earlier four-file final-finding set passed `95/95`. | +| Full suite | Pass | `262` files passed and `3` skipped; `1,649` tests passed and `5` skipped. Coverage: statements `31.53%`, branches `26.16%`, functions `36.55%`, lines `32.90%`. | +| TypeScript | Pass | `pnpm typecheck` exited successfully. | +| Dead-code boundary | Pass | `pnpm knip` reported no included file, dependency, dev-dependency, unlisted dependency, or binary findings. | +| Changed-file quality | Pass | Biome checked all `9` final-review files with no remaining fixes; `git diff --check` passed. | +| Production build | Pass | Next.js `16.3.3` compiled, typechecked, generated `129/129` pages, and exposed `/ase` plus `/ase/[domain]/[[...segments]]` as the only administration UI routes. | + +The build used an ephemeral local `AUTH_SECRET` because production validation correctly rejects the development environment without one. It was set only in the build process and was not written to `.env`. + +## Route and access probes + +The post-cutover local server returned: + +| Route | Result | +| --- | --- | +| `/admin` | `404`, no redirect | +| `/admin-next` | `404`, no redirect | +| `/ase-next` | `404`, no redirect | +| `/mod` | `404`, no redirect | +| `/ase` | `307` to `/login` for an anonymous request | +| `/api/health` | `200` | + +The production route manifest independently confirms that `/ase` is the only administration UI root while the retained `/api/admin/*` backend endpoints remain present. + +## Visual evidence boundary + +The authenticated pre-cutover workspace passed all `24/24` domain and viewport combinations at `1440x900`, `1024x768`, `390x844`, and `320x568`; details and screenshot locations are recorded in `2026-08-26-housekeeping-pre-cutover.md`. + +The final cutover moved that verified workspace to `/ase` without redesigning the rendered workspace. A new authenticated post-cutover browser session was not created because doing so would have required minting or impersonating a privileged session. Final validation therefore combines the existing authenticated visual matrix with the post-cutover source move, route manifest, automated UI tests, and anonymous access probes. No live mutation was submitted. + +## Known non-blocking environment debt + +- `REDIS_URL` is unset locally, so the build warns that multi-instance rate limits, settings cache, and JWT invalidation would fall back to process memory. Production must provide Redis. +- Turbopack warns that dynamic translation-file access in `mutation-runtime-external.ts` broadens filesystem tracing. The build still completes, but deployment bundle size should be monitored. +- The repository-wide `pnpm lint` remains affected by the existing Windows CRLF baseline. The final changed-file Biome gate and `git diff --check` pass; no unrelated whole-repository formatting churn was introduced. + +## Independent review + +A second read-only review of `222535e1` found no Critical or Important findings and assessed the change as ready to merge. Its two Minor recommendations were both implemented: hierarchy denial now runs against ban, unban, currency, and badge bulk operations, and the production workflow now proves a successful super-admin assignment to an existing configured rank above 7. + +## Release state + +The implementation and local release gates are complete. The branch is suitable for continued review in draft PR #52; merge and deployment remain separate operator decisions. diff --git a/docs/superpowers/evidence/2026-09-05-housekeeping-backend-review.md b/docs/superpowers/evidence/2026-09-05-housekeeping-backend-review.md new file mode 100644 index 00000000..d405240b --- /dev/null +++ b/docs/superpowers/evidence/2026-09-05-housekeeping-backend-review.md @@ -0,0 +1,38 @@ +# Housekeeping backend review checkpoint + +This is an incremental backend review, not a completion or deployment claim. +The current UI route matrix cannot establish operation-level parity. + +## Delivered blocks + +### Audit reasons and external outcomes (555bc75f) + +- Content and Economy preserve normalized reasons through the real service and production audit adapters. +- Hotel preserves reasons through command, service and audit. +- Successful Hotel RCON execution with unavailable completion auditing returns partial completion with external completed; it does not emit a false RCON failure. +- Regressions were observed failing before implementation. +- Full pre-push suite: 1,877 passed, 5 skipped. TypeScript and scoped Biome passed. Remote CI check passed. +- Independent scoped review: no Critical or Important findings. + +### Commerce editing concurrency + +- ASE marketplace cancellation reads and checks the listing under a transaction-held row lock; inactive listings return CONFLICT. +- Legacy marketplace cancellation includes the row lock, update and staff activity in one transaction. Audit exceptions propagate for rollback. +- ASE and legacy voucher edits lock the record and reject caps below recorded usage. Legacy edits reject missing vouchers rather than reporting a successful no-op. +- Four ASE and two legacy regressions failed before fixes; the expanded focused suite has 14 passing tests. +- Tests execute real Drizzle SQL generation against controlled transport responses. They do not simulate MariaDB locking or prove live multi-connection behavior. +- Independent scoped review: no Critical or Important findings. + +## Open backend work + +| Area | Evidence / required follow-up | +| --- | --- | +| Voucher redemption | Claim reservation now locks the voucher and duplicate claim, commits usage/cap with an audit intent, then dispatches the reward. Failed or uncertain dispatch retains the reservation and returns the audit reference. Automatic recovery of reward increments remains intentionally unavailable without emulator acknowledgment/idempotency. | +| Currency delivery | `src/lib/services/send-currency.ts` uses RCON followed by database fallback; socket dispatch is not emulator acknowledgment. Do not invent exactly-once guarantees or blindly replay increments. | +| Reason enforcement | Reason propagation is fixed for the named paths, but operation-level required-reason policies and denied/failure auditing still need a complete cross-entrypoint inventory. | +| Functional parity | Compare each query and mutation in Content, Economy, Hotel, People, System and Operations with retained legacy API/actions. A registered handler is not proof of complete functionality. | +| Commerce audit completeness | Review full before/after snapshots, voucher code-edit parity, and canonical audit coverage of legacy voucher actions. | +| Validation and references | Review bounded numeric/string inputs, missing targets, foreign references, bulk all-or-nothing behavior and duplicate conflicts per operation. | +| Live acceptance | Local DB and RCON refuse connections. This does not prevent source implementation; it prevents live integration claims. | + +Keep PR 53 draft. Preserve legacy pages, local untracked files, production routing and the existing database contents. diff --git a/docs/superpowers/evidence/2026-09-05-housekeeping-parity-gaps.md b/docs/superpowers/evidence/2026-09-05-housekeeping-parity-gaps.md new file mode 100644 index 00000000..492434a7 --- /dev/null +++ b/docs/superpowers/evidence/2026-09-05-housekeeping-parity-gaps.md @@ -0,0 +1,84 @@ +# Housekeeping functional parity audit + +Baseline: 8e54cdbc. CI aggregate success verified remotely. This is an open-work inventory, not a completion report. + +## Delivery checkpoints + +- Task 1 room/room-furniture legacy convergence: implemented in 184052fb and d0190bc1, independently reviewed, pushed; CI passed. Post-commit refresh warnings are truthful response metadata, but shared UI display remains open. +- Task 4 moderation authority and guarded legacy entrypoints: implemented in 8a894617; pool-starvation review finding fixed in 54f0345a with 112 focused tests passing. Independent scoped re-review clean. No live DB/RCON contention or emulator acknowledgment evidence. +- Task 2 radio settings/cache: implemented in f24adfcf and compatibility fix 36ac116d. Legacy Promise forms delegate to transactional Hotel operations, support all 102 curated keys within a 500-entry bound, redact values, display sanitized success/partial/error notices, and invalidate the shared cache only after commit. Independent fix re-review clean; cross-process instant freshness is not claimed. +- origin/main through 775d14f8 integrated by merge 9b83cdc3. Upstream Catalog Studio Git export, inspection/review, advisory source preflight, streamed errors, asset validation, ID reservation/remapping and conversion recovery were preserved alongside HK cancellation/completion/reset behavior. The cancellation-during-ID-queue defect found in integration review was fixed in e7549bbb and re-reviewed clean. No live import, browser or database acceptance was performed. +- Task6 System atomic configuration/access: implemented in1360b0ed with review fix3c23c6e6. Canonical and legacy settings, maintenance, ACL/rank and command-center paths share validated mutations and transactional audits. Rank changes invalidate permissions after commit even if RCON fails; editable rank snapshots include staff presentation fields. Emulator synchronization has correlated intent/outcome records; external commands retain bounded operation-specific audit details. Dispatcher preserves cache partials. Four Important review findings were corrected and the scoped re-review is clean. Full implementation suite2089 passed/5 skipped; post-fix45 focused tests/typecheck/scopedBiome passed. Transaction-double fidelity remains a deferred Minor; no live rollback/emulator acceptance claim. +- Origin/main advanced again to2619bec1 during Task6, introducing queued furniture imports with filesystem history and attachments. Task18 integrates that upstream before further Studio work; it does not complete the planned HK durable repository or shared finalization tasks. +- All other findings below remain open until their implementation, tests and review are recorded. A baseline finding is retained here for traceability even after its corresponding checkpoint is delivered. + +## Hotel / Studio / Operations + +| Priority | Confirmed gap | Evidence | Execution | +| --- | --- | --- | --- | +| P1 | Legacy radio editor can write unrelated site setting keys | src/actions/admin-radio-extra.ts saveRadioSetting | Functional parity Task 2 | +| P1 | Legacy room items allow arbitrary fields and wrong-room update/delete | src/actions/rooms.ts | Task 1 | +| P1 | Studio final persistence/readback failure reclassifies successful runner as failed | hotel/commands/studio-commands.ts createStudioOperationService | Task 3 | +| P1 | Hotel radio writes omit runtime cache invalidation | hotel/services/mutations-production.ts | Task 2 | +| P1 | Studio furniture import omits selected source and finalization | studio-commands.ts furni branch vs src/app/api/admin/import/furni/route.ts | Open orchestration task | +| P2 | Studio command reason is discarded | studio-commands.ts / hotel/queries/studio.ts | Task 3 | +| P2 | Production Studio get/list only reads process memory | hotel/queries/studio.ts | Open durable repository task | +| P2 | Clone ignores false catalog/items refresh delivery | studio-commands.ts consolidate | Task 3 | +| P2 | Repair ignores nested Nitro failure and error events | studio-commands.ts repair-icons branch | Task 3 | +| P2 | Hotel HAVING filters only final UNION branch | hotel/queries/hotel-production.ts | Open query task | + +Also requiring explicit parity review: one-time radio API-key delivery; catalog audit/repair bridge versus a history-only screen; radio CRUD legacy convergence. + +Radio follow-up confirmed: admin-radio-api-keys.ts, admin-radio-autodj.ts and admin-radio-moderation.ts still write directly and can audit absent targets or swallow failures. Canonical radio runtime checks existence but does not lock those rows before mutations. radio.api-key.create returns metadata without the generated key; the legacy API-key page only renders a masked prefix. Functional parity Task16 covers guarded convergence and a creation-only secret result separated from audit/list output. + +Controller confirmed shared site-settings freshness defect: an expired memory cache is returned before attempting a database refresh whenever Redis has no value. Cache invalidation also resets inFlight without protecting against stale in-flight work repopulating the cache. Include regression coverage in the settings task. + +Operations replacing legacy dashboard metrics is intentional in migration/operations.ts, not itself missing parity. + +Audit coverage: Hotel mutations, Studio service/runner/repository, Hotel query/search/inbox/widgets, legacy room/radio actions, furniture import API; Operations composition and migration contract. Auditors performed read-only code comparison, not service-backed acceptance. + +## Other domains + +| Domain | Gap | Execution | +| --- | --- | --- | +| People | Ban/moderation/CFH bypass target hierarchy or existence; CFH accepts unrelated user | Task 4 | +| People | Alert/trade-lock bypass established target guard | Task 4 | +| People | Commands lose audit reason | Task 5 | +| People | Missing IP/word-filter delete reports success | Task 5 | +| People | Missing canonical user creation and individual badge grant/removal | Task 7 | +| People | User detail omits legacy account/relations/investigation fields | Task 12 | +| System | Privileged configuration/external operations lack canonical audit | Task 6 | +| System | Multi-key settings/maintenance writes are non-atomic | Task 6 | +| System | Unknown permission slugs silently revoke grants; audit outside transaction | Task 6 | +| System | Canonical ACL changes omit the permissions cache invalidation used by legacy actions | Task 6 | +| System | Rank update accepts missing target and empty/unknown fields | Task 6 | +| System | Logs fixed to flattened latest 50, no investigation filters/details | Task 12 | +| System | Command-center query omits declared recent emulator-error/staff-activity feeds | Task 12 | +| Content | Theme Builder operations absent despite verified migration row | Task 9 | +| Content | CRUD synthetic snapshots/false success for absent records | Task 8 | +| Content | Prefix settings silently skip invalid keys | Task 8 | +| Content | Edit query payloads incomplete across banners/prefixes/help/writeables/email | Task 8 | +| Economy | Missing file-upload soundtrack responsibility | Task 11 | +| Economy | Catalog bulk-create catches row failures and audits success | Task 10 | +| Economy | Badge grant race and inconsistent code bounds | Task 7 | +| Economy | Voucher update omits editable code | Task 10 | +| Economy | Marketplace/transactions filtering and user identity projections incomplete | Task 12 | +| Economy | Expired active subscriptions included | Task 12 | +| Economy | Calendar/rare-values editable/grouped projections incomplete | Task 12 | + +Read-only audit coverage included all declared commands and production query routing for Content/Economy, and People/System commands, services, query models and affected legacy entrypoints. Findings are mapped to implementation work; each needs focused regression evidence. Proposed badge slot uniqueness is NOT accepted without model verification: slot semantics may allow multiple unequipped badges. + +No domain is declared complete by this document. UI-only omissions remain separately open even when their backend operation already exists. + +Support follow-up confirmed in people/services/support-mutations.ts: ticket, Help Center, template and CFH reads lack FOR UPDATE; ticket-template delete audits success for a missing row; ticket.assign writes a supplied assignee without a user/eligibility lookup. Legacy CFH assign/state/close still write directly in actions/moderation.ts. Functional parity Task17 covers state integrity and active staff-action convergence, preserving separately scoped public ticket flows. + +Acceptance infrastructure check: current e2e/smoke.spec.ts only checks health and homepage rendering; it does not exercise authenticated administration workflows. The supplied docker-compose.yml assumes existing host MariaDB/Redis/emulator services rather than provisioning an isolated test stack. No Docker/MySQL/MariaDB executable was found on the current PATH. These are live-acceptance limitations, not reasons to defer locally testable implementation or to use production data as fixtures. + +## Confirmed presentation gaps for the post-backend pass + +- Content Brand currently exposes theme values as raw JSON and requires manually entered theme IDs (content/pages/brand.tsx). The approved operator product requires the retained visual Theme Builder/preset workflow, not JSON fields as its replacement. +- Content, Economy and Hotel generic page frames render raw error message keys and simple title/status lists without visible pagination/filter controls, although query parsing accepts pagination/search. Typed read models alone will not repair these workflows. +- Shared Content command parsing silently clamps numbers and truncates text/JSON before submission. Workflow forms must preserve entered values and present validation instead of silently saving a changed input. +- Backend-delivered partial outcomes must be presented in the relevant operator forms, including the deferred room refresh warning. The current source checks are not visual or browser acceptance evidence. + +The detailed UI task sequence must use the completed backend contracts and compare each active legacy workflow. This section deliberately does not mark UI parity complete. diff --git a/docs/superpowers/evidence/2026-09-05-housekeeping-rank-convergence.md b/docs/superpowers/evidence/2026-09-05-housekeeping-rank-convergence.md new file mode 100644 index 00000000..3a3bd1cf --- /dev/null +++ b/docs/superpowers/evidence/2026-09-05-housekeeping-rank-convergence.md @@ -0,0 +1,52 @@ +# Housekeeping rank synchronization + +## Behavior + +Rank assignments commit the configured rank, user update, and audit intent before +attempting emulator synchronization. Assignment and rank deletion acquire the +configured-rank row lock first. Delivery acquires the user row lock, reads the +current database rank, and holds that lock until the transport settles. + +Retrying an old recovery reference therefore dispatches the current committed +rank rather than replaying the rank stored in the old audit record. A user deleted +after persistence produces an audited superseded result. SQL lock errors remain +dependency failures rather than being reported as missing ranks. + +The coordinated paths cover System operations, People user editing, the legacy +command centre, legacy user editing, the user-actions API, legacy rank deletion, +and shop rank upgrades. Administrative user creation also locks the selected rank. +Shop upgrades compare the locked current rank so they cannot overwrite a newer +staff promotion. A failed post-purchase rank delivery leaves a recovery intent +without turning the completed purchase into another charge. + +People and legacy responses preserve partial-completion information and recovery +references. Failure of the completion audit alone does not misreport successful +transport delivery as a transport failure. + +## Verification + +- Focused rank, legacy-entrypoint, shop, System and People tests: 94 passed. +- Full suite: 280 files passed, 3 skipped; 1,861 tests passed, 5 skipped. +- Next.js 16.3.4 production build passed and generated all 245 pages. +- TypeScript and canonical Knip checks passed. +- Biome passed on all 13 changed source/test files. +- Project-source lint without formatting passed on 1,412 files, with one existing + Catalog Studio warning. The full Windows checkout check also includes local + untracked brainstorm HTML and reports CRLF/LF formatting differences; those + local files were preserved and are not part of this change. +- Migration matrix: 138 historical rows valid; 138 legacy pages retained; + runtime discovered/mapped/verified 138/138/138, with 2 intentional removals. +- Independent read-only review found no remaining Important or Critical issues. + +## Validation boundary + +Tests exercise the parameterized locking SQL and controlled transaction/transport +ordering. No live two-connection MariaDB race test or production emulator +acceptance test was performed. + +TCP RCON success means the socket write completed. CMS dispatch is serialized, +but the current protocol does not acknowledge emulator processing or enforce its +processing order. End-to-end confirmation would require an emulator protocol +change. The existing transport timeout and retry policy bounds the delivery wait. + +The PR remains draft; these checks are not a deployment or preview-cutover claim. diff --git a/docs/superpowers/evidence/2026-09-05-voucher-reservation.md b/docs/superpowers/evidence/2026-09-05-voucher-reservation.md new file mode 100644 index 00000000..bd64583a --- /dev/null +++ b/docs/superpowers/evidence/2026-09-05-voucher-reservation.md @@ -0,0 +1,22 @@ +# Voucher claim reservation + +The public redemption action now serializes claims on the selected voucher row. +It validates amount, capacity and expiration, locks the duplicate-claim read, +inserts the used row, increments usage and stores the audit intent in one transaction. +No reward dispatch occurs until the transaction resolves successfully. A commit +acknowledgment failure prevents dispatch and returns a correlation reference. + +The reward transport remains the existing sendCurrency implementation. A false +or thrown result is unconfirmed, not proof that no increment occurred. Such a +claim stays consumed and receives a partial audit outcome. It must not be blindly +replayed or refunded; staff can inspect the correlated intent and final outcome. +A completion-audit failure after successful dispatch does not report failed delivery. + +This does not guarantee exactly-once emulator processing, introduce automatic +reward recovery, or claim that database reservation and external dispatch are +one distributed transaction. No migration or live data change is performed. + +Tests exercise the real action and generated Drizzle SQL with controlled database, +session, audit and currency transports. They prove boundary ordering and error +mapping, not actual multi-connection MariaDB locking or emulator acceptance. +The initial regression run had nine expected failures before implementation. diff --git a/docs/superpowers/plans/2026-08-26-housekeeping-completion.md b/docs/superpowers/plans/2026-08-26-housekeeping-completion.md new file mode 100644 index 00000000..414e7d2b --- /dev/null +++ b/docs/superpowers/plans/2026-08-26-housekeeping-completion.md @@ -0,0 +1,1191 @@ +# Housekeeping Completion Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (current session) or `superpowers:executing-plans` (separate session) to execute this plan task-by-task. + +**Goal:** Complete all 137 Housekeeping migrations behind `/ase-next`, then atomically publish the capability-aware Command Deck at `/ase` and remove the legacy `/admin`, `/admin-next`, and `/mod` UI trees without redirects. + +**Architecture:** Keep `src/features/housekeeping/foundation` limited to cross-domain contracts, registry projection, provider orchestration, shell composition, preferences, commands, and audit envelopes. Each of the six domains owns its route catalog, server adapters, commands, search, inbox, widgets, and workflow components. App Router entrypoints dispatch canonical route IDs to domain-owned renderers; the preview maps canonical `/ase/*` destinations to `/ase-next/*` without changing domain manifests. + +**Tech Stack:** Node.js 26, Next.js 16 App Router, React 19, TypeScript 7, Drizzle ORM/MySQL, next-intl, Zod 4, Vitest 4, Biome 2, Tailwind CSS 4, cmdk, dnd-kit. + +**Spec:** `docs/superpowers/specs/2026-08-26-housekeeping-completion-design.md` + +**Global Constraints:** Work only on `codex/housekeeping-complete`; do not use worktrees; preserve `.remember/` and unrelated changes; keep `/admin` and `/mod` behavior unchanged until Task 25; keep `/ase-next` unavailable in production; retain `/api/admin/*` as internal transport contracts unless a task explicitly changes one; add only backward-compatible database changes; use the current ACL permission slugs and never introduce rank thresholds; stage exact files; run `git diff --check` before every commit; do not push or open a pull request until the entire plan and final review pass. + +## File structure and ownership + +```text +src/app/ase-next/ gated preview App Router surface + layout.tsx production-denying preview gate + page.tsx first visible domain redirect + [domain]/layout.tsx capability-projected Command Deck shell + [domain]/[[...segments]]/page.tsx route ID resolution and domain dispatch +src/app/ase/ created only by atomic cutover + +src/features/housekeeping/foundation/ + contracts/ stable capability, result, route, command, + search, inbox, widget, and preference types + routing/ canonical/preview href mapping and matcher + providers/ timeout, cap, partial-result orchestration + commands/ dispatcher, confirmation, audit envelope + preferences/ schema validation and reconciliation + recent/ audit-derived recent work + shell/ rail, contextual nav, command deck, inbox, + widgets, favorites, and responsive chrome + page/ shared page states and workflow primitives + +src/features/housekeeping/domains// + manifest.ts declarative registry entry + routes.ts canonical `/ase` routes and handler IDs + route-handlers.ts domain-owned renderer dispatch + queries/ server-only read adapters + commands/ typed mutations around existing services + search.ts entity-search providers + inbox.ts derived work sources + widgets.ts mandatory and optional loaders + pages/ workflow-focused React server/client views + +src/actions/housekeeping-*.ts narrow server-action boundaries +drizzle/migrations/0023_housekeeping.sql additive audit and preference migration +src/features/housekeeping/cutover/ 137-row parity and legacy-removal contracts +``` + +The migration files remain the authoritative inventory of legacy source pages and dependencies. Domain route catalogs are authoritative for canonical `/ase` destinations. A contract joins the two by canonical route ID; no legacy page component is imported into the new route tree. + +### Task 1: Lock the `/ase` namespace and preview mapping + +**Files:** + +- Modify: `src/features/housekeeping/foundation/contracts/domain.ts` +- Create: `src/features/housekeeping/foundation/routing/href.ts` +- Create: `src/features/housekeeping/foundation/routing/href.test.ts` +- Modify: `src/features/housekeeping/foundation/registry.ts` +- Modify: `src/features/housekeeping/foundation/registry.test.ts` +- Modify: `src/features/housekeeping/foundation/navigation.ts` +- Modify: `src/features/housekeeping/foundation/navigation.test.ts` +- Modify: `src/features/housekeeping/domains/*/manifest.ts` +- Modify: `src/features/housekeeping/migration/operations.ts` +- Modify: `src/features/housekeeping/migration/people.ts` +- Modify: `src/features/housekeeping/migration/content.ts` +- Modify: `src/features/housekeeping/migration/economy.ts` +- Modify: `src/features/housekeeping/migration/hotel.ts` +- Modify: `src/features/housekeeping/migration/system.ts` +- Modify: `src/features/housekeeping/migration/validate-matrix.ts` +- Modify: `src/features/housekeeping/migration/validate-matrix.test.ts` +- Move: `src/app/admin-next` to `src/app/ase-next` +- Modify: `src/features/housekeeping/foundation/preview-route-contract.test.ts` +- Modify: `src/features/housekeeping/foundation/foundation-source-contract.test.ts` +- Modify: `src/lib/admin-theme-source-audit.test.ts` + +**Interfaces:** + +- Consumes: canonical route strings beginning with `/ase`. +- Produces: + +```ts +export type HousekeepingSurface = "preview" | "canonical"; +export type CanonicalHousekeepingHref = `/ase${string}`; +export function toHousekeepingHref( + href: CanonicalHousekeepingHref, + surface: HousekeepingSurface, +): CanonicalHousekeepingHref | `/ase-next${string}`; +``` + +- `HousekeepingDomainManifest.canonicalHref` is a `CanonicalHousekeepingHref`; + Operations uses `/ase`, while the other domains use `/ase/`. Route + definitions store canonical `/ase` hrefs only. + +- [ ] Write failing href tests proving `/ase` stays `/ase`, `/ase/people/users` maps to `/ase-next/people/users` in preview, and `/admin` input is rejected by the type/runtime guard. +- [ ] Run `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/foundation/routing/href.test.ts` and confirm RED. +- [ ] Implement `toHousekeepingHref`, replace `previewHref` with `canonicalHref`, and pass `surface` into `buildHousekeepingNavigation`. +- [ ] Change every non-null migration `targetPath` prefix from `/admin` to `/ase`; change validation to require `targetPath === "/ase" || targetPath.startsWith("/ase/")`. +- [ ] Move the preview route tree to `src/app/ase-next`, update source contracts/localized test fixtures, and prove production still calls `notFound()`. +- [ ] Run `pnpm hk:matrix:check` and the routing, registry, navigation, preview, matrix, and theme-source tests; confirm 137 discovered rows and zero issues. +- [ ] Run targeted Biome, `git diff --check`, stage only listed paths, and commit `refactor(housekeeping): adopt ase route namespace`. + +### Task 2: Stabilize result, error, correlation, and route contracts + +**Files:** + +- Modify: `src/features/housekeeping/foundation/contracts/result.ts` +- Modify: `src/features/housekeeping/foundation/contracts/query.ts` +- Modify: `src/features/housekeeping/foundation/contracts/command.ts` +- Modify: `src/features/housekeeping/foundation/contracts/search.ts` +- Modify: `src/features/housekeeping/foundation/contracts/inbox.ts` +- Modify: `src/features/housekeeping/foundation/contracts/widget.ts` +- Modify: `src/features/housekeeping/foundation/contracts/domain.ts` +- Modify: `src/features/housekeeping/foundation/contracts/index.ts` +- Modify: `src/features/housekeeping/foundation/contracts/contracts.test.ts` +- Create: `src/features/housekeeping/foundation/correlation.ts` +- Create: `src/features/housekeeping/foundation/correlation.test.ts` + +**Interfaces:** + +```ts +export type HousekeepingErrorCode = + | "UNAUTHENTICATED" | "FORBIDDEN" | "VALIDATION" | "NOT_FOUND" + | "CONFLICT" | "RATE_LIMITED" | "DEPENDENCY_UNAVAILABLE" + | "TIMEOUT" | "INTERNAL"; +export interface HousekeepingError { + code: HousekeepingErrorCode; + messageKey: string; + fieldErrors?: Readonly>; +} +export type HousekeepingResult = + | { ok: true; data: T; correlationId: string } + | { ok: false; error: HousekeepingError; correlationId: string }; +export function createCorrelationId(): string; +``` + +- [ ] Replace the obsolete error-code expectations with table-driven tests for all nine approved codes, field errors, success, and correlation preservation. +- [ ] Run the contracts and correlation tests and confirm RED on the renamed taxonomy. +- [ ] Implement the types, `ok`, `fail`, `mapUnknownError`, and a 64-character-safe correlation generator using `crypto.randomUUID()`. +- [ ] Extend search results with `type`, `description`, canonical href, and capability metadata; extend inbox items with priority, age/state, actions; allow widget loaders to accept an abort signal. +- [ ] Run the two tests plus `pnpm typecheck`; fix all foundation callers without weakening types. +- [ ] Run targeted Biome, `git diff --check`, stage exact contract files, and commit `refactor(housekeeping): stabilize service contracts`. + +### Task 3: Make capability context request-scoped and reusable + +**Files:** + +- Modify: `src/features/housekeeping/foundation/server-capability-context.ts` +- Modify: `src/features/housekeeping/foundation/server-capability-context.test.ts` +- Modify: `src/features/housekeeping/foundation/capability-context.ts` +- Modify: `src/features/housekeeping/foundation/capability-context.test.ts` +- Modify: `src/lib/admin/guard.ts` +- Modify: `src/lib/admin/guard.test.ts` +- Create: `src/features/housekeeping/foundation/authorization.ts` +- Create: `src/features/housekeeping/foundation/authorization.test.ts` + +**Interfaces:** + +```ts +export const getHousekeepingCapabilityContext: () => + Promise; +export function authorizeHousekeeping( + context: HousekeepingCapabilityContext, + requirement: CapabilityRequirement, +): HousekeepingResult; +export function requireHousekeepingCapability( + requirement: CapabilityRequirement, + context?: HousekeepingCapabilityContext, +): Promise; +``` + +- [ ] Write tests proving one request calls `getAdminContext()` once across shell, page, and command preflight; denied checks return `FORBIDDEN` without rank thresholds. +- [ ] Run the capability, authorization, and guard tests and confirm RED. +- [ ] Keep React `cache()` as the request boundary, add reusable authorization functions, and let HK guards accept an already-created context. +- [ ] Retain legacy `requireStaff`, `requirePermission`, and `/admin` fallbacks unchanged until cutover; remove only duplicate HK lookups. +- [ ] Run targeted tests and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `refactor(housekeeping): reuse request capability context`. + +### Task 4: Add the audit and preferences schema additively + +**Files:** + +- Create: `drizzle/migrations/0023_housekeeping.sql` +- Modify: `src/db/schema.ts` +- Create: `src/features/housekeeping/foundation/persistence-contract.test.ts` + +**Interfaces:** + +```ts +export type HousekeepingUserPreferenceRow = + typeof HousekeepingUserPreferences.$inferSelect; +// admin_audit_log adds correlationId, outcome, reason, and domain. +``` + +Migration shape: + +```sql +ALTER TABLE `admin_audit_log` + ADD COLUMN `correlation_id` VARCHAR(64) NULL, + ADD COLUMN `outcome` VARCHAR(32) NULL, + ADD COLUMN `reason` TEXT NULL, + ADD COLUMN `domain` VARCHAR(32) NULL, + ADD INDEX `admin_audit_log_correlation_id_idx` (`correlation_id`); +CREATE TABLE `housekeeping_user_preferences` ( + `user_id` INT NOT NULL, + `schema_version` INT NOT NULL DEFAULT 1, + `payload` LONGTEXT NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`user_id`) +); +``` + +- [ ] Write a source-contract test asserting exact nullable audit columns/index, exact preference columns, one primary key, and no `DROP`, `RENAME`, or legacy-column mutation. +- [ ] Run the persistence contract and confirm RED. +- [ ] Add migration `0023` and matching Drizzle declarations using existing schema naming conventions. +- [ ] Run the contract, `pnpm typecheck`, and `pnpm db:migrate:status` against the configured non-production environment only when available; record an unavailable DB as unexecuted, not passed. +- [ ] Run targeted Biome, `git diff --check`, stage the migration/schema/test, and commit `feat(housekeeping): add audit and preference storage`. + +### Task 5: Extend audit writing and correlation evidence + +**Files:** + +- Modify: `src/lib/services/audit.ts` +- Modify: `src/lib/services/audit.test.ts` +- Create: `src/features/housekeeping/foundation/commands/audit-envelope.ts` +- Create: `src/features/housekeeping/foundation/commands/audit-envelope.test.ts` + +**Interfaces:** + +```ts +export interface AuditEntry { + userId: number; + action: string; + target: string; + targetId?: number; + before?: Record; + after?: Record; + correlationId?: string; + outcome?: "intent" | "success" | "failure" | "partial" | "denied"; + reason?: string; + domain?: HousekeepingDomainId; + ipAddress?: string; +} +export interface HousekeepingAuditWriter { + write(entry: AuditEntry, transaction?: HousekeepingAuditTransaction): Promise; +} +``` + +- [ ] Add tests for recursive secret redaction, correlation/domain/outcome persistence, denied/failure evidence, and transaction-injected writes. +- [ ] Run audit tests and confirm RED. +- [ ] Export the audit payload type, preserve existing call compatibility, and add a writer adapter that can use either `db` or the supplied transaction. +- [ ] Implement `writeIntent` and `writeOutcome`; block external/file execution if intent persistence fails. +- [ ] Run audit and envelope tests plus `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): correlate command audit evidence`. + +### Task 6: Implement preferences validation, repository, and reconciliation + +**Files:** + +- Create: `src/features/housekeeping/foundation/preferences/schema.ts` +- Create: `src/features/housekeeping/foundation/preferences/schema.test.ts` +- Create: `src/features/housekeeping/foundation/preferences/repository.ts` +- Create: `src/features/housekeeping/foundation/preferences/repository.test.ts` +- Create: `src/features/housekeeping/foundation/preferences/reconcile.ts` +- Create: `src/features/housekeeping/foundation/preferences/reconcile.test.ts` +- Create: `src/actions/housekeeping-preferences.ts` +- Create: `src/actions/housekeeping-preferences.test.ts` + +**Interfaces:** + +```ts +export const housekeepingPreferencesSchema: z.ZodType<{ + schemaVersion: 1; + pinnedRouteIds: string[]; + pinnedCommandIds: string[]; + shortcutOrder: string[]; + widgetOrder: string[]; + enabledOptionalWidgetIds: string[]; +}>; +export interface HousekeepingPreferencesRepository { + read(userId: number): Promise; + upsert(userId: number, value: HousekeepingPreferences): Promise; +} +export function reconcilePreferences( + stored: HousekeepingPreferences, + registry: HousekeepingRegistry, + context: HousekeepingCapabilityContext, +): HousekeepingPreferences; +``` + +- [ ] Write failing schema tests for malformed JSON, unknown keys, duplicate IDs, and schema version; write repository tests for absent/default and upsert behavior. +- [ ] Write reconciliation tests proving unauthorized/removed IDs are dropped, mandatory widgets remain, and relative order of valid IDs is stable. +- [ ] Run all four preference test files and confirm RED. +- [ ] Implement Zod validation, injected repository DB adapter, deterministic reconciliation, and capability-checked load/save server actions. +- [ ] Run preference tests, `pnpm typecheck`, and the Housekeeping suite. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): persist operator preferences`. + +### Task 7: Build bounded provider orchestration + +**Files:** + +- Create: `src/features/housekeeping/foundation/providers/run-provider.ts` +- Create: `src/features/housekeeping/foundation/providers/run-provider.test.ts` +- Create: `src/features/housekeeping/foundation/providers/orchestrate.ts` +- Create: `src/features/housekeeping/foundation/providers/orchestrate.test.ts` + +**Interfaces:** + +```ts +export interface ProviderPolicy { + timeoutMs: number; + perProviderLimit: number; + combinedLimit: number; + sort(items: readonly T[]): readonly T[]; + dedupeKey(item: T): string; +} +export interface ProviderBatchResult { + items: readonly T[]; + errors: readonly { providerId: string; code: HousekeepingErrorCode }[]; + correlationId: string; +} +``` + +- [ ] Use fake timers to test a 2,000 ms abort, capability-skipped providers, thrown errors mapped once, stable dedupe, per-provider cap, combined cap, and deterministic ordering. +- [ ] Run both provider tests and confirm RED. +- [ ] Implement orchestration without importing domain modules or database clients; accept providers and policy through arguments. +- [ ] Prove one timeout returns successful sibling results with a typed partial error. +- [ ] Run provider tests and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): orchestrate partial providers`. + +### Task 8: Implement the typed command dispatcher + +**Files:** + +- Create: `src/features/housekeeping/foundation/commands/registry.ts` +- Create: `src/features/housekeeping/foundation/commands/registry.test.ts` +- Create: `src/features/housekeeping/foundation/commands/dispatcher.ts` +- Create: `src/features/housekeeping/foundation/commands/dispatcher.test.ts` +- Create: `src/features/housekeeping/foundation/commands/confirmation.ts` +- Create: `src/features/housekeeping/foundation/commands/confirmation.test.ts` +- Create: `src/actions/housekeeping-command.ts` +- Create: `src/actions/housekeeping-command.test.ts` + +**Interfaces:** + +```ts +export interface HousekeepingCommand { + id: string; + owner: HousekeepingDomainId; + risk: "safe" | "sensitive"; + capability: CapabilityRequirement; + input: z.ZodType; + requiresReason: boolean; + rateLimit: { attempts: number; windowMs: number }; + execute(ctx: HousekeepingCommandContext, input: I): Promise>; +} +export interface HousekeepingCommandContext { + capability: HousekeepingCapabilityContext; + correlationId: string; + ipAddress: string; +} +export interface HousekeepingCommandDependencies { + context: HousekeepingCapabilityContext; + audit: HousekeepingAuditWriter; + rateLimit(key: string, attempts: number, windowMs: number): Promise; +} +export async function dispatchHousekeepingCommand( + request: { commandId: string; input: unknown; reason?: string }, + dependencies: HousekeepingCommandDependencies, +): Promise>; +``` + +- [ ] Write tests for unknown command, denied capability, invalid input, missing reason, rate limit, safe success, sensitive intent/success, sensitive intent/failure, and sanitized unknown exception. +- [ ] Run dispatcher/action tests and confirm RED. +- [ ] Implement global ID/owner validation, server-side capability recheck, Zod parsing, per-actor/IP limit, confirmation metadata, and audit envelope usage. +- [ ] Ensure server actions accept plain serializable values and never trust client risk/capability metadata. +- [ ] Run all command tests, audit tests, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): dispatch audited commands`. + +### Task 9: Resolve canonical routes and dispatch domain pages + +**Files:** + +- Create: `src/features/housekeeping/foundation/routing/match-route.ts` +- Create: `src/features/housekeeping/foundation/routing/match-route.test.ts` +- Create: `src/features/housekeeping/route-handlers.ts` +- Create: `src/features/housekeeping/route-handlers.test.ts` +- Modify: `src/app/ase-next/page.tsx` +- Modify: `src/app/ase-next/[domain]/layout.tsx` +- Create: `src/app/ase-next/[domain]/[[...segments]]/page.tsx` +- Delete: `src/app/ase-next/[domain]/page.tsx` + +**Interfaces:** + +```ts +export interface HousekeepingRouteMatch { + routeId: string; + domain: HousekeepingDomainId; + params: Readonly>; + canonicalHref: CanonicalHousekeepingHref; +} +export interface HousekeepingRouteHandler { + routeId: string; + render(input: HousekeepingPageInput): Promise; +} +export function matchHousekeepingRoute( + registry: HousekeepingRegistry, + canonicalPath: string, +): HousekeepingRouteMatch | null; +``` + +- [ ] Write matcher tests for static, `:id`, nested, unknown, malformed, and cross-domain paths; test one-to-one equality between registered route IDs and handler IDs. +- [ ] Run matcher/handler tests and confirm RED. +- [ ] Implement segment-safe matching without regular-expression injection; reject duplicate dynamic shapes during registry creation. +- [ ] Update preview pages to map `/ase-next//` to canonical `/ase//`, reauthorize the matched route, and call its handler. +- [ ] Test inaccessible routes as `notFound()` and permitted routes with one request-scoped context. +- [ ] Run routing, registry, preview-route tests and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact paths, and commit `feat(housekeeping): dispatch canonical domain routes`. + +### Task 10: Deliver System access, configuration, observability, and operations + +**Files:** + +- Create: `src/features/housekeeping/domains/system/routes.ts` +- Create: `src/features/housekeeping/domains/system/routes.test.ts` +- Create: `src/features/housekeeping/domains/system/queries/access.ts` +- Create: `src/features/housekeeping/domains/system/queries/configuration.ts` +- Create: `src/features/housekeeping/domains/system/queries/observability.ts` +- Create: `src/features/housekeeping/domains/system/queries/operations.ts` +- Create: `src/features/housekeeping/domains/system/queries/system-queries.test.ts` +- Create: `src/features/housekeeping/domains/system/commands/system-commands.ts` +- Create: `src/features/housekeeping/domains/system/commands/system-commands.test.ts` +- Create: `src/features/housekeeping/domains/system/pages/access.tsx` +- Create: `src/features/housekeeping/domains/system/pages/configuration.tsx` +- Create: `src/features/housekeeping/domains/system/pages/observability.tsx` +- Create: `src/features/housekeeping/domains/system/pages/operations.tsx` +- Create: `src/features/housekeeping/domains/system/pages/system-pages.test.tsx` +- Create: `src/features/housekeeping/domains/system/route-handlers.ts` +- Modify: `src/features/housekeeping/domains/system/manifest.ts` +- Modify: `src/actions/admin-alerts.ts` +- Modify: `src/actions/admin-emulator.ts` +- Modify: `src/actions/admin-maintenance.ts` +- Modify: `src/actions/admin-settings.ts` +- Modify: `src/actions/commandocentrum.ts` +- Modify: `src/actions/permissions.ts` +- Modify: `src/lib/admin/ops-health.ts` +- Modify: `src/lib/admin/ops-online-users.ts` + +**Interfaces:** + +```ts +export const SYSTEM_ROUTE_IDS = [ + "system.access.permissions", "system.access.permission-detail", + "system.configuration.settings", "system.configuration.emulator", + "system.observability.analytics", "system.observability.analytics-activity", + "system.observability.analytics-economy", "system.observability.devops", + "system.observability.devops-errors", "system.observability.logs-staff", + "system.observability.logs-audit", "system.observability.logs-chat", + "system.observability.logs-commands", "system.observability.logs-trades", + "system.operations.alerts", "system.operations.command-center", + "system.operations.maintenance", +] as const; +``` + +- [ ] Write route tests asserting the exact route IDs above, canonical `/ase/system/*` destinations, matrix coverage, labels, and read capabilities. +- [ ] Write query tests using injected adapters for ACL/ranks, settings, emulator data, analytics/logs, health, errors, alerts, and maintenance; include dependency-unavailable mapping. +- [ ] Write command tests for rank/ACL writes, settings/emulator updates, alerts, RCON commands, and maintenance; require reasons for permission, RCON, and global-availability mutations. +- [ ] Run System tests and confirm RED before each production module is added. +- [ ] Extract redirect-free mutation functions from the listed legacy actions; keep legacy action wrappers and `/admin` redirects working until cutover. +- [ ] Build the four workflow pages with loading/empty/partial/error/forbidden states, then register real System routes and commands; Task 19 registers the System search, inbox, and widget providers. +- [ ] Run System, legacy action, audit, authorization, full HK tests, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact System/action files, and commit `feat(housekeeping): deliver system vertical`. + +### Task 11: Build People read adapters and canonical data models + +**Files:** + +- Create: `src/features/housekeeping/domains/people/routes.ts` +- Create: `src/features/housekeeping/domains/people/routes.test.ts` +- Create: `src/features/housekeeping/domains/people/queries/users.ts` +- Create: `src/features/housekeeping/domains/people/queries/community.ts` +- Create: `src/features/housekeeping/domains/people/queries/staff.ts` +- Create: `src/features/housekeeping/domains/people/queries/support.ts` +- Create: `src/features/housekeeping/domains/people/queries/moderation.ts` +- Create: `src/features/housekeeping/domains/people/queries/people-queries.test.ts` +- Create: `src/features/housekeeping/domains/people/models.ts` +- Create: `src/features/housekeeping/domains/people/models.test.ts` + +**Interfaces:** + +```ts +export interface PeopleUserSummary { + id: number; username: string; rank: number; online: boolean; + mail: string | null; ipCurrent: string | null; bannedUntil: number | null; +} +export interface PeopleQueueSnapshot { + tickets: number; helpTickets: number; cfh: number; activeBans: number; +} +export interface PeopleQueries { + users(input: ListInput): Promise>>; + user(id: number): Promise>; + queue(): Promise>; +} +``` + +- [ ] Write exact route-catalog tests for users, multi-accounts, online, guilds, applications, teams, bans, IP/VPN/wordfilter, tickets, help tickets, CFH, moderation actions, and mod-team workflows. +- [ ] Write query tests for pagination, stable sorting, missing users, capability-safe projections, ticket/CFH counts, guild detail, staff applications, sanctions, and dependency failures. +- [ ] Run People model/query/route tests and confirm RED. +- [ ] Implement server-only adapters around the matrix-listed Drizzle/RCON/service dependencies; never expose password hashes, auth tickets, secrets, or unredacted IPs without their explicit capability. +- [ ] Map all returned failures to the stable HK error set and canonical `/ase/people/*` links. +- [ ] Run People tests and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact People query files, and commit `feat(housekeeping): model people workflows`. + +### Task 12: Deliver People users, community, and staff workflows + +**Files:** + +- Create: `src/features/housekeeping/domains/people/commands/user-commands.ts` +- Create: `src/features/housekeeping/domains/people/commands/user-commands.test.ts` +- Create: `src/features/housekeeping/domains/people/commands/community-commands.ts` +- Create: `src/features/housekeeping/domains/people/commands/community-commands.test.ts` +- Create: `src/features/housekeeping/domains/people/pages/users.tsx` +- Create: `src/features/housekeeping/domains/people/pages/user-detail.tsx` +- Create: `src/features/housekeeping/domains/people/pages/user-edit.tsx` +- Create: `src/features/housekeeping/domains/people/pages/multi-accounts.tsx` +- Create: `src/features/housekeeping/domains/people/pages/community.tsx` +- Create: `src/features/housekeeping/domains/people/pages/staff.tsx` +- Create: `src/features/housekeeping/domains/people/pages/people-primary-pages.test.tsx` +- Modify: `src/actions/bulk-users.ts` +- Modify: `src/actions/users.ts` +- Modify: `src/actions/admin-guilds.ts` +- Modify: `src/actions/admin-applications.ts` +- Modify: `src/actions/admin-teams.ts` +- Modify: `src/actions/admin-ip.ts` +- Modify: `src/actions/admin-vpn.ts` +- Modify: `src/actions/admin-wordfilter.ts` + +**Interfaces:** + +```ts +export type UserCommandId = + | "people.user.update" | "people.user.ban" | "people.user.unban" + | "people.user.alert" | "people.user.disconnect" | "people.user.mute" + | "people.user.unmute" | "people.user.reset-password" + | "people.user.send-currency" | "people.user.trade-lock" + | "people.users.bulk-ban" | "people.users.bulk-unban" + | "people.users.bulk-currency" | "people.users.bulk-badge"; +``` + +- [ ] Write command tests for the exact IDs above plus guild disband, application decision, team change, IP action, VPN configuration, and wordfilter update; assert server capability rechecks and audit before/after. +- [ ] Run command tests and confirm RED. +- [ ] Extract redirect-free service functions from listed actions while preserving legacy server-action wrappers. +- [ ] Implement user list/detail/edit and multi-account pages; preserve existing fields/actions only when the matrix capability permits them. +- [ ] Implement community and staff workflows with canonical links and no duplicate show/edit aliases. +- [ ] Run People primary page/command tests, affected legacy tests, HK suite, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): deliver people account workflows`. +### Task 13: Deliver People support and moderation parity + +**Files:** + +- Create: `src/features/housekeeping/domains/people/commands/support-commands.ts` +- Create: `src/features/housekeeping/domains/people/commands/moderation-commands.ts` +- Create: `src/features/housekeeping/domains/people/commands/support-commands.test.ts` +- Create: `src/features/housekeeping/domains/people/commands/moderation-commands.test.ts` +- Create: `src/features/housekeeping/domains/people/pages/support.tsx` +- Create: `src/features/housekeeping/domains/people/pages/moderation.tsx` +- Create: `src/features/housekeeping/domains/people/pages/cfh-detail.tsx` +- Create: `src/features/housekeeping/domains/people/pages/ticket-detail.tsx` +- Create: `src/features/housekeeping/domains/people/pages/help-ticket-detail.tsx` +- Create: `src/features/housekeeping/domains/people/pages/people-support-pages.test.tsx` +- Create: `src/features/housekeeping/domains/people/search.ts` +- Create: `src/features/housekeeping/domains/people/inbox.ts` +- Create: `src/features/housekeeping/domains/people/widgets.ts` +- Create: `src/features/housekeeping/domains/people/people-providers.test.ts` +- Create: `src/features/housekeeping/domains/people/route-handlers.ts` +- Modify: `src/features/housekeeping/domains/people/manifest.ts` +- Modify: `src/actions/admin-bans.ts` +- Modify: `src/actions/admin-help-tickets.ts` +- Modify: `src/actions/help-tickets.ts` +- Modify: `src/actions/moderation.ts` +- Modify: `src/actions/tickets.ts` +- Modify: `src/actions/ticket-templates.ts` +- Modify: `src/lib/services/moderation.ts` +- Modify: `src/lib/services/ticket-replies.ts` + +**Interfaces:** + +```ts +export const PEOPLE_INBOX_SOURCE_IDS = [ + "people.tickets", "people.help-tickets", "people.cfh", "people.active-bans", +] as const; +export const PEOPLE_SEARCH_PROVIDER_IDS = [ + "people.users", "people.guilds", "people.tickets", +] as const; +``` + +- [ ] Write failing parity tests joining all People matrix rows, including every `/mod` row, to one route or removed decision. +- [ ] Write support/moderation command tests for assign/reply/close/reopen/template, CFH resolve/sanction, ban/unban, and moderation action; require reasons for sanctions and bans. +- [ ] Implement support and moderation pages with queue/detail flows, safe links, preserved mid-rank `mod.*` access, and no `admin.dashboard` dependency when the original route did not require it. +- [ ] Implement the exact People search/inbox providers and mandatory queue widget; enforce 25-result and item capability filtering at provider level. +- [ ] Register handlers/providers/widgets and prove no People manifest collection is empty. +- [ ] Run People, legacy moderation/ticket tests, HK suite, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): complete people moderation parity`. + +### Task 14: Deliver Content and engagement + +**Files:** + +- Create: `src/features/housekeeping/domains/content/routes.ts` +- Create: `src/features/housekeeping/domains/content/routes.test.ts` +- Create: `src/features/housekeeping/domains/content/queries/content-queries.ts` +- Create: `src/features/housekeeping/domains/content/queries/content-queries.test.ts` +- Create: `src/features/housekeeping/domains/content/commands/content-commands.ts` +- Create: `src/features/housekeeping/domains/content/commands/content-commands.test.ts` +- Create: `src/features/housekeeping/domains/content/pages/editorial.tsx` +- Create: `src/features/housekeeping/domains/content/pages/media.tsx` +- Create: `src/features/housekeeping/domains/content/pages/engagement.tsx` +- Create: `src/features/housekeeping/domains/content/pages/help.tsx` +- Create: `src/features/housekeeping/domains/content/pages/brand.tsx` +- Create: `src/features/housekeeping/domains/content/pages/localization.tsx` +- Create: `src/features/housekeeping/domains/content/pages/content-pages.test.tsx` +- Create: `src/features/housekeeping/domains/content/search.ts` +- Create: `src/features/housekeeping/domains/content/inbox.ts` +- Create: `src/features/housekeeping/domains/content/widgets.ts` +- Create: `src/features/housekeeping/domains/content/route-handlers.ts` +- Create: `src/features/housekeeping/domains/content/content-providers.test.ts` +- Modify: `src/features/housekeeping/domains/content/manifest.ts` +- Modify: `src/actions/admin-ads.ts` +- Modify: `src/actions/admin-articles.ts` +- Modify: `src/actions/admin-banners.ts` +- Modify: `src/actions/admin-email-templates.ts` +- Modify: `src/actions/admin-help.ts` +- Modify: `src/actions/admin-media.ts` +- Modify: `src/actions/admin-nav-menu.ts` +- Modify: `src/actions/admin-photos.ts` +- Modify: `src/actions/admin-tags.ts` +- Modify: `src/actions/admin-theme.ts` +- Modify: `src/actions/admin-writeable-boxes.ts` +- Modify: `src/actions/banners.ts` +- Modify: `src/actions/events.ts` +- Modify: `src/actions/polls.ts` +- Modify: `src/actions/prefixes.ts` +- Modify: `src/actions/save-favicon.ts` +- Modify: `src/actions/save-logo.ts` +- Modify: `src/actions/translations.ts` + +**Interfaces:** + +```ts +export const CONTENT_ROUTE_GROUPS = [ + "editorial", "media", "engagement", "help", "brand", "localization", +] as const; +export const CONTENT_SEARCH_PROVIDER_IDS = [ + "content.articles", "content.events", "content.media", "content.help", +] as const; +``` + +- [ ] Write route tests mapping every Content matrix row to one of the six exact route groups and canonical `/ase/content/*` destinations. +- [ ] Write query/command tests for articles, ads, banners, events/types, polls, photos/media, navigation, tags/prefixes, help questions, writable boxes, email templates, theme/favicon, and three translation stores. +- [ ] Run Content tests and confirm RED before production implementations. +- [ ] Extract redirect-free domain operations from the listed actions, retain legacy wrappers, and ensure global brand/localization mutations are sensitive and audited. +- [ ] Implement the six workflow pages, content search providers, publication/attention inbox source, mandatory editorial summary, and optional media/localization widgets. +- [ ] Register real routes/commands/providers/widgets and prove Content matrix closure. +- [ ] Run Content, affected legacy tests, HK suite, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): deliver content vertical`. + +### Task 15: Deliver Economy and catalog + +**Files:** + +- Create: `src/features/housekeeping/domains/economy/routes.ts` +- Create: `src/features/housekeeping/domains/economy/routes.test.ts` +- Create: `src/features/housekeeping/domains/economy/queries/catalog.ts` +- Create: `src/features/housekeeping/domains/economy/queries/commerce.ts` +- Create: `src/features/housekeeping/domains/economy/queries/value.ts` +- Create: `src/features/housekeeping/domains/economy/queries/economy-queries.test.ts` +- Create: `src/features/housekeeping/domains/economy/commands/economy-commands.ts` +- Create: `src/features/housekeeping/domains/economy/commands/economy-commands.test.ts` +- Create: `src/features/housekeeping/domains/economy/pages/catalog.tsx` +- Create: `src/features/housekeeping/domains/economy/pages/items.tsx` +- Create: `src/features/housekeeping/domains/economy/pages/commerce.tsx` +- Create: `src/features/housekeeping/domains/economy/pages/history.tsx` +- Create: `src/features/housekeeping/domains/economy/pages/value.tsx` +- Create: `src/features/housekeeping/domains/economy/pages/rewards.tsx` +- Create: `src/features/housekeeping/domains/economy/pages/economy-pages.test.tsx` +- Create: `src/features/housekeeping/domains/economy/search.ts` +- Create: `src/features/housekeeping/domains/economy/inbox.ts` +- Create: `src/features/housekeeping/domains/economy/widgets.ts` +- Create: `src/features/housekeeping/domains/economy/route-handlers.ts` +- Create: `src/features/housekeeping/domains/economy/economy-providers.test.ts` +- Modify: `src/features/housekeeping/domains/economy/manifest.ts` +- Modify: `src/actions/catalog.ts` +- Modify: `src/actions/catalog-bc.ts` +- Modify: `src/actions/catalog-items.ts` +- Modify: `src/actions/items-base.ts` +- Modify: `src/actions/admin-marketplace.ts` +- Modify: `src/actions/admin-rare-values.ts` +- Modify: `src/actions/admin-shop.ts` +- Modify: `src/actions/admin-vouchers.ts` +- Modify: `src/actions/shop.ts` +- Modify: `src/actions/soundtracks.ts` +- Modify: `src/actions/voucher.ts` +- Modify: `src/lib/services/catalog-audit.ts` +- Modify: `src/lib/services/catalog-items-loader.ts` +- Modify: `src/lib/services/catalog-tree.ts` +- Modify: `src/lib/services/paypal.ts` +- Modify: `src/lib/services/paypal-topup.ts` +- Modify: `src/lib/services/send-currency.ts` + +**Interfaces:** + +```ts +export const ECONOMY_ROUTE_GROUPS = [ + "catalog", "items", "commerce", "history", "value", "rewards", +] as const; +export const ECONOMY_SEARCH_PROVIDER_IDS = [ + "economy.catalog-pages", "economy.items", "economy.transactions", +] as const; +``` + +- [ ] Write route tests covering catalog/detail/Builder Club/maintenance, items/detail, shop, marketplace, transactions, vouchers, subscriptions, rare values, badges, achievements, sounds, and calendar matrix rows. +- [ ] Write adapter tests for catalog trees/items, commerce history, transactions, vouchers/subscriptions, marketplace, rare values, rewards, sounds, and calendar; verify stable pagination and money/value serialization. +- [ ] Write sensitive command tests for catalog/item edits, maintenance, vouchers, shop changes, rare values, badge/reward changes, and destructive catalog operations with reason/audit requirements. +- [ ] Run Economy tests and confirm RED before implementation. +- [ ] Extract redirect-free domain services, implement six pages plus exact providers/widgets, and retain specialized catalog editors as components inside canonical workflows. +- [ ] Register the Economy manifest and prove matrix closure and non-empty route/search/inbox/widget collections. +- [ ] Run Economy, catalog/service, HK tests, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): deliver economy vertical`. + +### Task 16: Deliver Hotel rooms, radio, badges, and runtime tools + +**Files:** + +- Create: `src/features/housekeeping/domains/hotel/routes.ts` +- Create: `src/features/housekeeping/domains/hotel/routes.test.ts` +- Create: `src/features/housekeeping/domains/hotel/queries/rooms.ts` +- Create: `src/features/housekeeping/domains/hotel/queries/radio.ts` +- Create: `src/features/housekeeping/domains/hotel/queries/assets.ts` +- Create: `src/features/housekeeping/domains/hotel/queries/hotel-queries.test.ts` +- Create: `src/features/housekeeping/domains/hotel/commands/room-commands.ts` +- Create: `src/features/housekeeping/domains/hotel/commands/radio-commands.ts` +- Create: `src/features/housekeeping/domains/hotel/commands/asset-commands.ts` +- Create: `src/features/housekeeping/domains/hotel/commands/hotel-commands.test.ts` +- Create: `src/features/housekeeping/domains/hotel/pages/rooms.tsx` +- Create: `src/features/housekeeping/domains/hotel/pages/room-detail.tsx` +- Create: `src/features/housekeeping/domains/hotel/pages/room-furni.tsx` +- Create: `src/features/housekeeping/domains/hotel/pages/radio.tsx` +- Create: `src/features/housekeeping/domains/hotel/pages/badges.tsx` +- Create: `src/features/housekeeping/domains/hotel/pages/sounds.tsx` +- Create: `src/features/housekeeping/domains/hotel/pages/hotel-pages.test.tsx` +- Modify: `src/actions/rooms.ts` +- Modify: `src/actions/admin-radio-api-keys.ts` +- Modify: `src/actions/admin-radio-autodj.ts` +- Modify: `src/actions/admin-radio-extra.ts` +- Modify: `src/actions/admin-radio-moderation.ts` +- Modify: `src/actions/admin-radio-points.ts` +- Modify: `src/actions/admin-badges.ts` +- Modify: `src/actions/admin-badge-upload.ts` +- Modify: `src/lib/services/radio.ts` +- Modify: `src/lib/services/import-badge.ts` +- Modify: `src/lib/services/rcon.ts` +- Modify: `src/lib/services/soundtracks.ts` + +**Interfaces:** + +```ts +export const HOTEL_PRIMARY_ROUTE_IDS = [ + "hotel.rooms", "hotel.room-detail", "hotel.room-furni", + "hotel.radio.overview", "hotel.radio.settings", "hotel.radio.monitoring", + "hotel.radio.moderation", "hotel.radio.autodj", "hotel.radio.history", + "hotel.radio.points", "hotel.radio.ranks", "hotel.radio.api-keys", + "hotel.radio.banners", "hotel.radio.embed", "hotel.badges", "hotel.sounds", +] as const; +``` + +- [ ] Write route tests proving room show/edit aliases merge into one detail and one furni route; assert every radio/badge/sound matrix row has a canonical destination. +- [ ] Write query tests for room/owner/furni, radio configuration/monitoring/history/ranks, badges, and soundtracks; include unavailable RCON/radio dependencies. +- [ ] Write command tests for room changes, furni movement/removal, radio configuration/moderation/API keys/points, badge upload, and sounds; require reasons for destructive or external operations. +- [ ] Run Hotel tests and confirm RED. +- [ ] Extract redirect-free services, implement the six workflow page modules, and keep legacy wrappers live until cutover. +- [ ] Run Hotel primary, RCON/radio/sound, HK tests, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): deliver hotel operations`. + +### Task 17: Integrate Studio and long-running asset operations + +**Files:** + +- Create: `src/features/housekeeping/domains/hotel/queries/studio.ts` +- Create: `src/features/housekeeping/domains/hotel/queries/studio.test.ts` +- Create: `src/features/housekeeping/domains/hotel/commands/studio-commands.ts` +- Create: `src/features/housekeeping/domains/hotel/commands/studio-commands.test.ts` +- Create: `src/features/housekeeping/domains/hotel/pages/studio.tsx` +- Create: `src/features/housekeeping/domains/hotel/pages/studio.test.tsx` +- Create: `src/features/housekeeping/domains/hotel/components/operation-progress.tsx` +- Create: `src/features/housekeeping/domains/hotel/components/operation-result.tsx` +- Create: `src/features/housekeeping/domains/hotel/components/operation-progress.test.tsx` +- Create: `src/features/housekeeping/domains/hotel/search.ts` +- Create: `src/features/housekeeping/domains/hotel/inbox.ts` +- Create: `src/features/housekeeping/domains/hotel/widgets.ts` +- Create: `src/features/housekeeping/domains/hotel/route-handlers.ts` +- Create: `src/features/housekeeping/domains/hotel/hotel-providers.test.ts` +- Modify: `src/features/housekeeping/domains/hotel/manifest.ts` +- Modify: `src/actions/import-furni.ts` +- Modify: `src/actions/furni-maintenance.ts` +- Modify: `src/lib/services/clone-import.ts` +- Modify: `src/lib/services/clothing-set-import.ts` +- Modify: `src/lib/services/effect-import.ts` +- Modify: `src/lib/services/figure-import.ts` +- Modify: `src/lib/services/furni-import.ts` +- Modify: `src/lib/services/furni-maintenance.ts` +- Modify: `src/lib/services/pet-import.ts` +- Modify: `src/lib/services/repair-icons.ts` +- Modify: `src/lib/services/repair-nitros.ts` +- Modify: `src/lib/services/upload-import.ts` + +**Interfaces:** + +```ts +export type StudioOperationKind = + | "badge" | "clone" | "clothing" | "effect" | "furni" + | "maintenance" | "pet" | "repair-icons" | "sync" | "upload"; +export interface StudioOperationEvent { + operationId: string; + kind: StudioOperationKind; + phase: "queued" | "running" | "completed" | "failed" | "partial"; + completed: number; + total: number | null; + messageKey: string; + correlationId: string; +} +``` + +- [ ] Write tests for all ten Studio kinds, progress ordering, partial batch results, cancellation/abort propagation, unavailable external sources, and persisted intent before filesystem/RCON work. +- [ ] Run Studio tests and confirm RED. +- [ ] Wrap existing services with typed operations while preserving their current progress/error semantics and stable internal `/api/admin/import/*` transport paths. +- [ ] Implement the canonical `/ase/hotel/studio/*` workflow, operation progress/result components, Studio search, operational inbox source, and mandatory active-operation widget. +- [ ] Register every Hotel matrix route and prove no duplicate Studio root or orphan handler. +- [ ] Run Studio/import/service tests, Hotel tests, HK suite, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): integrate studio operations`. + +### Task 18: Implement global navigation, entity search, and command discovery + +**Files:** + +- Create: `src/features/housekeeping/foundation/search/search-service.ts` +- Create: `src/features/housekeeping/foundation/search/search-service.test.ts` +- Create: `src/features/housekeeping/foundation/search/navigation-search.ts` +- Create: `src/features/housekeeping/foundation/search/navigation-search.test.ts` +- Create: `src/actions/housekeeping-search.ts` +- Create: `src/actions/housekeeping-search.test.ts` +- Create: `src/features/housekeeping/foundation/shell/command-deck.tsx` +- Create: `src/features/housekeeping/foundation/shell/command-deck.test.tsx` +- Modify: `src/features/housekeeping/foundation/shell/command-trigger.tsx` +- Modify: `src/features/housekeeping/foundation/shell/housekeeping-shell.tsx` +- Modify: `src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx` + +**Interfaces:** + +```ts +export interface HousekeepingSearchResponse { + navigation: readonly HousekeepingNavigationHit[]; + commands: readonly HousekeepingCommandHit[]; + entities: readonly HousekeepingSearchResult[]; + errors: readonly { providerId: string; code: HousekeepingErrorCode }[]; + correlationId: string; +} +export function searchHousekeeping( + term: string, + context: HousekeepingCapabilityContext, +): Promise; +``` + +- [ ] Write tests proving trimmed terms shorter than two characters search navigation/commands only; two-character terms invoke permitted entity providers. +- [ ] Add orchestration tests for 2,000 ms/provider, 25/provider, 50 combined, stable dedupe/order, forbidden provider omission, and partial errors. +- [ ] Run search/action/deck tests and confirm RED. +- [ ] Implement registry navigation/command matching, domain entity orchestration, and a cmdk-based keyboard dialog with grouped results and canonical-to-preview href projection. +- [ ] Test open/close shortcut, focus restoration, arrow navigation, Enter activation, Escape, loading, no-results, and partial-provider UI. +- [ ] Run search, shell, provider, HK tests, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): add global command deck search`. + +### Task 19: Implement the derived operational inbox + +**Files:** + +- Create: `src/features/housekeeping/foundation/inbox/inbox-service.ts` +- Create: `src/features/housekeeping/foundation/inbox/inbox-service.test.ts` +- Create: `src/actions/housekeeping-inbox.ts` +- Create: `src/actions/housekeeping-inbox.test.ts` +- Create: `src/features/housekeeping/foundation/shell/operational-inbox.tsx` +- Create: `src/features/housekeeping/foundation/shell/operational-inbox.test.tsx` +- Create: `src/features/housekeeping/domains/system/search.ts` +- Create: `src/features/housekeeping/domains/system/inbox.ts` +- Create: `src/features/housekeeping/domains/system/widgets.ts` +- Create: `src/features/housekeeping/domains/system/system-providers.test.ts` +- Modify: `src/features/housekeeping/domains/system/manifest.ts` + +**Interfaces:** + +```ts +export interface HousekeepingInboxResponse { + items: readonly HousekeepingWorkItem[]; + errors: readonly { sourceId: string; code: HousekeepingErrorCode }[]; + correlationId: string; +} +export const INBOX_POLICY = { + timeoutMs: 2_000, + combinedLimit: 200, + dedupe: "sourceId:itemId", +} as const; +``` + +- [ ] Write tests for `(sourceId,itemId)` dedupe, capability filtering before count, priority then age ordering, 2,000 ms/source, 200-item cap, and partial-source failures. +- [ ] Run inbox action/service/component tests and confirm RED. +- [ ] Implement composition over registered People, Content, Economy, Hotel, and System sources without adding assignment/read-status persistence. +- [ ] Add System alert, emulator-error, and operational-anomaly sources plus System search/widgets that Task 10 registered conceptually. +- [ ] Implement accessible filters by domain/state/priority, canonical links, source error summaries, and empty/loading/partial states. +- [ ] Run inbox, all domain-provider, HK tests, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): compose operational inbox`. + +### Task 20: Add recent work, favorites, and widget personalization + +**Files:** + +- Create: `src/features/housekeeping/foundation/recent/recent-work.ts` +- Create: `src/features/housekeeping/foundation/recent/recent-work.test.ts` +- Create: `src/actions/housekeeping-recent.ts` +- Create: `src/actions/housekeeping-recent.test.ts` +- Create: `src/features/housekeeping/foundation/shell/recent-work.tsx` +- Create: `src/features/housekeeping/foundation/shell/favorites.tsx` +- Create: `src/features/housekeeping/foundation/shell/widget-grid.tsx` +- Create: `src/features/housekeeping/foundation/shell/widget-settings.tsx` +- Create: `src/features/housekeeping/foundation/shell/personalization.test.tsx` +- Modify: `src/features/housekeeping/foundation/registry.ts` +- Modify: `src/features/housekeeping/foundation/registry.test.ts` + +**Interfaces:** + +```ts +export interface HousekeepingRecentItem { + routeId: string; + canonicalHref: CanonicalHousekeepingHref; + labelKey: string; + occurredAt: string; + source: "route-visit" | "audit"; +} +export interface HousekeepingWidgetLoadResult { + widgets: readonly { id: string; data: unknown }[]; + errors: readonly { widgetId: string; code: HousekeepingErrorCode }[]; +} +``` + +- [ ] Write tests deriving recent work from `housekeeping.route.visit` and mutation audit rows, deduping by route ID, capability-filtering, and limiting to 12. +- [ ] Write component tests for pin/unpin, drag/keyboard ordering, mandatory widget lock, optional widget enable/disable, reconciled stale IDs, and partial widget failure. +- [ ] Run recent/personalization/action tests and confirm RED. +- [ ] Implement route-visit recording in `admin_audit_log`, recent-work queries, reconciled preference actions, dnd-kit ordering, and provider-orchestrated widget loading. +- [ ] Ensure preferences never authorize content and failed preference saves retain the previous UI state with an error announcement. +- [ ] Run preference, recent, widget, HK tests, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): personalize command deck`. + +### Task 21: Build the Operations workspace and complete shell composition + +**Files:** + +- Create: `src/features/housekeeping/domains/operations/routes.ts` +- Create: `src/features/housekeeping/domains/operations/routes.test.ts` +- Create: `src/features/housekeeping/domains/operations/queries.ts` +- Create: `src/features/housekeeping/domains/operations/queries.test.ts` +- Create: `src/features/housekeeping/domains/operations/pages/workspace.tsx` +- Create: `src/features/housekeeping/domains/operations/pages/workspace.test.tsx` +- Create: `src/features/housekeeping/domains/operations/search.ts` +- Create: `src/features/housekeeping/domains/operations/inbox.ts` +- Create: `src/features/housekeeping/domains/operations/widgets.ts` +- Create: `src/features/housekeeping/domains/operations/route-handlers.ts` +- Modify: `src/features/housekeeping/domains/operations/manifest.ts` +- Modify: `src/features/housekeeping/foundation/shell/housekeeping-shell.tsx` +- Modify: `src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx` +- Modify: `src/app/ase-next/page.tsx` + +**Interfaces:** + +```ts +export const OPERATIONS_ROUTES = [{ + id: "operations.workspace", + href: "/ase", + capability: anyCapability(PERMS.ADMIN_DASHBOARD), +}] as const; +export interface OperationsWorkspaceModel { + inbox: HousekeepingInboxResponse; + recent: readonly HousekeepingRecentItem[]; + favorites: HousekeepingPreferences; + widgets: HousekeepingWidgetLoadResult; +} +``` + +- [ ] Write route/matrix tests mapping the legacy `/admin` row to `operations.workspace` at canonical `/ase`. +- [ ] Write workspace tests for capability-specific sections, independent partial failures, no accessible domain, and preview links. +- [ ] Run Operations tests and confirm RED. +- [ ] Implement parallel inbox/recent/preferences/widget loading and render the operational home directly at `/ase-next`; the canonical cutover renders the same handler directly at `/ase`. +- [ ] Populate the Operations manifest with real route, safe navigation command, operational inbox summary, and mandatory workspace widget. +- [ ] Run Operations, shell, registry, HK tests, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `feat(housekeeping): compose operations workspace`. +### Task 22: Finish responsive behavior, accessibility, and localization + +**Files:** + +- Modify: `src/features/housekeeping/foundation/shell/housekeeping-shell.tsx` +- Modify: `src/features/housekeeping/foundation/shell/domain-rail.tsx` +- Modify: `src/features/housekeeping/foundation/shell/context-nav.tsx` +- Modify: `src/features/housekeeping/foundation/shell/operator-summary.tsx` +- Modify: `src/features/housekeeping/foundation/shell/command-deck.tsx` +- Modify: `src/features/housekeeping/foundation/shell/operational-inbox.tsx` +- Modify: `src/features/housekeeping/foundation/shell/recent-work.tsx` +- Modify: `src/features/housekeeping/foundation/shell/favorites.tsx` +- Modify: `src/features/housekeeping/foundation/shell/widget-grid.tsx` +- Modify: `src/features/housekeeping/foundation/shell/widget-settings.tsx` +- Create: `src/features/housekeeping/foundation/shell/accessibility.test.tsx` +- Create: `src/features/housekeeping/foundation/shell/responsive-contract.test.tsx` +- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-shell.tsx` +- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-state.tsx` +- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx` +- Modify: `src/features/housekeeping/foundation/localization-contract.test.ts` +- Modify: `src/messages/ar.json` +- Modify: `src/messages/bg.json` +- Modify: `src/messages/cs.json` +- Modify: `src/messages/da.json` +- Modify: `src/messages/de.json` +- Modify: `src/messages/el.json` +- Modify: `src/messages/en.json` +- Modify: `src/messages/es.json` +- Modify: `src/messages/fi.json` +- Modify: `src/messages/fr.json` +- Modify: `src/messages/hr.json` +- Modify: `src/messages/hu.json` +- Modify: `src/messages/it.json` +- Modify: `src/messages/ja.json` +- Modify: `src/messages/nl.json` +- Modify: `src/messages/no.json` +- Modify: `src/messages/pl.json` +- Modify: `src/messages/pt.json` +- Modify: `src/messages/ro.json` +- Modify: `src/messages/ru.json` +- Modify: `src/messages/sk.json` +- Modify: `src/messages/sr.json` +- Modify: `src/messages/sv.json` +- Modify: `src/messages/tr.json` +- Modify: `src/messages/uk.json` +- Modify: `src/app/globals.css` + +**Interfaces:** + +```ts +export const HOUSEKEEPING_LANDMARKS = [ + "banner", "primary-navigation", "context-navigation", "main", +] as const; +export type HousekeepingPageState = + | "loading" | "empty" | "partial" | "error" | "forbidden" | "ready"; +``` + +- [ ] Write failing tests for one active nav item, landmark labels, heading hierarchy, skip link, focus-visible behavior, keyboard-reorder instructions, live error announcements, reduced-motion classes, and page-state semantics. +- [ ] Write responsive contracts for rail/context navigation collapse below `lg`, usable command deck at 320 px, non-overflowing tables/cards, and unchanged capabilities between mobile/desktop. +- [ ] Extend localization contract to collect every manifest/route/command/widget/state key and assert a non-empty value in all 25 locale files. +- [ ] Run accessibility/responsive/localization tests and confirm RED. +- [ ] Implement semantic shell behavior and complete every locale subtree with native-language operator copy; do not expose untranslated keys or preview wording after cutover. +- [ ] Run shell/page/localization tests, HK suite, and `pnpm typecheck`. +- [ ] Run targeted Biome, `git diff --check`, stage exact UI/locale files, and commit `feat(housekeeping): finish accessible command deck`. + +### Task 23: Close the 137-row matrix against the runtime registry + +**Files:** + +- Create: `src/features/housekeeping/cutover/parity.ts` +- Create: `src/features/housekeeping/cutover/parity.test.ts` +- Create: `src/features/housekeeping/cutover/source-boundaries.test.ts` +- Modify: `src/features/housekeeping/migration/operations.ts` +- Modify: `src/features/housekeeping/migration/people.ts` +- Modify: `src/features/housekeeping/migration/content.ts` +- Modify: `src/features/housekeeping/migration/economy.ts` +- Modify: `src/features/housekeeping/migration/hotel.ts` +- Modify: `src/features/housekeeping/migration/system.ts` +- Modify: `scripts/verify-housekeeping-matrix.ts` +- Modify: `package.json` + +**Interfaces:** + +```ts +export interface HousekeepingParityReport { + discovered: 137; + mapped: 137; + verified: 137; + removed: number; + unresolved: readonly string[]; + capabilityGaps: readonly string[]; + handlerGaps: readonly string[]; +} +export function verifyHousekeepingRuntimeParity( + matrix: readonly MigrationEntry[], + registry: HousekeepingRegistry, + handlers: readonly HousekeepingRouteHandler[], +): HousekeepingParityReport; +``` + +- [ ] Write a failing aggregate test requiring exactly 137 discovered/mapped/verified rows, zero unresolved/capability/handler gaps, every retained target under `/ase`, and every removed row with no handler. +- [ ] Write source-boundary tests forbidding foundation imports from domain internals/database/actions and forbidding one domain from another domain's internals. +- [ ] Run parity/boundary tests and confirm RED. +- [ ] Add concrete `parityEvidence` test IDs to each matrix row and change retained rows to `VERIFIED`, removed rows to `REMOVED` only after their evidence passes. +- [ ] Extend `hk:matrix:check` output with the runtime parity counts and make non-137 or any gap exit non-zero. +- [ ] Run `pnpm hk:matrix:check`, `pnpm test:housekeeping`, and `pnpm typecheck`. +- [ ] Run Biome, `git diff --check`, stage exact files, and commit `test(housekeeping): prove 137 route parity`. + +### Task 24: Pass cumulative pre-cutover verification + +**Files:** + +- Create: `docs/superpowers/evidence/2026-08-26-housekeeping-pre-cutover.md` +- Inspect: every file changed by Tasks 1-23; source failures return to their owning task and commit before this evidence-only task continues + +**Interfaces:** + +The evidence document records command, exit code, test count, date/time, environment limitations, visual viewport, route, actor capability fixture, and observed result. It never reports unavailable live services as passing. + +- [ ] Run `pnpm toolchain:check`, `pnpm hk:matrix:check`, `pnpm test:housekeeping`, `pnpm test`, and `pnpm typecheck`; record fresh output and fix only Housekeeping-caused failures with a RED/GREEN test. +- [ ] Run semantic Biome on all changed JS/TS/JSON with formatter disabled, run targeted formatter only on changed files, then run `git diff --check`. +- [ ] Run `pnpm build` with the repository's required temporary environment values; restore every environment file/value afterward and record restoration. +- [ ] Start the preview in non-production mode and verify `/ase-next` at 1440x900, 1024x768, 390x844, and 320x568 for all six domains plus loading, empty, partial, error, and forbidden states. +- [ ] Smoke permitted/denied access, safe/sensitive commands, provider timeout isolation, preference persistence/reconciliation, and all long-running Studio states. +- [ ] Record evidence, run `git diff --check`, stage only the evidence and verified fixes, and commit `test(housekeeping): record pre-cutover verification`. + +### Task 25: Perform the atomic `/ase` cutover + +**Files:** + +- Move: `src/app/ase-next` to `src/app/ase` +- Delete: `src/app/admin` +- Delete: `src/app/mod` +- Modify: `src/features/housekeeping/foundation/routing/href.ts` +- Modify: `src/features/housekeeping/foundation/preview-gate.ts` +- Modify: `src/features/housekeeping/foundation/preview-gate.test.ts` +- Modify: `src/features/housekeeping/foundation/preview-route-contract.test.ts` +- Create: `src/features/housekeeping/cutover/route-cutover.test.ts` +- Modify: `src/lib/admin/guard.ts` +- Modify: `src/lib/admin/guard.test.ts` +- Modify: `src/proxy.ts` +- Modify: `src/components/navigation.tsx` +- Modify: `src/components/top-header.tsx` +- Modify: `src/components/admin/admin-breadcrumb.tsx` +- Modify: `src/lib/admin-theme-source-audit.test.ts` +- Modify: `src/features/housekeeping/foundation/foundation-source-contract.test.ts` +- Modify: `src/env.ts` +- Modify: `.env.example` + +**Interfaces:** + +```ts +export const HOUSEKEEPING_ROOT = "/ase" as const; +// No runtime preview surface remains after cutover. +``` + +- [ ] Write the route-cutover test first: `src/app/ase/layout.tsx` and canonical dispatcher must exist; `src/app/admin`, `src/app/admin-next`, `src/app/ase-next`, and `src/app/mod` must not exist; proxy/global navigation/fallbacks must target `/ase`; no redirect maps removed UI paths. +- [ ] Run the cutover test and confirm RED before moving/removing route trees. +- [ ] Move the completed preview tree to `/ase`, remove preview-only badge/gate/flag behavior, and make all navigation/search/inbox/widget links canonical without preview projection. +- [ ] Remove legacy UI trees, rewrite global navigation and authorization fallbacks, and keep `/api/admin/*` internal endpoints unchanged because they are not UI compatibility routes. +- [ ] Remove imports/tests tied to deleted page modules; retain shared services/components only when the new domains import them without legacy route coupling. +- [ ] Run cutover, proxy/auth, source-boundary, theme, matrix, HK, full tests, and `pnpm typecheck`. +- [ ] Run Biome on changed files, `git diff --check`, stage the entire exact cutover set, and commit `feat(housekeeping): cut over administration to ase`. + +### Task 26: Run final review and release-quality verification + +**Files:** + +- Create: `docs/superpowers/evidence/2026-08-26-housekeeping-final.md` +- Inspect: every file changed on `origin/main...HEAD`; confirmed findings return to their owning task and commit before this evidence-only task continues + +**Interfaces:** + +Final acceptance requires a clean `git diff --check`, 137/137 runtime parity, production build success, no legacy UI route tree, no compatibility redirect, and recorded desktop/mobile evidence for `/ase`. + +- [ ] Review `git diff --stat origin/main...HEAD`, every commit, and the complete diff for authorization bypass, client-trusted capability, missing audit, leaked secret, unsafe external/file mutation, cross-domain import, dead legacy route, and unrelated churn. +- [ ] Invoke `superpowers:requesting-code-review`; classify each finding by evidence and fix confirmed findings in one reviewed wave using a failing regression test first. +- [ ] Re-run `pnpm toolchain:check`, `pnpm hk:matrix:check`, `pnpm test:housekeeping`, `pnpm test`, `pnpm typecheck`, semantic Biome, targeted formatting, and `git diff --check` from a clean process. +- [ ] Re-run `pnpm build` with temporary environment restoration and repeat canonical `/ase` route/access/command visual smoke at 1440x900 and 390x844. +- [ ] Verify direct requests to `/admin`, `/admin-next`, `/ase-next`, `/mod`, and removed routes are unreachable and not redirected; verify `/api/health` locally only if its dependencies are available. +- [ ] Record exact final evidence and residual pre-existing repository debt, stage only the evidence/fix wave, and commit `test(housekeeping): finalize release evidence`. +- [ ] Stop before network mutation. Present branch status, commits, checks, and evidence to the user; push and open the one final PR only after an explicit instruction. + +## Post-merge release gate + +After the final PR is explicitly authorized, pushed, reviewed, and merged, wait for the deployment pipeline to finish and verify the live `/api/health` response plus authenticated `/ase` access. A failed migration or health check blocks the release. Rollback deploys the prior application release; the additive `0023` schema remains compatible and is not destructively reversed. diff --git a/docs/superpowers/plans/2026-08-30-housekeeping-foundation-routing-recovery.md b/docs/superpowers/plans/2026-08-30-housekeeping-foundation-routing-recovery.md new file mode 100644 index 00000000..2611fdf7 --- /dev/null +++ b/docs/superpowers/plans/2026-08-30-housekeeping-foundation-routing-recovery.md @@ -0,0 +1,902 @@ +# Housekeeping Foundation and Routing Recovery Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restore a non-production `/ase-next` Housekeeping foundation whose generated navigation, concrete routes, handlers, capability checks, and HTTP access semantics cannot produce the orphaned domain links that caused the reverted cutover to return 404. + +**Architecture:** Keep `/admin` and `/mod` unchanged while replacing the old `/admin-next` preview namespace with a gated `/ase-next` surface. Separate static manifest validation from a runtime route-handler registry, project navigation only from accessible handled routes, and dispatch all preview pages through one deterministic matcher with explicit login, 403, and 404 behavior. This plan intentionally stops before People content; the People vertical receives its own implementation plan after this foundation passes review. + +**Tech Stack:** Next.js 16.3.3 App Router, React 19.2.8 server components, TypeScript 7.0.2, Vitest 4.1.11, next-intl, Biome 2.5.9, pnpm 11.24.0, Node.js 26.8.1. + +**Spec:** `docs/superpowers/specs/2026-08-30-housekeeping-stepwise-rebuild-design.md` + +## Global Constraints + +- Work only in the canonical checkout on `codex/housekeeping-rebuild-stepwise`; do not use Git worktrees. +- Keep `/admin` and `/mod` functional and unchanged throughout this plan. +- Expose the rebuild only at `/ase-next`; do not create `/ase` or alter production administration links. +- Keep `HOUSEKEEPING_NEXT_PREVIEW_ENABLED` defaulting to `false`, and keep the preview unavailable in `NODE_ENV=production`. +- Recover no page or service from `codex/housekeeping-complete` unless a task names it explicitly and first proves the behavior with a failing test. This plan names no such recovery. +- Generate navigation only for routes with a registered handler and satisfied domain/route capabilities. +- Use `forbidden()` for authenticated capability denial and `notFound()` only for unknown domains, paths, or entities. +- Use real workflow-specific content in later verticals; this foundation must not add placeholder dashboards or generic forms. +- Preserve the untracked `.remember/` directory and stage only paths named by the active task. +- Before every Node or pnpm command, select the required runtime: + +```powershell +$nodeDir = Join-Path (Join-Path $env:TEMP 'codex-node-v26.8.1') 'node-v26.8.1-win-x64' +if (-not (Test-Path -LiteralPath (Join-Path $nodeDir 'node.exe'))) { + throw 'Node 26.8.1 portable runtime not found' +} +$env:PATH = "$nodeDir;$env:PATH" +node --version +``` + +--- + +### Task 1: Rename the gated preview namespace to `/ase-next` + +**Files:** +- Create: `src/features/housekeeping/foundation/preview-namespace.test.ts` +- Move: `src/app/admin-next/layout.tsx` to `src/app/ase-next/layout.tsx` +- Move: `src/app/admin-next/page.tsx` to `src/app/ase-next/page.tsx` +- Move: `src/app/admin-next/[domain]/layout.tsx` to `src/app/ase-next/[domain]/layout.tsx` +- Move: `src/app/admin-next/[domain]/page.tsx` to `src/app/ase-next/[domain]/page.tsx` +- Modify: `src/features/housekeeping/foundation/contracts/domain.ts` +- Modify: `src/features/housekeeping/foundation/registry.ts` +- Modify: all six `src/features/housekeeping/domains/*/manifest.ts` files +- Modify: `src/features/housekeeping/foundation/contracts/contracts.test.ts` +- Modify: `src/features/housekeeping/foundation/foundation-source-contract.test.ts` +- Modify: `src/features/housekeeping/foundation/navigation.test.ts` +- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx` +- Modify: `src/features/housekeeping/foundation/preview-route-contract.test.ts` +- Modify: `src/features/housekeeping/foundation/registry.test.ts` +- Modify: `src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx` +- Modify: `src/lib/admin-theme-source-audit.test.ts` + +**Interfaces:** +- Consumes: existing `isHousekeepingPreviewEnabled()` and `HOUSEKEEPING_NEXT_PREVIEW_ENABLED` environment contract. +- Produces: preview source files and manifest hrefs that use only `/ase-next`; later tasks consume the new namespace without compatibility aliases. + +- [ ] **Step 1: Write the failing namespace contract** + +Create `preview-namespace.test.ts` with a tracked-source scan that ignores historical documentation and rejects the old runtime namespace: + +```ts +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +describe("Housekeeping preview namespace", () => { + it("uses /ase-next and removes /admin-next from runtime sources", () => { + const files = execFileSync("git", ["ls-files", "src", ".env.example"], { + encoding: "utf8", + }) + .trim() + .split(/\r?\n/) + .filter(Boolean); + + const offenders = files.filter((file) => + readFileSync(file, "utf8").includes("/admin-next"), + ); + + expect(offenders).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run the namespace test and verify RED** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-namespace.test.ts +``` + +Expected: FAIL listing the existing `/admin-next` route, manifest, and test files. + +- [ ] **Step 3: Move the route tree and replace runtime/test hrefs** + +Run the four `git mv` operations, then change the manifest type and registry invariant to the exact new namespace: + +```ts +export interface HousekeepingDomainManifest { + id: HousekeepingDomainId; + labelKey: string; + descriptionKey: string; + iconId: "inbox" | "users" | "file-text" | "gem" | "hotel" | "settings"; + previewHref: `/ase-next/${HousekeepingDomainId}`; + capability: CapabilityRequirement; + routes: readonly HousekeepingRouteDefinition[]; + searchProviders: readonly HousekeepingSearchProvider[]; + inboxSources: readonly HousekeepingInboxSource[]; + widgets: readonly HousekeepingWidgetDefinition[]; +} +``` + +```ts +if (manifest.previewHref !== `/ase-next/${manifest.id}`) { + throw new Error(`invalid preview href: ${manifest.previewHref}`); +} +``` + +Replace `/admin-next` with `/ase-next` in the six manifests and in the named tests. Update imports from `@/app/admin-next/...` to `@/app/ase-next/...`. Do not rename the environment flag in this task. + +- [ ] **Step 4: Verify GREEN and the unchanged preview gate** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-namespace.test.ts src/features/housekeeping/foundation/preview-gate.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/navigation.test.ts +``` + +Expected: all selected tests PASS and the production preview-gate cases remain denied. + +- [ ] **Step 5: Check the exact diff and commit** + +Run: + +```powershell +git diff --check +git status --short +git add -A -- src/app/admin-next src/app/ase-next +git add -- src/features/housekeeping/foundation/preview-namespace.test.ts src/features/housekeeping/foundation/contracts/domain.ts src/features/housekeeping/foundation/contracts/contracts.test.ts src/features/housekeeping/foundation/registry.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/foundation-source-contract.test.ts src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx src/features/housekeeping/domains src/lib/admin-theme-source-audit.test.ts +git commit -m "refactor(housekeeping): restore ase preview namespace" +``` + +Expected: `.remember/` remains untracked and is not staged. + +--- + +### Task 2: Add deterministic route matching and handler-runtime validation + +**Files:** +- Create: `src/features/housekeeping/foundation/routing/route-handler.ts` +- Create: `src/features/housekeeping/foundation/routing/match-route.ts` +- Create: `src/features/housekeeping/foundation/routing/match-route.test.ts` +- Create: `src/features/housekeeping/foundation/routing/runtime.ts` +- Create: `src/features/housekeeping/foundation/routing/runtime.test.ts` +- Modify: `src/features/housekeeping/foundation/contracts/domain.ts` +- Modify: `src/features/housekeeping/foundation/contracts/index.ts` +- Modify: `src/features/housekeeping/foundation/registry.ts` +- Modify: `src/features/housekeeping/foundation/registry.test.ts` +- Modify: all six `src/features/housekeeping/domains/*/manifest.ts` files + +**Interfaces:** +- Consumes: `HousekeepingRegistry`, `HousekeepingCapabilityContext`, and `HousekeepingDomainManifest`. +- Produces: `HousekeepingPreviewHref`, `HousekeepingRouteMatch`, `HousekeepingRouteHandler`, `HousekeepingRouteRuntime`, `createHousekeepingRouteRuntime()`, and `matchHousekeepingRoute()`. + +- [ ] **Step 1: Write failing route-matcher tests** + +Create cases that require exact, dynamic, and rejected matches: + +```ts +it("matches concrete and dynamic preview routes", () => { + expect(runtime.match("/ase-next/people/users")).toMatchObject({ + routeId: "people.users", + domain: "people", + params: {}, + }); + expect(runtime.match("/ase-next/people/users/42")).toMatchObject({ + routeId: "people.user-detail", + domain: "people", + params: { id: "42" }, + }); +}); + +it.each([ + "/ase-next/people", + "/ase-next/people/unknown", + "/ase-next/people/users/", + "/ase-next/people/users?rank=7", + "/ase-next/people/users/%2F", +])("rejects an unregistered canonical path: %s", (pathname) => { + expect(runtime.match(pathname)).toBeNull(); +}); +``` + +- [ ] **Step 2: Run matcher tests and verify RED** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/routing/match-route.test.ts +``` + +Expected: FAIL because the routing modules and runtime do not exist. + +- [ ] **Step 3: Add the concrete route and handler contracts** + +Add these public contracts: + +```ts +export type HousekeepingPreviewHref = `/ase-next${"" | `/${string}`}`; + +export interface HousekeepingRouteDefinition { + id: string; + labelKey: string; + href: HousekeepingPreviewHref; + capability: CapabilityRequirement; + matchPrefixes?: readonly string[]; +} + +export interface HousekeepingDomainManifest { + // existing fields remain + landingRouteId: string | null; +} +``` + +```ts +import type { ReactNode } from "react"; +import type { HousekeepingCapabilityContext } from "../contracts"; + +export interface HousekeepingRouteMatch { + routeId: string; + domain: HousekeepingDomainId; + params: Readonly>; + canonicalHref: HousekeepingPreviewHref; +} + +export interface HousekeepingRouteRenderInput { + context: HousekeepingCapabilityContext; + match: HousekeepingRouteMatch; + searchParams?: Readonly>; + translate: (key: string) => string; +} + +export interface HousekeepingRouteHandler { + routeId: string; + render(input: HousekeepingRouteRenderInput): Promise; +} +``` + +All six current empty manifests set `landingRouteId: null`. Registry validation permits `null` only while `routes` is empty; once routes exist, it requires a landing ID owned by that manifest. + +- [ ] **Step 4: Implement deterministic matching** + +Implement `matchHousekeepingRoute()` by parsing canonical path segments, sorting literal candidates ahead of dynamic `:parameter` candidates, requiring an exact segment count, decoding each segment once, and rejecting query strings, fragments, backslashes, empty segments, trailing slashes, `.`/`..`, and decoded slashes. + +The exported signature is: + +```ts +export function matchHousekeepingRoute( + registry: HousekeepingRegistry, + canonicalPath: string, +): HousekeepingRouteMatch | null; +``` + +- [ ] **Step 5: Write failing runtime-bijection tests** + +Add tests with a two-route manifest and assert these failures separately: + +```ts +expect(() => createHousekeepingRouteRuntime(registry, [])).toThrow( + "missing route handler: people.users", +); + +expect(() => + createHousekeepingRouteRuntime(registry, [ + handler("people.users"), + handler("people.users"), + ]), +).toThrow("duplicate route handler: people.users"); + +expect(() => + createHousekeepingRouteRuntime(registry, [handler("people.unknown")]), +).toThrow("handler without route: people.unknown"); +``` + +- [ ] **Step 6: Implement and verify the runtime registry** + +Implement this public shape: + +```ts +export interface HousekeepingRouteRuntime { + registry: HousekeepingRegistry; + handlers: ReadonlyMap; + match(pathname: string): HousekeepingRouteMatch | null; +} + +export function createHousekeepingRouteRuntime( + registry: HousekeepingRegistry, + handlers: readonly HousekeepingRouteHandler[], +): HousekeepingRouteRuntime; +``` + +The constructor rejects duplicate handlers, missing handlers for declared routes, and handlers without declared routes. Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/routing/match-route.test.ts src/features/housekeeping/foundation/routing/runtime.test.ts src/features/housekeeping/foundation/registry.test.ts +``` + +Expected: all selected tests PASS. + +- [ ] **Step 7: Commit the routing runtime** + +Run: + +```powershell +git diff --check +git add src/features/housekeeping/foundation/contracts src/features/housekeeping/foundation/registry.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/routing src/features/housekeeping/domains +git commit -m "feat(housekeeping): validate preview route runtime" +``` + +--- + +### Task 3: Project navigation only from accessible handled routes + +**Files:** +- Modify: `src/features/housekeeping/foundation/navigation.ts` +- Modify: `src/features/housekeeping/foundation/navigation.test.ts` +- Modify: `src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx` + +**Interfaces:** +- Consumes: `HousekeepingRouteRuntime`, handler-backed route definitions, and `HousekeepingCapabilityContext`. +- Produces: `buildHousekeepingNavigation(runtime, context, translate)` whose domain `href` is always a concrete route and whose items are all resolvable. + +- [ ] **Step 1: Write failing navigation reachability tests** + +Add these behaviors to `navigation.test.ts`: + +```ts +it("links a domain to its accessible handled landing route", () => { + const navigation = buildHousekeepingNavigation( + runtimeWithPeopleRoutes, + contextWith(PERMS.USERS_VIEW), + identityTranslate, + ); + + expect(navigation).toEqual([ + expect.objectContaining({ + id: "people", + href: "/ase-next/people/users", + items: [ + expect.objectContaining({ + id: "people.users", + href: "/ase-next/people/users", + }), + ], + }), + ]); +}); + +it("falls back to the first accessible handled route", () => { + const navigation = buildHousekeepingNavigation( + runtimeWithPreferredUsersAndTicketFallback, + contextWith(PERMS.TICKETS_VIEW), + identityTranslate, + ); + expect(navigation[0]?.href).toBe("/ase-next/people/support/tickets"); +}); + +it("omits domains with no accessible handled routes", () => { + expect( + buildHousekeepingNavigation(runtimeWithNoPeopleHandlers, moderator, identityTranslate), + ).toEqual([]); +}); +``` + +- [ ] **Step 2: Run navigation tests and verify RED** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/navigation.test.ts +``` + +Expected: FAIL because the current function consumes a static registry, links to `previewHref`, and retains empty domains. + +- [ ] **Step 3: Implement the navigation projection** + +Change the signature and projection: + +```ts +export function buildHousekeepingNavigation( + runtime: HousekeepingRouteRuntime, + context: HousekeepingCapabilityContext, + translate: (key: string) => string, +): readonly HousekeepingNavigationDomain[] { + return runtime.registry.domains.flatMap((domain) => { + if (!satisfiesCapability(context, domain.capability)) return []; + + const items = domain.routes + .filter( + (route) => + runtime.handlers.has(route.id) && + satisfiesCapability(context, route.capability), + ) + .map((route) => ({ + id: route.id, + href: route.href, + label: translate(route.labelKey), + })); + + if (items.length === 0) return []; + const landing = + items.find((item) => item.id === domain.landingRouteId) ?? items[0]; + if (!landing) return []; + + return [{ + id: domain.id, + href: landing.href, + iconId: domain.iconId, + label: translate(domain.labelKey), + description: translate(domain.descriptionKey), + items, + }]; + }); +} +``` + +- [ ] **Step 4: Verify route reachability for every projected link** + +Add one property-style loop over representative capability contexts: + +```ts +for (const context of capabilityProfiles) { + for (const domain of buildHousekeepingNavigation(runtime, context, identityTranslate)) { + expect(runtime.match(domain.href), domain.href).not.toBeNull(); + for (const item of domain.items) { + expect(runtime.match(item.href), item.href).not.toBeNull(); + } + } +} +``` + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx +``` + +Expected: PASS. + +- [ ] **Step 5: Commit the navigation invariant** + +Run: + +```powershell +git diff --check +git add src/features/housekeeping/foundation/navigation.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx +git commit -m "fix(housekeeping): link only reachable preview routes" +``` + +--- + +### Task 4: Dispatch `/ase-next` with distinct 403 and 404 behavior + +**Files:** +- Create: `src/features/housekeeping/route-handlers.ts` +- Create: `src/app/ase-next/[domain]/[[...segments]]/page.tsx` +- Create: `src/app/ase-next/[domain]/[[...segments]]/loading.tsx` +- Create: `src/app/ase-next/forbidden.tsx` +- Delete: `src/app/ase-next/[domain]/page.tsx` +- Modify: `src/app/ase-next/page.tsx` +- Modify: `src/app/ase-next/[domain]/layout.tsx` +- Modify: `src/features/housekeeping/foundation/preview-route-contract.test.ts` +- Modify: `src/features/housekeeping/foundation/foundation-source-contract.test.ts` +- Modify: `next.config.ts` + +**Interfaces:** +- Consumes: `createHousekeepingRouteRuntime()`, `buildHousekeepingNavigation()`, `getHousekeepingCapabilityContext()`, and the six manifests. +- Produces: an application dispatcher for exact handled routes, capability-aware bare-domain redirects, and Next.js HTTP access fallbacks. + +- [ ] **Step 1: Write failing app-route tests for the original 404 regression** + +Update route mocks to provide manifests plus handlers, then add: + +```ts +it("redirects a bare domain to its accessible handled landing page", async () => { + routeMocks.getHousekeepingCapabilityContext.mockResolvedValue( + capabilityContext([PERMS.USERS_VIEW]), + ); + + await expect( + HousekeepingDomainPage({ + params: Promise.resolve({ domain: "people", segments: [] }), + }), + ).rejects.toThrow("NEXT_REDIRECT:/ase-next/people/users"); +}); + +it("renders a known permitted handled route", async () => { + const html = await renderRoute( + HousekeepingDomainPage({ + params: Promise.resolve({ domain: "people", segments: ["users"] }), + }), + ); + expect(html).toContain("Rendered people.users"); +}); + +it("returns forbidden for a known route without capability", async () => { + await expect( + HousekeepingDomainPage({ + params: Promise.resolve({ domain: "people", segments: ["users"] }), + }), + ).rejects.toThrow("NEXT_FORBIDDEN"); +}); + +it("returns not found for an unknown path", async () => { + await expect( + HousekeepingDomainPage({ + params: Promise.resolve({ domain: "people", segments: ["missing"] }), + }), + ).rejects.toThrow("NEXT_NOT_FOUND"); +}); +``` + +- [ ] **Step 2: Run route tests and verify RED** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-route-contract.test.ts +``` + +Expected: FAIL because the existing domain page has no catch-all dispatch, handler runtime, redirect, or forbidden boundary. + +- [ ] **Step 3: Enable supported Next.js auth interrupts** + +Add the installed Next.js 16.3.3 option without changing other experimental flags: + +```ts +experimental: { + authInterrupts: true, + optimizePackageImports: ["lucide-react", "date-fns"], + useTypeScriptCli: true, + hideLogsAfterAbort: true, +}, +``` + +Create `src/app/ase-next/forbidden.tsx` as a localized, accessible 403 page with one link back to `/` and no privileged data. + +- [ ] **Step 4: Create the handler registry and catch-all dispatcher** + +The initial application registry is intentionally empty until People supplies real routes: + +```ts +import type { HousekeepingRouteHandler } from "./foundation/routing/route-handler"; + +export const HOUSEKEEPING_ROUTE_HANDLERS = + [] as const satisfies readonly HousekeepingRouteHandler[]; +``` + +In the catch-all page: + +1. create the static registry and runtime; +2. reject an unknown domain with `notFound()` before loading capability context; +3. load the request-scoped capability context; +4. build accessible navigation; +5. redirect an empty suffix to that domain's projected landing href; +6. match a non-empty canonical path; +7. call `notFound()` when no route/handler exists; +8. call `forbidden()` when domain or route capability is absent; +9. render the handler with context, match, translations, and awaited search params. + +Use this canonical path construction: + +```ts +const suffix = segments.map((segment) => encodeURIComponent(segment)).join("/"); +const canonicalPath = suffix + ? `${activeDomain.previewHref}/${suffix}` + : activeDomain.previewHref; +``` + +- [ ] **Step 5: Make root and layout consume the runtime projection** + +`/ase-next` redirects to `navigation[0].href`, not a domain namespace. If no accessible handled route exists, call `forbidden()`. The domain layout calls `notFound()` for an unknown domain and `forbidden()` for a known inaccessible domain, then renders the shell from the same runtime projection. + +- [ ] **Step 6: Verify app-route semantics** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/preview-gate.test.ts +``` + +Expected: PASS for bare-domain redirect, concrete render, true forbidden, true missing path, request-context reuse, and production gate denial. + +- [ ] **Step 7: Commit dispatcher and access semantics** + +Run: + +```powershell +git diff --check +git add next.config.ts src/features/housekeeping/route-handlers.ts src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/foundation-source-contract.test.ts +git add -A -- src/app/ase-next +git commit -m "feat(housekeeping): dispatch gated preview routes" +``` + +--- + +### Task 5: Complete shared loading, access, missing, and unexpected-error states + +**Files:** +- Create: `src/app/ase-next/error.tsx` +- Create: `src/app/ase-next/loading.tsx` +- Create: `src/app/ase-next/not-found.tsx` +- Modify: `src/app/ase-next/forbidden.tsx` +- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-state.tsx` +- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx` +- Modify: `src/features/housekeeping/foundation/localization-contract.test.ts` +- Modify: `src/messages/en.json` +- Modify: `src/messages/it.json` +- Modify: `src/messages/nl.json` + +**Interfaces:** +- Consumes: App Router error/access conventions and the existing semantic admin color tokens. +- Produces: localized state surfaces for `loading`, `empty`, `partial`, `validation`, `conflict`, `dependency`, `forbidden`, `not-found`, `error`, and `success` semantics. + +- [ ] **Step 1: Write failing page-state and localization tests** + +Extend the state union test matrix: + +```ts +it.each([ + ["loading", "status"], + ["empty", "status"], + ["partial", "status"], + ["conflict", "alert"], + ["dependency", "alert"], + ["error", "alert"], + ["success", "status"], +] as const)("renders %s with the expected live role", (state, role) => { + const html = renderState(state); + expect(html).toContain(`role="${role}"`); +}); +``` + +Require these English, Italian, and Dutch keys under `pages.housekeeping.states`: `loading`, `empty`, `partial`, `conflict`, `dependency`, `forbidden`, `notFound`, `error`, `success`, `retry`, and `backToSite`. + +- [ ] **Step 2: Run state tests and verify RED** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx src/features/housekeeping/foundation/localization-contract.test.ts +``` + +Expected: FAIL on the new state union and missing translation keys. + +- [ ] **Step 3: Implement state semantics and three locale sources** + +Use explicit tones rather than deriving every non-error as neutral: + +```ts +type HousekeepingPageState = + | "loading" + | "empty" + | "partial" + | "conflict" + | "dependency" + | "error" + | "success"; + +const ALERT_STATES = new Set([ + "conflict", + "dependency", + "error", +]); +``` + +Add complete operator-facing English, Italian, and Dutch messages. Other configured locales continue using the established English fallback and must never render raw keys. + +- [ ] **Step 4: Implement App Router fallback files** + +- `loading.tsx` renders the shared loading state. +- `not-found.tsx` renders an actual missing-route message and links to `/ase-next` only when preview is accessible. +- `forbidden.tsx` explains insufficient access without implying that the page is missing. +- `error.tsx` is a client component that shows `error.digest` as the support reference when present and invokes `reset()` from a localized retry button. + +Do not expose stack traces, exception messages, permission slugs, or database details. + +- [ ] **Step 5: Verify states and route boundaries** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx src/features/housekeeping/foundation/localization-contract.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts +``` + +Expected: PASS. + +- [ ] **Step 6: Commit shared state behavior** + +Run: + +```powershell +git diff --check +git add src/app/ase-next src/features/housekeeping/foundation/page src/features/housekeeping/foundation/localization-contract.test.ts src/messages/en.json src/messages/it.json src/messages/nl.json +git commit -m "feat(housekeeping): distinguish preview page states" +``` + +--- + +### Task 6: Lock preview isolation and run the full foundation gate + +**Files:** +- Create: `src/features/housekeeping/foundation/cutover-isolation.test.ts` +- Modify only if a test exposes a defect: files already named in Tasks 1-5 + +**Interfaces:** +- Consumes: the completed `/ase-next` foundation. +- Produces: an automated boundary proving that the stable surfaces remain present and the final `/ase` cutover is absent. + +- [ ] **Step 1: Write the isolation contract** + +Create: + +```ts +import { existsSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +describe("Housekeeping stepwise isolation", () => { + it("keeps legacy administration while exposing only the gated preview", () => { + expect(existsSync("src/app/admin/layout.tsx")).toBe(true); + expect(existsSync("src/app/mod/layout.tsx")).toBe(true); + expect(existsSync("src/app/ase-next/layout.tsx")).toBe(true); + expect(existsSync("src/app/admin-next/layout.tsx")).toBe(false); + expect(existsSync("src/app/ase/page.tsx")).toBe(false); + }); +}); +``` + +- [ ] **Step 2: Run the isolation and Housekeeping suites** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/cutover-isolation.test.ts +pnpm.cmd test:housekeeping +``` + +Expected: both commands PASS. If a failure occurs, fix only the owning foundation behavior and rerun its focused RED/GREEN test before rerunning the gate. + +- [ ] **Step 3: Run repository verification** + +Run: + +```powershell +pnpm.cmd typecheck +pnpm.cmd test +pnpm.cmd build +git diff --check +``` + +Expected: typecheck, all tests, production build, and whitespace validation PASS under Node 26.8.1. The production build must include the route tree while the runtime preview gate remains closed in production. + +- [ ] **Step 4: Inspect the final branch boundary** + +Run: + +```powershell +git diff --stat origin/main...HEAD +git diff --name-status origin/main...HEAD +git grep -n -I '/admin-next' -- src .env.example +git status --short --branch +``` + +Expected: no runtime `/admin-next` matches; no `/ase` cutover files; `/admin` and `/mod` are not deleted; `.remember/` is the only unrelated untracked path. + +- [ ] **Step 5: Commit the isolation gate** + +Run: + +```powershell +git add src/features/housekeeping/foundation/cutover-isolation.test.ts +git commit -m "test(housekeeping): lock stepwise preview isolation" +``` + +--- + +### Task 7: Publish the verified foundation as a draft pull request + +**Files:** +- No source changes. +- PR title: `Rebuild Housekeeping foundation and preview routing` +- PR body language order: English first, Dutch second. + +**Interfaces:** +- Consumes: a clean verified branch from Tasks 1-6 plus the committed design and this plan. +- Produces: remote branch `codex/housekeeping-rebuild-stepwise` and one draft PR targeting `main`. + +- [ ] **Step 1: Verify the publication boundary** + +Run: + +```powershell +git fetch origin +git rev-list --left-right --count origin/main...HEAD +git log --oneline origin/main..HEAD +git status --short --branch +``` + +Expected: the branch contains the design, plan, and focused foundation commits; no tracked modifications are pending; `.remember/` remains untracked. + +- [ ] **Step 2: Push the dedicated branch** + +Run: + +```powershell +git push -u origin codex/housekeeping-rebuild-stepwise +``` + +Expected: pre-push typecheck/tests PASS and the remote branch is created or fast-forwarded. + +- [ ] **Step 3: Create the bilingual draft PR through Forgejo** + +Use Git Credential Manager without printing the credential: + +```powershell +$credentialLines = @("protocol=https", "host=gitlab.epicnabbo.nl", "", "") | + git credential fill +$credential = @{} +foreach ($line in $credentialLines) { + if ($line -match '^([^=]+)=(.*)$') { $credential[$matches[1]] = $matches[2] } +} +if (-not $credential.password) { throw 'Forgejo credential unavailable' } + +$body = @' +## English + +### Scope +- Restores the gated Housekeeping preview at `/ase-next` while keeping `/admin` and `/mod` unchanged. +- Guarantees that every generated navigation link resolves to an accessible registered route with a handler. +- Separates authenticated forbidden access (403) from unknown routes (404). +- Adds localized loading, partial, conflict, dependency, forbidden, missing, error, and success states. + +### Verification +- Housekeeping tests +- Full test suite +- TypeScript typecheck +- Production build +- Preview isolation and route-reachability contracts + +This PR remains draft. People content and later verticals will be added only after this foundation checkpoint is reviewed. + +## Nederlands + +### Omvang +- Herstelt de afgeschermde Housekeeping-preview op `/ase-next`, terwijl `/admin` en `/mod` ongewijzigd blijven. +- Garandeert dat elke gegenereerde navigatielink verwijst naar een toegankelijke geregistreerde route met een handler. +- Maakt onderscheid tussen verboden toegang voor een aangemelde gebruiker (403) en een onbekende route (404). +- Voegt gelokaliseerde statussen toe voor laden, gedeeltelijke resultaten, conflicten, afhankelijkheidsfouten, verboden toegang, ontbrekende pagina's, fouten en succes. + +### Verificatie +- Housekeeping-tests +- Volledige testsuite +- TypeScript-typecontrole +- Productiebuild +- Contracttests voor preview-isolatie en bereikbare routes + +Deze PR blijft een concept. People-content en volgende domeinen worden pas toegevoegd nadat deze foundation-checkpoint is beoordeeld. +'@ + +$payload = @{ + base = "main" + head = "codex/housekeeping-rebuild-stepwise" + title = "Rebuild Housekeeping foundation and preview routing" + body = $body + draft = $true +} | ConvertTo-Json + +$headers = @{ Authorization = "token $($credential.password)" } +Invoke-RestMethod ` + -Method Post ` + -Uri 'https://gitlab.epicnabbo.nl/api/v1/repos/remco/EpicNext-Cms/pulls' ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $payload | + Select-Object number, html_url, state, draft +``` + +Expected: one draft PR targeting `main`; the credential value is never written to output or committed. + +- [ ] **Step 4: Verify the remote PR and checks** + +Query the returned PR number and branch status through the Forgejo API. Confirm `draft: true`, `base.ref: main`, `head.ref: codex/housekeeping-rebuild-stepwise`, and wait for all triggered checks to complete. Do not call the foundation deployed: the preview remains production-disabled and this task does not merge. + +--- + +## Plan completion boundary + +This plan is complete when the draft PR contains a green, non-production `/ase-next` routing foundation and the legacy administration surfaces remain untouched. The next written plan covers the People vertical: workflow audit, users, linked accounts, community/staff, moderation, support, real query/command services, content review, and visual approval. No People page is considered implemented by this foundation plan. diff --git a/docs/superpowers/plans/2026-09-01-housekeeping-content-events-vertical.md b/docs/superpowers/plans/2026-09-01-housekeeping-content-events-vertical.md new file mode 100644 index 00000000..8da3ac7a --- /dev/null +++ b/docs/superpowers/plans/2026-09-01-housekeeping-content-events-vertical.md @@ -0,0 +1,237 @@ +# Housekeeping Content Events Vertical Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver a complete ASE event and event-type operator workflow backed by real database data and existing audited mutations. + +**Architecture:** Keep the generic Content query/command foundation, add strict route-specific event payloads, and render them through a focused `ContentEventWorkflow`. Use the existing event tables and mutation runtime; introduce dedicated delete command IDs only to enforce reason confirmation. + +**Tech Stack:** Next.js 16, React 19, TypeScript, Drizzle/MySQL, Zod, Vitest, React server actions + +**Spec:** `docs/superpowers/specs/2026-09-01-housekeeping-content-events-vertical-design.md` + +## Global Constraints + +- Work directly on `codex/housekeeping-rebuild-stepwise`; do not create a worktree. +- Preserve `/admin/events` and all unrelated local/untracked files. +- Do not add dependencies or change the database schema. +- Keep Polls and Prefixes behavior unchanged. +- Write and run a failing test before each production behavior change. +- Keep every destructive event or event-type action reason-protected and auditable. + +--- + +### Task 1: Typed event query payloads and production loading + +**Files:** +- Modify: `src/features/housekeeping/domains/content/queries/content-queries.ts` +- Modify: `src/features/housekeeping/domains/content/queries/content-queries-production.ts` +- Test: `src/features/housekeeping/domains/content/queries/content-queries.test.ts` + +**Interfaces:** +- Produces: `ContentEventTypePayload`, `ContentEventSummaryPayload`, `ContentEventDetailPayload`, and their public type guards. +- Produces: validated private payloads for list, create, detail, and type routes. +- Consumes: `ContentQueryItem.privatePayload`, normalized `params.id`, `list.search`, `list.status`, `list.pageSize`, and `list.offset`. + +- [x] **Step 1: Write failing contract tests** + +Add literal fixtures proving that malformed event payloads fail closed, event list input normalizes a bounded status, and a detail adapter returning more than one item is rejected. + +- [x] **Step 2: Run the query test and confirm RED** + +Run: `yarn.cmd vitest run src/features/housekeeping/domains/content/queries/content-queries.test.ts` + +Expected: FAIL because event payload guards and status normalization do not exist. + +- [x] **Step 3: Implement the minimal event contracts** + +Add route-specific interfaces with JSON-safe primitive fields and type guards. Extend list input with `status`, normalized to lowercase and at most 32 characters. Extend `isValidData` so each event route accepts only its corresponding payload and detail accepts at most one selected item. + +- [x] **Step 4: Run the query test and confirm GREEN** + +Run the command from Step 2. Expected: PASS. + +- [x] **Step 5: Write failing production-loader tests** + +Add tests proving: + +```ts +expect(list.items[0]?.privatePayload).toMatchObject({ + typeName: "Tournament", + registrationCount: 12, +}); +expect(detail.items).toHaveLength(1); +expect(detail.items[0]?.privatePayload).toMatchObject({ + description: "Complete event", + eventTypes: [{ id: "2", name: "Tournament" }], + prizes: [{ id: "4", prizeType: "badge" }], + winners: [{ userId: "9", username: "Alice" }], + registrations: [{ userId: "10", username: "Bob" }], +}); +``` + +Also assert that the serialized detail SQL binds the requested ID and that status filtering is bound, not interpolated. + +- [x] **Step 6: Run the loader tests and confirm RED** + +Run the command from Step 2. Expected: FAIL because production definitions only expose generic summaries. + +- [x] **Step 7: Implement production event loaders** + +Update event list and type definitions to project full safe summaries. Load event-create options from active event types. Add a dedicated detail loader that performs bounded, parameterized reads for the selected event, event types, prizes, winners with usernames, and registrations with usernames. Return not-found as an empty successful result. + +- [x] **Step 8: Run the loader tests and confirm GREEN** + +Run the command from Step 2. Expected: PASS. + +### Task 2: Event command safety and form field semantics + +**Files:** +- Modify: `src/features/housekeeping/domains/content/commands/content-commands.ts` +- Modify: `src/features/housekeeping/domains/content/pages/content-command-form.tsx` +- Test: `src/features/housekeeping/domains/content/commands/content-commands.test.ts` +- Test: `src/features/housekeeping/domains/content/pages/content-pages.test.tsx` + +**Interfaces:** +- Produces: `content.engagement.event.delete` and `content.engagement.event-type.delete`, both mapped to existing mutations with `requiresReason: true`. +- Produces: `ContentCommandField.type === "datetime-local"`, submitted as the normalized browser value. + +- [x] **Step 1: Write failing command-policy tests** + +Assert the two delete command IDs exist, use `event.change` and `event-type.change`, retain `PERMS.EVENTS_EDIT`, and require reasons while non-destructive change commands do not. + +- [x] **Step 2: Run command tests and confirm RED** + +Run: `yarn.cmd vitest run src/features/housekeeping/domains/content/commands/content-commands.test.ts` + +Expected: FAIL because the dedicated delete commands are absent. + +- [x] **Step 3: Implement command metadata** + +Extend the command definition tuple with an optional `requiresReason` flag and register both dedicated delete command IDs without adding mutation operations. + +- [x] **Step 4: Run command tests and confirm GREEN** + +Run the command from Step 2. Expected: PASS. + +- [x] **Step 5: Write a failing date/time form test** + +Render a field with `type: "datetime-local"` and assert the real input type and default value. Submit it and assert the existing server action receives the exact normalized date/time string. + +- [x] **Step 6: Run page tests and confirm RED** + +Run: `yarn.cmd vitest run src/features/housekeeping/domains/content/pages/content-pages.test.tsx` + +Expected: FAIL because `datetime-local` is not supported. + +- [x] **Step 7: Implement the minimal field support** + +Add `datetime-local` to the field union and map it to ``; keep the existing bounded string parser. + +- [x] **Step 8: Run page tests and confirm GREEN** + +Run the command from Step 6. Expected: PASS. + +### Task 3: Complete Event workflow UI + +**Files:** +- Create: `src/features/housekeeping/domains/content/pages/event-workflow.tsx` +- Modify: `src/features/housekeeping/domains/content/pages/engagement.tsx` +- Create: `src/features/housekeeping/domains/content/pages/event-workflow.test.tsx` +- Modify: `src/features/housekeeping/domains/content/pages/content-pages.test.tsx` + +**Interfaces:** +- Produces: `ContentEventWorkflow(props)` for the four event routes. +- Consumes: typed payload guards from Task 1 and command IDs/field semantics from Task 2. + +- [x] **Step 1: Write failing list and state tests** + +Render real `HousekeepingResult` fixtures and assert loading, forbidden, dependency-error, empty, and ready states. The ready list must expose search, status filtering, result count, type, schedule, capacity, registrations, create/type links, and bounded previous/next links. + +- [x] **Step 2: Run workflow tests and confirm RED** + +Run: `yarn.cmd vitest run src/features/housekeeping/domains/content/pages/event-workflow.test.tsx` + +Expected: FAIL because the component does not exist. + +- [x] **Step 3: Implement the list/state slice** + +Create the focused workflow component and route the four event route IDs to it from `ContentEngagementPage`, leaving Polls and Prefixes on the existing generic implementation. + +- [x] **Step 4: Run workflow tests and confirm GREEN** + +Run the command from Step 2. Expected: PASS for list/state tests. + +- [x] **Step 5: Write failing create/detail tests** + +Assert create uses real type options and date/time inputs. Assert detail prepopulates title, description, selected type, schedule, capacity, room, status, recurrence, and image; automatically binds event IDs for prizes/winners; renders usernames for registrations; and exposes a reason-required delete form using `content.engagement.event.delete`. + +- [x] **Step 6: Run workflow tests and confirm RED** + +Run the command from Step 2. Expected: FAIL because create/detail workflow sections are incomplete. + +- [x] **Step 7: Implement create/detail** + +Build field factories from the validated payload. Render not-found separately from dependency failure. Keep related forms and lists inside the selected event detail; never expose manual event-ID inputs. + +- [x] **Step 8: Run workflow tests and confirm GREEN** + +Run the command from Step 2. Expected: PASS for create/detail tests. + +- [x] **Step 9: Write failing event-type tests** + +Assert a create form and one prefilled update form per type, plus a reason-required delete form using `content.engagement.event-type.delete`; no operator-entered type ID field is allowed. + +- [x] **Step 10: Run workflow tests and confirm RED** + +Run the command from Step 2. Expected: FAIL until type management is implemented. + +- [x] **Step 11: Implement event-type management and refactor** + +Add create/update/delete sections using typed payloads. Extract small field and formatting helpers while all tests stay green. + +- [x] **Step 12: Run focused Content tests and confirm GREEN** + +Run: + +`yarn.cmd vitest run src/features/housekeeping/domains/content/queries/content-queries.test.ts src/features/housekeeping/domains/content/commands/content-commands.test.ts src/features/housekeeping/domains/content/pages/content-pages.test.tsx src/features/housekeeping/domains/content/pages/event-workflow.test.tsx` + +Expected: all focused tests PASS. + +### Task 4: Full verification and draft PR update + +**Files:** +- Modify: `docs/superpowers/plans/2026-09-01-housekeeping-content-events-vertical.md` +- Modify: draft PR 53 body in English and Dutch + +**Interfaces:** +- Consumes: all deliverables from Tasks 1–3. +- Produces: verified commit(s), pushed branch, and current bilingual PR evidence. + +- [x] **Step 1: Run static and targeted checks** + +Run the repository TypeScript, Biome, Knip, focused test, and Housekeeping matrix commands from `package.json` and the existing Housekeeping evidence workflow. Fix only failures caused by this vertical. + +- [x] **Step 2: Run the full test suite with coverage** + +Run: `yarn.cmd test` + +Expected: zero failing test files and zero failing tests. + +- [x] **Step 3: Run the production build** + +Run: `yarn.cmd build` + +Expected: exit code 0 with canonical `/ase-next` routes generated. + +- [x] **Step 4: Review the exact diff** + +Run: `git diff --check`, `git status --short`, and `git diff --stat origin/main...HEAD` after committing. Confirm `.remember/` and `.superpowers/brainstorm/` remain untouched and untracked. + +- [x] **Step 5: Commit and push exact paths** + +Commit query/command/UI/test/plan files with a scoped message, then push `codex/housekeeping-rebuild-stepwise`. + +- [x] **Step 6: Update and verify the draft PR** + +Add the Events vertical and fresh verification counts to PR 53 in English and Dutch. Confirm the remote head matches local HEAD and inspect CI status without claiming deployment. diff --git a/docs/superpowers/plans/2026-09-01-housekeeping-content-polls-vertical.md b/docs/superpowers/plans/2026-09-01-housekeeping-content-polls-vertical.md new file mode 100644 index 00000000..a230cf93 --- /dev/null +++ b/docs/superpowers/plans/2026-09-01-housekeeping-content-polls-vertical.md @@ -0,0 +1,1075 @@ +# Housekeeping Content Polls Vertical Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver a complete dedicated ASE Polls workflow for discovery, authoring, question management, aggregate analysis, individual free-text review, and safe deletion without changing public voting behaviour. + +**Architecture:** Keep the existing Content query/command/mutation foundation, add strict route-specific poll payloads, and render them through focused `ContentPollWorkflow` and `ContentPollResults` components. Reuse the current poll tables and audited database transaction boundary, while sharing pure option/answer parsing between ASE and public consumers so single, multiple, and text semantics cannot drift. + +**Tech Stack:** Next.js 16, React 19, TypeScript, Drizzle/MySQL, Zod, Vitest, React server actions, Biome, Knip + +**Spec:** `docs/superpowers/specs/2026-09-01-housekeeping-content-polls-vertical-design.md` + +## Global Constraints + +- Work directly in `E:\Users\simol\Desktop\EpicNext-cms` on `codex/housekeeping-rebuild-stepwise`; do not create a worktree. +- Preserve `/admin/polls`, public `/polls`, `.remember/`, `.superpowers/brainstorm/`, and unrelated user changes. +- Do not add runtime dependencies and do not change the database schema. +- Keep question `type` authoritative for public single-choice, multiple-choice, and text vote semantics; the poll-level `multipleChoice` field remains compatibility metadata only. +- Keep `showResults` authoritative for public result visibility; individual free-text identity data is ASE-only. +- Enforce at most 100 questions per poll, 100 options per question, and 50 individual free-text responses per page. +- Require `admin.polls.edit` for mutations; allow `admin.polls.view` to inspect poll lists, details, and results without rendering write controls. +- Poll and question deletion must use dedicated reason-protected command IDs and explicit child-first deletion inside the existing audited transaction. +- Write and run a failing test before each production behaviour change; commit each task independently. +- Update draft PR 53 in English and Dutch only after implementation verification is current. + +## File Structure + +- `src/lib/polls/poll-semantics.ts`: pure canonical parsing and serialization shared by public and ASE code. +- `src/lib/polls/poll-semantics.test.ts`: public-compatibility and normalization regression tests. +- `src/lib/validators/poll.ts`: schedule and type-aware question validation schemas. +- `src/features/housekeeping/domains/content/queries/content-queries.ts`: typed poll payloads, guards, and normalized response-page input. +- `src/features/housekeeping/domains/content/queries/content-queries-production.ts`: bounded poll list/create/detail database adapters. +- `src/features/housekeeping/domains/content/services/mutation-runtime-database.ts`: validated poll/question writes and explicit child-first deletes. +- `src/features/housekeeping/domains/content/pages/content-command-form.tsx`: retained failed submissions and field-level errors. +- `src/features/housekeeping/domains/content/pages/poll-results.tsx`: aggregate and free-text result presentation. +- `src/features/housekeeping/domains/content/pages/poll-workflow.tsx`: Polls list, create, overview, questions, permissions, and state routing. +- `src/features/housekeeping/domains/content/pages/engagement.tsx`: delegates the three poll routes to the dedicated workflow and leaves Prefixes unchanged. + +--- + +### Task 1: Canonical poll semantics and type-aware validation + +**Files:** +- Create: `src/lib/polls/poll-semantics.ts` +- Create: `src/lib/polls/poll-semantics.test.ts` +- Modify: `src/lib/validators/poll.ts` +- Modify: `src/lib/validators/poll.test.ts` +- Modify: `src/actions/polls.ts` +- Modify: `src/app/(site)/polls/[id]/page.tsx` +- Modify: `src/app/(site)/polls/[id]/poll-vote-form.tsx` + +**Interfaces:** +- Produces: `PollQuestionType`, `parsePollOptions(value)`, `serializePollOptions(type, value)`, and `parsePollAnswerSelections(type, answer)`. +- Produces: `pollQuestionPatchSchema` for mutation updates that are merged with the stored question before full validation. +- Preserves: newline-delimited public multiple-choice answers and trimmed single/text answers. + +- [ ] **Step 1: Write failing semantics and validator tests** + +Create `poll-semantics.test.ts` with the public compatibility cases: + +```ts +import { describe, expect, it } from "vitest"; +import { + parsePollAnswerSelections, + parsePollOptions, + serializePollOptions, +} from "./poll-semantics"; + +describe("public poll compatibility", () => { + it("keeps newline-delimited multiple-choice answers", () => { + expect(parsePollAnswerSelections("multiple", "Red\nBlue\n")).toEqual([ + "Red", + "Blue", + ]); + }); + + it("keeps one trimmed answer for single and text questions", () => { + expect(parsePollAnswerSelections("single", " Red ")).toEqual(["Red"]); + expect(parsePollAnswerSelections("text", " Detailed answer ")).toEqual([ + "Detailed answer", + ]); + }); + + it("normalizes option lines without reordering them", () => { + expect(parsePollOptions(" Red \r\n\nBlue ")).toEqual(["Red", "Blue"]); + expect(serializePollOptions("text", "ignored")).toBe(""); + expect(serializePollOptions("multiple", " Red \nBlue ")).toBe("Red\nBlue"); + }); +}); +``` + +Extend `poll.test.ts` with these exact assertions: + +```ts +it("accepts text questions without options", () => { + expect( + pollQuestionSchema.safeParse({ + pollId: 1, + question: "Why?", + type: "text", + options: "", + }).success, + ).toBe(true); +}); + +it.each([ + ["one option", "single", "Only"], + ["duplicate options", "multiple", "Red\nred"], + ["options on text", "text", "Not allowed"], +] as const)("rejects %s", (_label, type, options) => { + expect( + pollQuestionSchema.safeParse({ pollId: 1, question: "Question", type, options }) + .success, + ).toBe(false); +}); + +it("rejects more than 100 options", () => { + const options = Array.from({ length: 101 }, (_, index) => `Option ${index}`).join("\n"); + expect( + pollQuestionSchema.safeParse({ pollId: 1, question: "Question", type: "single", options }) + .success, + ).toBe(false); +}); + +it("requires the end time to be later than the start time", () => { + expect( + createPollSchema.safeParse({ + title: "Schedule", + startsAt: "2026-09-02T12:00:00.000Z", + endsAt: "2026-09-02T11:59:00.000Z", + }).success, + ).toBe(false); +}); +``` + +- [ ] **Step 2: Run the focused tests and confirm RED** + +Run: + +```powershell +pnpm vitest run --coverage.enabled=false src/lib/polls/poll-semantics.test.ts src/lib/validators/poll.test.ts +``` + +Expected: FAIL because the semantics module and type-aware validation do not exist. + +- [ ] **Step 3: Implement the pure semantics module** + +Create the module with these signatures and behaviour: + +```ts +export const POLL_QUESTION_TYPES = ["single", "multiple", "text"] as const; +export type PollQuestionType = (typeof POLL_QUESTION_TYPES)[number]; + +export function parsePollOptions(value: string): string[] { + return value + .split(/\r?\n/u) + .map((option) => option.normalize("NFC").trim()) + .filter(Boolean); +} + +export function serializePollOptions( + type: PollQuestionType, + value: string, +): string { + return type === "text" ? "" : parsePollOptions(value).join("\n"); +} + +export function parsePollAnswerSelections( + type: PollQuestionType, + answer: string, +): string[] { + const normalized = answer.normalize("NFC"); + return type === "multiple" + ? parsePollOptions(normalized) + : [normalized.trim()].filter(Boolean); +} +``` + +- [ ] **Step 4: Implement Zod refinements and the patch schema** + +Build `pollQuestionSchema` from a reusable object, apply case-insensitive uniqueness and type-aware option rules, and export a patch that cannot carry `pollId`: + +```ts +const pollQuestionFields = z.object({ + pollId: z.coerce.number().int().positive(), + question: z.string().trim().min(1).max(500), + type: z.enum(POLL_QUESTION_TYPES).default("single"), + sortOrder: z.coerce.number().int().min(0).default(0), + options: z.string().max(20_000).default(""), +}); + +function validateQuestionOptions( + data: z.infer, + context: z.RefinementCtx, +): void { + const options = parsePollOptions(data.options); + const unique = new Set(options.map((option) => option.toLocaleLowerCase())); + if (data.type === "text" && options.length > 0) + context.addIssue({ code: "custom", path: ["options"], message: "Text questions cannot have options" }); + if (data.type !== "text" && options.length < 2) + context.addIssue({ code: "custom", path: ["options"], message: "At least two options are required" }); + if (unique.size !== options.length) + context.addIssue({ code: "custom", path: ["options"], message: "Options must be unique" }); + if (options.length > 100) + context.addIssue({ code: "custom", path: ["options"], message: "At most 100 options are allowed" }); +} + +export const pollQuestionSchema = pollQuestionFields.superRefine(validateQuestionOptions); +export const pollQuestionPatchSchema = pollQuestionFields + .omit({ pollId: true }) + .partial(); +``` + +Define poll fields once and apply the same schedule refinement to full creation and partial update input; the runtime will also merge partial updates with stored dates before invoking the full schema: + +```ts +const pollFields = z.object({ + title: z.string().trim().min(1, "Title is required").max(255), + description: z.string().max(2_000).nullable().optional(), + status: z.enum(["draft", "active", "closed"]).default("draft"), + showResults: z.coerce.number().int().min(0).max(1).default(1), + multipleChoice: z.coerce.number().int().min(0).max(1).default(0), + startsAt: z.coerce.date().nullable().optional(), + endsAt: z.coerce.date().nullable().optional(), +}); + +function validateSchedule( + data: { readonly startsAt?: Date | null; readonly endsAt?: Date | null }, + context: z.RefinementCtx, +): void { + if (data.startsAt && data.endsAt && data.endsAt <= data.startsAt) { + context.addIssue({ + code: "custom", + path: ["endsAt"], + message: "End time must be later than start time", + }); + } +} + +export const createPollSchema = pollFields.superRefine(validateSchedule); +export const updatePollSchema = pollFields.partial().superRefine(validateSchedule); +``` + +Update the existing valid-question fixture from `"Red|Blue|Green"` to `"Red\nBlue\nGreen"` so it uses the newline format already consumed by the public site. + +- [ ] **Step 5: Route public parsing through the shared helpers** + +Delete the three local option/answer split implementations and import the pure helpers. The public action validation loop must use: + +```ts +const options = parsePollOptions(question.options); +const selected = parsePollAnswerSelections( + question.type as PollQuestionType, + answer, +); +``` + +The public result page must use `parsePollOptions(q.options)` and `parsePollAnswerSelections(q.type as PollQuestionType, vote)` while keeping its existing `showResults`, voted, and ended conditions unchanged. The client vote form must use `parsePollOptions` and continue submitting multiple answers with `.join("\n")`. + +- [ ] **Step 6: Run focused tests and confirm GREEN** + +Run: + +```powershell +pnpm vitest run --coverage.enabled=false src/lib/polls/poll-semantics.test.ts src/lib/validators/poll.test.ts +pnpm typecheck +``` + +Expected: all tests PASS and TypeScript exits 0. + +- [ ] **Step 7: Commit Task 1** + +```powershell +git add -- src/lib/polls/poll-semantics.ts src/lib/polls/poll-semantics.test.ts src/lib/validators/poll.ts src/lib/validators/poll.test.ts src/actions/polls.ts 'src/app/(site)/polls/[id]/page.tsx' 'src/app/(site)/polls/[id]/poll-vote-form.tsx' +git commit -m "refactor(polls): centralize question semantics" +``` + +--- + +### Task 2: Typed poll query contract and response-page input + +**Files:** +- Modify: `src/features/housekeeping/domains/content/queries/content-queries.ts` +- Modify: `src/features/housekeeping/domains/content/queries/content-queries.test.ts` +- Modify: `src/features/housekeeping/domains/content/pages/content-page-frame.tsx` +- Modify: `src/features/housekeeping/domains/content/pages/content-pages.test.tsx` + +**Interfaces:** +- Produces: `ContentPollCreatePayload`, `ContentPollSummaryPayload`, `ContentPollQuestionPayload`, `ContentPollTextResponsePayload`, `ContentPollTextResponsePagePayload`, and `ContentPollDetailPayload`. +- Produces: `isContentPollCreatePayload`, `isContentPollSummaryPayload`, and `isContentPollDetailPayload`. +- Extends: `ContentQueryListInput` and `NormalizedContentQueryInput.list` with `responseQuestionId`, `responsePageSize`, and `responseOffset`. + +- [ ] **Step 1: Write failing contract and normalization tests** + +Add a valid detail fixture and mutate one field per case: + +```ts +const pollDetail = { + kind: "poll-detail" as const, + description: "Complete poll", + showResults: true, + multipleChoice: false, + startsAt: "2026-09-02T18:00:00.000Z", + endsAt: null, + questionCount: 1, + voterCount: 2, + answerCount: 2, + questions: [{ + id: "21", + question: "Favourite colour?", + type: "single" as const, + sortOrder: 0, + options: ["Red", "Blue"], + answerCount: 2, + choiceResults: [{ option: "Red", count: 2 }], + }], + textResponses: { + questionId: null, + items: [], + total: 0, + pageSize: 25, + offset: 0, + }, +}; + +it.each([ + ["mismatched id", "8", pollDetail], + ["too many questions", "7", { ...pollDetail, questions: Array(101).fill(pollDetail.questions[0]) }], + ["invalid response user", "7", { + ...pollDetail, + textResponses: { + questionId: "22", + items: [{ id: "1", questionId: "22", userId: "0", username: null, answer: "Text", createdAt: "2026-09-02T18:00:00.000Z" }], + total: 1, + pageSize: 25, + offset: 0, + }, + }], +] as const)("fails closed for poll detail with %s", async (_label, itemId, payload) => { + const query = createContentQuery({ load: async () => ({ + kind: "engagement", + items: [{ id: itemId, title: "Poll", privatePayload: payload }], + total: 1, + partialDependencies: [], + }) }); + const result = await query.run(context([PERMS.POLLS_VIEW]), { + routeId: "content.engagement.poll-detail", + params: { id: "7" }, + }); + expect(result).toMatchObject({ ok: false, error: { code: "DEPENDENCY_UNAVAILABLE" } }); +}); +``` + +Add an adapter-input assertion: + +```ts +expect(load).toHaveBeenCalledWith({ + routeId: "content.engagement.poll-detail", + params: { id: "7" }, + list: { + search: "", + pageSize: 25, + offset: 0, + responseQuestionId: "22", + responsePageSize: 50, + responseOffset: 100_000, + }, +}); +``` + +- [ ] **Step 2: Run contract tests and confirm RED** + +```powershell +pnpm vitest run --coverage.enabled=false src/features/housekeeping/domains/content/queries/content-queries.test.ts src/features/housekeeping/domains/content/pages/content-pages.test.tsx +``` + +Expected: FAIL because poll guards and response-page input fields do not exist. + +- [ ] **Step 3: Add exact poll payload interfaces** + +Use these stable field names: + +```ts +export type ContentPollQuestionType = PollQuestionType; + +export interface ContentPollCreatePayload { + readonly kind: "poll-create"; +} + +export interface ContentPollSummaryPayload { + readonly kind: "poll-summary"; + readonly showResults: boolean; + readonly multipleChoice: boolean; + readonly startsAt: string | null; + readonly endsAt: string | null; + readonly questionCount: number; + readonly voterCount: number; + readonly answerCount: number; +} + +export interface ContentPollChoiceResultPayload { + readonly option: string; + readonly count: number; +} + +export interface ContentPollQuestionPayload { + readonly id: string; + readonly question: string; + readonly type: ContentPollQuestionType; + readonly sortOrder: number; + readonly options: readonly string[]; + readonly answerCount: number; + readonly choiceResults: readonly ContentPollChoiceResultPayload[]; +} + +export interface ContentPollTextResponsePayload { + readonly id: string; + readonly questionId: string; + readonly userId: string; + readonly username: string | null; + readonly answer: string; + readonly createdAt: string; +} + +export interface ContentPollTextResponsePagePayload { + readonly questionId: string | null; + readonly items: readonly ContentPollTextResponsePayload[]; + readonly total: number; + readonly pageSize: number; + readonly offset: number; +} + +export interface ContentPollDetailPayload { + readonly kind: "poll-detail"; + readonly description: string; + readonly showResults: boolean; + readonly multipleChoice: boolean; + readonly startsAt: string | null; + readonly endsAt: string | null; + readonly questionCount: number; + readonly voterCount: number; + readonly answerCount: number; + readonly questions: readonly ContentPollQuestionPayload[]; + readonly textResponses: ContentPollTextResponsePagePayload; +} +``` + +The guards must enforce positive decimal-string IDs, canonical ISO timestamps, non-negative safe counts, valid question types, maximum array sizes, response item count `<= pageSize`, and response-question ownership. Poll detail must contain at most one item whose ID equals `params.id`; create must contain exactly one `{ id: "create", privatePayload: { kind: "poll-create" } }` item. An offset beyond the exact total is a valid empty response page, not a dependency failure. + +- [ ] **Step 4: Normalize bounded response-page input** + +Extend the normalized list with: + +```ts +responseQuestionId: String(input.list?.responseQuestionId ?? "") + .normalize("NFC") + .trim() + .slice(0, 32), +responsePageSize: boundedInteger(input.list?.responsePageSize, 25, 1, 50), +responseOffset: boundedInteger(input.list?.responseOffset, 0, 0, 100_000), +``` + +Extend `parseContentListInput` using search parameters named `responseQuestionId`, `responsePageSize`, and `responseOffset` with the same bounds. Update the one existing exact normalized-input fixture to include default response values. + +- [ ] **Step 5: Run contract tests and confirm GREEN** + +Run the command from Step 2. Expected: all focused tests PASS. + +- [ ] **Step 6: Commit Task 2** + +```powershell +git add -- src/features/housekeeping/domains/content/queries/content-queries.ts src/features/housekeeping/domains/content/queries/content-queries.test.ts src/features/housekeeping/domains/content/pages/content-page-frame.tsx src/features/housekeeping/domains/content/pages/content-pages.test.tsx +git commit -m "feat(housekeeping): define typed poll query contract" +``` + +--- + +### Task 3: Bounded production poll queries + +**Files:** +- Modify: `src/features/housekeeping/domains/content/queries/content-queries-production.ts` +- Modify: `src/features/housekeeping/domains/content/queries/content-queries.test.ts` + +**Interfaces:** +- Consumes: payload types and normalized response input from Task 2. +- Consumes: parsing helpers from Task 1. +- Produces: real list, create, and selected-detail payloads without materializing raw choice-vote rows in ASE. + +- [ ] **Step 1: Write failing production-adapter tests** + +Mock the list count/page calls and assert the safe summary: + +```ts +expect(result.items[0]?.privatePayload).toEqual({ + kind: "poll-summary", + showResults: true, + multipleChoice: false, + startsAt: "2026-09-02T18:00:00.000Z", + endsAt: null, + questionCount: 3, + voterCount: 12, + answerCount: 30, +}); +``` + +Mock detail calls for the poll, questions, grouped choice answers, text total, and text page. Assert: + +```ts +expect(result.items[0]?.privatePayload).toMatchObject({ + kind: "poll-detail", + questions: [ + { + id: "21", + options: ["Red", "Blue"], + answerCount: 3, + choiceResults: [ + { option: "Red", count: 3 }, + { option: "Blue", count: 1 }, + ], + }, + ], + textResponses: { + questionId: "22", + total: 51, + pageSize: 25, + offset: 25, + items: [ + { userId: "9", username: "Alice", answer: "More events" }, + { userId: "10", username: null, answer: "Better prizes" }, + ], + }, +}); +``` + +Also assert a selected ID is bound, list status/search are bound, question SQL contains `LIMIT 101`, grouped answer SQL contains `LIMIT 10001`, and free-text SQL binds the selected question, limit, and offset. + +- [ ] **Step 2: Run query tests and confirm RED** + +```powershell +pnpm vitest run --coverage.enabled=false src/features/housekeeping/domains/content/queries/content-queries.test.ts +``` + +Expected: FAIL because Polls still use generic query definitions. + +- [ ] **Step 3: Implement the enriched list and typed create payload** + +Replace the list statement with one projected row per poll: + +```sql +SELECT p.id, p.title, p.status, p.updated_at, p.show_results, p.multiple_choice, + p.starts_at, p.ends_at, + (SELECT COUNT(*) FROM website_poll_questions q WHERE q.poll_id = p.id) AS question_count, + (SELECT COUNT(DISTINCT v.user_id) FROM website_poll_votes v + INNER JOIN website_poll_questions q ON q.id = v.question_id + WHERE q.poll_id = p.id) AS voter_count, + (SELECT COUNT(*) FROM website_poll_votes v + INNER JOIN website_poll_questions q ON q.id = v.question_id + WHERE q.poll_id = p.id) AS answer_count +FROM website_polls p +ORDER BY p.created_at DESC +``` + +Map it with `pollSummaryPayload(row)`. Remove poll-create from `EMPTY_ROUTES` and return one stable create item: + +```ts +return { + kind: "engagement", + items: [{ + id: "create", + title: "Create poll", + href: "/ase-next/content/engagement/polls/create", + privatePayload: { kind: "poll-create" }, + }], + total: 1, + partialDependencies: [], +}; +``` + +- [ ] **Step 4: Implement the selected detail loader** + +Use constants: + +```ts +const POLL_QUESTION_LIMIT = 100; +const POLL_OPTION_LIMIT = 100; +const POLL_AGGREGATE_ROW_LIMIT = 10_000; +``` + +Load the selected poll with `WHERE id = ${id} LIMIT 1`; return a successful empty result when absent. Load questions ordered by `sort_order, id` with `LIMIT 101` and throw on overflow or more than 100 parsed options. Load grouped choice answers with: + +```sql +SELECT v.question_id, v.answer, COUNT(*) AS answer_count +FROM website_poll_votes v +INNER JOIN website_poll_questions q ON q.id = v.question_id +WHERE q.poll_id = ${id} AND q.type IN ('single', 'multiple') +GROUP BY v.question_id, v.answer +LIMIT 10001 +``` + +For each grouped row, add its weight to every value returned by `parsePollAnswerSelections(question.type, answer)` and add the weight once to the question's `answerCount`. Keep only configured options in `choiceResults` and retain configured option order. + +Choose `input.list.responseQuestionId` only when it identifies a text question in this poll; otherwise use the first text question or `null`. For a selected text question, run an exact count and a bounded page query: + +```sql +SELECT v.id, v.question_id, v.user_id, u.username, v.answer, v.created_at +FROM website_poll_votes v +LEFT JOIN users u ON u.id = v.user_id +WHERE v.question_id = ${questionId} +ORDER BY v.created_at DESC, v.id DESC +LIMIT ${input.list.responsePageSize} OFFSET ${input.list.responseOffset} +``` + +Return `username: null` when the user join is absent; never synthesize a username. + +Dispatch both dedicated adapters before the generic definition lookup: + +```ts +if (input.routeId === "content.engagement.poll-create") return pollCreate(input); +if (input.routeId === "content.engagement.poll-detail") return pollDetail(input); +``` + +- [ ] **Step 5: Run query tests and confirm GREEN** + +Run the command from Step 2. Expected: all query tests PASS. + +- [ ] **Step 6: Commit Task 3** + +```powershell +git add -- src/features/housekeeping/domains/content/queries/content-queries-production.ts src/features/housekeeping/domains/content/queries/content-queries.test.ts +git commit -m "feat(housekeeping): load complete poll operator data" +``` + +--- + +### Task 4: Reason-protected commands and transactional poll mutations + +**Files:** +- Modify: `src/features/housekeeping/domains/content/commands/content-commands.ts` +- Modify: `src/features/housekeeping/domains/content/commands/content-commands.test.ts` +- Modify: `src/features/housekeeping/domains/content/services/mutation-runtime-database.ts` +- Modify: `src/features/housekeeping/domains/content/services/mutation-runtime-database.test.ts` + +**Interfaces:** +- Produces: `content.engagement.poll.delete` mapped to `poll.change` with `requiresReason: true`. +- Produces: `content.engagement.poll-question.delete` mapped to `poll-question.change` with `requiresReason: true`. +- Keeps: existing `poll.change` and `poll-question.change` mutation operation IDs and audited transaction classification. + +- [ ] **Step 1: Write failing command-policy tests** + +Extend the expected command matrix and assert protected/unprotected inputs: + +```ts +for (const id of [ + "content.engagement.poll.delete", + "content.engagement.poll-question.delete", +]) { + expect(byId.get(id)?.requiresReason, id).toBe(true); + expect(byId.get(id)?.input.safeParse({ action: "delete", id: 7 }).success).toBe(true); + expect(byId.get(id)?.input.safeParse({ action: "update", id: 7 }).success).toBe(false); +} + +for (const id of [ + "content.engagement.poll.change", + "content.engagement.poll-question.change", +]) { + expect(byId.get(id)?.requiresReason, id).toBe(false); + expect(byId.get(id)?.input.safeParse({ action: "delete", id: 7 }).success).toBe(false); +} +``` + +- [ ] **Step 2: Write failing database mutation tests** + +Name the mocked poll tables and add `WebsitePollVote`. Assert exact deletion order: + +```ts +expect(database.removedTables()).toEqual([ + "WebsitePollVote", + "WebsitePollQuestion", + "WebsitePoll", +]); +``` + +For question deletion assert `["WebsitePollVote", "WebsitePollQuestion"]`. Add tests that an update carrying another `pollId` fails with `VALIDATION`, text questions persist `options: ""`, duplicate options fail with an `options` field error, an end-before-start update fails with an `endsAt` field error, and creating question 101 fails without insertion. + +- [ ] **Step 3: Run command and mutation tests and confirm RED** + +```powershell +pnpm vitest run --coverage.enabled=false src/features/housekeeping/domains/content/commands/content-commands.test.ts src/features/housekeeping/domains/content/services/mutation-runtime-database.test.ts +``` + +Expected: FAIL because protected poll deletes and child-first mutation behaviour are absent. + +- [ ] **Step 4: Register dedicated delete commands** + +Add these definitions: + +```ts +["content.engagement.poll.change", "poll.change", PERMS.POLLS_EDIT], +["content.engagement.poll.delete", "poll.change", PERMS.POLLS_EDIT, true], +["content.engagement.poll-question.change", "poll-question.change", PERMS.POLLS_EDIT], +["content.engagement.poll-question.delete", "poll-question.change", PERMS.POLLS_EDIT, true], +``` + +Apply the existing create/update-only schema to both unprotected poll commands and the existing delete-only schema to both protected poll commands. + +- [ ] **Step 5: Map Zod errors to field errors** + +Extend the local validation helper without changing existing callers: + +```ts +function validation(error?: import("zod").ZodError): ContentMutationFailure { + const fieldErrors: Record = {}; + for (const issue of error?.issues ?? []) { + fieldErrors[String(issue.path[0] ?? "input")] = ["errors.validation.invalid"]; + } + return new ContentMutationFailure( + "VALIDATION", + "errors.housekeeping.validation", + Object.keys(fieldErrors).length > 0 ? fieldErrors : undefined, + ); +} +``` + +Every Poll Zod failure must call `validation(parsed.error)`. + +- [ ] **Step 6: Implement safe poll create/update/delete** + +For update, select every editable stored field, parse the patch with `updatePollSchema`, merge it with the stored values, validate the merged object with `createPollSchema`, and update only submitted columns plus `updatedAt`. For delete, remove child rows in this order inside the existing transaction: + +```ts +await connection.delete(WebsitePollVote).where( + inArray( + WebsitePollVote.questionId, + connection + .select({ id: WebsitePollQuestion.id }) + .from(WebsitePollQuestion) + .where(eq(WebsitePollQuestion.pollId, id)), + ), +); +await connection.delete(WebsitePollQuestion).where(eq(WebsitePollQuestion.pollId, id)); +await connection.delete(WebsitePoll).where(eq(WebsitePoll.id, id)); +``` + +- [ ] **Step 7: Implement safe question create/update/delete** + +On create, lock the parent poll row within the current transaction, count its questions, fail with `CONFLICT` at 100, validate the full input, and persist `serializePollOptions(parsed.data.type, parsed.data.options)`. + +On update, load the existing question first; reject any supplied `pollId`; parse with `pollQuestionPatchSchema`; merge with stored values; validate the merged full object; and write only submitted keys, replacing submitted options with the canonical serialization. On delete, load the existing question for the audit snapshot, delete its votes first, then delete the question. + +- [ ] **Step 8: Run command and mutation tests and confirm GREEN** + +Run the command from Step 3. Expected: all tests PASS. + +- [ ] **Step 9: Commit Task 4** + +```powershell +git add -- src/features/housekeeping/domains/content/commands/content-commands.ts src/features/housekeeping/domains/content/commands/content-commands.test.ts src/features/housekeeping/domains/content/services/mutation-runtime-database.ts src/features/housekeeping/domains/content/services/mutation-runtime-database.test.ts +git commit -m "fix(housekeeping): protect poll destructive actions" +``` + +--- + +### Task 5: Retained form values and field-level errors + +**Files:** +- Modify: `src/features/housekeeping/domains/content/pages/content-command-form.tsx` +- Modify: `src/features/housekeeping/domains/content/pages/content-pages.test.tsx` + +**Interfaces:** +- Produces: `ContentCommandFormState` containing the command result and serializable submitted string values. +- Produces: `ContentCommandFieldError({ result, fieldName, errorId })` for accessible field feedback. +- Preserves: entered non-file values and reason text when a command returns validation or conflict. + +- [ ] **Step 1: Write failing retained-value and field-error tests** + +Mock `executeHousekeepingCommand` to return: + +```ts +fail( + "VALIDATION", + "errors.housekeeping.validation", + "poll-validation", + { title: ["errors.validation.invalid"] }, +) +``` + +Submit `title=Draft title` and assert: + +```ts +expect(state.result).toMatchObject({ ok: false, error: { code: "VALIDATION" } }); +expect(state.values).toMatchObject({ title: "Draft title" }); +``` + +Render `ContentCommandFieldError` and assert `role="alert"`, the stable error ID, and `errors.validation.invalid`. Add a conflict submission with a reason and assert both values are retained. + +- [ ] **Step 2: Run page tests and confirm RED** + +```powershell +pnpm vitest run --coverage.enabled=false src/features/housekeeping/domains/content/pages/content-pages.test.tsx +``` + +Expected: FAIL because the action currently returns only `HousekeepingResult` and fields render no error association. + +- [ ] **Step 3: Implement the serializable form state** + +Use this shape: + +```ts +export interface ContentCommandFormState { + readonly result: HousekeepingResult | null; + readonly values: Readonly>; +} + +const initialState: ContentCommandFormState = { result: null, values: {} }; +``` + +Before command execution, capture only string `FormData` values for configured fields and `reason`; exclude `File` values. Return `{ result, values: result.ok ? {} : submittedValues }`. In `ContentCommandForm`, use retained values as defaults after failure and include the correlation ID in the remount key so the browser reset cannot erase failed input. + +- [ ] **Step 4: Render accessible field and command feedback** + +Each field with an error must have `aria-invalid="true"`, `aria-describedby={errorId}`, and: + +```tsx + +``` + +The command-level status must display `result.error.messageKey` for validation, conflict, and dependency failures instead of only the word `Failed`; successful commands keep `Completed`. + +- [ ] **Step 5: Run page tests and confirm GREEN** + +Run the command from Step 2. Expected: all page tests PASS. + +- [ ] **Step 6: Commit Task 5** + +```powershell +git add -- src/features/housekeeping/domains/content/pages/content-command-form.tsx src/features/housekeeping/domains/content/pages/content-pages.test.tsx +git commit -m "feat(housekeeping): retain invalid command input" +``` + +--- + +### Task 6: Dedicated ASE Polls workflow and results UI + +**Files:** +- Create: `src/features/housekeeping/domains/content/pages/poll-results.tsx` +- Create: `src/features/housekeeping/domains/content/pages/poll-results.test.tsx` +- Create: `src/features/housekeeping/domains/content/pages/poll-workflow.tsx` +- Create: `src/features/housekeeping/domains/content/pages/poll-workflow.test.tsx` +- Modify: `src/features/housekeeping/domains/content/pages/engagement.tsx` +- Modify: `src/features/housekeeping/domains/content/pages/content-pages.test.tsx` +- Modify: `src/features/housekeeping/domains/content/routes.ts` +- Modify: `src/features/housekeeping/domains/content/routes.test.ts` + +**Interfaces:** +- Produces: `ContentPollResults({ pollId, detail, list })` for aggregate and free-text analysis. +- Produces: `ContentPollWorkflow(props)` for list, create, and detail routes. +- Consumes: typed guards from Task 2, protected commands from Task 4, and retained-error forms from Task 5. + +- [ ] **Step 1: Write failing result presentation tests** + +Render a detail fixture and assert: + +```ts +expect(html).toContain("Red"); +expect(html).toContain("3 votes"); +expect(html).toContain("100%"); +expect(html).toContain("Alice"); +expect(html).toContain("user 9"); +expect(html).toContain("Unavailable user"); +expect(html).toContain("Better prizes"); +expect(html).toContain('dateTime="2026-09-02T20:00:00.000Z"'); +expect(html).toContain("responseQuestionId=22"); +expect(html).toContain("responseOffset=0"); +expect(html).toContain("responseOffset=50"); +``` + +Use a multiple-choice fixture where one option count is greater than half of `answerCount` and assert the percentage uses respondent answers, not the sum of selected options. + +- [ ] **Step 2: Write failing workflow and permission tests** + +Cover loading, forbidden, dependency error, true not-found, empty list, and ready states. The ready list must expose search, status, totals, schedule, question/voter/answer counts, pagination, and a create link only for `POLLS_EDIT`. + +Create must render native UTC date inputs and no manual ID. Detail must render Overview, Questions, and Results; view-only users see data but no command forms. Editors see prefilled poll/question forms, an automatically bound poll ID, and reason-required dedicated delete commands. + +Add a route assertion: + +```ts +expect(contentRouteById("content.engagement.poll-detail").capability.slugs).toEqual([ + PERMS.POLLS_VIEW, +]); +``` + +- [ ] **Step 3: Run UI tests and confirm RED** + +```powershell +pnpm vitest run --coverage.enabled=false src/features/housekeeping/domains/content/pages/poll-results.test.tsx src/features/housekeeping/domains/content/pages/poll-workflow.test.tsx src/features/housekeeping/domains/content/pages/content-pages.test.tsx src/features/housekeeping/domains/content/routes.test.ts +``` + +Expected: FAIL because the dedicated components do not exist and detail still requires edit permission. + +- [ ] **Step 4: Implement `ContentPollResults`** + +Use this public interface: + +```ts +export interface ContentPollResultsProps { + readonly pollId: string; + readonly detail: ContentPollDetailPayload; + readonly list: Pick< + ContentPollListState, + "responseQuestionId" | "responsePageSize" | "responseOffset" + >; +} +``` + +Render one aggregate block per choice question. Compute `Math.round((count / Math.max(1, question.answerCount)) * 100)`. Render text-question selector links and the selected page table with username or `Unavailable user`, immutable user ID, answer, and UTC `