From 601a3e746f876292dcd84790424f772a64d72135 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 24 Aug 2026 20:37:37 +0200 Subject: [PATCH 01/29] docs: design housekeeping modernization --- ...08-24-housekeeping-modernization-design.md | 438 ++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-24-housekeeping-modernization-design.md diff --git a/docs/superpowers/specs/2026-08-24-housekeeping-modernization-design.md b/docs/superpowers/specs/2026-08-24-housekeeping-modernization-design.md new file mode 100644 index 00000000..77e5e106 --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-housekeeping-modernization-design.md @@ -0,0 +1,438 @@ +# Housekeeping modernization design + +Date: 2026-08-24 +Status: approved in design review; awaiting review of this written specification + +## Purpose + +Replace the current administration experience with one coherent, role-adaptive Housekeeping (HK) at `/admin`. + +The new HK is a modular part of the existing Next.js application. It is built in parallel, validated against the current system, and exposed with one atomic cutover. It unifies the current `/admin` and `/mod` surfaces, removes duplicated workflows, and preserves reliable domain services without automatically preserving their current pages. + +This document is the master architecture for the program. It is deliberately not one giant implementation plan. Delivery is split into independently specified and verified subprojects, beginning with **Inventory & Foundation**. + +## Current-state findings + +- The repository currently contains 124 `page.tsx` files below `src/app/admin` and 13 below `src/app/mod`: 137 administration pages in total. +- `src/lib/admin-nav.ts` currently exposes nine navigation groups and seven hub definitions. +- `/admin` and `/mod` provide overlapping moderation, ticket, ban, team, and user workflows with separate shells. +- `/admin/housekeeping` is a legacy permission archive/comparison/export surface, while `/admin/permissions` is the live permission-management surface. +- The current dashboard reports useful counts but is not an operational work queue. +- Page composition, localization, ACL checks, filtering, error handling, and action feedback are not yet uniform across the administration surface. + +The migration must therefore classify every current page. A visual refresh without workflow and boundary changes is insufficient. + +## Approved decisions + +| Area | Decision | +| --- | --- | +| Audience | One role-adaptive HK. Effective capabilities, not rank names alone, determine what an operator sees and can do. | +| Entry point | `/admin` is the only administration entry point after cutover. `/mod` is removed. | +| Layout | Command Deck: compact domain rail, contextual navigation, global command palette, operational workspace. | +| Personalization | Hybrid: the system supplies mandatory capability-derived content; the operator may pin and reorder allowed shortcuts and optional widgets. | +| Compatibility | Clean break. Old subroute compatibility and legacy UX are not preserved through redirects. | +| Build strategy | Build the new HK in parallel, keep it unavailable to normal production operators, then switch atomically. | +| Work queue | “Da fare ora” is derived from existing sources. It is not a second task database and never owns workflow state. | +| Command palette | It navigates, searches entities, and executes only safe commands. Sensitive actions open a dedicated contextual flow. | +| Architecture | Modular hybrid replacement inside the current application: reuse sound services, rebuild weak UI/workflows, merge duplicates, and remove obsolete surfaces. | + +## Goals + +1. Give each operator one clear, capability-appropriate place to work. +2. Replace feature sprawl with six stable domains and consistent page contracts. +3. Make urgent work visible without copying or diverging from source workflow state. +4. Enforce authorization, validation, transaction boundaries, error semantics, and audit behavior server-side. +5. Remove `/mod`, the legacy HK archive page, duplicate hubs, and manual navigation concepts that the new foundation owns. +6. Reach explicit functional, authorization, audit, localization, accessibility, and data-parity gates before cutover. +7. Keep rollback practical without exposing a mixed legacy/new experience. + +## Non-goals + +- Creating a separate HK application, microservice, or deployment. +- Creating a new assignment/task system for the operational inbox. +- Preserving every current page, route, component, or interaction. +- Adding backward-compatible redirects for removed administration subroutes. +- Providing full sensitive-workflow parity on phones. The target is desktop-first with usable tablet layouts. +- Redesigning public CMS or game-client experiences as part of this program. +- Replacing sound domain logic solely for architectural uniformity. + +## Architecture + +### Modular monolith + +The HK remains inside EpicNext CMS and uses the application's existing authentication, database, service, localization, and deployment infrastructure. + +The target source organization separates composition from behavior: + +```text +src/app/admin/ route composition only +src/features/housekeeping/ + foundation/ shell, registry, ACL context, preferences + domains/ + operations/ derived inbox, global search, recent work + people/ + content/ + economy/ + hotel/ + system/ +src/lib/services/ existing and extracted domain services +``` + +The exact filenames are an implementation-plan concern, but the boundaries are mandatory: + +- App Router files compose pages and bind route parameters; they do not own business rules. +- The foundation owns cross-cutting HK behavior and does not mutate domain data. +- Each domain owns its queries, commands, search providers, inbox providers, widgets, and page composition. +- Domains do not import another domain's UI internals. Cross-domain interaction uses registered contracts or links to the owning route. +- Existing reliable services are adapted behind domain contracts rather than copied into the new UI. + +### Module manifest and registry + +Every domain exports a manifest with stable identifiers for: + +- domain metadata and localized labels; +- routes and contextual navigation; +- required capabilities; +- command-palette entries; +- entity-search providers; +- derived-inbox sources; +- mandatory and optional dashboard widgets. + +The foundation composes these manifests into the rail, contextual navigation, palette, dashboard, and route metadata. Contract tests reject duplicate IDs, duplicate routes, missing localization keys, unknown capability slugs, and commands without an owner. + +The manifest registry replaces hand-maintained duplication between the sidebar, hubs, search, and dashboards. It is code-owned and reviewable. Operator preferences can alter presentation only within what the registry and capability context permit. + +### Capability context + +The server creates one request-scoped capability context from the authenticated operator and the existing ACL source. + +- Capability checks are based on effective permission slugs. +- Super-administrator behavior remains explicit and testable. +- Rank may help choose default presentation, but never grants access by itself. +- Navigation filtering is a usability feature, not an authorization boundary. +- Every query and command rechecks its capability on the server and defaults to deny. + +## Functional domains + +| Domain | Owns | Representative current areas | +| --- | --- | --- | +| Da fare & operations | Derived inbox, global search, recent work, favorites, operational summaries | Dashboard, selected alerts and cross-domain counts; projections only | +| People & community | Users, online state, accounts, guilds, applications, staff directory, moderation, support | Users, multi-accounts, guilds, applications, CFH, moderation actions, bans, IP/VPN, word filter, tickets, help tickets, `/mod/*` | +| Content & engagement | Public/editorial content and engagement workflows | Articles, photos, media, banners, ads, events, polls, help content, tags, prefixes, writable boxes, email content, branding/localization surfaces | +| Economy & catalog | Products, value, commercial assets, and economic history | Catalog, items, import/maintenance, shop, marketplace, transactions, vouchers, subscriptions, rare values, badges, achievements, sounds | +| Hotel & world | Live hotel surfaces and world-management tools | Rooms, navigator, radio, studio/runtime asset tools, contextual hotel actions | +| System, access & observability | Configuration, authorization, diagnostics, and privileged operations | Permissions, access audit, settings, maintenance, emulator, command center, logs, analytics, alerts, DevOps | + +Where an existing feature spans two domains, responsibility follows the action rather than the old route. For example, the staff directory belongs to People, while the policy granting staff capabilities belongs to System and Access. + +Domain landing pages summarize their own workflows. They do not recreate the global dashboard or become a second source of state. + +## Operator experience + +### Command Deck shell + +The shared shell contains: + +1. A compact rail for the six domains. +2. Contextual navigation generated from the active domain manifest. +3. A global command/search field available by keyboard. +4. A main workspace using consistent title, context, primary action, filters, content, and feedback regions. +5. Operator identity, effective-capability context, notifications, and session controls. + +The shell is desktop-first, fully keyboard operable, and responsive for tablets. Phone layouts may support inspection and low-risk triage, but sensitive multi-step operations are not optimized for phone use. + +### Adaptive dashboard + +ACL and capability data determine: + +- visible domains and routes; +- mandatory queues and warnings; +- permitted metrics and widgets; +- available commands and search providers. + +The operator may: + +- pin allowed routes and safe commands; +- reorder shortcuts and optional widgets; +- add or remove optional allowed widgets; +- persist preferred filters and presentation density where supported. + +The operator may not hide mandatory warnings, reveal unauthorized data, or preserve a shortcut after its required capability is lost. + +Preferences are server-persisted, user-scoped, schema-versioned, and non-authoritative. If no suitable existing preference store exists, the foundation adds one additive `housekeeping_user_preferences` store containing presentation state only. It never stores task status or authorization decisions. Every preference is reconciled with the current manifest and capability context when read. + +### Standard page contract + +Every target page follows the same structural contract: + +- localized title, description, breadcrumb/context, and one clear primary action; +- capability-derived actions with server authorization; +- shared filtering, pagination, empty, loading, partial, and error states; +- explicit unsaved-change behavior for editable forms; +- consistent confirmation and outcome feedback; +- stable deep links to owned entities and workflows; +- responsive table-to-detail behavior without hiding critical fields; +- audit context for mutations. + +## Operational inbox + +The inbox is a read model over domain-owned sources such as tickets, CFH reports, alerts, emulator errors, and detected anomalies. + +Each source emits normalized work items containing at least: + +- stable source and item IDs; +- domain and required capability; +- severity and source timestamp; +- localized summary and optional context; +- stable destination route and entity target; +- deduplication key; +- freshness/availability metadata. + +The aggregator: + +1. Requests sources independently with bounded timeouts. +2. Filters every result against the operator's capability context. +3. Deduplicates by stable source identity. +4. Orders by severity, age, and domain policy. +5. Returns both items and per-source availability. + +The aggregator never creates, assigns, dismisses, or completes work. Selecting an item opens the owning workflow. If that workflow supports assignment or resolution, those state changes occur there. + +A failed or timed-out source does not erase successful sources. The UI labels the missing source and the freshness of remaining data instead of presenting the whole system as healthy. + +## Global search and command palette + +The palette has three provider types: + +1. **Navigation providers** for permitted routes and favorites. +2. **Entity providers** for capability-filtered entities such as users, rooms, tickets, articles, or catalog entries. +3. **Safe command providers** for narrowly scoped, validated, idempotent or reversible actions. + +A mutation may run directly from the palette only when it is single-target, low impact, reviewable in the palette, protected by a specific capability, and safe against duplicate submission. It still uses the normal server command and audit path. + +Destructive, economic, moderation, permission, bulk, or otherwise sensitive actions return a navigation intent. The target page receives validated context and shows impact, current state, required reason, confirmation, and final outcome. + +## Data and command flow + +### Queries + +```text +page or shell + -> request-scoped capability context + -> typed domain query + -> existing API/repository through an adapter + -> sanitized response +``` + +The UI does not query arbitrary tables or reproduce sensitive filter rules. Authorization-sensitive results are filtered at the query boundary. Short-lived caching may be used for operational counts, but authorization is applied after cache lookup and sensitive per-user results are not shared across capability contexts. + +### Commands + +```text +intent + -> server capability check + -> schema validation + -> current-state/concurrency check + -> domain transaction or controlled external call + -> audit outcome + -> typed result and cache invalidation +``` + +Every command receives a server-issued action ID used as an idempotency key. Duplicate submissions return the original known outcome rather than repeating the mutation. + +For records with a revision or update timestamp, edits use optimistic concurrency. A stale edit returns a conflict result and current-state reference; it is not silently overwritten. Where a source cannot expose a revision, the command performs the strongest available transactional re-read before mutation. + +## Security and audit + +- Default-deny server checks protect every query and command. +- Sensitive actions require a dedicated flow, an explicit target, an impact summary, confirmation, and a non-empty operator reason. +- Domain validation occurs after authorization and before mutation. +- Audit is append-only from the HK application: no HK route can edit or delete audit events. +- Audit records include actor, target, command, reason, sanitized before/after details where appropriate, outcome, timestamp, action ID, and correlation ID. +- Secrets, credentials, tokens, and unnecessary personal data are excluded from audit payloads. +- When data and audit share a transactional store, a privileged mutation and its audit record commit together. +- For external operations, an intent/pending audit record is written before dispatch and completed with success or failure afterward. +- A privileged mutation fails closed if its required audit trail cannot be established. + +## Error model + +Domain boundaries return typed outcomes rather than leaking raw infrastructure errors: + +- validation failure; +- authentication required; +- capability denied; +- not found; +- stale/conflicting state; +- dependency unavailable; +- partial aggregate result; +- unexpected internal failure. + +Expected outcomes have localized, actionable messages. Unexpected failures expose a correlation ID to the operator and retain technical detail only in server logs. Forms preserve safe input after recoverable failures. Lists and the operational dashboard distinguish empty results from unavailable data. + +## Migration inventory + +The first subproject creates a committed migration matrix covering all 137 current pages. Each row contains: + +- legacy path and source surface (`admin` or `mod`); +- target domain and owning workflow; +- target path; +- decision: `REHOST`, `REBUILD`, `MERGE`, or `REMOVE`; +- required read and mutation capabilities; +- source queries and mutations; +- audit requirement; +- localization and accessibility status; +- required unit, integration, and E2E coverage; +- parity evidence and migration status. + +Decision meanings: + +- **REHOST**: the current UI and service are sound enough to enter the new shell after contract and ACL adaptation. +- **REBUILD**: preserve the workflow and sound service logic, but reconstruct its interaction and page composition. +- **MERGE**: combine duplicated routes or variants into one owning workflow with contextual views. +- **REMOVE**: eliminate obsolete or foundation-owned behavior at cutover. + +Mandatory consolidations: + +- All 13 `/mod` pages merge into People and Community workflows. `/mod` does not redirect after cutover. +- `/admin/housekeeping` ceases to exist as a named feature. Useful comparison/export history moves into System, Access, and Audit. +- `/admin/permissions` remains the live policy editor under System and Access. +- Legacy dashboard, hub, and manual HK-navigation concepts are removed when their responsibilities are supplied by the registry and Command Deck. + +No page is considered migrated merely because it renders in the new shell. Its matrix row closes only after data, actions, capability behavior, audit, localization, accessibility, and required tests pass. + +## Delivery decomposition + +This master design controls the program. For delivery purposes it is also the approved design specification for subproject 01. Subprojects 02 through 06 require their own scoped design specifications before their implementation plans. Subprojects are delivered in this order: + +### 01. Inventory & Foundation + +This is the first and only scope of the initial implementation plan. + +Deliverables: + +- the complete 137-page migration matrix; +- HK manifest contracts and registry validation; +- request-scoped capability context and server guard interfaces; +- domain query, command, search, inbox, and widget contracts; +- the Command Deck shell primitives and standard page-state contract; +- six domain manifests with no migrated business workflow yet; +- a non-production/test-only entry mechanism that cannot expose a mixed HK to normal production operators; +- contract, capability, localization-key, accessibility-smoke, and shell tests. + +Explicit exclusions: + +- no current `/admin` or `/mod` route changes; +- no production operator exposure; +- no operational inbox aggregation; +- no entity search implementation; +- no domain mutation migration; +- no legacy deletion. + +### 02. Access, audit & system core + +Implement the capability enforcement adapters, audit command path, error taxonomy, correlation IDs, and core observability used by every later vertical. + +### 03. People, moderation & support + +Deliver the first complete vertical and unify user, ticket, CFH, moderation-action, and ban workflows. This vertical proves the future removal of `/mod` without exposing a partial cutover. + +### 04. Command Deck operations + +Implement global search, safe commands, favorites, preferences, and the derived inbox against the sources available from completed verticals. + +### 05. Remaining domain verticals + +Deliver separate scoped specifications and plans for: + +1. Content and Engagement; +2. Hotel and World; +3. Economy and Catalog; +4. remaining System, Access, and Observability pages. + +Economy and permission-affecting mutations receive the strictest confirmation, concurrency, and audit coverage. + +### 06. Parity, cutover & cleanup + +Close the migration matrix, run cross-role journeys and data comparisons, switch `/admin`, make `/mod` unreachable, observe the release, then delete unreachable legacy code and later remove obsolete schema safely. + +Subproject 01 uses this specification; every later subproject has its own spec, implementation plan, tests, review, and completion gate. A later subproject may not silently expand an earlier approved scope. + +## Verification strategy + +Every subproject runs proportionate checks from these layers: + +1. **Unit tests** for manifest parsing, normalizers, policy functions, reducers, and domain services. +2. **Contract tests** for unique IDs/routes, capability declarations, localization keys, command ownership, and provider behavior. +3. **Integration tests** against representative repository/API implementations, including transactions, external failures, idempotency, and conflicts. +4. **ACL matrix tests** covering permitted, denied, capability-revoked, and super-administrator cases at both render and server boundaries. +5. **E2E journeys** for moderation, support, editorial, economy, hotel operations, and administration roles defined by capabilities rather than rank labels. +6. **Audit assertions** after every tested mutation. +7. **Accessibility checks** for keyboard use, focus order, names, contrast, live feedback, dialogs, and table/detail transitions. +8. **Localization checks** rejecting new hard-coded operator copy and missing translation keys. +9. **Visual regression checks** for the shared shell and high-risk standard states. +10. **Performance comparison** against a recorded legacy baseline using the same environment and dataset. Comparable new flows may not regress median or p95 response time by more than 10% without an explicit reviewed exception. Performance improvements are reported only from measurements. + +## Cutover gate + +The atomic switch is permitted only when all of the following are true: + +- all 137 migration rows are closed with evidence; +- every exposed query and command has a declared and tested capability; +- every mutation has validation and required audit coverage; +- no blocking or critical defect remains open; +- equivalent legacy/new counts and records have been compared for migrated read workflows; +- role journeys for moderator, support operator, editor, economy operator, hotel operator, and administrator pass; +- localization, accessibility, build, type, lint, test, and visual checks pass; +- production-like smoke tests, backup verification, rollback procedure, and health checks have been rehearsed; +- the new HK is not dependent on legacy UI routes; +- communication and operator runbooks are ready for the clean break. + +## Cutover and rollback + +Before cutover, the new HK is exercised through test/staging or an explicit non-production mechanism. Read-only shadow comparisons may run against representative data. There is no production dual-write. + +At cutover: + +1. `/admin` changes to the new route composition in one release/flag transition. +2. `/mod` and removed legacy subroutes become unreachable without compatibility redirects. +3. Smoke tests verify authentication, capability filtering, representative reads, one controlled mutation per risk class, audit, and health signals. + +Database changes required before cutover are additive and backward-compatible for the emergency rollback window. A flag or previous release can temporarily restore the legacy application if the cutover fails. During normal operation, only one HK is exposed. + +After the agreed stability window, unreachable legacy code and flags are removed. Destructive schema cleanup is a later migration and is not coupled to the cutover release. + +## Success criteria + +The program is complete when: + +- `/admin` is the single role-adaptive administration surface; +- `/mod` and the legacy Housekeeping archive surface are gone; +- all 137 legacy pages have an evidenced migration decision; +- all exposed data, navigation, commands, widgets, and inbox items are capability-correct; +- the operational inbox derives live work without owning duplicate workflow state; +- all mutations use the domain command, validation, concurrency, idempotency, and audit path appropriate to their risk; +- no mixed legacy/new production experience exists; +- measured performance meets the approved comparison gate; +- rollback and eventual legacy cleanup are complete. + +## Rejected alternatives + +### Full greenfield rewrite + +Rejected because it would discard reliable existing services and maximize parity, timing, and regression risk across 137 pages. + +### Cosmetic refactor of the existing HK + +Rejected because it would preserve duplicated `/admin` and `/mod` workflows, inconsistent page boundaries, and manual navigation debt. + +### Separate HK service/application + +Rejected because the current requirement does not justify another deployment, authentication boundary, or distributed consistency problem. + +### Persistent cross-domain task database + +Rejected because it would duplicate ticket, moderation, alert, and anomaly state and create reconciliation failure modes. + +## Final design invariant + +The migration may be incremental internally, but the operator-facing product is not. Until the cutover gate passes, the current HK remains the only normal production surface. After cutover, the new HK is the only surface. -- 2.54.0 From 3f10db41dbb80a7a8399cc8e7adb059d14cdfb06 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 24 Aug 2026 20:50:21 +0200 Subject: [PATCH 02/29] docs: plan housekeeping inventory foundation --- ...08-24-housekeeping-inventory-foundation.md | 1401 +++++++++++++++++ 1 file changed, 1401 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-24-housekeeping-inventory-foundation.md diff --git a/docs/superpowers/plans/2026-08-24-housekeeping-inventory-foundation.md b/docs/superpowers/plans/2026-08-24-housekeeping-inventory-foundation.md new file mode 100644 index 00000000..171b4b62 --- /dev/null +++ b/docs/superpowers/plans/2026-08-24-housekeeping-inventory-foundation.md @@ -0,0 +1,1401 @@ +# Housekeeping Inventory & Foundation 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:** Produce the complete 137-page HK migration inventory and a tested, non-production Command Deck foundation without changing the current `/admin` or `/mod` experience. + +**Architecture:** Add a modular `src/features/housekeeping` boundary containing migration evidence, capability-aware contracts, a validated domain registry, shell primitives, and a non-production preview. Existing authentication and ACL data remain authoritative through `getAdminContext()`; no business workflow is migrated in this subproject. + +**Tech Stack:** Node.js `>=26.7.0 <27`, pnpm `11.22.0`, Next.js `16.3.2`, React `19.2.8`, TypeScript `7.0.2`, Vitest `4.1.11`, Zod `4.4.3`, next-intl `4.13.7`, Tailwind CSS `4.3.3`, Biome `2.5.9`. + +**Spec:** `docs/superpowers/specs/2026-08-24-housekeeping-modernization-design.md` + +## Global Constraints + +- Work directly in `E:\Users\simol\Desktop\EpicNext-cms`; do not create a Git worktree or another checkout. +- Preserve all unrelated tracked, deleted, untracked, and committed local work. Stage only the files named by the current task. +- Pull from the configured canonical upstream before execution. This repository has `origin/main` and no `origin/dev`. +- Keep the current `/admin` and `/mod` route behavior unchanged throughout this subproject. +- Do not expose the preview in production. `NODE_ENV=production` must disable it even if the preview flag is set. +- Do not add operational inbox aggregation, entity search, command execution, domain mutations, preferences persistence, redirects, legacy deletion, or database migrations. +- Reuse `getAdminContext()` and the existing `PermissionSet`; do not add rank thresholds or duplicate ACL queries. +- Rank may influence presentation defaults only. Effective capability slugs determine visibility and access. +- Use only semantic `--admin-*` theme tokens for HK chrome. Do not add hard-coded Tailwind palette colors to ordinary HK UI. +- Put source copy under `pages.housekeeping` in `src/messages/en.json` and `src/messages/it.json`; all other locales use the repository's English fallback. +- Keep files focused. Foundation modules must not import `@/lib/db`, `@/actions/*`, or domain page modules. +- Use TDD: observe the requested failing assertion before writing its implementation, then run the narrow test before widening validation. +- Use CRLF-compatible edits and run `git diff --check` before every commit. + +## Locked File Structure + +```text +src/features/housekeeping/ + migration/ + types.ts migration row and evidence types + discover-legacy-pages.ts filesystem-to-route discovery + validate-matrix.ts structural and coverage validation + operations.ts `/admin` dashboard row + people.ts people, support, moderation, all `/mod` rows + content.ts editorial and engagement rows + economy.ts economy and catalog rows + hotel.ts hotel, radio, rooms, studio rows + system.ts configuration, ACL, logs, DevOps rows + matrix.ts aggregate export only + *.test.ts per-domain audit contracts + foundation/ + contracts/ + capability.ts capability requirements + result.ts typed success/error outcomes + domain.ts domain and route manifests + query.ts query contract + command.ts command metadata/runtime contract + search.ts search provider contract + inbox.ts derived work-item/source contract + widget.ts dashboard widget contract + index.ts public contract barrel + capability-context.ts pure capability helpers + server-capability-context.ts `getAdminContext()` adapter + registry.ts manifest validation and visibility + navigation.ts registry-to-shell navigation model + preview-gate.ts non-production flag policy + shell/ + housekeeping-shell.tsx Command Deck frame + domain-rail.tsx six-domain primary navigation + context-nav.tsx active-domain navigation + command-trigger.tsx disabled phase-01 command affordance + operator-summary.tsx authenticated actor summary + page/ + housekeeping-page-shell.tsx standard title/action/content frame + housekeeping-page-state.tsx loading/empty/partial/error states + domains/ + operations/manifest.ts + people/manifest.ts + content/manifest.ts + economy/manifest.ts + hotel/manifest.ts + system/manifest.ts + manifests.ts aggregate of the six manifests +src/app/admin-next/ + layout.tsx global preview gate + page.tsx first-visible-domain redirect + [domain]/layout.tsx capability-filtered Command Deck + [domain]/page.tsx foundation-only domain placeholder +scripts/verify-housekeeping-matrix.ts +``` + +The feature folder contains no database access and no migrated business workflow. The only route added is `/admin-next`, which returns 404 outside an explicitly enabled non-production environment. + +--- + +### Task 1: Legacy page discovery and migration schema + +**Files:** +- Create: `src/features/housekeeping/migration/types.ts` +- Create: `src/features/housekeeping/migration/discover-legacy-pages.ts` +- Create: `src/features/housekeeping/migration/validate-matrix.ts` +- Test: `src/features/housekeeping/migration/discover-legacy-pages.test.ts` +- Test: `src/features/housekeeping/migration/validate-matrix.test.ts` + +**Interfaces:** +- Consumes: filesystem roots `src/app/admin` and `src/app/mod`. +- Produces: `discoverLegacyPages(rootDir?: string): LegacyPage[]`, `ownedLegacyPages(prefixes: readonly string[], pages?: readonly LegacyPage[]): LegacyPage[]`, `validateMigrationEntries(discovered: readonly LegacyPage[], entries: readonly MigrationEntry[]): string[]`, and the migration types used by Tasks 2-7. + +- [ ] **Step 1: Write the failing discovery tests** + +```ts +import { describe, expect, it } from "vitest"; +import { discoverLegacyPages } from "./discover-legacy-pages"; + +describe("discoverLegacyPages", () => { + it("discovers the exact legacy administration inventory", () => { + const pages = discoverLegacyPages(); + expect(pages).toHaveLength(137); + expect(pages).toContainEqual({ + surface: "admin", + legacyPath: "/admin/users/:id/edit", + sourceFile: "src/app/admin/users/[id]/edit/page.tsx", + }); + expect(pages).toContainEqual({ + surface: "mod", + legacyPath: "/mod/cfh/:id", + sourceFile: "src/app/mod/cfh/[id]/page.tsx", + }); + }); + + it("sorts by surface, route, then source file", () => { + const pages = discoverLegacyPages(); + expect(pages).toEqual([...pages].sort((a, b) => + `${a.surface}:${a.legacyPath}:${a.sourceFile}`.localeCompare( + `${b.surface}:${b.legacyPath}:${b.sourceFile}`, + ), + )); + }); +}); +``` + +- [ ] **Step 2: Run the discovery test and confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/discover-legacy-pages.test.ts` + +Expected: FAIL because `discover-legacy-pages.ts` does not exist. + +- [ ] **Step 3: Define the exact migration types** + +```ts +export const HOUSEKEEPING_DOMAIN_IDS = [ + "operations", + "people", + "content", + "economy", + "hotel", + "system", +] as const; + +export type HousekeepingDomainId = (typeof HOUSEKEEPING_DOMAIN_IDS)[number]; +export type LegacySurface = "admin" | "mod"; +export type MigrationDecision = "REHOST" | "REBUILD" | "MERGE" | "REMOVE"; +export type AuditRequirement = "NONE" | "MUTATION" | "PRIVILEGED_MUTATION"; +export type AuditState = "UNAUDITED" | "PARTIAL" | "COMPLETE"; +export type MigrationStatus = "PLANNED" | "IN_PROGRESS" | "VERIFIED" | "REMOVED"; + +export interface LegacyPage { + surface: LegacySurface; + legacyPath: string; + sourceFile: string; +} + +export interface MigrationEntry extends LegacyPage { + targetDomain: HousekeepingDomainId; + targetPath: string | null; + decision: MigrationDecision; + capabilities: { read: readonly string[]; mutate: readonly string[] }; + dependencies: { queries: readonly string[]; mutations: readonly string[] }; + auditRequirement: AuditRequirement; + localization: AuditState; + accessibility: AuditState; + requiredTests: readonly ("unit" | "integration" | "e2e" | "visual")[]; + parityEvidence: readonly string[]; + status: MigrationStatus; + notes: readonly string[]; +} +``` + +- [ ] **Step 4: Implement deterministic route discovery** + +Use `readdirSync(..., { withFileTypes: true })`, recurse only below the two locked roots, retain `page.tsx`, remove route-group segments such as `(group)`, and convert `[id]` to `:id` and `[...slug]` to `:slug*`. Normalize every file path with `/` separators before sorting. + +```ts +export function discoverLegacyPages(rootDir = process.cwd()): LegacyPage[] { + return (["admin", "mod"] as const) + .flatMap((surface) => discoverSurface(rootDir, surface)) + .sort((a, b) => + `${a.surface}:${a.legacyPath}:${a.sourceFile}`.localeCompare( + `${b.surface}:${b.legacyPath}:${b.sourceFile}`, + ), + ); +} + +export function ownedLegacyPages( + prefixes: readonly string[], + pages: readonly LegacyPage[] = discoverLegacyPages(), +): LegacyPage[] { + return pages.filter((page) => prefixes.some((prefix) => + page.legacyPath === prefix || page.legacyPath.startsWith(`${prefix}/`), + )); +} +``` + +- [ ] **Step 5: Write the failing validator tests** + +```ts +const page = (legacyPath: string): LegacyPage => ({ + surface: legacyPath.startsWith("/mod") ? "mod" : "admin", + legacyPath, + sourceFile: `src/app${legacyPath}/page.tsx`, +}); + +const entry = (legacyPath: string): MigrationEntry => ({ + ...page(legacyPath), + targetDomain: "system", + targetPath: "/admin/system/example", + decision: "REHOST", + capabilities: { read: ["admin.dashboard"], mutate: [] }, + dependencies: { queries: [], mutations: [] }, + auditRequirement: "NONE", + localization: "COMPLETE", + accessibility: "COMPLETE", + requiredTests: ["unit"], + parityEvidence: [], + status: "PLANNED", + notes: [], +}); + +it("reports missing, duplicate, and unknown legacy rows", () => { + const discovered = [page("/admin"), page("/admin/users")]; + const issues = validateMigrationEntries(discovered, [ + entry("/admin"), + entry("/admin"), + entry("/admin/ghost"), + ]); + expect(issues).toEqual([ + "duplicate legacyPath: /admin", + "missing legacyPath: /admin/users", + "unknown legacyPath: /admin/ghost", + ]); +}); + +it("rejects incomplete decisions", () => { + const issues = validateMigrationEntries([page("/admin")], [ + { ...entry("/admin"), targetPath: null, decision: "REBUILD" }, + ]); + expect(issues).toContain("REBUILD requires targetPath: /admin"); +}); +``` + +- [ ] **Step 6: Implement structural validation** + +Validation must return sorted strings and enforce: + +- one entry for every discovered page and no unknown entry; +- `targetPath === null` only for `REMOVE`; +- non-`REMOVE` targets start with `/admin/`; +- `MERGE` and `REBUILD` declare at least one required test; +- mutation dependencies require mutation capabilities and non-`NONE` audit; +- `VERIFIED` requires non-empty parity evidence; +- no string field contains `TBD`, `TODO`, `FIXME`, or `UNCLASSIFIED`. + +- [ ] **Step 7: Run the narrow tests** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/discover-legacy-pages.test.ts src/features/housekeeping/migration/validate-matrix.test.ts` + +Expected: PASS with 137 discovered pages. + +- [ ] **Step 8: Commit Task 1** + +```powershell +git add src/features/housekeeping/migration/types.ts src/features/housekeeping/migration/discover-legacy-pages.ts src/features/housekeeping/migration/validate-matrix.ts src/features/housekeeping/migration/discover-legacy-pages.test.ts src/features/housekeeping/migration/validate-matrix.test.ts +git diff --cached --check +git commit -m "test: define housekeeping migration inventory" +``` + +### Task 2: Operations and People migration audit + +**Files:** +- Create: `src/features/housekeeping/migration/operations.ts` +- Create: `src/features/housekeeping/migration/people.ts` +- Test: `src/features/housekeeping/migration/operations.test.ts` +- Test: `src/features/housekeeping/migration/people.test.ts` + +**Interfaces:** +- Consumes: `discoverLegacyPages()`, `MigrationEntry`, and `validateMigrationEntries()` from Task 1. +- Produces: `operationsMigrationEntries` and `peopleMigrationEntries` with one reviewed row for every owned page. + +Owned prefixes are exact: + +```ts +const PEOPLE_PREFIXES = [ + "/admin/users", "/admin/online", "/admin/guilds", "/admin/applications", + "/admin/teams", "/admin/bans", "/admin/ip", "/admin/vpn", + "/admin/wordfilter", "/admin/moderation", "/admin/tickets", + "/admin/help-tickets", "/mod", +] as const; +``` + +Operations owns only `/admin` in this subproject. + +- [ ] **Step 1: Write failing ownership tests** + +```ts +it("covers the legacy dashboard once", () => { + expect(operationsMigrationEntries).toHaveLength(1); + expect(operationsMigrationEntries[0]).toMatchObject({ + legacyPath: "/admin", + targetDomain: "operations", + targetPath: "/admin/work", + decision: "REBUILD", + status: "PLANNED", + }); +}); + +it("merges every mod page into a people workflow", () => { + const modRows = peopleMigrationEntries.filter((row) => row.surface === "mod"); + expect(modRows).toHaveLength(13); + expect(modRows.every((row) => row.decision === "MERGE")).toBe(true); + expect(modRows.every((row) => row.targetPath?.startsWith("/admin/people/"))) + .toBe(true); +}); +``` + +- [ ] **Step 2: Run the tests and confirm missing audit modules** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/operations.test.ts src/features/housekeeping/migration/people.test.ts` + +Expected: FAIL because both entry arrays are missing. + +- [ ] **Step 3: Audit Operations** + +Read `src/app/admin/page.tsx` and record its real Drizzle query dependencies, current localization state, current accessibility state, and required parity checks. Use this locked decision: + +```ts +{ + surface: "admin", + legacyPath: "/admin", + sourceFile: "src/app/admin/page.tsx", + targetDomain: "operations", + targetPath: "/admin/work", + decision: "REBUILD", + capabilities: { read: [PERMS.ADMIN_DASHBOARD], mutate: [] }, + dependencies: { + queries: ["users", "active bans", "website articles", "staff activities"], + mutations: [], + }, + auditRequirement: "NONE", + localization: "COMPLETE", + accessibility: "PARTIAL", + requiredTests: ["integration", "e2e", "visual"], + parityEvidence: [], + status: "PLANNED", + notes: ["Replace metric dashboard with capability-derived operational home"], +} +``` + +- [ ] **Step 4: Audit People route groups in small batches** + +Review these batches separately so every row names actual imported actions/services: + +1. users, multi-account aliases, online, guilds, applications; +2. teams, moderation, CFH, bans, IP, VPN, word filter; +3. tickets and help tickets; +4. all 13 `/mod` pages. + +Primary `/admin` workflows may be `REHOST` only when their current UI, ACL, localization, and service boundary are already sound. Duplicate show/edit aliases and every `/mod` route are `MERGE`. Targets use `/admin/people/users`, `/admin/people/community`, `/admin/people/staff`, `/admin/people/moderation`, or `/admin/people/support` plus stable `:id` suffixes. + +- [ ] **Step 5: Add per-domain validation** + +```ts +const expected = discoverLegacyPages().filter( + (page) => page.legacyPath === "/admin" || + PEOPLE_PREFIXES.some((prefix) => + page.legacyPath === prefix || page.legacyPath.startsWith(`${prefix}/`), + ), +); +expect(validateMigrationEntries(expected, [ + ...operationsMigrationEntries, + ...peopleMigrationEntries, +])).toEqual([]); +``` + +- [ ] **Step 6: Run and commit the audit** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/operations.test.ts src/features/housekeeping/migration/people.test.ts` + +Expected: PASS; exactly 13 `mod` rows are `MERGE`. + +```powershell +git add src/features/housekeeping/migration/operations.ts src/features/housekeeping/migration/people.ts src/features/housekeeping/migration/operations.test.ts src/features/housekeeping/migration/people.test.ts +git diff --cached --check +git commit -m "docs: audit housekeeping people workflows" +``` + +### Task 3: Content and Engagement migration audit + +**Files:** +- Create: `src/features/housekeeping/migration/content.ts` +- Test: `src/features/housekeeping/migration/content.test.ts` + +**Interfaces:** +- Consumes: Task 1 discovery and validation. +- Produces: `contentMigrationEntries` for every path below the locked prefixes. + +```ts +const CONTENT_PREFIXES = [ + "/admin/articles", "/admin/photos", "/admin/media", "/admin/banners", + "/admin/ads", "/admin/events", "/admin/polls", "/admin/help-questions", + "/admin/tags", "/admin/prefixes", "/admin/writeable-boxes", + "/admin/email-templates", "/admin/theme", "/admin/favicon", + "/admin/translations", +] as const; +``` + +- [ ] **Step 1: Write the failing coverage and target tests** + +```ts +it("covers every content page without legacy-domain targets", () => { + const expected = ownedLegacyPages(CONTENT_PREFIXES); + expect(validateMigrationEntries(expected, contentMigrationEntries)).toEqual([]); + expect(contentMigrationEntries.every((row) => + row.targetPath === null || row.targetPath.startsWith("/admin/content/"), + )).toBe(true); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/content.test.ts` + +Expected: FAIL because `contentMigrationEntries` is missing. + +- [ ] **Step 3: Audit editorial and media batches** + +Inspect collection, create, and detail pages for articles, photos, media, banners, and ads. Record actual action/service imports in `dependencies`; preserve `:id` in target paths. Classify duplicate create/detail variants as `MERGE` only when one target workflow owns the same state. + +- [ ] **Step 4: Audit engagement and help-content batches** + +Inspect events, polls, help questions, tags, prefixes, writable boxes, and email templates. Targets use `/admin/content/editorial`, `/admin/content/media`, `/admin/content/engagement`, or `/admin/content/help`. + +- [ ] **Step 5: Audit brand and localization batches** + +Inspect theme, favicon, and translations. Targets use `/admin/content/brand` and `/admin/content/localization`. Mark mutation pages `MUTATION` or `PRIVILEGED_MUTATION` based on whether they affect global runtime configuration. + +- [ ] **Step 6: Run and commit the audit** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/content.test.ts` + +Expected: PASS with every content-owned legacy page represented once. + +```powershell +git add src/features/housekeeping/migration/content.ts src/features/housekeeping/migration/content.test.ts +git diff --cached --check +git commit -m "docs: audit housekeeping content workflows" +``` + +### Task 4: Economy and Catalog migration audit + +**Files:** +- Create: `src/features/housekeeping/migration/economy.ts` +- Test: `src/features/housekeeping/migration/economy.test.ts` + +**Interfaces:** +- Consumes: Task 1 discovery and validation. +- Produces: `economyMigrationEntries` for the locked economy prefixes. + +```ts +const ECONOMY_PREFIXES = [ + "/admin/catalog", "/admin/items", "/admin/shop", "/admin/marketplace", + "/admin/transactions", "/admin/vouchers", "/admin/subscriptions", + "/admin/rare-values", "/admin/badges", "/admin/achievements", + "/admin/sounds", "/admin/calendar", +] as const; +``` + +- [ ] **Step 1: Write the failing economy audit test** + +```ts +it("requires privileged audit for economy mutations", () => { + const mutationRows = economyMigrationEntries.filter( + (row) => row.dependencies.mutations.length > 0, + ); + expect(mutationRows.length).toBeGreaterThan(0); + expect(mutationRows.every( + (row) => row.auditRequirement === "PRIVILEGED_MUTATION", + )).toBe(true); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/economy.test.ts` + +Expected: FAIL because the audit module is missing. + +- [ ] **Step 3: Audit catalog and item workflows** + +Inspect catalog collection/detail/builder-club/maintenance and item collection/detail pages. Record direct database access separately from service/action dependencies. Target `/admin/economy/catalog` and `/admin/economy/items`; mark overlapping editors `MERGE` when they mutate the same catalog entity. + +- [ ] **Step 4: Audit commerce and value workflows** + +Inspect shop, marketplace, transactions, vouchers, subscriptions, rare values, badges, achievements, sounds, and calendar. Use `/admin/economy/commerce`, `/admin/economy/history`, `/admin/economy/value`, and `/admin/economy/rewards` targets. + +- [ ] **Step 5: Validate the domain inventory** + +```ts +expect(validateMigrationEntries( + ownedLegacyPages(ECONOMY_PREFIXES), + economyMigrationEntries, +)).toEqual([]); +``` + +- [ ] **Step 6: Run and commit the audit** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/economy.test.ts` + +Expected: PASS and every economy mutation row requires privileged audit. + +```powershell +git add src/features/housekeeping/migration/economy.ts src/features/housekeeping/migration/economy.test.ts +git diff --cached --check +git commit -m "docs: audit housekeeping economy workflows" +``` + +### Task 5: Hotel and World migration audit + +**Files:** +- Create: `src/features/housekeeping/migration/hotel.ts` +- Test: `src/features/housekeeping/migration/hotel.test.ts` + +**Interfaces:** +- Consumes: Task 1 discovery and validation. +- Produces: `hotelMigrationEntries` for rooms, navigator, radio, and studio. + +```ts +const HOTEL_PREFIXES = [ + "/admin/rooms", "/admin/navigation", "/admin/radio", "/admin/studio", +] as const; +``` + +- [ ] **Step 1: Write the failing hotel coverage test** + +```ts +it("maps all hotel tools below the hotel target root", () => { + expect(validateMigrationEntries( + ownedLegacyPages(HOTEL_PREFIXES), + hotelMigrationEntries, + )).toEqual([]); + expect(hotelMigrationEntries.every((row) => + row.targetPath === null || row.targetPath.startsWith("/admin/hotel/"), + )).toBe(true); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/hotel.test.ts` + +Expected: FAIL because the hotel audit module is missing. + +- [ ] **Step 3: Audit rooms and navigator** + +Inspect room list/show/edit/furni paths and navigator management. Merge duplicate room detail aliases into `/admin/hotel/rooms/:id`; keep furni as `/admin/hotel/rooms/:id/furni`. + +- [ ] **Step 4: Audit radio and studio** + +Inspect all radio and studio pages, including API keys, monitoring, moderation, imports, sync, maintenance, and audit surfaces. Record external RCON/API/filesystem dependencies explicitly and mark credential or runtime mutation pages `PRIVILEGED_MUTATION`. + +- [ ] **Step 5: Run and commit the audit** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/hotel.test.ts` + +Expected: PASS with all owned pages represented once. + +```powershell +git add src/features/housekeeping/migration/hotel.ts src/features/housekeeping/migration/hotel.test.ts +git diff --cached --check +git commit -m "docs: audit housekeeping hotel workflows" +``` + +### Task 6: System audit and complete 137-row matrix + +**Files:** +- Create: `src/features/housekeeping/migration/system.ts` +- Create: `src/features/housekeeping/migration/matrix.ts` +- Create: `scripts/verify-housekeeping-matrix.ts` +- Modify: `package.json: scripts` +- Test: `src/features/housekeeping/migration/system.test.ts` +- Test: `src/features/housekeeping/migration/matrix.test.ts` + +**Interfaces:** +- Consumes: all migration arrays from Tasks 2-5. +- Produces: `HOUSEKEEPING_MIGRATION_MATRIX` and the `pnpm hk:matrix:check` CLI. + +```ts +const SYSTEM_PREFIXES = [ + "/admin/alerts", "/admin/analytics", "/admin/commandocentrum", + "/admin/devops", "/admin/emulator", "/admin/housekeeping", + "/admin/logs", "/admin/maintenance", "/admin/menu", + "/admin/permissions", "/admin/settings", +] as const; +``` + +- [ ] **Step 1: Write the failing system decisions** + +```ts +it("removes legacy foundation-owned pages", () => { + expect(systemMigrationEntries).toEqual(expect.arrayContaining([ + expect.objectContaining({ legacyPath: "/admin/housekeeping", decision: "REMOVE", targetPath: null }), + expect.objectContaining({ legacyPath: "/admin/menu", decision: "REMOVE", targetPath: null }), + ])); +}); + +it("keeps live permissions as a system workflow", () => { + expect(systemMigrationEntries).toContainEqual(expect.objectContaining({ + legacyPath: "/admin/permissions", + targetPath: "/admin/system/access/permissions", + decision: expect.not.stringMatching("REMOVE"), + })); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/migration/system.test.ts` + +Expected: FAIL because `systemMigrationEntries` is missing. + +- [ ] **Step 3: Audit system route groups** + +Inspect alerts, analytics, command center, DevOps, emulator, logs, maintenance, permissions, and settings. Targets use `/admin/system/access`, `/admin/system/configuration`, `/admin/system/observability`, or `/admin/system/operations`. Preserve the read-only value of the legacy HK export/comparison in the row notes even though its route decision is `REMOVE`. + +- [ ] **Step 4: Write the failing aggregate coverage test** + +```ts +it("covers all 137 legacy pages exactly once", () => { + const discovered = discoverLegacyPages(); + expect(HOUSEKEEPING_MIGRATION_MATRIX).toHaveLength(137); + expect(validateMigrationEntries( + discovered, + HOUSEKEEPING_MIGRATION_MATRIX, + )).toEqual([]); +}); + +it("contains no undecided evidence markers", () => { + expect(JSON.stringify(HOUSEKEEPING_MIGRATION_MATRIX)).not.toMatch( + /TBD|TODO|FIXME|UNCLASSIFIED/, + ); +}); +``` + +- [ ] **Step 5: Aggregate the six arrays without adding behavior** + +```ts +export const HOUSEKEEPING_MIGRATION_MATRIX = [ + ...operationsMigrationEntries, + ...peopleMigrationEntries, + ...contentMigrationEntries, + ...economyMigrationEntries, + ...hotelMigrationEntries, + ...systemMigrationEntries, +].sort((a, b) => a.legacyPath.localeCompare(b.legacyPath)); +``` + +- [ ] **Step 6: Add the CLI verifier** + +`scripts/verify-housekeeping-matrix.ts` calls `validateMigrationEntries(discoverLegacyPages(), HOUSEKEEPING_MIGRATION_MATRIX)`. It prints every returned issue to stderr and sets `process.exitCode = 1`; a valid matrix prints `Housekeeping migration matrix: 137/137 valid`. Add: + +```json +"hk:matrix:check": "tsx scripts/verify-housekeeping-matrix.ts" +``` + +- [ ] **Step 7: Run and commit the complete matrix** + +Run: + +```powershell +pnpm exec vitest run src/features/housekeeping/migration +pnpm hk:matrix:check +``` + +Expected: all migration tests PASS and CLI prints `137/137 valid`. + +```powershell +git add src/features/housekeeping/migration/system.ts src/features/housekeeping/migration/matrix.ts src/features/housekeeping/migration/system.test.ts src/features/housekeeping/migration/matrix.test.ts scripts/verify-housekeeping-matrix.ts package.json +git diff --cached --check +git commit -m "docs: complete housekeeping migration matrix" +``` + +### Task 7: Foundation contract types + +**Files:** +- Create: `src/features/housekeeping/foundation/contracts/capability.ts` +- Create: `src/features/housekeeping/foundation/contracts/result.ts` +- Create: `src/features/housekeeping/foundation/contracts/domain.ts` +- Create: `src/features/housekeeping/foundation/contracts/query.ts` +- Create: `src/features/housekeeping/foundation/contracts/command.ts` +- Create: `src/features/housekeeping/foundation/contracts/search.ts` +- Create: `src/features/housekeeping/foundation/contracts/inbox.ts` +- Create: `src/features/housekeeping/foundation/contracts/widget.ts` +- Create: `src/features/housekeeping/foundation/contracts/index.ts` +- Test: `src/features/housekeeping/foundation/contracts/contracts.test.ts` + +**Interfaces:** +- Consumes: `HousekeepingDomainId` from migration types only. +- Produces: every public foundation type used by Tasks 8-11. + +- [ ] **Step 1: Write failing constructor tests** + +```ts +it("creates typed success and error results", () => { + expect(ok({ count: 2 }, "corr-1")).toEqual({ + ok: true, + data: { count: 2 }, + correlationId: "corr-1", + }); + expect(fail("CAPABILITY_DENIED", "corr-2")).toEqual({ + ok: false, + error: { code: "CAPABILITY_DENIED" }, + correlationId: "corr-2", + }); +}); + +it("rejects an empty capability requirement", () => { + expect(() => anyCapability()).toThrow("capability requirement is empty"); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/foundation/contracts/contracts.test.ts` + +Expected: FAIL because the contract barrel does not exist. + +- [ ] **Step 3: Define capability and result contracts** + +```ts +export type CapabilityRequirement = + | { mode: "all"; slugs: readonly string[] } + | { mode: "any"; slugs: readonly string[] }; + +export interface HousekeepingActor { + id: number; + username: string; + rank: number; +} + +export interface HousekeepingCapabilityContext { + actor: HousekeepingActor; + isSuperAdmin: boolean; + has(slug: string): boolean; + hasAny(...slugs: string[]): boolean; + hasAll(...slugs: string[]): boolean; +} + +export type HousekeepingErrorCode = + | "VALIDATION_FAILED" | "AUTHENTICATION_REQUIRED" | "CAPABILITY_DENIED" + | "NOT_FOUND" | "CONFLICT" | "DEPENDENCY_UNAVAILABLE" + | "PARTIAL_RESULT" | "INTERNAL_ERROR"; +``` + +- [ ] **Step 4: Define domain, query, and command contracts** + +```ts +export interface HousekeepingRouteDefinition { + id: string; + labelKey: string; + href: string; + capability: CapabilityRequirement; + matchPrefixes?: readonly string[]; +} + +export interface HousekeepingDomainManifest { + id: HousekeepingDomainId; + labelKey: string; + descriptionKey: string; + iconId: "inbox" | "users" | "file-text" | "gem" | "hotel" | "settings"; + previewHref: `/admin-next/${HousekeepingDomainId}`; + capability: CapabilityRequirement; + routes: readonly HousekeepingRouteDefinition[]; +} + +export interface HousekeepingQuery { + id: string; + owner: HousekeepingDomainId; + capability: CapabilityRequirement; + run(context: HousekeepingCapabilityContext, input: I): Promise>; +} + +export interface HousekeepingCommand { + id: string; + owner: HousekeepingDomainId; + risk: "safe" | "sensitive"; + capability: CapabilityRequirement; + requiresReason: boolean; + execute(context: HousekeepingCapabilityContext, input: I): Promise>; +} +``` + +- [ ] **Step 5: Define search, inbox, and widget contracts** + +Use these exact public methods: + +- `HousekeepingSearchProvider.search(context, { term, limit }): Promise>` +- `HousekeepingInboxSource.getItems(context, signal): Promise>` +- `HousekeepingWidgetDefinition.load(context): Promise>` + +`HousekeepingWorkItem` contains `sourceId`, `itemId`, `deduplicationKey`, `domain`, `capability`, `severity`, `occurredAt`, `titleKey`, `context`, `href`, and `freshness`. No assignment, dismissal, completion, or task-owner field is permitted. + +- [ ] **Step 6: Run tests and typecheck the public barrel** + +Run: + +```powershell +pnpm exec vitest run src/features/housekeeping/foundation/contracts/contracts.test.ts +pnpm typecheck +``` + +Expected: PASS; no type or unused-export errors. + +- [ ] **Step 7: Commit Task 7** + +```powershell +git add src/features/housekeeping/foundation/contracts +git diff --cached --check +git commit -m "feat: define housekeeping foundation contracts" +``` + +### Task 8: Request-scoped capability context + +**Files:** +- Create: `src/features/housekeeping/foundation/capability-context.ts` +- Create: `src/features/housekeeping/foundation/server-capability-context.ts` +- Test: `src/features/housekeeping/foundation/capability-context.test.ts` +- Test: `src/features/housekeeping/foundation/server-capability-context.test.ts` + +**Interfaces:** +- Consumes: `PermissionSet`, `getAdminContext()`, and Task 7 capability contracts. +- Produces: `createHousekeepingCapabilityContext()`, `satisfiesCapability()`, and cached `getHousekeepingCapabilityContext()`. + +- [ ] **Step 1: Write pure failing capability tests** + +```ts +const actor: HousekeepingActor = { id: 42, username: "operator", rank: 7 }; + +const permissionSet = ( + slugs: readonly string[], + isSuperAdmin = false, +): PermissionSet => { + const granted = new Set(slugs); + const has = (slug: string) => isSuperAdmin || granted.has(slug); + return { + isSuperAdmin, + has, + hasAny: (...requested) => requested.some(has), + hasAll: (...requested) => requested.every(has), + }; +}; + +const superAdminPermissionSet = () => permissionSet([], true); + +it("evaluates any/all requirements from effective permission methods", () => { + const context = createHousekeepingCapabilityContext(actor, permissionSet([ + "admin.users.view", + "admin.tickets.view", + ])); + expect(satisfiesCapability(context, anyCapability("admin.users.view", "admin.logs.view"))).toBe(true); + expect(satisfiesCapability(context, allCapabilities("admin.users.view", "admin.logs.view"))).toBe(false); +}); + +it("keeps the super administrator explicit", () => { + const context = createHousekeepingCapabilityContext(actor, superAdminPermissionSet()); + expect(context.isSuperAdmin).toBe(true); + expect(context.has("unknown.future.slug")).toBe(true); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/foundation/capability-context.test.ts` + +Expected: FAIL because the helper module is missing. + +- [ ] **Step 3: Implement the pure adapter** + +```ts +export function createHousekeepingCapabilityContext( + actor: HousekeepingActor, + permissions: PermissionSet, +): HousekeepingCapabilityContext { + return { + actor, + isSuperAdmin: permissions.isSuperAdmin, + has: (slug) => permissions.has(slug), + hasAny: (...slugs) => permissions.hasAny(...slugs), + hasAll: (...slugs) => permissions.hasAll(...slugs), + }; +} +``` + +`satisfiesCapability()` returns true for super administrators, then delegates to `hasAny` or `hasAll`; an empty requirement is impossible because Task 7 constructors throw. + +- [ ] **Step 4: Write the failing server adapter test** + +Mock `@/lib/permissions` and assert that the adapter calls `getAdminContext()` once, uses the database-refreshed `session.user` actor, and forwards the returned `PermissionSet` without querying the database directly. + +```ts +expect(await getHousekeepingCapabilityContext()).toMatchObject({ + actor: { id: 42, username: "operator", rank: 7 }, + isSuperAdmin: false, +}); +expect(getAdminContext).toHaveBeenCalledTimes(1); +``` + +- [ ] **Step 5: Implement the cached server adapter** + +```ts +export const getHousekeepingCapabilityContext = cache(async () => { + const { session, permissions } = await getAdminContext(); + return createHousekeepingCapabilityContext( + { + id: session.user.id, + username: session.user.username, + rank: session.user.rank, + }, + permissions, + ); +}); +``` + +- [ ] **Step 6: Run authorization contracts and commit** + +Run: + +```powershell +pnpm exec vitest run src/features/housekeeping/foundation/capability-context.test.ts src/features/housekeeping/foundation/server-capability-context.test.ts src/lib/admin/authorization-contract.test.ts src/lib/admin/guard.test.ts +pnpm typecheck +``` + +Expected: PASS; central authorization still contains no fixed rank threshold. + +```powershell +git add src/features/housekeeping/foundation/capability-context.ts src/features/housekeeping/foundation/server-capability-context.ts src/features/housekeeping/foundation/capability-context.test.ts src/features/housekeeping/foundation/server-capability-context.test.ts +git diff --cached --check +git commit -m "feat: add housekeeping capability context" +``` + +### Task 9: Domain registry, manifests, and navigation model + +**Files:** +- Create: `src/features/housekeeping/foundation/registry.ts` +- Create: `src/features/housekeeping/foundation/navigation.ts` +- Create: `src/features/housekeeping/domains/operations/manifest.ts` +- Create: `src/features/housekeeping/domains/people/manifest.ts` +- Create: `src/features/housekeeping/domains/content/manifest.ts` +- Create: `src/features/housekeeping/domains/economy/manifest.ts` +- Create: `src/features/housekeeping/domains/hotel/manifest.ts` +- Create: `src/features/housekeeping/domains/system/manifest.ts` +- Create: `src/features/housekeeping/manifests.ts` +- Modify: `src/messages/en.json: pages` +- Modify: `src/messages/it.json: pages` +- Test: `src/features/housekeeping/foundation/registry.test.ts` +- Test: `src/features/housekeeping/foundation/navigation.test.ts` +- Test: `src/features/housekeeping/foundation/localization-contract.test.ts` + +**Interfaces:** +- Consumes: Task 7 manifests and Task 8 `satisfiesCapability()`. +- Produces: `HOUSEKEEPING_MANIFESTS`, `createHousekeepingRegistry()`, and `buildHousekeepingNavigation()`. + +- [ ] **Step 1: Write failing registry validation tests** + +```ts +const manifest = ( + id: HousekeepingDomainId, + capabilitySlug = PERMS.ADMIN_DASHBOARD, +): HousekeepingDomainManifest => ({ + id, + labelKey: `pages.housekeeping.domains.${id}.title`, + descriptionKey: `pages.housekeeping.domains.${id}.description`, + iconId: "settings", + previewHref: `/admin-next/${id}`, + capability: anyCapability(capabilitySlug), + routes: [], +}); + +const duplicateA = manifest("operations"); +const duplicateB = manifest("operations"); +const duplicateRouteManifest: HousekeepingDomainManifest = { + ...manifest("people"), + routes: [ + { id: "one", labelKey: "one", href: "/admin-next/people/users", capability: anyCapability(PERMS.ADMIN_DASHBOARD) }, + { id: "two", labelKey: "two", href: "/admin-next/people/users", capability: anyCapability(PERMS.ADMIN_DASHBOARD) }, + ], +}; +const unknownPermissionManifest = manifest("system", "admin.ghost.view"); + +it("rejects duplicate domains, routes, and unknown capability slugs", () => { + expect(() => createHousekeepingRegistry([duplicateA, duplicateB])) + .toThrow(/duplicate domain id/); + expect(() => createHousekeepingRegistry([duplicateRouteManifest])) + .toThrow(/duplicate route href/); + expect(() => createHousekeepingRegistry([unknownPermissionManifest])) + .toThrow(/unknown capability slug: admin\.ghost\.view/); +}); + +it("registers exactly the six approved domains", () => { + const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS); + expect(registry.domains.map((domain) => domain.id)).toEqual([ + "operations", "people", "content", "economy", "hotel", "system", + ]); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/foundation/registry.test.ts` + +Expected: FAIL because registry and manifests are missing. + +- [ ] **Step 3: Implement registry validation** + +Build the known permission catalog from `Object.values(PERMS)`. Validate domain IDs, preview hrefs, route IDs, route hrefs, non-empty label keys, and every `CapabilityRequirement` slug. Freeze the returned domain array. + +- [ ] **Step 4: Define the six empty-workflow manifests** + +Each manifest has no migrated business route; only its preview href and domain capability are present. Use these capability groups: + +- operations: `admin.dashboard`; +- people: any user, moderation, ticket, ban, or `mod.*` view capability; +- content: any news, pages, banners, events, polls, or prefixes view capability; +- economy: any catalog or shop view capability; +- hotel: any room, radio, or asset-import capability; +- system: any settings, logs, analytics, DevOps, notifications, permissions, or RCON capability. + +- [ ] **Step 5: Add English and Italian source copy** + +Under `pages.housekeeping`, add `preview`, `navigation`, `domains`, and `states`. Domain keys are `operations`, `people`, `content`, `economy`, `hotel`, and `system`, each with `title` and `description`. Add explicit strings for preview badge, disabled command trigger, back to site, empty foundation state, partial state, error state, and loading state. + +- [ ] **Step 6: Implement and test the navigation projection** + +```ts +export interface HousekeepingNavigationDomain { + id: HousekeepingDomainId; + href: string; + iconId: HousekeepingDomainManifest["iconId"]; + label: string; + description: string; + items: readonly { id: string; href: string; label: string }[]; +} +``` + +`buildHousekeepingNavigation(registry, context, translate)` filters unauthorized domains and routes before translating. Tests must prove a moderator without `admin.dashboard` can see People when a `mod.*` capability is present, while Economy stays hidden. + +- [ ] **Step 7: Verify localization keys** + +The localization contract reads `en.json` and `it.json`, resolves every manifest key by dot path, and fails on missing or non-string values. + +- [ ] **Step 8: Run and commit Task 9** + +Run: + +```powershell +pnpm exec vitest run src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/localization-contract.test.ts +pnpm typecheck +``` + +Expected: PASS; six domains in locked order and all manifest keys present in English and Italian. + +```powershell +git add src/features/housekeeping/foundation/registry.ts src/features/housekeeping/foundation/navigation.ts src/features/housekeeping/domains src/features/housekeeping/manifests.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/localization-contract.test.ts src/messages/en.json src/messages/it.json +git diff --cached --check +git commit -m "feat: add housekeeping domain registry" +``` + +### Task 10: Command Deck shell and standard page states + +**Files:** +- Create: `src/features/housekeeping/foundation/shell/housekeeping-shell.tsx` +- Create: `src/features/housekeeping/foundation/shell/domain-rail.tsx` +- Create: `src/features/housekeeping/foundation/shell/context-nav.tsx` +- Create: `src/features/housekeeping/foundation/shell/command-trigger.tsx` +- Create: `src/features/housekeeping/foundation/shell/operator-summary.tsx` +- Create: `src/features/housekeeping/foundation/page/housekeeping-page-shell.tsx` +- Create: `src/features/housekeeping/foundation/page/housekeeping-page-state.tsx` +- Test: `src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx` +- Test: `src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx` + +**Interfaces:** +- Consumes: `HousekeepingActor`, `HousekeepingDomainId`, and `HousekeepingNavigationDomain`. +- Produces: pure server-renderable shell/page components with no search, mutation, persistence, or database behavior. + +- [ ] **Step 1: Write the failing shell accessibility test** + +```tsx +const navigation: readonly HousekeepingNavigationDomain[] = [{ + id: "people", + href: "/admin-next/people", + iconId: "users", + label: "People", + description: "Users, moderation, and support", + items: [{ id: "users", href: "/admin-next/people/users", label: "Users" }], +}]; + +it("renders the Command Deck landmarks and active domain", () => { + const html = renderToStaticMarkup( + +

content

+
, + ); + expect(html).toContain('href="#housekeeping-content"'); + expect(html).toContain('aria-current="page"'); + expect(html).toContain('id="housekeeping-content"'); + expect(html).toContain("operator"); +}); +``` + +- [ ] **Step 2: Confirm the red state** + +Run: `pnpm exec vitest run src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx` + +Expected: FAIL because the shell components are missing. + +- [ ] **Step 3: Implement the shell components** + +Use `Link`, semantic `