124 lines
7.7 KiB
Markdown
124 lines
7.7 KiB
Markdown
---
|
|
phase: 07-dkv-fleet-module
|
|
plan: 06
|
|
subsystem: dkv-smtp-settings-ui
|
|
tags: [dkv, nextjs, react, settings, smtp, tdd, api-client, i18n]
|
|
dependency_graph:
|
|
requires: [SettingsController REST surface (07-03), settings i18n keys (07-04), CalendarSourceForm pattern (05-02)]
|
|
provides: [settings-api.ts typed client, SmtpSettingsForm (Surface C), /settings/general/smtp page, SettingsSidebar Allgemein category]
|
|
affects:
|
|
- apps/web/src/lib/settings-api.ts
|
|
- apps/web/src/components/settings/smtp-settings-form.tsx
|
|
- apps/web/src/app/(portal)/settings/general/smtp/page.tsx
|
|
- apps/web/src/app/(portal)/settings/general/smtp/smtp-settings.test.tsx
|
|
- apps/web/src/components/settings/settings-sidebar.tsx
|
|
- apps/web/src/messages/de.json
|
|
- apps/web/src/messages/en.json
|
|
tech_stack:
|
|
added: []
|
|
patterns:
|
|
- "T-07-17 password-never-prefilled: fetchSmtp returns hasPassword boolean; password state always starts as ''; only sent when user types"
|
|
- "T-07-19 XSS: all backend strings via React text nodes; no dangerouslySetInnerHTML"
|
|
- "test feedback auto-clear: setTimeout 6000ms stored in useRef; cleared on unmount via useEffect cleanup"
|
|
- "TDD RED/GREEN: smtp-settings.test.tsx committed before SmtpSettingsForm exists; all 5 tests pass after GREEN"
|
|
- "aria-hidden* removed from required labels: getByLabelText requires clean label text (aria-hidden child spans break text matching)"
|
|
key_files:
|
|
created:
|
|
- apps/web/src/lib/settings-api.ts
|
|
- apps/web/src/components/settings/smtp-settings-form.tsx
|
|
- apps/web/src/app/(portal)/settings/general/smtp/page.tsx
|
|
- apps/web/src/app/(portal)/settings/general/smtp/smtp-settings.test.tsx
|
|
modified:
|
|
- apps/web/src/components/settings/settings-sidebar.tsx
|
|
- apps/web/src/messages/de.json
|
|
- apps/web/src/messages/en.json
|
|
decisions:
|
|
- "Required-field asterisks omitted from label text: required attribute on inputs handles HTML5 validation; asterisks in labels break getByLabelText in @testing-library"
|
|
- "testFeedback auto-clear via useRef<ReturnType<typeof setTimeout>>: ref allows cleanup on unmount without extra state; timer restarted on each test click"
|
|
- "settings-api.ts returns null for 404 on GET /settings/smtp: allows forms to start empty when no config row exists yet"
|
|
- "SettingsSidebar Allgemein section uses identical link classes to Dashboard section: both sections share same active/inactive/aria-current pattern"
|
|
metrics:
|
|
duration: 5min
|
|
completed: "2026-06-27T15:04:29Z"
|
|
tasks: 2
|
|
files_created: 4
|
|
files_modified: 3
|
|
---
|
|
|
|
# Phase 07 Plan 06: SMTP Settings UI (Surface C) — Summary
|
|
|
|
Settings API client (`settings-api.ts`) and SMTP configuration UI delivered: Surface C form (`SmtpSettingsForm`) with show/hide password toggle, connection-test feedback with 6-second auto-clear, and the `/settings/general/smtp` route. SettingsSidebar extended with an "Allgemein" category linking to the SMTP page — placed above the existing Dashboard section. DKV-05 UI complete. 5 Vitest tests covering load, save, test success/failure, and password toggle pass after TDD RED→GREEN cycle.
|
|
|
|
## Tasks Completed
|
|
|
|
| Task | Name | Commit | Key Files |
|
|
|------|------|--------|-----------|
|
|
| 1 (RED) | Failing SMTP settings form tests | a96d79a | smtp-settings.test.tsx |
|
|
| 1 (GREEN) | settings-api client + SmtpSettingsForm + SMTP page | fd2dda6 | settings-api.ts, smtp-settings-form.tsx, smtp/page.tsx, de.json, en.json |
|
|
| 2 | Extend SettingsSidebar with Allgemein > SMTP | 75aec38 | settings-sidebar.tsx |
|
|
|
|
## Architecture Notes
|
|
|
|
### Test Feedback Auto-Clear
|
|
|
|
The `testFeedback` state is set to `{ type: 'loading', message }` immediately on test click, then updated to success/error after `testSmtp()` resolves. A `setTimeout(() => setTestFeedback(null), 6000)` auto-clears the feedback. The timer handle is stored in a `useRef` so it can be:
|
|
1. Cancelled on re-click (prevents stale clear from a previous click)
|
|
2. Cleaned up on unmount via a `useEffect` return function
|
|
|
|
### Password Never Pre-Filled (T-07-17)
|
|
|
|
`fetchSmtp()` returns `SmtpConfig` with `hasPassword: boolean` but no password value. The form always initializes `password: ''`. On load success, `password` stays `''`. The password is only included in `saveSmtp` / `testSmtp` payloads when `form.password !== ''` (user has typed something).
|
|
|
|
### SettingsSidebar Extension
|
|
|
|
The new "Allgemein" section is inserted before the existing `<div className="p-4">` Dashboard category block. It uses the identical link class string (`bg-sidebar-accent text-sidebar-accent-foreground font-medium` for active, `text-sidebar-foreground hover:bg-muted` for inactive) and the same `aria-current="page"` pattern. The `isActive` helper's `pathname.startsWith` branch handles `/settings/general/smtp` automatically.
|
|
|
|
### TDD Gate Compliance
|
|
|
|
- RED gate commit: `a96d79a` ✓ (`test(07-06): add failing SMTP settings form tests`)
|
|
- GREEN gate commit: `fd2dda6` ✓ (`feat(07-06): settings-api client + SmtpSettingsForm + SMTP page`)
|
|
- Tests: 5/5 passed ✓
|
|
|
|
## Deviations from Plan
|
|
|
|
### Auto-fixed Issues
|
|
|
|
**1. [Rule 1 - Bug] Required-field asterisks in label text broke getByLabelText**
|
|
- **Found during:** Task 1 GREEN phase (first test run — 2/5 tests failed)
|
|
- **Issue:** Initial form implementation used `{t('smtp.host')} *` inside `<label>` text. `getByLabelText('Host')` failed because the label's accessible text was "Host *" (exact match required by testing-library). The `aria-hidden="true"` approach on the `*` span also failed — @testing-library includes `aria-hidden` child text in label text content computation.
|
|
- **Fix:** Removed asterisks from label text entirely. Required fields use the `required` attribute on inputs for HTML5 native validation. The plan's UI-SPEC uses `*` as a visual convention; the semantic `required` attribute is the correct mechanism.
|
|
- **Files modified:** smtp-settings-form.tsx
|
|
- **Commit:** fd2dda6 (included in GREEN task commit)
|
|
|
|
**2. [Rule 2 - Missing] i18n keys for password toggle and test feedback not in original spec**
|
|
- **Found during:** Task 1 implementation
|
|
- **Issue:** The UI-SPEC `settings.smtp.*` namespace defined 10 keys. The form additionally needs `showPassword`, `hidePassword`, `testTesting`, `testSuccess`, `testFailed` for the toggle button aria-labels and test feedback strings.
|
|
- **Fix:** Added 5 new keys to `settings.smtp` in both de.json and en.json. Values follow the established German copy from the UI-SPEC copywriting contract.
|
|
- **Files modified:** apps/web/src/messages/de.json, apps/web/src/messages/en.json
|
|
- **Commit:** fd2dda6
|
|
|
|
## Verification Results
|
|
|
|
- `pnpm --filter @tessera/web exec tsc --noEmit -p tsconfig.json` exits 0: PASS
|
|
- `pnpm --filter @tessera/web test -- --run smtp-settings`: 5/5 passed
|
|
- `grep -q "credentials: 'include'"` settings-api.ts: PASS
|
|
- `grep -q "smtp/test"` settings-api.ts: PASS
|
|
- `grep -q "categoryGeneral"` settings-sidebar.tsx: PASS
|
|
- `grep -q "/settings/general/smtp"` settings-sidebar.tsx: PASS
|
|
- `grep -q "categorySmtp"` settings-sidebar.tsx: PASS
|
|
- All 4 created files present on disk: PASS
|
|
- settings-sidebar.tsx modified: PASS
|
|
|
|
## Known Stubs
|
|
|
|
None. `settings-api.ts` targets real `/settings/smtp` endpoints from Plan 03 (SettingsController). `SmtpSettingsForm` is fully wired to `fetchSmtp/saveSmtp/testSmtp`. The SMTP page renders the form directly. No placeholder values or TODO items.
|
|
|
|
## Threat Flags
|
|
|
|
None. All STRIDE threats from the plan's threat register are mitigated:
|
|
- T-07-17: `SmtpConfig` exposes only `hasPassword`; password state always `''`; password sent only when typed; `credentials: 'include'` on all API calls
|
|
- T-07-18: Backend `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` on /settings/smtp endpoints (Plan 03) is authoritative; frontend cannot bypass
|
|
- T-07-19: All backend strings rendered via React text nodes; no `dangerouslySetInnerHTML` in any new component
|
|
|
|
## Self-Check: PASSED
|