---
name: step-03d-layer3-frontend
description: "Layer 3: Frontend pages, i18n, routes, documentation, and tests"
model: opus
parent_step: steps/step-03-execute.md
---

**ULTRA THINK about frontend completeness: new pages, routes, i18n keys, AND permissions/navigation alignment with Layer 1 seed data.**

# Layer 3 — Frontend (Pages + I18n + Documentation)

### Companion Specs Loading (delegate mode)

```javascript
if (delegate_mode && specification_loading_plan) {
  const specsDir = path.dirname(delegate_prd_path);
  // Layer 3 loads: screens + usecases companions
  for (const file of specification_loading_plan.layer3_frontend) {
    const specPath = path.join(specsDir, file);
    if (fileExists(specPath)) {
      const specData = readJSON(specPath);
      if (specData.screens) {
        // Full screen definitions: columns[], fields[], filters[], actions[], kpis[]
        // Use for: DataTable columns, form fields, filter configs, action buttons, dashboard KPIs
        console.log(`Loaded screens: ${specData.screens.length} screen specs with columns/fields/filters`);
      }
      if (specData.useCases || specData.usecases) {
        const ucs = specData.useCases || specData.usecases;
        // UC steps inform form validation, navigation flows, error states
        console.log(`Loaded usecases: ${ucs.length} use cases for form flows`);
      }
    }
  }
}
```

> When companion specs are loaded:
> - **List pages**: use `screen.columns[]` for exact DataTable column definitions (key, label, type, sortable)
> - **Form pages**: use `screen.fields[]` for exact form field definitions (name, type, required, validation)
> - **Filter configs**: use `screen.filters[]` for search/filter UI generation
> - **Dashboard pages**: use `screen.kpis[]` for KPI card and chart definitions
> - **Actions**: use `screen.actions[]` for action buttons (create, export, bulk operations)

### Build Per-Entity PRD Context Block (delegate mode)

> **CRITICAL:** This block builds the authoritative specification that MUST be passed to `/ui-components`.
> Without it, `/ui-components` infers columns from entity structure and misses PRD-specified fields
> (e.g., Person Extension Pattern display fields like DisplayFirstName, DisplayLastName).

When companion specs are loaded, build a `{prd_context}` map keyed by entity name:

```javascript
const prdContextMap = {};  // entityName → structured PRD block

if (screensData?.sections) {
  for (const section of screensData.sections) {
    for (const resource of (section.resources || [])) {
      if (!resource.entity) continue;
      const entityName = resource.entity;

      if (!prdContextMap[entityName]) {
        prdContextMap[entityName] = { screens: [], labels: {}, permissions: [] };
      }

      // Collect screen resource specs
      prdContextMap[entityName].screens.push({
        sectionCode: section.code,
        sectionType: section.sectionType,
        resourceType: resource.type,
        columns: resource.columns || null,
        fields: resource.fields || null,
        actions: resource.actions || [],
        defaultSort: resource.defaultSort || null,
        defaultPageSize: resource.defaultPageSize || 20
      });

      // Collect section labels (4 languages)
      if (section.labels) {
        prdContextMap[entityName].labels[section.code] = section.labels;
      }

      // Collect permissions
      if (section.permission) {
        prdContextMap[entityName].permissions.push(section.permission);
      }
    }
  }
}
```

Then, for each entity, serialize the PRD context into a text block to inject into `/ui-components`:

