From f7c9c96478fd532f821f2ae71da573639d8e6650 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 30 Aug 2026 21:52:15 +0200 Subject: [PATCH 01/62] docs: design stepwise housekeeping rebuild --- ...30-housekeeping-stepwise-rebuild-design.md | 369 ++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-30-housekeeping-stepwise-rebuild-design.md diff --git a/docs/superpowers/specs/2026-08-30-housekeeping-stepwise-rebuild-design.md b/docs/superpowers/specs/2026-08-30-housekeeping-stepwise-rebuild-design.md new file mode 100644 index 00000000..6a011953 --- /dev/null +++ b/docs/superpowers/specs/2026-08-30-housekeeping-stepwise-rebuild-design.md @@ -0,0 +1,369 @@ +# Housekeeping Stepwise Rebuild Design + +**Date:** 2026-08-30 +**Status:** Approved section by section in conversation; awaiting review of this written specification +**Delivery branch:** `codex/housekeeping-rebuild-stepwise` +**Stable production surface:** `/admin` +**Preview surface:** `/ase-next` +**Final surface after a separately approved cutover:** `/ase` + +## Purpose + +Rebuild the EpicNext CMS Housekeeping as a complete, reliable operator product rather than a new shell around incomplete or generic pages. The rebuild covers routing, navigation, content, data presentation, filters, actions, permissions, feedback, error handling, accessibility, responsive behavior, tests, and operational usefulness. + +Work proceeds in complete vertical slices. The existing `/admin` remains the stable administration surface until every retained workflow has a verified replacement and the final cutover receives explicit approval. + +## Relationship to the earlier Housekeeping work + +The master domain architecture from `2026-08-24-housekeeping-modernization-design.md` remains useful: one capability-adaptive Housekeeping, six functional domains, server-side authorization, real domain services, derived operational work, and an atomic final cutover. + +This specification supersedes `2026-08-26-housekeeping-completion-design.md` for delivery strategy, recovery policy, content quality, route verification, review gates, and cutover readiness. The implementation merged through PR #52 and subsequently reverted is not accepted as functional or visual evidence merely because it compiled or passed its former tests. + +Reusable foundations and services from the reverted branch may be recovered only after focused review and regression tests. Its pages, generic content compositions, migration claims, and route cutover are not recovered wholesale. + +## Problem statement + +The reverted implementation had two product-level failures: + +1. Primary domain links such as `/ase/people`, `/ase/content`, `/ase/economy`, `/ase/hotel`, and `/ase/system` were generated by navigation but had no registered page handler. The dispatcher therefore called `notFound()` for every primary domain entry. +2. Passing parity and build checks did not demonstrate that operator-facing content was complete, useful, visually acceptable, or functionally equivalent to the working administration surface. + +The deeper issue was verification by structure rather than by operator outcome. A route counted as migrated when it had a declared destination and handler, even if the menu could not reach it or its content did not deliver the former workflow at acceptable quality. + +## Approved product principles + +1. **Function before cutover.** Nothing replaces a working legacy workflow until the replacement works independently. +2. **Real content only.** Pages use real data, real actions, and workflow-specific copy. Placeholder panels, decorative statistics, fake forms, and generic command forms do not count as delivery. +3. **Improve wherever evidence supports it.** Every workflow is reviewed for content, information hierarchy, filters, actions, feedback, performance, safety, and usability instead of being copied mechanically. +4. **No useful-function loss.** A legacy capability may be retained, improved, merged into a clearer workflow, or explicitly removed only when it is genuinely obsolete. Every decision records the old source and new destination. +5. **Complete vertical slices.** A domain is implemented and reviewed end to end before the next domain becomes the primary focus. +6. **Stable production during construction.** `/admin` remains available; the new work lives behind the non-production `/ase-next` preview. +7. **Evidence over file counts.** Completion is measured through navigability, real data, successful actions, authorization, audit, visual review, and workflow comparison. + +## Scope + +The program covers the complete administration inventory represented by the existing migration matrix and the currently working `/admin` and `/mod` responsibilities. The functional domains remain: + +1. Foundation, routing, access, and shared page behavior. +2. People, community, moderation, and support. +3. Content and engagement. +4. Economy and catalog. +5. Hotel and world operations. +6. System, access, and observability. +7. Operations workspace and cross-domain composition. +8. Final atomic cutover. + +Public CMS and game-client redesign remain outside this program. Existing specialized engines are not rewritten solely for uniformity; they are integrated behind reliable page and service contracts. + +## Delivery architecture + +### Stable baseline + +Development starts from the post-revert `main`, not from the reverted feature head. This preserves the functioning legacy administration surface and the known production rollback state. + +All work is performed in the canonical checkout on `codex/housekeeping-rebuild-stepwise`. No Git worktrees are used. + +### Preview isolation + +The rebuild is exposed at `/ase-next` only in approved development and test contexts. Production operators continue using `/admin`. The preview gate must default to closed, must not depend on client-side hiding, and must use the same authenticated capability source as the final product. + +### Selective recovery + +Earlier code is classified before reuse: + +- **Recover:** a focused foundation or service is behaviorally sound, has a clear boundary, and gains a regression test in the new branch. +- **Rewrite:** the responsibility is valid but its route, content, interaction, state model, or service boundary is inadequate. +- **Replace with existing legacy service:** the earlier implementation duplicated or weakened already reliable behavior. +- **Discard:** the code is placeholder-like, generic, unreachable, misleading, or unnecessary. + +No bulk restoration of the reverted `src/features/housekeeping` tree and no mass cherry-pick of vertical commits is permitted. Recovery happens at the responsibility level. + +## Vertical delivery order + +### Phase 1: Foundation and routing + +- Restore a gated `/ase-next` composition surface without changing `/admin`. +- Establish a registry contract connecting domains, concrete routes, handlers, capabilities, labels, and navigation. +- Make every primary domain entry resolve to a concrete accessible landing route. +- Add explicit unauthenticated, forbidden, missing, loading, empty, partial, conflict, and unexpected-error semantics. +- Establish workflow inventory and comparison evidence used by every later vertical. + +### Phase 2: People + +- Users and linked accounts. +- Online users, communities, and guilds. +- Applications, staff, and teams. +- Moderation overview, actions, CFH, bans, IP, VPN, and word filtering. +- Tickets, help tickets, templates, and support workflows. + +### Phase 3: Content + +- Articles, media, photos, banners, advertising, events, polls, help content, tags, prefixes, writable boxes, email content, branding, and localization. + +### Phase 4: Economy + +- Catalog, items, Builder Club, maintenance, shop, marketplace, transactions, vouchers, subscriptions, rare values, badges, achievements, sounds, and calendar rewards. + +### Phase 5: Hotel + +- Rooms, room furni, navigator content, radio, runtime asset operations, imports, and Studio tools. + +### Phase 6: System + +- Permissions, access management, settings, emulator configuration, analytics, logs, DevOps, alerts, command center, and maintenance. + +### Phase 7: Operations + +- Capability-derived operational home. +- Global navigation/entity search and safe commands. +- Derived inbox, recent work, favorites, mandatory summaries, and optional widgets. +- Cross-domain actions link to or invoke the owning domain without duplicating mutation logic. + +### Phase 8: Cutover + +- Execute only after all vertical gates and final whole-product review pass. +- Requires explicit user approval separate from approval of any individual vertical. +- Publishes `/ase`, updates all internal administration links, and removes old route surfaces atomically. +- Retains a release-level rollback path and uses only backward-compatible data migrations before cutover. + +## Route and navigation contract + +### Concrete landing destinations + +Every visible domain has at least one concrete accessible route with a registered handler. Navigation does not link to a manifest namespace that has no page. + +For a capability context: + +1. inaccessible routes are removed; +2. domains with no accessible routes are removed; +3. a domain rail link targets its declared accessible landing route; +4. a bare domain URL such as `/ase-next/people` redirects server-side to the same accessible landing route; +5. if no route is available, direct access returns a clear forbidden result rather than a fabricated missing-page result. + +Operations may own the exact root `/ase-next` because it has a real root handler. + +### Registry invariants + +Automated contracts reject: + +- a navigation href that the route matcher cannot resolve; +- a matched route without a handler; +- a handler without a registered route; +- a domain without a valid landing destination; +- a domain displayed with zero accessible routes; +- duplicate domain, route, handler, command, provider, inbox, or widget IDs; +- invalid ownership or localization keys; +- links outside the preview/final canonical namespace; +- a retained migration row without a reachable implementation and functional evidence. + +### Response semantics + +- Missing session: redirect to login. +- Authenticated operator without permission: render a dedicated 403 experience. +- Unknown route or entity: render a genuine 404 experience. +- Known route with unavailable dependency: preserve the shell and unaffected content, then render an actionable partial or dependency error. + +Authorization remains server-enforced at query and mutation boundaries. Visible navigation never grants access. + +## Content and interaction standard + +### Operator questions + +Every page must quickly answer: + +1. What am I looking at? +2. What needs attention? +3. What can I do next? + +Page content is designed around the operator's task rather than around the database schema or the desire to fill a dashboard grid. + +### Required content review + +Each workflow receives a written audit covering: + +- operator and purpose; +- legacy route and current behavior; +- data sources and freshness; +- useful information currently present; +- missing, duplicated, misleading, or low-value information; +- filters, sorting, pagination, and search requirements; +- safe and sensitive actions; +- validation, confirmation, reason, feedback, and undo/rollback behavior; +- authorization and audit requirements; +- empty, loading, partial, error, forbidden, and success states; +- desktop and mobile/tablet usability; +- performance risks and query boundaries; +- migration decision and canonical destination. + +### Shared structure without generic content + +The foundation may provide semantic page regions, state primitives, confirmation patterns, tables, filters, pagination, and feedback components. It must not manufacture domain copy, fake metrics, generic form fields, or placeholder workflows. + +Domain pages own their information hierarchy and use shared components only where behavior is genuinely common. + +### Copy quality + +- Titles name the actual operator task. +- Descriptions clarify scope or consequences instead of repeating the title. +- Labels use domain language already understood by operators. +- Empty states distinguish no records from no filter matches and lack of access. +- Error messages explain the recoverable next action. +- Sensitive confirmations state the target, consequence, and required reason. +- Success messages confirm the resulting state, not merely that a button was clicked. + +## People vertical design + +### Users + +The user list supports real search and useful filtering, including fields supported reliably by the live schema such as identity, rank, status, ban state, and activity. Columns prioritize operator decisions and remain configurable only where configuration adds value. + +The user detail presents identity, current status, rank, currencies, activity, sanctions, linked-account evidence, badges, rooms, and relevant audit history through focused sections. It avoids a wall of unrelated cards. Permitted actions are contextual, capability-checked, confirmed according to risk, and followed by visible state refresh. + +### Linked accounts + +Signals such as shared identifiers or network history are shown as evidence, not as automatic proof of wrongdoing. The UI explains why accounts are related, what data is unavailable, and which moderation actions remain independent decisions. + +### Community and staff + +Online, guild, application, team, and staff views expose the data and actions necessary for their actual workflows. Application and team pages show state, relevant decision context, and permitted next actions instead of static summaries. + +### Moderation + +The moderation overview is an operational entry point, not a decorative dashboard. CFH, ticket, ban, and action surfaces show priority, age, status, target context, evidence, and the next permitted action. + +Ban, IP, VPN, word-filter, and moderation actions make actor, target, reason, duration, evidence, and outcome explicit. Forged or unauthorized mutations remain blocked by the server even when the UI hides them. + +### Support + +Ticket and help-ticket queues provide operational filters and clear state. Detail pages keep conversation, user context, status, and response/closure actions together where practical. Templates assist the operator without silently replacing authored responses. + +## Data and service boundaries + +- App Router files bind parameters and compose pages only. +- Domain query services produce page-specific read models and remain server-side. +- Domain commands accept validated typed input and return typed outcomes. +- Existing reliable legacy services are adapted rather than copied. +- Pages do not own SQL, transaction policy, capability rules, or audit serialization. +- Queries return only data the capability context permits. +- Mutations revalidate session, capability, target state, and input immediately before execution. +- Database mutation and audit evidence share one transaction whenever they use the same datastore boundary. +- External or file operations persist audit intent before execution and final outcome afterward when required by risk. +- Long-running operations expose progress and partial/failure outcomes rather than pretending to complete synchronously. + +## Error model + +The stable error categories are: + +- `UNAUTHENTICATED`; +- `FORBIDDEN`; +- `VALIDATION`; +- `NOT_FOUND`; +- `CONFLICT`; +- `RATE_LIMITED`; +- `DEPENDENCY_UNAVAILABLE`; +- `TIMEOUT`; +- `INTERNAL`. + +Validation errors are attached to the relevant control. Conflicts explain that state changed and offer reload or comparison. Partial provider failure preserves successful content. Unexpected errors are sanitized for the operator, logged once, and correlated by an identifier safe to share with support. + +## Authorization and audit + +- Effective ACL permissions, not hardcoded rank thresholds, authorize behavior. +- Rank is informative and may influence defaults only. +- Page loaders, queries, commands, and underlying mutation services enforce their own relevant boundaries. +- Sensitive operations require a reason when their workflow contract says so. +- Denied, failed, and partially completed sensitive attempts produce appropriate audit evidence. +- Audit payloads redact secrets and do not log unnecessary search terms or private result payloads. +- Capability loss invalidates stored shortcuts, favorites, and optional content on the next reconciliation. + +## Definition of done for a vertical + +A vertical is complete only when all of the following are true: + +1. Every legacy workflow in scope has a reviewed migration decision and canonical destination. +2. Every retained useful function is available or intentionally improved in the new vertical. +3. Every exposed page uses real data and real actions; no placeholder or generic workflow remains. +4. Navigation, direct URLs, route matching, handlers, and landing behavior are consistent. +5. Permission-allowed and permission-denied paths are tested at page, query, and command boundaries. +6. Loading, empty, partial, validation, conflict, dependency, forbidden, missing, success, and unexpected-error states are covered where applicable. +7. Sensitive operations have confirmation, reason, server validation, result feedback, and audit behavior appropriate to their risk. +8. Desktop and mobile/tablet layouts are visually inspected for every page and shared state. +9. The vertical passes targeted tests, cumulative Housekeeping tests, full project tests, typecheck, formatting/lint gates, and production build. +10. A functional comparison records what was retained, improved, merged, or removed and why. +11. The user reviews the vertical before work advances to the next primary domain. + +## Testing strategy + +### TDD cycle + +Every behavioral change begins with a failing test that demonstrates the missing or broken operator outcome. The smallest implementation makes it pass, then the design is cleaned up while the test remains green. + +### Contract tests + +- Registry and navigation reachability. +- Domain landing resolution for representative capability sets. +- Route-handler bijection. +- Capability filtering and direct-access denial semantics. +- Migration inventory destination reachability. +- Localization and source-boundary contracts. + +### Domain tests + +- Query read models with representative, empty, partial, and failure data. +- Commands with allowed, denied, invalid, conflicting, failed, and successful outcomes. +- Transaction and audit behavior for sensitive mutations. +- Page rendering and interaction for the workflow's meaningful states. + +### Integration and smoke tests + +- Authenticated preview entry and primary domain navigation. +- Every generated visible href returns the expected route instead of 404. +- Representative read and mutation flows for each capability profile. +- `/admin` remains functional throughout construction. +- `/ase` remains unavailable until final cutover. + +### Visual verification + +Every page and shared state is reviewed at desktop and mobile/tablet widths using representative real or deterministic development data. Review covers hierarchy, density, wrapping, overflow, focus, keyboard navigation, actionable feedback, and whether the content helps the operator complete the task. + +Screenshots and review notes are stored as vertical evidence. A page is not visually approved solely because it uses the shared theme tokens. + +## Branch and pull-request workflow + +- All rebuild work remains on `codex/housekeeping-rebuild-stepwise`. +- A draft pull request targets `main` and is updated after each reviewed checkpoint. +- Commits remain responsibility-focused and preserve a readable red-green history where practical. +- The draft PR description records completed verticals, current gates, known limitations, and rollback posture. +- The PR is not marked ready and the cutover is not added merely because an individual vertical is green. +- The old reverted branch remains historical evidence; it is not force-updated or treated as the new delivery branch. + +## Cutover and rollback + +The final cutover receives a dedicated review covering all routes, authenticated capability profiles, mutations, visual states, build output, CI, deployment, and live health. + +Before merge: + +- the full legacy inventory has no unresolved useful workflow; +- every generated administration link is reachable; +- `/ase` is tested as the final namespace; +- legacy removal is confined to the final cutover change; +- only additive, backward-compatible migrations are present; +- the previous application release can operate against the resulting schema. + +After merge, success requires completed deployment plus a live `/api/health` response. Operator-facing smoke checks must cover the final domain landing routes. A green build alone is insufficient. + +Rollback redeploys the last stable application release. Destructive schema cleanup is a later project and is not part of this cutover. + +## Success criteria + +The rebuild is successful when: + +- all retained administration responsibilities work through the new Housekeeping; +- every visible link and direct canonical route resolves correctly; +- each page contains useful, workflow-specific content and actions; +- no placeholder, generic imitation, or decorative-only operational page remains; +- authorization, validation, audit, errors, partial failure, and feedback behave consistently; +- the new experience is demonstrably better without losing useful legacy function; +- every vertical has functional and visual approval evidence; +- `/admin` remains stable until the explicitly approved atomic cutover; +- deployment, health, and final operator-route smoke checks pass after merge. -- 2.54.0 From 3da84ffd8795496fa3ae5ef4cbefae747304462c Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 30 Aug 2026 22:01:49 +0200 Subject: [PATCH 02/62] docs: plan housekeeping routing recovery --- ...ousekeeping-foundation-routing-recovery.md | 902 ++++++++++++++++++ 1 file changed, 902 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-30-housekeeping-foundation-routing-recovery.md diff --git a/docs/superpowers/plans/2026-08-30-housekeeping-foundation-routing-recovery.md b/docs/superpowers/plans/2026-08-30-housekeeping-foundation-routing-recovery.md new file mode 100644 index 00000000..2611fdf7 --- /dev/null +++ b/docs/superpowers/plans/2026-08-30-housekeeping-foundation-routing-recovery.md @@ -0,0 +1,902 @@ +# Housekeeping Foundation and Routing Recovery Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restore a non-production `/ase-next` Housekeeping foundation whose generated navigation, concrete routes, handlers, capability checks, and HTTP access semantics cannot produce the orphaned domain links that caused the reverted cutover to return 404. + +**Architecture:** Keep `/admin` and `/mod` unchanged while replacing the old `/admin-next` preview namespace with a gated `/ase-next` surface. Separate static manifest validation from a runtime route-handler registry, project navigation only from accessible handled routes, and dispatch all preview pages through one deterministic matcher with explicit login, 403, and 404 behavior. This plan intentionally stops before People content; the People vertical receives its own implementation plan after this foundation passes review. + +**Tech Stack:** Next.js 16.3.3 App Router, React 19.2.8 server components, TypeScript 7.0.2, Vitest 4.1.11, next-intl, Biome 2.5.9, pnpm 11.24.0, Node.js 26.8.1. + +**Spec:** `docs/superpowers/specs/2026-08-30-housekeeping-stepwise-rebuild-design.md` + +## Global Constraints + +- Work only in the canonical checkout on `codex/housekeeping-rebuild-stepwise`; do not use Git worktrees. +- Keep `/admin` and `/mod` functional and unchanged throughout this plan. +- Expose the rebuild only at `/ase-next`; do not create `/ase` or alter production administration links. +- Keep `HOUSEKEEPING_NEXT_PREVIEW_ENABLED` defaulting to `false`, and keep the preview unavailable in `NODE_ENV=production`. +- Recover no page or service from `codex/housekeeping-complete` unless a task names it explicitly and first proves the behavior with a failing test. This plan names no such recovery. +- Generate navigation only for routes with a registered handler and satisfied domain/route capabilities. +- Use `forbidden()` for authenticated capability denial and `notFound()` only for unknown domains, paths, or entities. +- Use real workflow-specific content in later verticals; this foundation must not add placeholder dashboards or generic forms. +- Preserve the untracked `.remember/` directory and stage only paths named by the active task. +- Before every Node or pnpm command, select the required runtime: + +```powershell +$nodeDir = Join-Path (Join-Path $env:TEMP 'codex-node-v26.8.1') 'node-v26.8.1-win-x64' +if (-not (Test-Path -LiteralPath (Join-Path $nodeDir 'node.exe'))) { + throw 'Node 26.8.1 portable runtime not found' +} +$env:PATH = "$nodeDir;$env:PATH" +node --version +``` + +--- + +### Task 1: Rename the gated preview namespace to `/ase-next` + +**Files:** +- Create: `src/features/housekeeping/foundation/preview-namespace.test.ts` +- Move: `src/app/admin-next/layout.tsx` to `src/app/ase-next/layout.tsx` +- Move: `src/app/admin-next/page.tsx` to `src/app/ase-next/page.tsx` +- Move: `src/app/admin-next/[domain]/layout.tsx` to `src/app/ase-next/[domain]/layout.tsx` +- Move: `src/app/admin-next/[domain]/page.tsx` to `src/app/ase-next/[domain]/page.tsx` +- Modify: `src/features/housekeeping/foundation/contracts/domain.ts` +- Modify: `src/features/housekeeping/foundation/registry.ts` +- Modify: all six `src/features/housekeeping/domains/*/manifest.ts` files +- Modify: `src/features/housekeeping/foundation/contracts/contracts.test.ts` +- Modify: `src/features/housekeeping/foundation/foundation-source-contract.test.ts` +- Modify: `src/features/housekeeping/foundation/navigation.test.ts` +- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx` +- Modify: `src/features/housekeeping/foundation/preview-route-contract.test.ts` +- Modify: `src/features/housekeeping/foundation/registry.test.ts` +- Modify: `src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx` +- Modify: `src/lib/admin-theme-source-audit.test.ts` + +**Interfaces:** +- Consumes: existing `isHousekeepingPreviewEnabled()` and `HOUSEKEEPING_NEXT_PREVIEW_ENABLED` environment contract. +- Produces: preview source files and manifest hrefs that use only `/ase-next`; later tasks consume the new namespace without compatibility aliases. + +- [ ] **Step 1: Write the failing namespace contract** + +Create `preview-namespace.test.ts` with a tracked-source scan that ignores historical documentation and rejects the old runtime namespace: + +```ts +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +describe("Housekeeping preview namespace", () => { + it("uses /ase-next and removes /admin-next from runtime sources", () => { + const files = execFileSync("git", ["ls-files", "src", ".env.example"], { + encoding: "utf8", + }) + .trim() + .split(/\r?\n/) + .filter(Boolean); + + const offenders = files.filter((file) => + readFileSync(file, "utf8").includes("/admin-next"), + ); + + expect(offenders).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run the namespace test and verify RED** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-namespace.test.ts +``` + +Expected: FAIL listing the existing `/admin-next` route, manifest, and test files. + +- [ ] **Step 3: Move the route tree and replace runtime/test hrefs** + +Run the four `git mv` operations, then change the manifest type and registry invariant to the exact new namespace: + +```ts +export interface HousekeepingDomainManifest { + id: HousekeepingDomainId; + labelKey: string; + descriptionKey: string; + iconId: "inbox" | "users" | "file-text" | "gem" | "hotel" | "settings"; + previewHref: `/ase-next/${HousekeepingDomainId}`; + capability: CapabilityRequirement; + routes: readonly HousekeepingRouteDefinition[]; + searchProviders: readonly HousekeepingSearchProvider[]; + inboxSources: readonly HousekeepingInboxSource[]; + widgets: readonly HousekeepingWidgetDefinition[]; +} +``` + +```ts +if (manifest.previewHref !== `/ase-next/${manifest.id}`) { + throw new Error(`invalid preview href: ${manifest.previewHref}`); +} +``` + +Replace `/admin-next` with `/ase-next` in the six manifests and in the named tests. Update imports from `@/app/admin-next/...` to `@/app/ase-next/...`. Do not rename the environment flag in this task. + +- [ ] **Step 4: Verify GREEN and the unchanged preview gate** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-namespace.test.ts src/features/housekeeping/foundation/preview-gate.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/navigation.test.ts +``` + +Expected: all selected tests PASS and the production preview-gate cases remain denied. + +- [ ] **Step 5: Check the exact diff and commit** + +Run: + +```powershell +git diff --check +git status --short +git add -A -- src/app/admin-next src/app/ase-next +git add -- src/features/housekeeping/foundation/preview-namespace.test.ts src/features/housekeeping/foundation/contracts/domain.ts src/features/housekeeping/foundation/contracts/contracts.test.ts src/features/housekeeping/foundation/registry.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/foundation-source-contract.test.ts src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx src/features/housekeeping/domains src/lib/admin-theme-source-audit.test.ts +git commit -m "refactor(housekeeping): restore ase preview namespace" +``` + +Expected: `.remember/` remains untracked and is not staged. + +--- + +### Task 2: Add deterministic route matching and handler-runtime validation + +**Files:** +- Create: `src/features/housekeeping/foundation/routing/route-handler.ts` +- Create: `src/features/housekeeping/foundation/routing/match-route.ts` +- Create: `src/features/housekeeping/foundation/routing/match-route.test.ts` +- Create: `src/features/housekeeping/foundation/routing/runtime.ts` +- Create: `src/features/housekeeping/foundation/routing/runtime.test.ts` +- Modify: `src/features/housekeeping/foundation/contracts/domain.ts` +- Modify: `src/features/housekeeping/foundation/contracts/index.ts` +- Modify: `src/features/housekeeping/foundation/registry.ts` +- Modify: `src/features/housekeeping/foundation/registry.test.ts` +- Modify: all six `src/features/housekeeping/domains/*/manifest.ts` files + +**Interfaces:** +- Consumes: `HousekeepingRegistry`, `HousekeepingCapabilityContext`, and `HousekeepingDomainManifest`. +- Produces: `HousekeepingPreviewHref`, `HousekeepingRouteMatch`, `HousekeepingRouteHandler`, `HousekeepingRouteRuntime`, `createHousekeepingRouteRuntime()`, and `matchHousekeepingRoute()`. + +- [ ] **Step 1: Write failing route-matcher tests** + +Create cases that require exact, dynamic, and rejected matches: + +```ts +it("matches concrete and dynamic preview routes", () => { + expect(runtime.match("/ase-next/people/users")).toMatchObject({ + routeId: "people.users", + domain: "people", + params: {}, + }); + expect(runtime.match("/ase-next/people/users/42")).toMatchObject({ + routeId: "people.user-detail", + domain: "people", + params: { id: "42" }, + }); +}); + +it.each([ + "/ase-next/people", + "/ase-next/people/unknown", + "/ase-next/people/users/", + "/ase-next/people/users?rank=7", + "/ase-next/people/users/%2F", +])("rejects an unregistered canonical path: %s", (pathname) => { + expect(runtime.match(pathname)).toBeNull(); +}); +``` + +- [ ] **Step 2: Run matcher tests and verify RED** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/routing/match-route.test.ts +``` + +Expected: FAIL because the routing modules and runtime do not exist. + +- [ ] **Step 3: Add the concrete route and handler contracts** + +Add these public contracts: + +```ts +export type HousekeepingPreviewHref = `/ase-next${"" | `/${string}`}`; + +export interface HousekeepingRouteDefinition { + id: string; + labelKey: string; + href: HousekeepingPreviewHref; + capability: CapabilityRequirement; + matchPrefixes?: readonly string[]; +} + +export interface HousekeepingDomainManifest { + // existing fields remain + landingRouteId: string | null; +} +``` + +```ts +import type { ReactNode } from "react"; +import type { HousekeepingCapabilityContext } from "../contracts"; + +export interface HousekeepingRouteMatch { + routeId: string; + domain: HousekeepingDomainId; + params: Readonly>; + canonicalHref: HousekeepingPreviewHref; +} + +export interface HousekeepingRouteRenderInput { + context: HousekeepingCapabilityContext; + match: HousekeepingRouteMatch; + searchParams?: Readonly>; + translate: (key: string) => string; +} + +export interface HousekeepingRouteHandler { + routeId: string; + render(input: HousekeepingRouteRenderInput): Promise; +} +``` + +All six current empty manifests set `landingRouteId: null`. Registry validation permits `null` only while `routes` is empty; once routes exist, it requires a landing ID owned by that manifest. + +- [ ] **Step 4: Implement deterministic matching** + +Implement `matchHousekeepingRoute()` by parsing canonical path segments, sorting literal candidates ahead of dynamic `:parameter` candidates, requiring an exact segment count, decoding each segment once, and rejecting query strings, fragments, backslashes, empty segments, trailing slashes, `.`/`..`, and decoded slashes. + +The exported signature is: + +```ts +export function matchHousekeepingRoute( + registry: HousekeepingRegistry, + canonicalPath: string, +): HousekeepingRouteMatch | null; +``` + +- [ ] **Step 5: Write failing runtime-bijection tests** + +Add tests with a two-route manifest and assert these failures separately: + +```ts +expect(() => createHousekeepingRouteRuntime(registry, [])).toThrow( + "missing route handler: people.users", +); + +expect(() => + createHousekeepingRouteRuntime(registry, [ + handler("people.users"), + handler("people.users"), + ]), +).toThrow("duplicate route handler: people.users"); + +expect(() => + createHousekeepingRouteRuntime(registry, [handler("people.unknown")]), +).toThrow("handler without route: people.unknown"); +``` + +- [ ] **Step 6: Implement and verify the runtime registry** + +Implement this public shape: + +```ts +export interface HousekeepingRouteRuntime { + registry: HousekeepingRegistry; + handlers: ReadonlyMap; + match(pathname: string): HousekeepingRouteMatch | null; +} + +export function createHousekeepingRouteRuntime( + registry: HousekeepingRegistry, + handlers: readonly HousekeepingRouteHandler[], +): HousekeepingRouteRuntime; +``` + +The constructor rejects duplicate handlers, missing handlers for declared routes, and handlers without declared routes. Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/routing/match-route.test.ts src/features/housekeeping/foundation/routing/runtime.test.ts src/features/housekeeping/foundation/registry.test.ts +``` + +Expected: all selected tests PASS. + +- [ ] **Step 7: Commit the routing runtime** + +Run: + +```powershell +git diff --check +git add src/features/housekeeping/foundation/contracts src/features/housekeeping/foundation/registry.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/foundation/routing src/features/housekeeping/domains +git commit -m "feat(housekeeping): validate preview route runtime" +``` + +--- + +### Task 3: Project navigation only from accessible handled routes + +**Files:** +- Modify: `src/features/housekeeping/foundation/navigation.ts` +- Modify: `src/features/housekeeping/foundation/navigation.test.ts` +- Modify: `src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx` + +**Interfaces:** +- Consumes: `HousekeepingRouteRuntime`, handler-backed route definitions, and `HousekeepingCapabilityContext`. +- Produces: `buildHousekeepingNavigation(runtime, context, translate)` whose domain `href` is always a concrete route and whose items are all resolvable. + +- [ ] **Step 1: Write failing navigation reachability tests** + +Add these behaviors to `navigation.test.ts`: + +```ts +it("links a domain to its accessible handled landing route", () => { + const navigation = buildHousekeepingNavigation( + runtimeWithPeopleRoutes, + contextWith(PERMS.USERS_VIEW), + identityTranslate, + ); + + expect(navigation).toEqual([ + expect.objectContaining({ + id: "people", + href: "/ase-next/people/users", + items: [ + expect.objectContaining({ + id: "people.users", + href: "/ase-next/people/users", + }), + ], + }), + ]); +}); + +it("falls back to the first accessible handled route", () => { + const navigation = buildHousekeepingNavigation( + runtimeWithPreferredUsersAndTicketFallback, + contextWith(PERMS.TICKETS_VIEW), + identityTranslate, + ); + expect(navigation[0]?.href).toBe("/ase-next/people/support/tickets"); +}); + +it("omits domains with no accessible handled routes", () => { + expect( + buildHousekeepingNavigation(runtimeWithNoPeopleHandlers, moderator, identityTranslate), + ).toEqual([]); +}); +``` + +- [ ] **Step 2: Run navigation tests and verify RED** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/navigation.test.ts +``` + +Expected: FAIL because the current function consumes a static registry, links to `previewHref`, and retains empty domains. + +- [ ] **Step 3: Implement the navigation projection** + +Change the signature and projection: + +```ts +export function buildHousekeepingNavigation( + runtime: HousekeepingRouteRuntime, + context: HousekeepingCapabilityContext, + translate: (key: string) => string, +): readonly HousekeepingNavigationDomain[] { + return runtime.registry.domains.flatMap((domain) => { + if (!satisfiesCapability(context, domain.capability)) return []; + + const items = domain.routes + .filter( + (route) => + runtime.handlers.has(route.id) && + satisfiesCapability(context, route.capability), + ) + .map((route) => ({ + id: route.id, + href: route.href, + label: translate(route.labelKey), + })); + + if (items.length === 0) return []; + const landing = + items.find((item) => item.id === domain.landingRouteId) ?? items[0]; + if (!landing) return []; + + return [{ + id: domain.id, + href: landing.href, + iconId: domain.iconId, + label: translate(domain.labelKey), + description: translate(domain.descriptionKey), + items, + }]; + }); +} +``` + +- [ ] **Step 4: Verify route reachability for every projected link** + +Add one property-style loop over representative capability contexts: + +```ts +for (const context of capabilityProfiles) { + for (const domain of buildHousekeepingNavigation(runtime, context, identityTranslate)) { + expect(runtime.match(domain.href), domain.href).not.toBeNull(); + for (const item of domain.items) { + expect(runtime.match(item.href), item.href).not.toBeNull(); + } + } +} +``` + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx +``` + +Expected: PASS. + +- [ ] **Step 5: Commit the navigation invariant** + +Run: + +```powershell +git diff --check +git add src/features/housekeeping/foundation/navigation.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx +git commit -m "fix(housekeeping): link only reachable preview routes" +``` + +--- + +### Task 4: Dispatch `/ase-next` with distinct 403 and 404 behavior + +**Files:** +- Create: `src/features/housekeeping/route-handlers.ts` +- Create: `src/app/ase-next/[domain]/[[...segments]]/page.tsx` +- Create: `src/app/ase-next/[domain]/[[...segments]]/loading.tsx` +- Create: `src/app/ase-next/forbidden.tsx` +- Delete: `src/app/ase-next/[domain]/page.tsx` +- Modify: `src/app/ase-next/page.tsx` +- Modify: `src/app/ase-next/[domain]/layout.tsx` +- Modify: `src/features/housekeeping/foundation/preview-route-contract.test.ts` +- Modify: `src/features/housekeeping/foundation/foundation-source-contract.test.ts` +- Modify: `next.config.ts` + +**Interfaces:** +- Consumes: `createHousekeepingRouteRuntime()`, `buildHousekeepingNavigation()`, `getHousekeepingCapabilityContext()`, and the six manifests. +- Produces: an application dispatcher for exact handled routes, capability-aware bare-domain redirects, and Next.js HTTP access fallbacks. + +- [ ] **Step 1: Write failing app-route tests for the original 404 regression** + +Update route mocks to provide manifests plus handlers, then add: + +```ts +it("redirects a bare domain to its accessible handled landing page", async () => { + routeMocks.getHousekeepingCapabilityContext.mockResolvedValue( + capabilityContext([PERMS.USERS_VIEW]), + ); + + await expect( + HousekeepingDomainPage({ + params: Promise.resolve({ domain: "people", segments: [] }), + }), + ).rejects.toThrow("NEXT_REDIRECT:/ase-next/people/users"); +}); + +it("renders a known permitted handled route", async () => { + const html = await renderRoute( + HousekeepingDomainPage({ + params: Promise.resolve({ domain: "people", segments: ["users"] }), + }), + ); + expect(html).toContain("Rendered people.users"); +}); + +it("returns forbidden for a known route without capability", async () => { + await expect( + HousekeepingDomainPage({ + params: Promise.resolve({ domain: "people", segments: ["users"] }), + }), + ).rejects.toThrow("NEXT_FORBIDDEN"); +}); + +it("returns not found for an unknown path", async () => { + await expect( + HousekeepingDomainPage({ + params: Promise.resolve({ domain: "people", segments: ["missing"] }), + }), + ).rejects.toThrow("NEXT_NOT_FOUND"); +}); +``` + +- [ ] **Step 2: Run route tests and verify RED** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-route-contract.test.ts +``` + +Expected: FAIL because the existing domain page has no catch-all dispatch, handler runtime, redirect, or forbidden boundary. + +- [ ] **Step 3: Enable supported Next.js auth interrupts** + +Add the installed Next.js 16.3.3 option without changing other experimental flags: + +```ts +experimental: { + authInterrupts: true, + optimizePackageImports: ["lucide-react", "date-fns"], + useTypeScriptCli: true, + hideLogsAfterAbort: true, +}, +``` + +Create `src/app/ase-next/forbidden.tsx` as a localized, accessible 403 page with one link back to `/` and no privileged data. + +- [ ] **Step 4: Create the handler registry and catch-all dispatcher** + +The initial application registry is intentionally empty until People supplies real routes: + +```ts +import type { HousekeepingRouteHandler } from "./foundation/routing/route-handler"; + +export const HOUSEKEEPING_ROUTE_HANDLERS = + [] as const satisfies readonly HousekeepingRouteHandler[]; +``` + +In the catch-all page: + +1. create the static registry and runtime; +2. reject an unknown domain with `notFound()` before loading capability context; +3. load the request-scoped capability context; +4. build accessible navigation; +5. redirect an empty suffix to that domain's projected landing href; +6. match a non-empty canonical path; +7. call `notFound()` when no route/handler exists; +8. call `forbidden()` when domain or route capability is absent; +9. render the handler with context, match, translations, and awaited search params. + +Use this canonical path construction: + +```ts +const suffix = segments.map((segment) => encodeURIComponent(segment)).join("/"); +const canonicalPath = suffix + ? `${activeDomain.previewHref}/${suffix}` + : activeDomain.previewHref; +``` + +- [ ] **Step 5: Make root and layout consume the runtime projection** + +`/ase-next` redirects to `navigation[0].href`, not a domain namespace. If no accessible handled route exists, call `forbidden()`. The domain layout calls `notFound()` for an unknown domain and `forbidden()` for a known inaccessible domain, then renders the shell from the same runtime projection. + +- [ ] **Step 6: Verify app-route semantics** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/navigation.test.ts src/features/housekeeping/foundation/preview-gate.test.ts +``` + +Expected: PASS for bare-domain redirect, concrete render, true forbidden, true missing path, request-context reuse, and production gate denial. + +- [ ] **Step 7: Commit dispatcher and access semantics** + +Run: + +```powershell +git diff --check +git add next.config.ts src/features/housekeeping/route-handlers.ts src/features/housekeeping/foundation/preview-route-contract.test.ts src/features/housekeeping/foundation/foundation-source-contract.test.ts +git add -A -- src/app/ase-next +git commit -m "feat(housekeeping): dispatch gated preview routes" +``` + +--- + +### Task 5: Complete shared loading, access, missing, and unexpected-error states + +**Files:** +- Create: `src/app/ase-next/error.tsx` +- Create: `src/app/ase-next/loading.tsx` +- Create: `src/app/ase-next/not-found.tsx` +- Modify: `src/app/ase-next/forbidden.tsx` +- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-state.tsx` +- Modify: `src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx` +- Modify: `src/features/housekeeping/foundation/localization-contract.test.ts` +- Modify: `src/messages/en.json` +- Modify: `src/messages/it.json` +- Modify: `src/messages/nl.json` + +**Interfaces:** +- Consumes: App Router error/access conventions and the existing semantic admin color tokens. +- Produces: localized state surfaces for `loading`, `empty`, `partial`, `validation`, `conflict`, `dependency`, `forbidden`, `not-found`, `error`, and `success` semantics. + +- [ ] **Step 1: Write failing page-state and localization tests** + +Extend the state union test matrix: + +```ts +it.each([ + ["loading", "status"], + ["empty", "status"], + ["partial", "status"], + ["conflict", "alert"], + ["dependency", "alert"], + ["error", "alert"], + ["success", "status"], +] as const)("renders %s with the expected live role", (state, role) => { + const html = renderState(state); + expect(html).toContain(`role="${role}"`); +}); +``` + +Require these English, Italian, and Dutch keys under `pages.housekeeping.states`: `loading`, `empty`, `partial`, `conflict`, `dependency`, `forbidden`, `notFound`, `error`, `success`, `retry`, and `backToSite`. + +- [ ] **Step 2: Run state tests and verify RED** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx src/features/housekeeping/foundation/localization-contract.test.ts +``` + +Expected: FAIL on the new state union and missing translation keys. + +- [ ] **Step 3: Implement state semantics and three locale sources** + +Use explicit tones rather than deriving every non-error as neutral: + +```ts +type HousekeepingPageState = + | "loading" + | "empty" + | "partial" + | "conflict" + | "dependency" + | "error" + | "success"; + +const ALERT_STATES = new Set([ + "conflict", + "dependency", + "error", +]); +``` + +Add complete operator-facing English, Italian, and Dutch messages. Other configured locales continue using the established English fallback and must never render raw keys. + +- [ ] **Step 4: Implement App Router fallback files** + +- `loading.tsx` renders the shared loading state. +- `not-found.tsx` renders an actual missing-route message and links to `/ase-next` only when preview is accessible. +- `forbidden.tsx` explains insufficient access without implying that the page is missing. +- `error.tsx` is a client component that shows `error.digest` as the support reference when present and invokes `reset()` from a localized retry button. + +Do not expose stack traces, exception messages, permission slugs, or database details. + +- [ ] **Step 5: Verify states and route boundaries** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx src/features/housekeeping/foundation/localization-contract.test.ts src/features/housekeeping/foundation/preview-route-contract.test.ts +``` + +Expected: PASS. + +- [ ] **Step 6: Commit shared state behavior** + +Run: + +```powershell +git diff --check +git add src/app/ase-next src/features/housekeeping/foundation/page src/features/housekeeping/foundation/localization-contract.test.ts src/messages/en.json src/messages/it.json src/messages/nl.json +git commit -m "feat(housekeeping): distinguish preview page states" +``` + +--- + +### Task 6: Lock preview isolation and run the full foundation gate + +**Files:** +- Create: `src/features/housekeeping/foundation/cutover-isolation.test.ts` +- Modify only if a test exposes a defect: files already named in Tasks 1-5 + +**Interfaces:** +- Consumes: the completed `/ase-next` foundation. +- Produces: an automated boundary proving that the stable surfaces remain present and the final `/ase` cutover is absent. + +- [ ] **Step 1: Write the isolation contract** + +Create: + +```ts +import { existsSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +describe("Housekeeping stepwise isolation", () => { + it("keeps legacy administration while exposing only the gated preview", () => { + expect(existsSync("src/app/admin/layout.tsx")).toBe(true); + expect(existsSync("src/app/mod/layout.tsx")).toBe(true); + expect(existsSync("src/app/ase-next/layout.tsx")).toBe(true); + expect(existsSync("src/app/admin-next/layout.tsx")).toBe(false); + expect(existsSync("src/app/ase/page.tsx")).toBe(false); + }); +}); +``` + +- [ ] **Step 2: Run the isolation and Housekeeping suites** + +Run: + +```powershell +pnpm.cmd vitest run --coverage.enabled=false src/features/housekeeping/foundation/cutover-isolation.test.ts +pnpm.cmd test:housekeeping +``` + +Expected: both commands PASS. If a failure occurs, fix only the owning foundation behavior and rerun its focused RED/GREEN test before rerunning the gate. + +- [ ] **Step 3: Run repository verification** + +Run: + +```powershell +pnpm.cmd typecheck +pnpm.cmd test +pnpm.cmd build +git diff --check +``` + +Expected: typecheck, all tests, production build, and whitespace validation PASS under Node 26.8.1. The production build must include the route tree while the runtime preview gate remains closed in production. + +- [ ] **Step 4: Inspect the final branch boundary** + +Run: + +```powershell +git diff --stat origin/main...HEAD +git diff --name-status origin/main...HEAD +git grep -n -I '/admin-next' -- src .env.example +git status --short --branch +``` + +Expected: no runtime `/admin-next` matches; no `/ase` cutover files; `/admin` and `/mod` are not deleted; `.remember/` is the only unrelated untracked path. + +- [ ] **Step 5: Commit the isolation gate** + +Run: + +```powershell +git add src/features/housekeeping/foundation/cutover-isolation.test.ts +git commit -m "test(housekeeping): lock stepwise preview isolation" +``` + +--- + +### Task 7: Publish the verified foundation as a draft pull request + +**Files:** +- No source changes. +- PR title: `Rebuild Housekeeping foundation and preview routing` +- PR body language order: English first, Dutch second. + +**Interfaces:** +- Consumes: a clean verified branch from Tasks 1-6 plus the committed design and this plan. +- Produces: remote branch `codex/housekeeping-rebuild-stepwise` and one draft PR targeting `main`. + +- [ ] **Step 1: Verify the publication boundary** + +Run: + +```powershell +git fetch origin +git rev-list --left-right --count origin/main...HEAD +git log --oneline origin/main..HEAD +git status --short --branch +``` + +Expected: the branch contains the design, plan, and focused foundation commits; no tracked modifications are pending; `.remember/` remains untracked. + +- [ ] **Step 2: Push the dedicated branch** + +Run: + +```powershell +git push -u origin codex/housekeeping-rebuild-stepwise +``` + +Expected: pre-push typecheck/tests PASS and the remote branch is created or fast-forwarded. + +- [ ] **Step 3: Create the bilingual draft PR through Forgejo** + +Use Git Credential Manager without printing the credential: + +```powershell +$credentialLines = @("protocol=https", "host=gitlab.epicnabbo.nl", "", "") | + git credential fill +$credential = @{} +foreach ($line in $credentialLines) { + if ($line -match '^([^=]+)=(.*)$') { $credential[$matches[1]] = $matches[2] } +} +if (-not $credential.password) { throw 'Forgejo credential unavailable' } + +$body = @' +## English + +### Scope +- Restores the gated Housekeeping preview at `/ase-next` while keeping `/admin` and `/mod` unchanged. +- Guarantees that every generated navigation link resolves to an accessible registered route with a handler. +- Separates authenticated forbidden access (403) from unknown routes (404). +- Adds localized loading, partial, conflict, dependency, forbidden, missing, error, and success states. + +### Verification +- Housekeeping tests +- Full test suite +- TypeScript typecheck +- Production build +- Preview isolation and route-reachability contracts + +This PR remains draft. People content and later verticals will be added only after this foundation checkpoint is reviewed. + +## Nederlands + +### Omvang +- Herstelt de afgeschermde Housekeeping-preview op `/ase-next`, terwijl `/admin` en `/mod` ongewijzigd blijven. +- Garandeert dat elke gegenereerde navigatielink verwijst naar een toegankelijke geregistreerde route met een handler. +- Maakt onderscheid tussen verboden toegang voor een aangemelde gebruiker (403) en een onbekende route (404). +- Voegt gelokaliseerde statussen toe voor laden, gedeeltelijke resultaten, conflicten, afhankelijkheidsfouten, verboden toegang, ontbrekende pagina's, fouten en succes. + +### Verificatie +- Housekeeping-tests +- Volledige testsuite +- TypeScript-typecontrole +- Productiebuild +- Contracttests voor preview-isolatie en bereikbare routes + +Deze PR blijft een concept. People-content en volgende domeinen worden pas toegevoegd nadat deze foundation-checkpoint is beoordeeld. +'@ + +$payload = @{ + base = "main" + head = "codex/housekeeping-rebuild-stepwise" + title = "Rebuild Housekeeping foundation and preview routing" + body = $body + draft = $true +} | ConvertTo-Json + +$headers = @{ Authorization = "token $($credential.password)" } +Invoke-RestMethod ` + -Method Post ` + -Uri 'https://gitlab.epicnabbo.nl/api/v1/repos/remco/EpicNext-Cms/pulls' ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $payload | + Select-Object number, html_url, state, draft +``` + +Expected: one draft PR targeting `main`; the credential value is never written to output or committed. + +- [ ] **Step 4: Verify the remote PR and checks** + +Query the returned PR number and branch status through the Forgejo API. Confirm `draft: true`, `base.ref: main`, `head.ref: codex/housekeeping-rebuild-stepwise`, and wait for all triggered checks to complete. Do not call the foundation deployed: the preview remains production-disabled and this task does not merge. + +--- + +## Plan completion boundary + +This plan is complete when the draft PR contains a green, non-production `/ase-next` routing foundation and the legacy administration surfaces remain untouched. The next written plan covers the People vertical: workflow audit, users, linked accounts, community/staff, moderation, support, real query/command services, content review, and visual approval. No People page is considered implemented by this foundation plan. -- 2.54.0 From d868c990bf55b9168a953d0554903f745f315c33 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 30 Aug 2026 22:06:54 +0200 Subject: [PATCH 03/62] refactor(housekeeping): restore ase preview namespace --- .../[domain]/layout.tsx | 0 .../[domain]/page.tsx | 0 src/app/{admin-next => ase-next}/layout.tsx | 0 src/app/{admin-next => ase-next}/page.tsx | 0 .../housekeeping/domains/content/manifest.ts | 2 +- .../housekeeping/domains/economy/manifest.ts | 2 +- .../housekeeping/domains/hotel/manifest.ts | 2 +- .../domains/operations/manifest.ts | 2 +- .../housekeeping/domains/people/manifest.ts | 2 +- .../housekeeping/domains/system/manifest.ts | 2 +- .../foundation/contracts/contracts.test.ts | 6 +- .../foundation/contracts/domain.ts | 2 +- .../foundation-source-contract.test.ts | 2 +- .../foundation/navigation.test.ts | 18 +-- .../page/housekeeping-page-state.test.tsx | 8 +- .../foundation/preview-namespace.test.ts | 21 +++ .../foundation/preview-route-contract.test.ts | 120 +++++++++--------- .../housekeeping/foundation/registry.test.ts | 32 ++--- .../housekeeping/foundation/registry.ts | 2 +- .../shell/housekeeping-shell.test.tsx | 20 +-- src/lib/admin-theme-source-audit.test.ts | 2 +- 21 files changed, 133 insertions(+), 112 deletions(-) rename src/app/{admin-next => ase-next}/[domain]/layout.tsx (100%) rename src/app/{admin-next => ase-next}/[domain]/page.tsx (100%) rename src/app/{admin-next => ase-next}/layout.tsx (100%) rename src/app/{admin-next => ase-next}/page.tsx (100%) create mode 100644 src/features/housekeeping/foundation/preview-namespace.test.ts diff --git a/src/app/admin-next/[domain]/layout.tsx b/src/app/ase-next/[domain]/layout.tsx similarity index 100% rename from src/app/admin-next/[domain]/layout.tsx rename to src/app/ase-next/[domain]/layout.tsx diff --git a/src/app/admin-next/[domain]/page.tsx b/src/app/ase-next/[domain]/page.tsx similarity index 100% rename from src/app/admin-next/[domain]/page.tsx rename to src/app/ase-next/[domain]/page.tsx diff --git a/src/app/admin-next/layout.tsx b/src/app/ase-next/layout.tsx similarity index 100% rename from src/app/admin-next/layout.tsx rename to src/app/ase-next/layout.tsx diff --git a/src/app/admin-next/page.tsx b/src/app/ase-next/page.tsx similarity index 100% rename from src/app/admin-next/page.tsx rename to src/app/ase-next/page.tsx diff --git a/src/features/housekeeping/domains/content/manifest.ts b/src/features/housekeeping/domains/content/manifest.ts index f367587c..1b3bb223 100644 --- a/src/features/housekeeping/domains/content/manifest.ts +++ b/src/features/housekeeping/domains/content/manifest.ts @@ -9,7 +9,7 @@ export const contentManifest = { labelKey: "pages.housekeeping.domains.content.title", descriptionKey: "pages.housekeeping.domains.content.description", iconId: "file-text", - previewHref: "/admin-next/content", + previewHref: "/ase-next/content", capability: anyCapability( PERMS.NEWS_VIEW, PERMS.PAGES_VIEW, diff --git a/src/features/housekeeping/domains/economy/manifest.ts b/src/features/housekeeping/domains/economy/manifest.ts index 8eddf3b5..adeae0c6 100644 --- a/src/features/housekeeping/domains/economy/manifest.ts +++ b/src/features/housekeeping/domains/economy/manifest.ts @@ -9,7 +9,7 @@ export const economyManifest = { labelKey: "pages.housekeeping.domains.economy.title", descriptionKey: "pages.housekeeping.domains.economy.description", iconId: "gem", - previewHref: "/admin-next/economy", + previewHref: "/ase-next/economy", capability: anyCapability( PERMS.CATALOG_VIEW, PERMS.SHOP_VIEW, diff --git a/src/features/housekeeping/domains/hotel/manifest.ts b/src/features/housekeeping/domains/hotel/manifest.ts index 8c0ef95b..b7a043b4 100644 --- a/src/features/housekeeping/domains/hotel/manifest.ts +++ b/src/features/housekeeping/domains/hotel/manifest.ts @@ -9,7 +9,7 @@ export const hotelManifest = { labelKey: "pages.housekeeping.domains.hotel.title", descriptionKey: "pages.housekeeping.domains.hotel.description", iconId: "hotel", - previewHref: "/admin-next/hotel", + previewHref: "/ase-next/hotel", capability: anyCapability( PERMS.ROOMS_VIEW, PERMS.RADIO_VIEW, diff --git a/src/features/housekeeping/domains/operations/manifest.ts b/src/features/housekeeping/domains/operations/manifest.ts index c06d2687..823bddc1 100644 --- a/src/features/housekeeping/domains/operations/manifest.ts +++ b/src/features/housekeeping/domains/operations/manifest.ts @@ -9,7 +9,7 @@ export const operationsManifest = { labelKey: "pages.housekeeping.domains.operations.title", descriptionKey: "pages.housekeeping.domains.operations.description", iconId: "inbox", - previewHref: "/admin-next/operations", + previewHref: "/ase-next/operations", capability: anyCapability(PERMS.ADMIN_DASHBOARD), routes: [], searchProviders: [], diff --git a/src/features/housekeeping/domains/people/manifest.ts b/src/features/housekeeping/domains/people/manifest.ts index 21d1f3ea..3ca6e4d1 100644 --- a/src/features/housekeeping/domains/people/manifest.ts +++ b/src/features/housekeeping/domains/people/manifest.ts @@ -9,7 +9,7 @@ export const peopleManifest = { labelKey: "pages.housekeeping.domains.people.title", descriptionKey: "pages.housekeeping.domains.people.description", iconId: "users", - previewHref: "/admin-next/people", + previewHref: "/ase-next/people", capability: anyCapability( PERMS.USERS_VIEW, PERMS.MODERATION_VIEW, diff --git a/src/features/housekeeping/domains/system/manifest.ts b/src/features/housekeeping/domains/system/manifest.ts index f4ba2345..ad665a58 100644 --- a/src/features/housekeeping/domains/system/manifest.ts +++ b/src/features/housekeeping/domains/system/manifest.ts @@ -9,7 +9,7 @@ export const systemManifest = { labelKey: "pages.housekeeping.domains.system.title", descriptionKey: "pages.housekeeping.domains.system.description", iconId: "settings", - previewHref: "/admin-next/system", + previewHref: "/ase-next/system", capability: anyCapability( PERMS.SETTINGS_VIEW, PERMS.LOGS_VIEW, diff --git a/src/features/housekeeping/foundation/contracts/contracts.test.ts b/src/features/housekeeping/foundation/contracts/contracts.test.ts index caf21dd0..7dfa659e 100644 --- a/src/features/housekeeping/foundation/contracts/contracts.test.ts +++ b/src/features/housekeeping/foundation/contracts/contracts.test.ts @@ -28,7 +28,7 @@ const workItem = { occurredAt: "2026-08-24T12:00:00.000Z", titleKey: "pages.housekeeping.items.ticket", context: { ticketId: "42" }, - href: "/admin-next/people/tickets/42", + href: "/ase-next/people/tickets/42", freshness: "fresh", } satisfies HousekeepingWorkItem; @@ -36,7 +36,7 @@ const searchResult = { id: "user-42", domain: "people", title: "operator", - href: "/admin-next/people/users/42", + href: "/ase-next/people/users/42", } satisfies HousekeepingSearchResult; const searchProvider: HousekeepingSearchProvider = { @@ -87,7 +87,7 @@ const manifest: HousekeepingDomainManifest = { labelKey: "pages.housekeeping.domains.people.title", descriptionKey: "pages.housekeeping.domains.people.description", iconId: "users", - previewHref: "/admin-next/people", + previewHref: "/ase-next/people", capability, routes: [], searchProviders: [searchProvider], diff --git a/src/features/housekeeping/foundation/contracts/domain.ts b/src/features/housekeeping/foundation/contracts/domain.ts index 9ccb4f54..ccb4f329 100644 --- a/src/features/housekeeping/foundation/contracts/domain.ts +++ b/src/features/housekeeping/foundation/contracts/domain.ts @@ -17,7 +17,7 @@ export interface HousekeepingDomainManifest { labelKey: string; descriptionKey: string; iconId: "inbox" | "users" | "file-text" | "gem" | "hotel" | "settings"; - previewHref: `/admin-next/${HousekeepingDomainId}`; + previewHref: `/ase-next/${HousekeepingDomainId}`; capability: CapabilityRequirement; routes: readonly HousekeepingRouteDefinition[]; searchProviders: readonly HousekeepingSearchProvider[]; diff --git a/src/features/housekeeping/foundation/foundation-source-contract.test.ts b/src/features/housekeeping/foundation/foundation-source-contract.test.ts index f3ad7eb0..4c3c019f 100644 --- a/src/features/housekeeping/foundation/foundation-source-contract.test.ts +++ b/src/features/housekeeping/foundation/foundation-source-contract.test.ts @@ -555,7 +555,7 @@ describe("housekeeping foundation completion contracts", () => { for (const path of [ "src/app/admin/layout.tsx", "src/app/mod/layout.tsx", - "src/app/admin-next/layout.tsx", + "src/app/ase-next/layout.tsx", ]) { expect(existsSync(path), path).toBe(true); } diff --git a/src/features/housekeeping/foundation/navigation.test.ts b/src/features/housekeeping/foundation/navigation.test.ts index 88dce236..32465ac6 100644 --- a/src/features/housekeeping/foundation/navigation.test.ts +++ b/src/features/housekeeping/foundation/navigation.test.ts @@ -23,7 +23,7 @@ describe("housekeeping navigation", () => { labelKey: "pages.housekeeping.domains.people.title", descriptionKey: "pages.housekeeping.domains.people.description", iconId: "users", - previewHref: "/admin-next/people", + previewHref: "/ase-next/people", capability: anyCapability(PERMS.MOD_CFH_VIEW), routes: [], searchProviders: [], @@ -35,7 +35,7 @@ describe("housekeeping navigation", () => { labelKey: "pages.housekeeping.domains.economy.title", descriptionKey: "pages.housekeeping.domains.economy.description", iconId: "gem", - previewHref: "/admin-next/economy", + previewHref: "/ase-next/economy", capability: anyCapability(PERMS.CATALOG_VIEW), routes: [], searchProviders: [], @@ -53,7 +53,7 @@ describe("housekeeping navigation", () => { expect(navigation).toEqual([ { id: "people", - href: "/admin-next/people", + href: "/ase-next/people", iconId: "users", label: "pages.housekeeping.domains.people.title", description: "pages.housekeeping.domains.people.description", @@ -69,19 +69,19 @@ describe("housekeeping navigation", () => { labelKey: "people.title", descriptionKey: "people.description", iconId: "users", - previewHref: "/admin-next/people", + previewHref: "/ase-next/people", capability: anyCapability(PERMS.USERS_VIEW), routes: [ { id: "users", labelKey: "people.users", - href: "/admin-next/people/users", + href: "/ase-next/people/users", capability: anyCapability(PERMS.USERS_VIEW), }, { id: "bans", labelKey: "people.bans", - href: "/admin-next/people/bans", + href: "/ase-next/people/bans", capability: anyCapability(PERMS.BANS_VIEW), }, ], @@ -94,7 +94,7 @@ describe("housekeeping navigation", () => { labelKey: "system.title", descriptionKey: "system.description", iconId: "settings", - previewHref: "/admin-next/system", + previewHref: "/ase-next/system", capability: anyCapability(PERMS.SETTINGS_VIEW), routes: [], searchProviders: [], @@ -113,14 +113,14 @@ describe("housekeeping navigation", () => { expect(navigation).toEqual([ { id: "people", - href: "/admin-next/people", + href: "/ase-next/people", iconId: "users", label: "translated:people.title", description: "translated:people.description", items: [ { id: "users", - href: "/admin-next/people/users", + href: "/ase-next/people/users", label: "translated:people.users", }, ], diff --git a/src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx b/src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx index b6c9ecbf..13c02396 100644 --- a/src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx +++ b/src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx @@ -75,12 +75,12 @@ describe("HousekeepingPageState", () => { state="error" title="Could not load" description="Try again later" - retryAction={Retry preview} + retryAction={Retry preview} />, ); expect(html).toContain('role="alert"'); - expect(html).toContain('href="/admin-next/operations"'); + expect(html).toContain('href="/ase-next/operations"'); expect(html).toContain(">Retry preview<"); }); }); @@ -92,7 +92,7 @@ describe("HousekeepingPageShell", () => { title="People" description="Review operator-facing people data" context={Preview context} - primaryAction={Create preview} + primaryAction={Create preview} >

