--- phase: 07-dkv-fleet-module plan: 06 type: execute wave: 2 depends_on: ["07-01", "07-03"] files_modified: - 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 autonomous: true requirements: [DKV-05] user_setup: [] must_haves: truths: - "User can open /settings/general/smtp and see the SMTP configuration form" - "User can enter SMTP host, port, encryption, username, password, and from-address and save it" - "User can test the SMTP connection and see inline success/error feedback" - "The settings sub-sidebar shows an 'Allgemein' category with an 'SMTP' link above the existing Dashboard category" - "The saved password is never rendered back into the form (only a hasPassword indicator is known)" artifacts: - path: "apps/web/src/lib/settings-api.ts" provides: "Typed fetch client for GET/PUT /settings/smtp + POST /settings/smtp/test" contains: "credentials: 'include'" - path: "apps/web/src/components/settings/smtp-settings-form.tsx" provides: "SMTP configuration form (Surface C)" min_lines: 40 - path: "apps/web/src/app/(portal)/settings/general/smtp/page.tsx" provides: "SMTP settings route within the settings layout" min_lines: 10 - path: "apps/web/src/app/(portal)/settings/general/smtp/smtp-settings.test.tsx" provides: "Vitest coverage for the SMTP settings form (Wave 0 / VALIDATION DKV-05)" contains: "vi.mock" - path: "apps/web/src/components/settings/settings-sidebar.tsx" provides: "Settings sub-sidebar extended with the Allgemein > SMTP category" contains: "categoryGeneral" key_links: - from: "apps/web/src/components/settings/smtp-settings-form.tsx" to: "apps/web/src/lib/settings-api.ts (fetchSmtp/saveSmtp/testSmtp)" via: "form load + save + connection test" pattern: "saveSmtp|testSmtp" - from: "apps/web/src/app/(portal)/settings/general/smtp/page.tsx" to: "apps/web/src/components/settings/smtp-settings-form.tsx" via: "page renders the form" pattern: "SmtpSettingsForm" - from: "apps/web/src/components/settings/settings-sidebar.tsx" to: "/settings/general/smtp" via: "Allgemein category Link" pattern: "/settings/general/smtp" --- Deliver the shared SMTP configuration UI (Surface C from 07-UI-SPEC.md): a settings API client for `/settings/smtp`, the SMTP configuration form, the `/settings/general/smtp` route inside the settings layout, and the settings sub-sidebar extension that adds an "Allgemein" category linking to the SMTP page. Consumes the SettingsController endpoints built in Plan 03. Purpose: This is the user-facing half of DKV-05 — SMTP settings configurable in general settings, shared across modules. Each task is a vertical slice: after Task 1 a user can save and test SMTP config; after Task 2 they can navigate to it from the settings sidebar. Output: settings-api client, smtp-settings-form, smtp page (+ test), extended settings-sidebar. ## Phase Goal **Als** Administrator **möchte ich** DKV-Tankkarten-Rechnungen automatisch aus einem E-Mail-Postfach verarbeiten lassen, **damit** Flotten-Tankdaten ohne manuelle Eingabe als Excel-Datei exportiert und per SMTP zugestellt werden. This plan lets the administrator configure the shared SMTP server that the DKV export delivery (and other modules) use. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/phases/07-dkv-fleet-module/07-CONTEXT.md @.planning/phases/07-dkv-fleet-module/07-UI-SPEC.md @.planning/phases/07-dkv-fleet-module/07-PATTERNS.md @apps/web/src/lib/calendar-api.ts @apps/web/src/components/settings/calendar-source-form.tsx @apps/web/src/components/settings/settings-sidebar.tsx @apps/web/src/app/(portal)/settings/dashboard/page.tsx @apps/web/src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx ## Artifacts this phase produces (Plan 06 portion) New symbols (exclude from drift verification): - File `apps/web/src/lib/settings-api.ts`: type `SmtpConfig` (host, port, encryption, username, fromAddress, hasPassword — never a password); functions `fetchSmtp`, `saveSmtp`, `testSmtp` - Component `SmtpSettingsForm` (`apps/web/src/components/settings/smtp-settings-form.tsx`) - Component `SmtpSettingsPage` (default export, `apps/web/src/app/(portal)/settings/general/smtp/page.tsx`) Modified (existing) symbols: - `apps/web/src/components/settings/settings-sidebar.tsx` — adds the "Allgemein" category (SMTP link) above the existing Dashboard category > **Path notes:** The API-client path uses the repo convention `apps/web/src/lib/settings-api.ts` (matching `apps/web/src/lib/calendar-api.ts`); the checker brief's `lib/api/settings.ts` does not exist in the repo. The form lives at `apps/web/src/components/settings/smtp-settings-form.tsx` (per the checker brief, consistent with the existing `components/settings/` location of `calendar-source-form.tsx` and `settings-sidebar.tsx`). The `@` import alias maps to `apps/web/src`. Task 1: settings-api client + SmtpSettingsForm + SMTP page (DKV-05) 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/lib/calendar-api.ts — API-client convention: `API_URL` const, `credentials: 'include'`, typed exports, throw on non-ok (mirror structure) - apps/web/src/components/settings/calendar-source-form.tsx — form pattern: controlled inputs, label `mb-1 block text-sm text-foreground`, input `h-9 w-full max-w-md rounded border border-border bg-background px-3 text-sm`, select, password input, actions row `flex gap-3`, submit `data-testid` + disabled styling - apps/web/src/app/(portal)/settings/dashboard/page.tsx — settings page heading `mb-6 text-lg font-semibold text-foreground`, fetch-in-useEffect pattern - apps/web/src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx — Vitest pattern: `vi.mock('next-intl', ...)`, `vi.mock('@/lib/...')`, dynamic `await import(...)`, render/screen/waitFor/fireEvent, afterEach cleanup — mirror exactly (mock `@/lib/settings-api`) - 07-UI-SPEC.md "Surface C: General Settings SMTP" — exact field list/order (Host, Port default 587, Verschlüsselung default STARTTLS, Benutzername, Passwort w/ show-hide, Absenderadresse), `max-w-md` on all inputs, page heading `text-lg font-semibold`, form actions row `flex gap-3 pt-2`, test feedback colors + auto-clear 6s - 07-UI-SPEC.md "i18n Translation Keys" → `settings.smtp.*` + `settings.categorySmtp` (shipped by Plan 04) - 07-CONTEXT.md D-05 (SMTP in general settings: host, port, username, password, encryption, sender address) smtp-settings.test.tsx (write FIRST — RED): - On mount the form loads existing config via mocked `fetchSmtp` and populates host/port/encryption/username/fromAddress fields (password stays blank even when `hasPassword` is true) - Submitting the form calls `saveSmtp` with the entered values - Clicking "Verbindung testen" calls `testSmtp` and renders the success message when it resolves `{ success: true }` and the error message when `{ success: false }` - The password input has a working show/hide toggle button (type switches password↔text) Write `smtp-settings.test.tsx` FIRST and confirm RED before implementing — mirror calendar-settings.test.tsx mocking (mock `next-intl` to echo short strings, mock `@/lib/settings-api`). Create `apps/web/src/lib/settings-api.ts` following calendar-api.ts: `const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';`, `credentials: 'include'`, throw on non-ok. Export type `SmtpConfig` (host string, port number, encryption 'none'|'starttls'|'ssl-tls', username?, fromAddress string, hasPassword boolean — NEVER a password field) and a `SaveSmtpPayload` (same minus hasPassword, plus optional `password`). Export `fetchSmtp()` GET /settings/smtp (may return null); `saveSmtp(payload)` PUT /settings/smtp; `testSmtp(payload)` POST /settings/smtp/test → `{ success: boolean }`. Create `smtp-settings-form.tsx` (`'use client'`, exported component `SmtpSettingsForm`, `useTranslations('settings')`): on mount `fetchSmtp()` to populate controlled state; render fields in UI-SPEC Surface C order — Host (text, placeholder smtp.example.com), Port (number, default 587), Verschlüsselung (select Keine/STARTTLS/SSL-TLS → none/starttls/ssl-tls, default starttls), Benutzername (text optional), Passwort (password optional with show/hide toggle button — `type="button"`, inline eye/eye-off SVG 16×16, toggles input type), Absenderadresse (email required, placeholder tessera@example.com, with help text). All inputs `max-w-md`, using the calendar-source-form label/input classes. Never pre-fill the password from server data (config returns `hasPassword` only); only send `password` when the user types one. Actions row `flex gap-3 pt-2`: "Einstellungen speichern" (primary, disabled while saving) → `saveSmtp`; "Verbindung testen" (secondary) → `testSmtp(currentPayload)` showing inline feedback below the row (loading muted text, success green `oklch(0.40 0.15 148)`, error `text-destructive`), auto-clearing after 6 seconds. All copy via `t('smtp.*')`. Both light + dark mode must work; inline SVG only. Create `apps/web/src/app/(portal)/settings/general/smtp/page.tsx` (`'use client'`, default export `SmtpSettingsPage`, `useTranslations('settings')`): heading `