```javascript
function buildPrdBlock(entityName) {
  const ctx = prdContextMap[entityName];
  if (!ctx || ctx.screens.length === 0) return '';

  let block = '\n--- PRD SPECIFICATION (AUTHORITATIVE — use these EXACT definitions) ---\n';

  // SmartTable columns
  const tableScreens = ctx.screens.filter(s => s.resourceType === 'SmartTable' && s.columns);
  for (const screen of tableScreens) {
    block += `\nCOLUMNS for DataTable (section: "${screen.sectionCode}") — use ALL, in this EXACT order:\n`;
    for (const col of screen.columns) {
      block += `  - key: "${col.field}", label_fr: "${col.label?.fr || col.field}", ` +
               `label_en: "${col.label?.en || col.field}", format: "${col.format}", ` +
               `sortable: ${col.sortable || false}\n`;
    }
    if (screen.actions.length > 0) {
      block += `ACTIONS: ${screen.actions.join(', ')}\n`;
    }
    if (screen.defaultSort) {
      block += `DEFAULT SORT: field="${screen.defaultSort.field}", direction="${screen.defaultSort.direction}"\n`;
    }
    block += `PAGE SIZE: ${screen.defaultPageSize}\n`;
  }

  // SmartForm fields
  const formScreens = ctx.screens.filter(s => s.resourceType === 'SmartForm' && s.fields);
  for (const screen of formScreens) {
    block += `\nFORM FIELDS (section: "${screen.sectionCode}"):\n`;
    for (const field of screen.fields) {
      block += `  - name: "${field.name}", type: "${field.type}", required: ${field.required || false}\n`;
    }
  }

  // Labels
  block += '\nSECTION LABELS (use for page titles, breadcrumbs, i18n — 4 languages):\n';
  for (const [code, labels] of Object.entries(ctx.labels)) {
    block += `  ${code}: fr="${labels.fr}" en="${labels.en}" it="${labels.it}" de="${labels.de}"\n`;
  }

  // Permissions
  if (ctx.permissions.length > 0) {
    block += `\nPERMISSIONS: ${ctx.permissions.join(', ')}\n`;
  }

  block += '--- END PRD SPECIFICATION ---\n';
  return block;
}
```

**Usage:** When invoking `Skill("ui-components")`, append `buildPrdBlock(entityName)` to the arguments.
The `/ui-components` skill reads free-text instructions — the structured block is parsed naturally.

### ⛔ HARD RULE — /ui-components is NON-NEGOTIABLE (read BEFORE any Layer 3 action)

> **VIOLATION CHECK:** If ANY .tsx page file was created by Write tool WITHOUT
> a prior Skill("ui-components") call in this execution, the frontend layer is INVALID.
>
> **You MUST NOT:**
> - Generate .tsx page code in Agent prompts and dispatch to Snipper agents
> - Write page content directly via the Write tool
> - Copy-paste page templates from your knowledge
>
> **You MUST:**
> - Call Skill("ui-components") which loads the style guide, responsive guidelines,
>   accessibility rules, and pattern files (entity-card, data-table, dashboard-chart, grid-layout, kanban)
> - Let /ui-components generate all pages with correct conventions
>
> **Why this matters:** Without the skill, pages miss CSS variables, DataTable/EntityCard,
> memo()/useCallback, responsive mobile-first design, and accessibility patterns.
>
> **Agent boundary rule:** Snipper sub-agents DO NOT have access to the Skill tool.
> Therefore, .tsx page generation MUST NEVER be delegated to Snipper agents.
> Pages are ALWAYS generated by the principal agent via Skill("ui-components").
> Snipper agents handle: API clients, routes, wiring, i18n, tests — NOT pages.

### Load Frontend References (deferred from top of step)

- Read `references/smartstack-frontend.md` now — lazy loading, i18n, page structure, CSS variables, EntityLookup (sections 1-6)
- Read `references/smartstack-frontend-compliance.md` now — documentation, form testing, compliance gates (sections 7-9)

### Pre-flight: Shared component existence check

Before generating any page, verify shared components exist:

```
Glob("src/components/ui/DataTable.*")
Glob("src/components/ui/EntityCard.*")
Glob("src/components/ui/PageLoader.*")
Glob("src/components/ui/EntityLookup.*")
```

If ANY shared component is MISSING:
  → Log warning: "Shared component {Component} not found — will be generated locally"
  → When invoking Skill("ui-components"), add instruction:
    "MISSING SHARED COMPONENTS: {list}. Generate these locally in src/components/ui/"

