---
name: frontend-routes
description: >
  Generates PageRegistry registrations for SmartStack's DB-driven routing
  (DynamicRouter). Validates componentKey format before emission and fails
  loud on bad specs rather than producing silent-spinner registries.
phase: development/frontend
cli: cli/scaffold-routes
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# Frontend Routes — PageRegistry + DynamicRouter

Generates route registrations for SmartStack's database-driven routing system.
The CLI emits **one file per module** (`src/extensions/{module}Registry.ts`)
that calls `PageRegistry.register()` for each page.

## Output pattern

```ts
// src/extensions/hrmRegistry.ts
import React from 'react';
import { PageRegistry, lazyWithRetry } from '@atlashub/smartstack';

const EmployeeListPage = lazyWithRetry(() =>
  import('@/pages/myapp/hrm/employees/EmployeeListPage').then((m) => {
    const mod = m as { default?: React.ComponentType; EmployeeListPage?: React.ComponentType };
    const resolved = mod.EmployeeListPage ?? mod.default;
    if (!resolved) {
      throw new Error("Page 'EmployeeListPage' at @/pages/myapp/hrm/employees/EmployeeListPage has no default or named export matching its file name.");
    }
    return { default: resolved };
  })
);

PageRegistry.register('myapp.hrm.employees', EmployeeListPage);
// ... detail, create, edit
```

`lazyWithRetry` (re-exported from `@atlashub/smartstack` since v3.51+) wraps
`React.lazy` with exponential-backoff retry on transient `ChunkLoadError`s
(HMR rebundle window, CDN flap, post-deploy stale cache). Without retry, a
transient `Failed to fetch dynamically imported module` surfaces immediately
as a `RouteErrorBoundary` screen on the user's first navigation after a
deploy. With retry, the next attempt picks up the fresh chunk.

The wrapper also tolerates both `export default` and `export const {PageName}`
forms. If neither exists, it throws at resolution time with a developer-readable
message — preventing the classic silent-spinner failure.

## ⚠ BLOCKING — componentKey validation

The CLI validates every emitted `componentKey` against SmartStack's contract
BEFORE writing files. On failure, the CLI throws — no partial registry is emitted.

Rules (enforced by `validateComponentKey` in `types.ts`):
- 2 to 5 dot-separated segments: `{app}.{module}`, `{app}.{module}.{section}`, `{app}.{module}.{section}.{view}`, or `{app}.{module}.{parentSection}.{section}.{view}` (nested resources)
- Each segment matches `/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/` (kebab-case, starts with letter)
- Final segment may be one of the 24 implicit suffixes (detail, edit, create, new, duplicate, settings, configure, permissions, members, history, logs, analytics, preview, versions, comments, attachments, audit, export, notifications, schedule, workflow, summary, test, runs, import)

Rejected examples:
- `Hrm.Employees.List` (not kebab-case)
- `hrm.employees` (only 2 segments when app is required)
- `myapp.hrm..employees` (empty segment)
- `myapp.hrm.employees.detail.extra.view` (6 segments)

## Entity fields

Each entity in the spec defines one or more pages:

| Field | Type | Required | Notes |
|---|---|---|---|
| `name` | string | yes | Entity name (PascalCase). |
| `section` | string | yes | Section code (kebab-case). |
| `views` | array | yes | Views to generate: `list`, `detail`, `create`, `edit`, `dashboard`, `kanban`, `reconduction`, `app-home`, `module-home`, `section-home`. Default `['list', 'detail', 'create']`. |
| `pluralName` | string | no | Explicit plural form (PascalCase). When omitted, falls back to `${name}s`. |
| `parentSection` | string | no | Optional parent section (kebab-case) for nested resources. When set, componentKey becomes `{app}.{module}.{parentSection}.{section}[.{view}]`. |
| `pageFilePaths` | object | no | Per-view file path overrides (e.g. `{ "list": "src/pages/budgeting/budgets/BudgetsListPage.tsx" }`). Keys: `'list'`, `'detail'`, `'create'`, `'edit'`. When provided, overrides the default `@/pages/{appCode}/{module}/{section}/` layout. |

## ⚠ BLOCKING — Wiring in main.tsx

Every generated `{module}Registry.ts` MUST be imported in `src/main.tsx` BEFORE
`createRoot(...).render(...)`:

```tsx
import './extensions/hrmRegistry';
import './extensions/catalogueRegistry';
// ... one per module

createRoot(...).render(<App />);
```

Missing this import = the `PageRegistry.register()` calls never run = `DynamicRouter`
finds nothing = blank screen for every route in that module.

