1402 lines
54 KiB
Markdown
1402 lines
54 KiB
Markdown
# 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<I, O> {
|
|
id: string;
|
|
owner: HousekeepingDomainId;
|
|
capability: CapabilityRequirement;
|
|
run(context: HousekeepingCapabilityContext, input: I): Promise<HousekeepingResult<O>>;
|
|
}
|
|
|
|
export interface HousekeepingCommand<I, O> {
|
|
id: string;
|
|
owner: HousekeepingDomainId;
|
|
risk: "safe" | "sensitive";
|
|
capability: CapabilityRequirement;
|
|
requiresReason: boolean;
|
|
execute(context: HousekeepingCapabilityContext, input: I): Promise<HousekeepingResult<O>>;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Define search, inbox, and widget contracts**
|
|
|
|
Use these exact public methods:
|
|
|
|
- `HousekeepingSearchProvider.search(context, { term, limit }): Promise<HousekeepingResult<readonly HousekeepingSearchResult[]>>`
|
|
- `HousekeepingInboxSource.getItems(context, signal): Promise<HousekeepingResult<HousekeepingInboxSourceResult>>`
|
|
- `HousekeepingWidgetDefinition.load(context): Promise<HousekeepingResult<unknown>>`
|
|
|
|
`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(
|
|
<HousekeepingShell
|
|
actor={{ id: 42, username: "operator", rank: 7 }}
|
|
activeDomainId="people"
|
|
domains={navigation}
|
|
labels={{
|
|
skipToContent: "Skip to content",
|
|
primaryNavigation: "Housekeeping domains",
|
|
contextualNavigation: "People navigation",
|
|
command: "Search and commands are enabled in a later subproject",
|
|
preview: "Foundation preview",
|
|
backToSite: "Back to site",
|
|
}}
|
|
>
|
|
<p>content</p>
|
|
</HousekeepingShell>,
|
|
);
|
|
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 `<aside>`, `<nav>`, `<header>`, and `<main>` elements. The rail is compact at desktop width; contextual navigation collapses below the desktop breakpoint. All colors use existing admin variables such as `var(--admin-canvas)`, `var(--admin-surface)`, `var(--admin-border)`, `var(--admin-text)`, `var(--admin-text-muted)`, and `var(--admin-accent)`.
|
|
|
|
The command trigger is a disabled `<button type="button" disabled>` with localized explanatory text. It must not import `cmdk`, register keyboard listeners, or expose an executable callback in phase 01.
|
|
|
|
- [ ] **Step 4: Write failing page-state tests**
|
|
|
|
```tsx
|
|
it.each([
|
|
["loading", "status"],
|
|
["empty", "status"],
|
|
["partial", "status"],
|
|
["error", "alert"],
|
|
] as const)("renders %s with role %s", (state, role) => {
|
|
const html = renderToStaticMarkup(
|
|
<HousekeepingPageState state={state} title="State title" description="State detail" />,
|
|
);
|
|
expect(html).toContain(`role="${role}"`);
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 5: Implement page shell and states**
|
|
|
|
`HousekeepingPageShell` accepts `title`, `description`, optional `primaryAction`, optional `context`, and `children`. `HousekeepingPageState` accepts only the four locked states plus localized `title`, `description`, and optional retry action. Loading uses `aria-live="polite"`; partial uses a visible warning label; error uses `role="alert"`.
|
|
|
|
- [ ] **Step 6: Run component tests and commit**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
pnpm exec vitest run src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx
|
|
pnpm typecheck
|
|
```
|
|
|
|
Expected: PASS; static markup contains the required landmarks and roles.
|
|
|
|
```powershell
|
|
git add src/features/housekeeping/foundation/shell src/features/housekeeping/foundation/page
|
|
git diff --cached --check
|
|
git commit -m "feat: build housekeeping command deck shell"
|
|
```
|
|
|
|
### Task 11: Non-production preview route
|
|
|
|
**Files:**
|
|
- Create: `src/features/housekeeping/foundation/preview-gate.ts`
|
|
- Test: `src/features/housekeeping/foundation/preview-gate.test.ts`
|
|
- Modify: `src/env.ts: schema`
|
|
- Modify: `.env.example: core runtime flags`
|
|
- Create: `src/app/admin-next/layout.tsx`
|
|
- Create: `src/app/admin-next/page.tsx`
|
|
- Create: `src/app/admin-next/[domain]/layout.tsx`
|
|
- Create: `src/app/admin-next/[domain]/page.tsx`
|
|
- Test: `src/features/housekeeping/foundation/preview-route-contract.test.ts`
|
|
|
|
**Interfaces:**
|
|
- Consumes: Task 8 server capability context, Task 9 registry/navigation, and Task 10 shell/page components.
|
|
- Produces: `isHousekeepingPreviewEnabled()` and the gated `/admin-next` preview.
|
|
|
|
- [ ] **Step 1: Write the failing gate matrix test**
|
|
|
|
```ts
|
|
it.each([
|
|
["development", true, true],
|
|
["test", true, true],
|
|
["development", false, false],
|
|
["production", true, false],
|
|
["production", false, false],
|
|
] as const)("NODE_ENV=%s flag=%s => %s", (nodeEnv, flag, expected) => {
|
|
expect(isHousekeepingPreviewEnabled({ nodeEnv, flag })).toBe(expected);
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Confirm the red state**
|
|
|
|
Run: `pnpm exec vitest run src/features/housekeeping/foundation/preview-gate.test.ts`
|
|
|
|
Expected: FAIL because `preview-gate.ts` is missing.
|
|
|
|
- [ ] **Step 3: Implement the fail-closed gate and env field**
|
|
|
|
```ts
|
|
export function isHousekeepingPreviewEnabled(input: {
|
|
nodeEnv: "development" | "test" | "production";
|
|
flag: boolean;
|
|
}): boolean {
|
|
return input.nodeEnv !== "production" && input.flag;
|
|
}
|
|
```
|
|
|
|
Add `HOUSEKEEPING_NEXT_PREVIEW_ENABLED` to `src/env.ts` as an optional string transformed from `"true"` or `"1"` to boolean. Add `HOUSEKEEPING_NEXT_PREVIEW_ENABLED=false` to `.env.example` with a comment that production always returns 404.
|
|
|
|
- [ ] **Step 4: Write the failing route source contract**
|
|
|
|
The contract reads all four preview route files and asserts:
|
|
|
|
```ts
|
|
expect(layoutSource).toContain("isHousekeepingPreviewEnabled");
|
|
expect(layoutSource).toContain("notFound");
|
|
expect(domainLayoutSource).toContain("getHousekeepingCapabilityContext");
|
|
expect(domainLayoutSource).toContain("buildHousekeepingNavigation");
|
|
expect(allPreviewSource).not.toMatch(/@\/lib\/db|@\/actions\//);
|
|
expect(allPreviewSource).not.toMatch(/AdminSidebarNav|AdminHubChrome/);
|
|
```
|
|
|
|
- [ ] **Step 5: Implement gated route composition**
|
|
|
|
- `admin-next/layout.tsx` calls `isHousekeepingPreviewEnabled({ nodeEnv: env.NODE_ENV, flag: env.HOUSEKEEPING_NEXT_PREVIEW_ENABLED })` and calls `notFound()` before rendering children when it returns false.
|
|
- `admin-next/page.tsx` loads the capability context, finds the first visible domain, and redirects to its `previewHref`; it calls `notFound()` if none is visible.
|
|
- `admin-next/[domain]/layout.tsx` resolves `params: Promise<{ domain: string }>`, rejects unknown or unauthorized domains with `notFound()`, builds localized navigation, and renders `HousekeepingShell`.
|
|
- `admin-next/[domain]/page.tsx` renders a localized `HousekeepingPageShell` plus the foundation empty state. It performs no domain query.
|
|
|
|
- [ ] **Step 6: Run preview and authorization checks**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
pnpm exec vitest run src/features/housekeeping/foundation/preview-gate.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts src/lib/admin/authorization-contract.test.ts
|
|
pnpm typecheck
|
|
```
|
|
|
|
Expected: PASS; production remains disabled and preview files have no database/action imports.
|
|
|
|
- [ ] **Step 7: Commit Task 11**
|
|
|
|
```powershell
|
|
git add src/features/housekeeping/foundation/preview-gate.ts src/features/housekeeping/foundation/preview-gate.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts src/app/admin-next src/env.ts .env.example
|
|
git diff --cached --check
|
|
git commit -m "feat: add gated housekeeping foundation preview"
|
|
```
|
|
|
|
### Task 12: Source contracts and final foundation verification
|
|
|
|
**Files:**
|
|
- Modify: `src/lib/admin-theme-source-audit.test.ts: ROOTS`
|
|
- Create: `src/features/housekeeping/foundation/foundation-source-contract.test.ts`
|
|
- Modify: `package.json: scripts`
|
|
|
|
**Interfaces:**
|
|
- Consumes: every Task 1-11 deliverable.
|
|
- Produces: source-boundary regression checks and `pnpm test:housekeeping`.
|
|
|
|
- [ ] **Step 1: Extend the theme audit and observe the initial failure if semantic tokens were missed**
|
|
|
|
```ts
|
|
const ROOTS = [
|
|
"src/app/admin",
|
|
"src/components/admin",
|
|
"src/app/admin-next",
|
|
"src/features/housekeeping",
|
|
];
|
|
```
|
|
|
|
Run: `pnpm exec vitest run src/lib/admin-theme-source-audit.test.ts`
|
|
|
|
Expected: PASS if Task 10 used only semantic admin tokens; otherwise FAIL with the exact offending file and class, which must be corrected before continuing.
|
|
|
|
- [ ] **Step 2: Write the foundation boundary tests**
|
|
|
|
```ts
|
|
it("keeps foundation independent from database and actions", () => {
|
|
for (const file of sourceFiles("src/features/housekeeping/foundation")) {
|
|
const source = readFileSync(file, "utf8");
|
|
expect(source, file).not.toMatch(/@\/lib\/db|@\/actions\//);
|
|
}
|
|
});
|
|
|
|
it("leaves the current administration route trees present", () => {
|
|
expect(existsSync("src/app/admin/layout.tsx")).toBe(true);
|
|
expect(existsSync("src/app/mod/layout.tsx")).toBe(true);
|
|
expect(existsSync("src/app/admin-next/layout.tsx")).toBe(true);
|
|
});
|
|
|
|
it("contains no executable phase-01 command provider", () => {
|
|
const source = readTree("src/features/housekeeping");
|
|
expect(source).not.toMatch(/registerCommand|executeCommand|CommandPaletteProvider/);
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 3: Add the narrow project script**
|
|
|
|
```json
|
|
"test:housekeeping": "vitest run src/features/housekeeping src/lib/admin-theme-source-audit.test.ts src/lib/admin/authorization-contract.test.ts"
|
|
```
|
|
|
|
- [ ] **Step 4: Run the complete narrow verification**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
pnpm toolchain:check
|
|
pnpm hk:matrix:check
|
|
pnpm test:housekeeping
|
|
pnpm typecheck
|
|
pnpm lint
|
|
```
|
|
|
|
Expected:
|
|
|
|
- Node toolchain accepted at `>=26.7.0 <27`;
|
|
- matrix prints `137/137 valid`;
|
|
- all HK, theme, and authorization tests pass;
|
|
- TypeScript and Biome exit `0`.
|
|
|
|
- [ ] **Step 5: Run the production safety build**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
$env:HOUSEKEEPING_NEXT_PREVIEW_ENABLED = "true"
|
|
pnpm build
|
|
Remove-Item Env:HOUSEKEEPING_NEXT_PREVIEW_ENABLED
|
|
```
|
|
|
|
Expected: build exits `0`; the route compiles, while the runtime gate still evaluates production to disabled in the Task 11 unit test. This step does not start or deploy the application.
|
|
|
|
- [ ] **Step 6: Review scope and current-route diffs**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
git diff --name-only origin/main...HEAD
|
|
git diff --check
|
|
git status --short
|
|
```
|
|
|
|
Expected: no modification below `src/app/admin` or `src/app/mod`; only the approved design/plan documents, new preview tree, HK feature files, matrix verifier, two source locale files, env documentation/schema, package scripts, and the extended theme audit are present.
|
|
|
|
- [ ] **Step 7: Commit Task 12**
|
|
|
|
```powershell
|
|
git add src/lib/admin-theme-source-audit.test.ts src/features/housekeeping/foundation/foundation-source-contract.test.ts package.json
|
|
git diff --cached --check
|
|
git commit -m "test: verify housekeeping foundation boundaries"
|
|
```
|
|
|
|
- [ ] **Step 8: Record final evidence without claiming deployment**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
git log --oneline --max-count=12
|
|
git status --short --branch
|
|
```
|
|
|
|
Expected: every task commit is visible, the checkout is clean, and the branch is ahead of `origin/main`. Report local test/build evidence separately from any future push, CI, or deployment status.
|
|
|
|
## Plan Completion Criteria
|
|
|
|
The subproject is complete only when:
|
|
|
|
- the committed matrix contains 137 unique, structurally valid, reviewed rows;
|
|
- every `/mod` row is `MERGE` and targets People;
|
|
- `/admin/housekeeping` and `/admin/menu` are `REMOVE`, while live permissions have a System target;
|
|
- the six domain manifests validate against existing `PERMS` values;
|
|
- capability visibility uses `PermissionSet` methods and no fixed rank threshold;
|
|
- the Command Deck and four page states pass static accessibility smoke tests;
|
|
- `/admin-next` is disabled in production regardless of its flag;
|
|
- `/admin` and `/mod` behavior and source trees are unchanged;
|
|
- matrix, targeted tests, typecheck, lint, build, and `git diff --check` pass;
|
|
- no database migration, domain query, domain mutation, command execution, inbox aggregation, or preference persistence was added.
|