**EntityLookup special handling (FK fields):**
If ANY entity has FK Guid fields (e.g., EmployeeId, DepartmentId) AND EntityLookup is missing:
  → Include EntityLookup generation in the Skill("ui-components") invocation
  → Add to the skill prompt: "MISSING SHARED COMPONENT: EntityLookup. Generate EntityLookup in src/components/ui/EntityLookup.tsx"
  → The /ui-components skill has the EntityLookup pattern in smartstack-frontend.md section 6
  → Critical requirement for the skill: response parsing MUST use `(res.data.items || res.data).map(mapOption)`
  → Verify after generation: the component MUST contain `(res.data.items || res.data).map(mapOption)` — if not, fix it
  → Do NOT generate EntityLookup manually via Write tool — always via Skill("ui-components")

### Task Progress
TaskUpdate(taskId: layer3_task_id, status: "in_progress")
TaskUpdate(taskId: progress_tracker_id,
  description: "Module: {module_code}. Current: step-03 (Execute), Layer 3",
  activeForm: "Executing Layer 3")

> **Frontend patterns** are in `references/smartstack-frontend.md` (already loaded above):
> - API client generation (MCP scaffold_api_client)
> - Route scaffolding (MCP scaffold_routes) — section 1 + 3b
> - Page types (ListPage, DetailPage, CreatePage, EditPage) — section 3
> - FK field handling (EntityLookup component) — section 6
> - Form structure (no modals — all full pages) — section 3b
> - Detail page tabs (local state only) — section 3 "Tab Behavior Rules"
> - Testing patterns (co-located form tests) — section 8
> - Section-level routes and permissions — section 3b
> - I18n JSON structure and registration — section 2
> - Compliance gates (5 mandatory checks) — section 9

### Frontend Tasks (sequential or parallel within layer)

For each module:
- API client: MCP scaffold_api_client — **MANDATORY, do not skip or generate manually.**
  → The generated API client uses `apiClient` from `@/services/api` (auth, token refresh, tenant isolation).
  → Hooks MUST import from the generated API service, NEVER use raw `fetch()` or direct axios.
  → If `scaffold_api_client` is unavailable: generate the service manually using `apiClient` import,
    but NEVER using `fetch()`. Pattern: `import apiClient from '@/services/api'; const res = await apiClient.get('/api/...');`
- Routes — TWO mandatory steps:
  1. Generate registry: MCP scaffold_routes (source: 'controllers', outputFormat: 'componentRegistry', dryRun: false)
     → Creates/updates componentRegistry.generated.ts + navRoutes.generated.ts
  2. Verify PageRegistry registration (see below)
- Pages per section: When a section exists (e.g., "list"), generate ALL 4 page types:
  → {Section}ListPage.tsx (index route)
  → {Section}DetailPage.tsx (/:id route) — click on list item navigates here
  → Create{Section}Page.tsx (/create route)
  → Edit{Section}Page.tsx (/:id/edit route)
  "detail" is NEVER a separate section — it's the /:id route of the list section.
- **CRUD Page Completeness (MANDATORY for every entity section with a ListPage):**
  1. ALL 4 page types MUST exist (List, Detail, Create/Form, Edit/Form)
  2. **Create button on ListPage (MANDATORY):** Every ListPage MUST include a "New" / "Create" button
     in the header that calls `navigate('create')`. Without this button, users cannot access the create form.
  3. **componentRegistry registration:** ALL page types MUST be registered via `PageRegistry.register()`.
     After `scaffold_routes`, verify the registry contains keys for ALL pages.
     If form pages (create/edit) are missing from registry, add them manually.
  4. **POST-CHECK Gate PG-2b:** Grep every `*ListPage.tsx` for `navigate.*create`.
     If absent → BLOCKING error — add the create button before proceeding.