Page body

, @@ -102,7 +102,7 @@ describe("HousekeepingPageShell", () => { expect(html).toContain(">People<"); expect(html).toContain(">Review operator-facing people data<"); expect(html).toContain(">Preview context<"); - expect(html).toContain('href="/admin-next/people/new"'); + expect(html).toContain('href="/ase-next/people/new"'); expect(html).toContain(">Create preview<"); expect(html).toContain(">Page body

"); }); diff --git a/src/features/housekeeping/foundation/preview-namespace.test.ts b/src/features/housekeeping/foundation/preview-namespace.test.ts new file mode 100644 index 00000000..b6e5c8eb --- /dev/null +++ b/src/features/housekeeping/foundation/preview-namespace.test.ts @@ -0,0 +1,21 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +describe("Housekeeping preview namespace", () => { + it("uses /ase-next and removes the previous runtime namespace", () => { + const removedNamespace = ["/admin", "-next"].join(""); + const files = execFileSync("git", ["ls-files", "src", ".env.example"], { + encoding: "utf8", + }) + .trim() + .split(/\r?\n/) + .filter(Boolean); + + const offenders = files.filter((file) => + readFileSync(file, "utf8").includes(removedNamespace), + ); + + expect(offenders).toEqual([]); + }); +}); diff --git a/src/features/housekeeping/foundation/preview-route-contract.test.ts b/src/features/housekeeping/foundation/preview-route-contract.test.ts index ae9bd9b6..1791c959 100644 --- a/src/features/housekeeping/foundation/preview-route-contract.test.ts +++ b/src/features/housekeeping/foundation/preview-route-contract.test.ts @@ -78,16 +78,16 @@ vi.mock("@/app/actions", () => { throw new Error("preview routes must not import actions"); }); -import AdminNextDomainLayout from "@/app/admin-next/[domain]/layout"; -import AdminNextDomainPage from "@/app/admin-next/[domain]/page"; -import AdminNextLayout from "@/app/admin-next/layout"; -import AdminNextPage from "@/app/admin-next/page"; +import AdminNextDomainLayout from "@/app/ase-next/[domain]/layout"; +import AdminNextDomainPage from "@/app/ase-next/[domain]/page"; +import AdminNextLayout from "@/app/ase-next/layout"; +import AdminNextPage from "@/app/ase-next/page"; const routeFiles = [ - "src/app/admin-next/layout.tsx", - "src/app/admin-next/page.tsx", - "src/app/admin-next/[domain]/layout.tsx", - "src/app/admin-next/[domain]/page.tsx", + "src/app/ase-next/layout.tsx", + "src/app/ase-next/page.tsx", + "src/app/ase-next/[domain]/layout.tsx", + "src/app/ase-next/[domain]/page.tsx", ] as const; const forbiddenModuleRoots = [ @@ -360,7 +360,7 @@ async function renderRoute(route: ReactNode | Promise) { return renderToStaticMarkup(await route); } -describe("/admin-next preview gate", () => { +describe("/ase-next preview gate", () => { beforeEach(() => { vi.clearAllMocks(); routeMocks.env.NODE_ENV = "test"; @@ -402,7 +402,7 @@ describe("/admin-next preview gate", () => { ); }); -describe("/admin-next first visible domain", () => { +describe("/ase-next first visible domain", () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -413,9 +413,9 @@ describe("/admin-next first visible domain", () => { ); await expect(AdminNextPage()).rejects.toThrow( - "NEXT_REDIRECT:/admin-next/operations", + "NEXT_REDIRECT:/ase-next/operations", ); - expect(routeMocks.redirect).toHaveBeenCalledWith("/admin-next/operations"); + expect(routeMocks.redirect).toHaveBeenCalledWith("/ase-next/operations"); expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes( 1, ); @@ -428,9 +428,9 @@ describe("/admin-next first visible domain", () => { ); await expect(AdminNextPage()).rejects.toThrow( - "NEXT_REDIRECT:/admin-next/people", + "NEXT_REDIRECT:/ase-next/people", ); - expect(routeMocks.redirect).toHaveBeenCalledWith("/admin-next/people"); + expect(routeMocks.redirect).toHaveBeenCalledWith("/ase-next/people"); expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes( 1, ); @@ -450,7 +450,7 @@ describe("/admin-next first visible domain", () => { }); }); -describe("/admin-next/[domain] layout", () => { +describe("/ase-next/[domain] layout", () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -515,7 +515,7 @@ describe("/admin-next/[domain] layout", () => { }); }); -describe("/admin-next/[domain] page", () => { +describe("/ase-next/[domain] page", () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -561,247 +561,247 @@ describe("preview route import boundary", () => { it.each([ [ "relative database import with resolver extension", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import db from "../../lib/db.js";', "src/lib/db", ], [ "aliased database import with resolver extension", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import db from "@/lib/db.js";', "src/lib/db", ], [ "action root import with resolver extension", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import actions from "../../actions.mjs";', "src/actions", ], [ "legacy mod root import with resolver extension", - "src/app/admin-next/layout.tsx", + "src/app/ase-next/layout.tsx", 'import mod from "../mod.cjs";', "src/app/mod", ], [ "database import with query suffix", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import db from "../../lib/db?server-only";', "src/lib/db", ], [ "action import with hash suffix", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import("../../actions/users#server")', "src/actions/users", ], [ "Windows-style relative database import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", String.raw`import db from "..\\..\\lib\\db";`, "src/lib/db", ], [ "Windows-style aliased database import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", String.raw`import db from "@\\lib\\db";`, "src/lib/db", ], [ "percent-encoded database import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import db from "../../lib/%64%62";', "src/lib/db", ], [ "optional CommonJS database require", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'require?.("../../lib/db")', "src/lib/db", ], [ "module database require", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'module.require("../../lib/db")', "src/lib/db", ], [ "require.resolve database access", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'require.resolve("../../lib/db")', "src/lib/db", ], [ "optional module database require", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'module.require?.("../../lib/db")', "src/lib/db", ], [ "optional require.resolve database access", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'require.resolve?.("../../lib/db")', "src/lib/db", ], [ "TypeScript import-equals database access", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import db = require("../../lib/db");', "src/lib/db", ], [ "U+2028 line-continuation database import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import db from "../\\' + "\u2028" + '../lib/db";', "src/lib/db", ], [ "U+2029 line-continuation database import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import db from "../\\' + "\u2029" + '../lib/db";', "src/lib/db", ], [ "parenthesized dynamic action import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import(("../../actions/users"))', "src/actions/users", ], [ "regex-brace template-expression action import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", `const x = \`${interpolationOpen}/}/.test(value) ? import("../../actions/users") : null}\`;`, "src/actions/users", ], [ "CommonJS database require", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'require("../../lib/db")', "src/lib/db", ], [ "template-literal dynamic action import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", "import(`../../actions/users`)", "src/actions/users", ], [ "TypeScript-asserted dynamic action import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import(("../../actions/users" as string))', "src/actions/users", ], [ "template-expression dynamic action import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", `const x = \`${interpolationOpen}import("../../actions/users")}\`;`, "src/actions/users", ], [ "nested template-expression dynamic action import", - "src/app/admin-next/[domain]/page.tsx", + "src/app/ase-next/[domain]/page.tsx", `const x = \`${interpolationOpen}ready ? \`${interpolationOpen}import("../../../actions/nested")}\` : ""}\`;`, "src/actions/nested", ], [ "unicode escaped dynamic app-action import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import("\\u002e\\u002e/actions/users")', "src/app/actions/users", ], [ "code-point escaped dynamic app-action import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import("\\u{2e}\\u{2e}/actions/users")', "src/app/actions/users", ], [ "hex escaped export-from auth import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'export * from "\\x2e\\x2e/\\x2e\\x2e/lib/auth";', "src/lib/auth", ], [ "escaped-slash permissions import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import permissions from "..\\/..\\/lib\\/permissions";', "src/lib/permissions", ], [ "unknown escape database import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import db from "../../\\lib/db";', "src/lib/db", ], [ "line-continuation database import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import db from "../\\' + "\n" + '../lib/db";', "src/lib/db", ], [ "aliased database descendant import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import { query } from "@/lib/db/query";', "src/lib/db/query", ], [ "root relative database import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import { db } from "../../lib/db";', "src/lib/db", ], [ "domain relative auth side-effect import", - "src/app/admin-next/[domain]/layout.tsx", + "src/app/ase-next/[domain]/layout.tsx", 'import "../../../lib/auth";', "src/lib/auth", ], [ "domain relative permissions export", - "src/app/admin-next/[domain]/page.tsx", + "src/app/ase-next/[domain]/page.tsx", 'export { getAdminContext } from "../../../lib/permissions";', "src/lib/permissions", ], [ "root relative action dynamic import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import("../../actions/users")', "src/actions/users", ], [ "root relative app action export", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'export * from "../actions";', "src/app/actions", ], [ "domain relative legacy admin import", - "src/app/admin-next/[domain]/layout.tsx", + "src/app/ase-next/[domain]/layout.tsx", 'import page from "../../admin/users/page";', "src/app/admin/users/page", ], [ "root relative legacy mod dynamic import", - "src/app/admin-next/layout.tsx", + "src/app/ase-next/layout.tsx", 'import("../mod/users/page")', "src/app/mod/users/page", ], [ "Prisma TypeScript import type", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'type PrismaClient = import("@prisma/client").PrismaClient;', "@prisma/client", ], [ "Drizzle package import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import { sql } from "drizzle-orm";', "drizzle-orm", ], [ "mysql2 package import", - "src/app/admin-next/page.tsx", + "src/app/ase-next/page.tsx", 'import type { Pool } from "mysql2";', "mysql2", ], @@ -834,7 +834,7 @@ describe("preview route import boundary", () => { ], ] as const)("fails closed for non-literal %s", (_name, source, violation) => { expect( - findRouteImportBoundaryViolations(source, "src/app/admin-next/page.tsx"), + findRouteImportBoundaryViolations(source, "src/app/ase-next/page.tsx"), ).toContain(violation); }); @@ -843,7 +843,7 @@ describe("preview route import boundary", () => { 'import database from "@/lib/database.js?raw";', 'import dbTools from "../../lib/db-tools.ts";', 'import auth from "../../lib/authentication";', - 'import preview from "../admin-next-shared";', + 'import preview from "../ase-next-shared";', 'import prismaTools from "@prisma/client-tools";', 'import drizzleTools from "drizzle-orm-kit";', 'import mysqlTools from "mysql2-wrapper";', @@ -853,7 +853,7 @@ describe("preview route import boundary", () => { ].join("\n"); expect( - findRouteImportBoundaryViolations(source, "src/app/admin-next/page.tsx"), + findRouteImportBoundaryViolations(source, "src/app/ase-next/page.tsx"), ).toEqual([]); }); }); diff --git a/src/features/housekeeping/foundation/registry.test.ts b/src/features/housekeeping/foundation/registry.test.ts index 5bbc528d..b82dcb4a 100644 --- a/src/features/housekeeping/foundation/registry.test.ts +++ b/src/features/housekeeping/foundation/registry.test.ts @@ -25,7 +25,7 @@ const manifest = ( labelKey: `pages.housekeeping.domains.${id}.title`, descriptionKey: `pages.housekeeping.domains.${id}.description`, iconId: "settings", - previewHref: `/admin-next/${id}`, + previewHref: `/ase-next/${id}`, capability: anyCapability(capabilitySlug), routes: [], searchProviders: [], @@ -73,7 +73,7 @@ const expectedManifests = [ { id: "operations", iconId: "inbox", - previewHref: "/admin-next/operations", + previewHref: "/ase-next/operations", labelKey: "pages.housekeeping.domains.operations.title", descriptionKey: "pages.housekeeping.domains.operations.description", slugs: [PERMS.ADMIN_DASHBOARD], @@ -81,7 +81,7 @@ const expectedManifests = [ { id: "people", iconId: "users", - previewHref: "/admin-next/people", + previewHref: "/ase-next/people", labelKey: "pages.housekeeping.domains.people.title", descriptionKey: "pages.housekeeping.domains.people.description", slugs: [ @@ -112,7 +112,7 @@ const expectedManifests = [ { id: "content", iconId: "file-text", - previewHref: "/admin-next/content", + previewHref: "/ase-next/content", labelKey: "pages.housekeeping.domains.content.title", descriptionKey: "pages.housekeeping.domains.content.description", slugs: [ @@ -135,7 +135,7 @@ const expectedManifests = [ { id: "economy", iconId: "gem", - previewHref: "/admin-next/economy", + previewHref: "/ase-next/economy", labelKey: "pages.housekeeping.domains.economy.title", descriptionKey: "pages.housekeeping.domains.economy.description", slugs: [ @@ -148,7 +148,7 @@ const expectedManifests = [ { id: "hotel", iconId: "hotel", - previewHref: "/admin-next/hotel", + previewHref: "/ase-next/hotel", labelKey: "pages.housekeeping.domains.hotel.title", descriptionKey: "pages.housekeeping.domains.hotel.description", slugs: [ @@ -165,7 +165,7 @@ const expectedManifests = [ { id: "system", iconId: "settings", - previewHref: "/admin-next/system", + previewHref: "/ase-next/system", labelKey: "pages.housekeeping.domains.system.title", descriptionKey: "pages.housekeeping.domains.system.description", slugs: [ @@ -208,7 +208,7 @@ describe("housekeeping registry", () => { it("rejects invalid preview and empty translation keys", () => { expect(() => createHousekeepingRegistry([ - { ...manifest("people"), previewHref: "/admin-next/operations" }, + { ...manifest("people"), previewHref: "/ase-next/operations" }, ]), ).toThrow("invalid preview href"); expect(() => @@ -232,13 +232,13 @@ describe("housekeeping registry", () => { { id: "users", labelKey: "pages.housekeeping.domains.people.title", - href: "/admin-next/people/users", + href: "/ase-next/people/users", capability: anyCapability(PERMS.USERS_VIEW), }, { id: "users", labelKey: "pages.housekeeping.domains.people.title", - href: "/admin-next/people/staff", + href: "/ase-next/people/staff", capability: anyCapability(PERMS.USERS_VIEW), }, ], @@ -253,13 +253,13 @@ describe("housekeeping registry", () => { { id: "users", labelKey: "pages.housekeeping.domains.people.title", - href: "/admin-next/people/users", + href: "/ase-next/people/users", capability: anyCapability(PERMS.USERS_VIEW), }, { id: "staff", labelKey: "pages.housekeeping.domains.people.title", - href: "/admin-next/people/users", + href: "/ase-next/people/users", capability: anyCapability(PERMS.USERS_VIEW), }, ], @@ -272,7 +272,7 @@ describe("housekeeping registry", () => { const route = { id: "users", labelKey: "pages.housekeeping.domains.people.title", - href: "/admin-next/people/users", + href: "/ase-next/people/users", capability: anyCapability(PERMS.USERS_VIEW), }; @@ -481,7 +481,7 @@ describe("housekeeping registry", () => { { id: "users", labelKey: "pages.housekeeping.domains.people.title", - href: "/admin-next/people/users", + href: "/ase-next/people/users", capability: { mode: "any", slugs: [] }, }, ], @@ -494,7 +494,7 @@ describe("housekeeping registry", () => { const route = { id: "shared", labelKey: "pages.housekeeping.domains.people.title", - href: "/admin-next/shared", + href: "/ase-next/shared", capability: anyCapability(PERMS.USERS_VIEW), }; @@ -503,7 +503,7 @@ describe("housekeeping registry", () => { { ...manifest("people"), routes: [route] }, { ...manifest("content"), - routes: [{ ...route, href: "/admin-next/content/shared" }], + routes: [{ ...route, href: "/ase-next/content/shared" }], }, ]), ).toThrow("duplicate route id"); diff --git a/src/features/housekeeping/foundation/registry.ts b/src/features/housekeeping/foundation/registry.ts index f301ebbe..d72ccce4 100644 --- a/src/features/housekeeping/foundation/registry.ts +++ b/src/features/housekeeping/foundation/registry.ts @@ -34,7 +34,7 @@ export function createHousekeepingRegistry( } domainIds.add(manifest.id); - if (manifest.previewHref !== `/admin-next/${manifest.id}`) { + if (manifest.previewHref !== `/ase-next/${manifest.id}`) { throw new Error(`invalid preview href: ${manifest.previewHref}`); } validateNonEmpty(manifest.labelKey, "label key"); diff --git a/src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx b/src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx index 0aac7178..f462514a 100644 --- a/src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx +++ b/src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx @@ -17,17 +17,17 @@ const labels = { const domains: readonly HousekeepingNavigationDomain[] = [ { id: "operations", - href: "/admin-next/operations", + href: "/ase-next/operations", iconId: "inbox", label: "Operations", description: "Manage operations", items: [ - { id: "queue", href: "/admin-next/operations/queue", label: "Queue" }, + { id: "queue", href: "/ase-next/operations/queue", label: "Queue" }, ], }, { id: "people", - href: "/admin-next/people", + href: "/ase-next/people", iconId: "users", label: "People", description: "Manage people", @@ -35,7 +35,7 @@ const domains: readonly HousekeepingNavigationDomain[] = [ }, { id: "content", - href: "/admin-next/content", + href: "/ase-next/content", iconId: "file-text", label: "Content", description: "Manage content", @@ -43,7 +43,7 @@ const domains: readonly HousekeepingNavigationDomain[] = [ }, { id: "economy", - href: "/admin-next/economy", + href: "/ase-next/economy", iconId: "gem", label: "Economy", description: "Manage economy", @@ -51,7 +51,7 @@ const domains: readonly HousekeepingNavigationDomain[] = [ }, { id: "hotel", - href: "/admin-next/hotel", + href: "/ase-next/hotel", iconId: "hotel", label: "Hotel", description: "Manage hotel", @@ -59,7 +59,7 @@ const domains: readonly HousekeepingNavigationDomain[] = [ }, { id: "system", - href: "/admin-next/system", + href: "/ase-next/system", iconId: "settings", label: "System", description: "Manage system", @@ -178,7 +178,7 @@ describe("HousekeepingShell", () => { expect(html).toContain('