--- phase: 09-cert-manager-module plan: "02" subsystem: frontend tags: [cert-manager, frontend, i18n, components, shell] requires: [] provides: [cert-manager-page-shell, cert-manager-i18n, cert-manager-actions] affects: [apps/web] tech_stack: added: [] patterns: [useTranslations, vi.mock next-intl, client-component, conditional-render] key_files: created: - apps/web/src/app/(portal)/modules/cert-manager/page.tsx - apps/web/src/app/(portal)/modules/cert-manager/actions.ts - apps/web/src/app/(portal)/modules/cert-manager/components/DropZone.tsx - apps/web/src/app/(portal)/modules/cert-manager/components/PasswordField.tsx - apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx - apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.tsx - apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.tsx - apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.tsx - apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx modified: - apps/web/src/messages/de.json - apps/web/src/messages/en.json decisions: - "Used vi.mock('next-intl') test pattern per project convention instead of NextIntlClientProvider (matches VehicleTable.test.tsx, sidebar.test.tsx)" - "PasswordField renders null when show=false — no DOM reflow, satisfies T-09-02 threat mitigation" - "File/PEM mutual exclusion enforced in page state: selecting one clears the other" metrics: duration: "~6 minutes" completed: "2026-07-01" tasks_completed: 3 files_created: 9 files_modified: 2 status: complete --- # Phase 09 Plan 02: Cert-Manager Frontend Shell Summary **One-liner:** Cert-manager tab shell with DropZone, conditional PasswordField, shared input card, four tab stubs, downloadBase64/postForm client helpers, and full certManager i18n namespace in de + en. ## Tasks Completed | Task | Name | Commit | Files | |------|------|--------|-------| | 1 | Add certManager i18n namespace (de + en) | 82a80e7 | de.json, en.json | | 2 | Build page shell, components, client helpers | 42a41f7 | page.tsx, actions.ts, 6 components | | 3 | Shell render tests (GREEN) | 2cb01f7 | cert-manager.test.tsx | ## What Was Built ### i18n Namespace (Task 1) `certManager` namespace added to both `de.json` and `en.json` with identical key structure: - `title`, `description` - `tabs.{inspect,split,merge,convert}` - `dropZone.{placeholder,formats}`, `paste.placeholder`, `password.label`, `or` - `actions.{inspect,split,merge,convert,download,processing}` - `emptyState.{inspect,inspectBody,split,splitBody,merge,mergeBody,convert,convertBody}` - `error.{generic,wrongPassword,unknownFormat}` German copy matches UI-SPEC Copywriting Contract exactly. ### Page Shell (Task 2) **page.tsx** — `'use client'` component with: - State: `activeTab`, `file`, `pemText`, `password` - Layout: `max-w-4xl mx-auto p-6 space-y-6` per UI-SPEC - Shared input card with DropZone + OR divider + PEM textarea + conditional PasswordField - Tab nav: 4 tabs with `border-b-2 border-primary` active indicator - Renders active tab component with `{ file, pemText, password }` props - File/PEM mutual exclusion enforced **actions.ts** — Client helpers: - `API_URL` const (`NEXT_PUBLIC_API_URL || 'http://localhost:3001'`) - `downloadBase64(filename, content, mimeType)` — atob → Uint8Array → Blob → URL.createObjectURL - `postForm(endpoint, form)` — fetch with `credentials: 'include'`, throws on !ok **DropZone.tsx** — Drag-and-drop + click-to-browse, drag-over highlight (`border-primary bg-primary/5`), file display with clear button, resets `e.target.value` for re-selection. **PasswordField.tsx** — `input[type=password]` with inline SVG eye toggle; renders `null` when `show=false` (T-09-02). **Tab stubs** — InspectTab, SplitTab, MergeTab, ConvertTab render empty state strings only. ### Tests (Task 3) 7 tests, all GREEN: - Page renders title 'Zertifikat-Manager' and description - All 4 tab labels rendered (Analysieren, Aufteilen, Zusammenfuehren, Konvertieren) - Password field NOT in DOM on initial render (T-09-02 assertion) - Inspect empty state on initial render - Split / Merge / Convert empty states when respective tabs activated ## Deviations from Plan ### Auto-applied conventions **1. [Convention] vi.mock('next-intl') instead of NextIntlClientProvider** - **Found during:** Task 3 - **Issue:** Plan specified NextIntlClientProvider, but every existing test in the project mocks next-intl directly (sidebar.test.tsx, VehicleTable.test.tsx) - **Fix:** Used `vi.mock('next-intl', ...)` pattern for consistency - **Impact:** Tests are simpler and match project convention; no behavior difference **2. [Convention] node_modules symlink for worktree test execution** - **Found during:** Task 3 - **Issue:** Worktree has no node_modules; vitest not available - **Fix:** Created temporary symlink `apps/web/node_modules -> main repo node_modules` to run tests; symlink is gitignored - **Impact:** Tests pass correctly; symlink does not affect production ## Threat Model Compliance | Threat ID | Status | |-----------|--------| | T-09-02 (password in state/URL) | Mitigated — PasswordField renders null when show=false; password never in URL or console | | T-09-04 (unauthenticated fetch) | Mitigated — postForm uses credentials:'include'; ModuleGuard enforced server-side | | T-09-05 (client accept filter) | Accepted — UX only, server validates | ## Known Stubs | Component | Location | Reason | |-----------|----------|--------| | InspectTab | cert-manager/components/InspectTab.tsx | Empty state only — API wire-up in later plan | | SplitTab | cert-manager/components/SplitTab.tsx | Empty state only — API wire-up in later plan | | MergeTab | cert-manager/components/MergeTab.tsx | Empty state only — API wire-up in later plan | | ConvertTab | cert-manager/components/ConvertTab.tsx | Empty state only — API wire-up in later plan | These stubs are intentional. Plan 02 explicitly scopes to the shell; later plans fill in each tab's API logic. The stubs do not prevent the plan goal (visible shell + navigation) from being achieved. ## Verification Results | Check | Status | Notes | |-------|--------|-------| | `pnpm --filter @tessera/web test cert-manager --run` | PASS | 7/7 tests green | | `tsc --noEmit` (cert-manager files) | PASS | No cert-manager type errors | | I18N_OK node verify script | PASS | de.tabs keys correct, en.certManager.actions.download exists | | `grep -q "max-w-4xl"` | PASS | Layout contract met | | `grep -q "credentials: 'include'"` | PASS | T-09-04 mitigation present | | `grep -q "URL.createObjectURL"` | PASS | downloadBase64 helper present | ## Self-Check: PASSED - [x] apps/web/src/app/(portal)/modules/cert-manager/page.tsx — created - [x] apps/web/src/app/(portal)/modules/cert-manager/actions.ts — created - [x] apps/web/src/app/(portal)/modules/cert-manager/components/*.tsx (6 files) — created - [x] apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx — created - [x] apps/web/src/messages/de.json — certManager namespace added - [x] apps/web/src/messages/en.json — certManager namespace added - [x] Commit 82a80e7 exists (i18n) - [x] Commit 42a41f7 exists (shell + components) - [x] Commit 2cb01f7 exists (tests)