- Pages: **MANDATORY — INVOKE Skill("ui-components")** for ALL page generation.
  ⚠ BLOCKING REQUIREMENT: You MUST call Skill("ui-components") for EVERY page (.tsx).
  NEVER generate .tsx page code directly. NEVER write page content in Agent prompts.
  NEVER use Write tool to create pages without first calling Skill("ui-components").
  The skill loads the full style guide + patterns (entity-card, data-table, dashboard-chart, grid-layout).
  Follow smartstack-frontend.md patterns:
  → React.lazy() for all page imports (named export wrapping)
  → `<Suspense fallback={<PageLoader />}>` around all lazy components
  → Page structure: hooks → useEffect(load) → loading → error → content
  → CSS variables only: bg-[var(--bg-card)], text-[var(--text-primary)]
  → DataTable/EntityCard (not raw HTML `<table>`)
  → If entity has FK Guid fields: generate EntityLookup component in @/components/ui/EntityLookup (see smartstack-frontend.md section 6)
  → Dashboard pages: generate StatCard + ChartCard locally (see ui-components patterns/dashboard-chart.md)
- Form pages: Create/Edit forms are full pages with own routes:
  → EntityCreatePage.tsx with route /{module}/create
  → EntityEditPage.tsx with route /{module}/:id/edit
  → Zero modals/popups/drawers/dialogs for forms
  → Back button with navigate(-1) on every form page
  → See smartstack-frontend.md section 3b for templates
- Tabs on detail pages: Tabs must switch content locally:
  → Tab click handler: setActiveTab(tabKey) only — do not navigate() to another page
  → Tab content: render inline with {activeTab === 'tabKey' && <TabContent />}
  → Sub-resource data: fetch via API filtered by parent ID, display in tab panel
  → Do not use navigate('../leaves?employee=${id}') in tab handler
  → See smartstack-frontend.md section 3 'Tab Behavior Rules'
- FK fields: Foreign key Guid fields (e.g., EmployeeId) must use EntityLookup:
  → Do not render FK fields as `<input>` — users cannot type GUIDs
  → Do not render FK fields as `<select>` — even with API-loaded options, `<select>` is not acceptable
  → Only use `<EntityLookup />` from @/components/ui/EntityLookup for searchable selection
  → Each FK field needs: apiEndpoint, mapOption (display name), search support on backend
  → Do not use `<select value={formData.departmentId}>`, `<option value={dept.id}>`, `e.target.value` for FK fields
  → See smartstack-frontend.md section 6 for the full EntityLookup pattern
<!-- TODO A12: Replace direct i18n generation with MCP scaffold_i18n when B4 is ready -->
- I18n: Generate 4 JSON files per module namespace (fr, en, it, de)
  → Follow JSON template from smartstack-frontend.md
  → Keys: actions, labels, errors, validation, columns, form, messages, empty
  → All t() calls must use namespace prefix + fallback: t('ns:key', 'Default text')
  → **UTF-8 accents are MANDATORY** — NEVER use ASCII-only text for accented languages:
    - FR: "Département" (not "Departement"), "employés" (not "employes"), "trouvée" (not "trouvee")
    - DE: "Übersicht" (not "Ubersicht"), "Beschäftigung" (not "Beschaftigung")
    - IT: "attività" (not "attivita"), "dipendentì" (not "dipendenti")
    - EN: no accents needed (but proper apostrophes: "employee's" not "employees")
  → **Register namespace in i18n config** (critical — POST-CHECK C39 + GATE PG-4 catch this, but fix it now):
    1. Find i18n config: `Glob("src/**/i18n/config.ts")` or `index.ts` or `i18n.ts`
    2. Read the config → find the resources/ns registration pattern
    3. Add the new namespace import + registration for all 4 languages
    4. If config uses dynamic imports: add namespace to the `ns` array
    5. Verify: `grep -q "{module_namespace}" src/**/i18n/config.ts` → must match
    6. **I18n File/Import Alignment Check:** Verify that every locale JSON file on disk for this module
       has a corresponding import in the i18n config index.ts. Glob `src/**/i18n/locales/*/` for module files,
       then verify each is imported. Missing imports cause silent translation failures (keys show as raw strings).
    7. **If registration is missing after step 6:** This is a BLOCKING error. The page will show raw keys
       (`list.title`, `form.department`) instead of translated text. Fix before proceeding.