{t('smtp.title')}

` (matches settings/dashboard/page.tsx) then ``. The route sits under the existing settings layout (SettingsSidebar + back link already wrap it).
pnpm --filter @tessera/web test -- --run smtp-settings && pnpm --filter @tessera/web exec tsc --noEmit -p tsconfig.json && grep -q "credentials: 'include'" apps/web/src/lib/settings-api.ts && grep -q "smtp/test" apps/web/src/lib/settings-api.ts - `smtp-settings.test.tsx` exists, mocks `@/lib/settings-api` + `next-intl`, and passes (was RED before the form existed) - `settings-api.ts` exports `fetchSmtp`, `saveSmtp`, `testSmtp` (all `credentials: 'include'`) and a `SmtpConfig` type with `hasPassword` but no `password` - The form renders all Surface C fields in order with the show/hide password toggle and never pre-fills the password - "Einstellungen speichern" calls `saveSmtp`; "Verbindung testen" calls `testSmtp` and shows inline success/error feedback - `page.tsx` renders `t('smtp.title')` heading and `` - `pnpm --filter @tessera/web test` (smtp-settings) and web type-check pass The administrator can view, save, and connection-test the shared SMTP configuration from /settings/general/smtp, covered by a passing Vitest test.
Task 2: Extend SettingsSidebar with the Allgemein > SMTP category apps/web/src/components/settings/settings-sidebar.tsx - apps/web/src/components/settings/settings-sidebar.tsx — current sidebar: `items` array, `isActive` helper, category header `