The `frontend/structure` SKILL's `main.tsx` template lists the expected shape.

## ⚠ BLOCKING — Key Rules

1. **DB-driven routes**: routes come from `GET /api/navigation/menu`, NEVER hardcoded in the React tree.
2. **PageRegistry**: central registry mapping `componentKey` → lazy-loaded component.
3. **Implicit suffixes**: `.detail` → `/:id`, `.edit` → `/:id/edit`, `.create` → `/create`. Handled by `DynamicRouter` — do NOT add `<Route path="/:id">` manually.
4. **ProtectedRoute**: applied globally by `DynamicRouter` — no per-route wrapping needed.
5. **Lazy loading**: ALL pages via `lazyWithRetry()` (re-exported from `@atlashub/smartstack`) — never bare `React.lazy()` (no retry on chunk-load races) and never static imports of page components.
6. **No hardcoded navigation**: `<Link to={getPath('myapp.hrm.employees')}>` via SDK helper, or relative `<Link to="../employees/123">`.
7. **componentKey MUST match the seed ComponentKey**: backend seed + frontend registry are two halves of the same contract. Diff = silent spinner.
8. **Permission matching via `hasRouteAccess(permKey, routePath)`** — never bypass with `hasFullAccess`, never compare strings raw. Rules: `*` matches everything (super-admin only), `app.*` matches any `app.module.action`, `app.module.read` implies access to `app.module` (read is the baseline access action), otherwise exact match.

## Menu API contract (DB → DTO → DynamicRouter)

`GET /api/navigation/menu` returns a **4-level hierarchy of DTOs** that the frontend's `useRouteConfig` flattens into routes:

```
ApplicationMenuDto  →  ModuleMenuDto  →  SectionMenuDto  →  ResourceMenuDto
       │                    │                  │                  │
       ├─ ComponentKey      ├─ ComponentKey    ├─ ComponentKey    ├─ ComponentKey
       ├─ PermissionPath    ├─ PermissionPath  ├─ PermissionPath  └─ PermissionPath
       ├─ IsOpen ⚠          ├─ RequiredFeature
       └─ RequiredFeature
```

- `ComponentKey` + `PermissionPath` exist at **every** level (computed at runtime from hierarchical code paths).
- **`IsOpen` exists ONLY on `ApplicationMenuDto`** — domain property persisted on `core.nav_Applications`. When `true`, bypasses permission + feature checks at the Application level (e.g. `myspace.*` is always accessible to authenticated users). The seeding skill MUST set `IsOpen` on the Application, never on lower levels.
- **`RequiredFeature` exists on `ApplicationMenuDto` and `ModuleMenuDto`** (2 levels, sourced from `LicenseFeatureMapping`) — gates by license edition.
- `OUTLET_SECTIONS` config (in `DynamicRouter`) declares sections that render their sub-pages via React Router `<Outlet>` (tab UIs like `Settings`, `AppSettings`). Components resolve from `PageRegistry` — never hardcode the Outlet tree.

## After generation — MANDATORY

```
@.claude/skills/development/audit/SKILL.md
Audit the route registrations I just generated. Check lazy-loading,
componentKey format, main.tsx wiring, no hardcoded <Route path=>.
```

## Invocation

```bash
npx --prefer-offline tsx skills/development/frontend/routes/cli/scaffold-routes/index.ts \
  --spec '{"module":"hrm","appCode":"myapp","entities":[{"name":"Employee","section":"employees","views":["list","detail","create"]}],"projectPath":"/path"}'
```

## Output files

- `src/extensions/{module}Registry.ts` — one file per module, contains all `PageRegistry.register(...)` calls.
- `src/extensions/{module}Routes.ts` — single source of truth for URL paths in
  the module. Generated alongside the registry from the same spec, so URL
  paths and componentKeys can never drift. Generated pages MUST consume it
  (`import { routes } from '@/extensions/${module}Routes'`) instead of
  hardcoding URLs in `navigate()` calls.

  ```ts
  // Example: src/extensions/budgetsRoutes.ts
  export const routes = {
    budgets: {
      list:   () => '/budgets/budgets',
      detail: (id: string) => `/budgets/budgets/${id}`,
      edit:   (id: string) => `/budgets/budgets/${id}/edit`,
      create: () => '/budgets/budgets/create',
    },
  } as const;
  ```

  URL conventions match `DynamicRouter`'s `IMPLICIT_SUFFIXES` table : `.create`
  → `/create` (NOT `/new`), `.detail` → `/:id`, `.edit` → `/:id/edit`. Audit
  DEV-UI-013 catches any page that bypasses the helper with a hardcoded URL.