- Permissions: Call MCP generate_permissions for the module permission root (2 segments: {app}.{module}),
  then also call MCP generate_permissions for each section (3 segments: {app}.{module}.{section}).
- Section routes: Ensure navigation seed data has ComponentKey matching PageRegistry keys.
  ComponentKey convention: `{app_code}.{module_code}.{section_code}` (dot-separated, lowercase).
  DynamicRouter generates the URL paths from seed data Route field.
  **CRITICAL: PageRegistry keys use DOTS as hierarchy separator, NEVER hyphens.**
  - Correct: `PageRegistry.register('rh.employes.list', lazy(...))`
  - WRONG:   `PageRegistry.register('rh-employes-list', lazy(...))`
  NavigationService computes `ComponentKey = "{app.Code}.{module.Code}.{section.Code}"` at runtime.
  DynamicRouter calls `PageRegistry.resolve(componentKey)` — keys MUST match exactly.
  For client projects: `import { PageRegistry } from '@atlashub/smartstack';`
  For SmartStack core: `import { PageRegistry } from '@/extensions/PageRegistry';`
- MUST use src/pages/{App}/{Module}/ hierarchy (NOT flat)

### Documentation (after frontend pages exist)

> **After frontend pages are created, generate module documentation via `/documentation` skill.**

```
Invoke /documentation {module_code} --type user

This generates:
- src/pages/docs/business/{app}/{module}/doc-data.ts (data file)
- src/pages/docs/business/{app}/{module}/index.tsx (page wrapper)
- src/i18n/locales/fr/docs-{app}-{module}.json (French doc translations)
- Updates App.tsx routing for doc page
- Updates docs-manifest.json
```

**Verify DocToggleButton presence in list/detail pages:**
- Every `*ListPage.tsx` and `*DetailPage.tsx` MUST import and render `DocToggleButton` in their header
- Import: `import { DocToggleButton } from '@/components/docs/DocToggleButton';`
- Placement: inside the header actions area (see `smartstack-frontend.md` section 7)

### If NOT economy_mode AND multiple entities: Parallel Agents (within layer)

> **Protocol:** See `references/parallel-execution.md`