`, nav `flex flex-col gap-1 px-3`, active link `bg-sidebar-accent text-sidebar-accent-foreground font-medium` else `text-sidebar-foreground hover:bg-muted`, `aria-current` - 07-UI-SPEC.md "Surface C → SettingsSidebar extension" — add "Allgemein" section ABOVE the existing Dashboard section, with an "SMTP" link to `/settings/general/smtp`; copy active/inactive link styling verbatim - 07-UI-SPEC.md "i18n Translation Keys" → `settings.categoryGeneral` = "Allgemein", `settings.categorySmtp` = "SMTP" (shipped by Plan 04) Extend `settings-sidebar.tsx` to render a new "Allgemein" category ABOVE the existing "Dashboard" category. Add a category header `

` using `t('categoryGeneral')` with the exact `text-xs font-semibold uppercase tracking-wider text-muted-foreground` class, and a nav list containing a single `{t('categorySmtp')}` using the SAME active/inactive link classes and `aria-current` logic already present for the Dashboard items (factor the existing link rendering into a reusable map or duplicate the markup — keep styling identical). Ensure `isActive('/settings/general/smtp')` works via the existing `pathname.startsWith` branch. Do not remove or restyle the existing Dashboard/Widgets/Calendar items. Both light + dark themes must remain correct (sidebar tokens only). pnpm --filter @tessera/web exec tsc --noEmit -p tsconfig.json && grep -q "categoryGeneral" apps/web/src/components/settings/settings-sidebar.tsx && grep -q "/settings/general/smtp" apps/web/src/components/settings/settings-sidebar.tsx && grep -q "categorySmtp" apps/web/src/components/settings/settings-sidebar.tsx - The sidebar renders an "Allgemein" category header (`t('categoryGeneral')`) above the existing Dashboard category - An "SMTP" link (`t('categorySmtp')`) points to `/settings/general/smtp` and uses the existing active/inactive styling + `aria-current` - Existing Dashboard/Widgets/Calendar links are unchanged - Web type-check passes Users can navigate to the SMTP settings page from the settings sub-sidebar's new Allgemein category. ## Trust Boundaries | Boundary | Description | |----------|-------------| | browser form → /settings/smtp | Admin-entered SMTP host/credentials cross to the backend | | API response → React render | Backend-supplied SMTP config (without password) is rendered in the DOM | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-07-17 | Information Disclosure | smtp-settings-form / settings-api.ts | mitigate | `SmtpConfig` exposes only `hasPassword`, never the secret (mirrors calendar T-05-09); password field never pre-filled; password sent only when newly typed; cookie auth via `credentials: 'include'` (V3) | | T-07-18 | Tampering | settings-api.ts | mitigate | Backend `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` on every /settings/smtp endpoint (Plan 03) and `SmtpConfigDto` validation are authoritative; the client cannot bypass them | | T-07-19 | Injection (XSS) | smtp-settings-form / settings-sidebar | mitigate | All backend strings rendered via React text nodes (auto-escaped); no `dangerouslySetInnerHTML` | | T-07-SC | — | npm/pnpm installs | n/a | No new frontend dependencies are added (inline SVG icons, existing Vitest/Testing-Library only) | - `pnpm --filter @tessera/web exec tsc --noEmit -p tsconfig.json` exits 0 - `pnpm --filter @tessera/web test -- --run smtp-settings` passes - SMTP form never renders a password back from the server - Sidebar Allgemein > SMTP link navigates to /settings/general/smtp with correct active state - Components honor 07-UI-SPEC color/typography/spacing tokens in light + dark mode - DKV-05 SMTP settings UI complete: configurable + connection-testable in general settings, password never disclosed - Settings sub-sidebar exposes the Allgemein > SMTP category per 07-UI-SPEC Surface C Create `.planning/phases/07-dkv-fleet-module/07-06-SUMMARY.md` when done. Record how the test-feedback auto-clear is implemented and any UI-SPEC deviations.