```
IF NOT economy_mode AND entities.length > 1:
  # PHASE A — Parallel: infrastructure frontend (NO page generation)
  # Snipper agents DO NOT have access to the Skill tool, so they CANNOT call /ui-components.
  # Pages MUST be generated by the principal agent in Phase B.
  For each entity, launch in parallel (single message):
    Agent(subagent_type='Snipper', model='opus',
      prompt='Execute Layer 3 INFRASTRUCTURE for {EntityName}:
        **MANDATORY: Read references/smartstack-frontend.md FIRST**
        - API client: MCP scaffold_api_client
        - Routes: MCP scaffold_routes (outputFormat: "componentRegistry", dryRun: false)
          → MUST generate componentRegistry.generated.ts + navRoutes.generated.ts
        - Verify: componentRegistry.generated.ts has PageRegistry.register() for new pages
        - Verify: main.tsx imports componentRegistry.generated (one-time check)
        - NO manual App.tsx wiring needed — DynamicRouter handles routing
        - I18n: 4 JSON files (fr, en, it, de) + REGISTER namespace in i18n config
        - DO NOT generate any .tsx page files — pages are handled by the principal agent
        - Your task ID is {task_id}. Call TaskUpdate(status: "in_progress") before starting.
        - Call TaskUpdate(status: "completed", metadata: { files_created: [...] }) when done.')
  # Wait for all agents to complete

  # PHASE B — Sequential: pages via /ui-components (principal agent)
  # Snipper agents cannot call Skill() — only the principal agent can.
  For each entity (sequentially):
    **INVOKE Skill("ui-components")** — pass entity context + PRD spec:
      - Entity: {EntityName}, Module: {ModuleName}, App: {AppName}
      - Page types: List, Detail, Create, Edit (+ Dashboard if applicable)
      - "CSS: Use CSS variables ONLY — bg-[var(--bg-card)], text-[var(--text-primary)]"
      - "Forms: Create/Edit are FULL PAGES with own routes (/create, /:id/edit)"
      - "FK FIELDS: EntityLookup for ALL FK Guid fields"
      - "I18n: ALL text uses t('namespace:key', 'Fallback')"
      - {buildPrdBlock(EntityName)} — append the PRD specification block from companion specs
      - "COLUMNS ARE AUTHORITATIVE: Do NOT infer columns from entity attributes. Use ALL columns from the PRD SPECIFICATION block above, in the EXACT order listed. Do NOT add, remove, or reorder columns."
      - "LABELS ARE AUTHORITATIVE: Use PRD SECTION LABELS for i18n t() fallback text and page titles. Do NOT invent labels."
      - "ACTIONS ARE AUTHORITATIVE: Implement ALL action buttons listed in the PRD SPECIFICATION (create, edit, delete, approve, reject, export, etc.)."
    Generate form tests: co-located .test.tsx for Create and Edit pages

ELSE:
  # Economy mode: Agent principal handles all entities SEQUENTIALLY.
  # MANDATORY: You MUST still call Skill("ui-components") for page generation.
  # economy_mode only disables parallel agents — it does NOT skip /ui-components.
  For each entity (sequentially):
    1. MCP scaffold_api_client
    2. MCP scaffold_routes (outputFormat: 'componentRegistry')
    3. Verify PageRegistry registration (componentRegistry.generated.ts)
       No manual App.tsx wiring needed — DynamicRouter resolves at runtime
    4. **INVOKE Skill("ui-components")** — pass entity context + PRD spec:
       - Entity: {EntityName}, Module: {ModuleName}, App: {AppName}
       - Page types: List, Detail, Create, Edit (+ Dashboard if applicable)
       - "CSS: Use CSS variables ONLY — bg-[var(--bg-card)], text-[var(--text-primary)]"
       - "Forms: Create/Edit are FULL PAGES with own routes (/create, /:id/edit)"
       - "I18n: ALL text uses t('namespace:key', 'Fallback')"
       - {buildPrdBlock(EntityName)} — append the PRD specification block from companion specs
       - "COLUMNS ARE AUTHORITATIVE: Do NOT infer columns from entity attributes. Use ALL columns from the PRD SPECIFICATION block above, in the EXACT order listed."
       - "LABELS ARE AUTHORITATIVE: Use PRD SECTION LABELS for i18n t() fallback text."
       - "ACTIONS ARE AUTHORITATIVE: Implement ALL action buttons listed in the PRD SPECIFICATION."
    5. I18n: 4 JSON files (fr, en, it, de) + register namespace in i18n config
    6. Form tests: co-located .test.tsx for Create and Edit pages
```

### Parallel Agents + TaskCreate Integration

When launching agents for multi-entity layers:
- Each agent receives its task ID and metadata context in the prompt
- Agent must call TaskUpdate(status: "in_progress") before starting
- Agent must call TaskUpdate(status: "completed", metadata: { files_created: [...] }) when done
- Agent principal monitors via TaskList() after all agents complete
- Agent principal updates activeForm after each entity completion:
  `TaskUpdate(taskId: layer3_task_id, activeForm: "Building {EntityName} frontend (2/5 entities)")`

### Frontend Compliance Gate

> See `references/smartstack-frontend-compliance.md` section 9 "Compliance Gates" for all 6 required checks:
> 1. CSS Variables (theme system)
> 2. Forms as Pages (zero modals/drawers/slide-overs)
> 3. I18n File Structure (4 languages, separate JSON files)
> 4. Lazy Loading (React.lazy() — no static imports)
> 5. useTranslation in Pages (all text translated)
> 6. SmartStack Components (no raw HTML tables — DataTable/EntityCard required)

Do not commit frontend changes until all 6 gates pass.

When delegating to `/ui-components` skill, include explicit instructions:
- "CSS: Use CSS variables ONLY — `bg-[var(--bg-card)]`, `text-[var(--text-primary)]`."
- "Forms: Create/Edit forms are FULL PAGES with own routes (e.g., `/create`, `/:id/edit`)."
- "I18n: ALL text must use `t('namespace:key', 'Fallback')`. Generate JSON files in `src/i18n/locales/`."
- "Hooks: MUST use apiClient from @/services/api — NEVER raw fetch() or direct axios import."
- "PRD COLUMNS: If a `--- PRD SPECIFICATION ---` block is present, use its columns[] as the EXACT DataTable columns. Do NOT add, remove, or reorder. Do NOT infer columns from entity attributes — the PRD is the source of truth."
- "PRD LABELS: If a `--- PRD SPECIFICATION ---` block is present, use its SECTION LABELS for page titles and i18n fallback text in all 4 languages."
- "PRD ACTIONS: If a `--- PRD SPECIFICATION ---` block lists actions (approve, reject, export, etc.), generate the corresponding buttons and handlers."

### ⛔ POST-GENERATION VERIFICATION GATES (MANDATORY — run after ALL pages are created)

> **These checks are BLOCKING.** Do not commit until ALL pass. Each check targets a known failure mode
> observed in production runs where the agent bypassed /ui-components or delegated pages to Snipper agents.

```
### GATE PG-1: /ui-components Skill Invocation Check
# Verify that Skill("ui-components") was actually called during this layer execution.
# The HARD RULE says: "If ANY .tsx page was created by Write tool WITHOUT a prior
# Skill('ui-components') call, the frontend layer is INVALID."
#
# How to verify: Review your own tool call history in this conversation.
# IF you created ANY .tsx file in src/pages/ WITHOUT calling Skill("ui-components") first:
#   → FAIL — delete ALL pages in src/pages/, invoke Skill("ui-components"), regenerate.
#   → This is NOT optional. Pages generated without the skill WILL use fictional CSS classes,
#     raw HTML <table>, and miss DataTable/EntityCard/CSS variables/responsive design.

### GATE PG-2: Page Completeness Check (4 page types per section)
# For each section that has a ListPage, verify ALL 4 page types exist:
for section_dir in $(find src/pages -mindepth 2 -maxdepth 3 -type d); do
  section_name=$(basename "$section_dir")
  has_list=$(find "$section_dir" -name "*List*Page.tsx" | head -1)
  if [ -n "$has_list" ]; then
    has_detail=$(find "$section_dir" -name "*Detail*Page.tsx" | head -1)
    has_create=$(find "$section_dir" -name "*Create*Page.tsx" -o -name "Create*Page.tsx" | head -1)
    has_edit=$(find "$section_dir" -name "*Edit*Page.tsx" -o -name "Edit*Page.tsx" | head -1)
    if [ -z "$has_detail" ] || [ -z "$has_create" ] || [ -z "$has_edit" ]; then
      echo "FAIL PG-2: $section_dir missing pages: detail=${has_detail:-MISSING} create=${has_create:-MISSING} edit=${has_edit:-MISSING}"
      # → Generate missing pages via Skill("ui-components")
    fi
  fi
done

### GATE PG-3: No Stub Pages ("Under development" detection)
# Pages must be fully implemented — not placeholders.
grep -rl "Under development\|Under construction\|TODO.*implement\|Coming soon" src/pages/ && {
  echo "FAIL PG-3: Stub pages detected — all pages must be fully implemented"
  # → For each stub: invoke Skill("ui-components") to generate the real implementation
  # → Stubs are NEVER acceptable as deliverables
}

### GATE PG-4: I18n Namespace Registration Check
# JSON translation files MUST be imported in i18n config. Missing imports cause
# all translation keys to render as raw strings (e.g., "list.title" instead of "Liste des employés").
I18N_CONFIG=$(find src -path "*/i18n/index.ts" -o -path "*/i18n/config.ts" | head -1)
if [ -n "$I18N_CONFIG" ]; then
  for json_file in $(find src -path "*/i18n/locales/fr/*.json" -type f); do
    namespace=$(basename "$json_file" .json)
    if ! grep -q "$namespace" "$I18N_CONFIG"; then
      echo "FAIL PG-4: Namespace '$namespace' not registered in $I18N_CONFIG"
      # → Add import + registration for all 4 languages (fr, en, it, de)
    fi
  done
fi

### GATE PG-5: No Raw fetch() in Hooks
# Hooks MUST use apiClient from @/services/api (which handles auth, token refresh, tenant isolation).
# Raw fetch() bypasses authentication and session management.
grep -rl "fetch(" src/hooks/ && {
  echo "FAIL PG-5: Raw fetch() found in hooks — must use apiClient from @/services/api"
  # → Rewrite hooks to use: import apiClient from '@/services/api';
  # → Replace fetch() calls with apiClient.get(), apiClient.post(), etc.
}

### GATE PG-6: No Fictional CSS Classes
# Pages MUST use Tailwind + CSS variables. Fictional classes indicate /ui-components was bypassed.
FICTIONAL_CLASSES="page-container\|page-header\|page-actions\|filter-bar\|data-table\|btn btn-primary\|btn btn-outline\|btn-sm\|loading-spinner\|empty-state"
grep -rl "$FICTIONAL_CLASSES" src/pages/ && {
  echo "FAIL PG-6: Fictional CSS classes found — pages were not generated by /ui-components"
  # → Delete ALL pages, invoke Skill("ui-components"), regenerate from scratch
}

### GATE PG-7: Agent Boundary Verification
# This is a self-check: review all Agent() calls made during Layer 3.
# IF any Agent(subagent_type='Snipper') prompt contained "Page.tsx" or "pages/" file creation:
#   → VIOLATION of the agent boundary rule (Snipper cannot generate pages)
#   → Delete pages created by Snipper, regenerate via Skill("ui-components")
```

> **If ANY gate fails:** Fix the issue, then re-run ALL gates. Do not commit with failures.

### Frontend Tests Inline

> **Tests are scaffolded and run WITHIN Layer 3, not deferred to step-07.**

```
1. Scaffold frontend tests:
   → For each module with create/edit pages:
     → Generate EntityCreatePage.test.tsx and EntityEditPage.test.tsx
     → Co-located next to the page component (NOT in __tests__/ folder)
     → Cover: rendering, validation, submit, pre-fill, navigation, errors
     → Tools: Vitest + React Testing Library + @testing-library/user-event
     → Mock API: vi.mock() or MSW — NEVER real API calls

2. Run tests:
   WEB_DIR=$(find . -name "vitest.config.ts" -not -path "*/node_modules/*" -exec dirname {} \; 2>/dev/null | head -1)
   if [ -n "$WEB_DIR" ]; then
     (cd "$WEB_DIR" && npm run test)
   fi

3. Fix loop (max 3 iterations):
   IF tests fail:
     → Identify root cause (ALWAYS code bug, not test bug)
     → Fix production CODE
     → Re-run tests
     → Repeat until pass or max 3

4. If still failing after 3 iterations: note failures, continue.
   Step-07 "Final Test Sweep" will handle remaining failures.
```

### Typecheck Gate

```bash
npm run typecheck
```

Must pass before commit.

TaskUpdate(taskId: layer3_task_id, status: "completed",
  metadata: { build_gate: "pass" })

### Layer 3 Commits

```
feat({module}): [frontend] pages, routes, i18n, documentation
test({module}): frontend form tests
```

---

## NEXT SUB-STEP (MANDATORY — IMMEDIATE)

> **DO NOT STOP.** DO NOT ask the user. DO NOT present options. IMMEDIATELY load the next sub-step.
> Stopping here is a VIOLATION of the execution guarantee.

Load `steps/step-03e-layer4-devdata.md`
