6 plans covering full pipeline: PDF parsing foundation (Wave 0), inbox providers + export/SMTP services (Wave 1), pipeline orchestration + frontend pages + settings UI (Wave 2). Includes D-06 MailModule DB-config migration and Nyquist validation strategy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
16 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | user_setup | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-dkv-fleet-module | 06 | execute | 2 |
|
|
true |
|
|
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.
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>
@.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.tsxArtifacts this phase produces (Plan 06 portion)
New symbols (exclude from drift verification):
- File
apps/web/src/lib/settings-api.ts: typeSmtpConfig(host, port, encryption, username, fromAddress, hasPassword — never a password); functionsfetchSmtp,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
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 `Path notes: The API-client path uses the repo convention
apps/web/src/lib/settings-api.ts(matchingapps/web/src/lib/calendar-api.ts); the checker brief'slib/api/settings.tsdoes not exist in the repo. The form lives atapps/web/src/components/settings/smtp-settings-form.tsx(per the checker brief, consistent with the existingcomponents/settings/location ofcalendar-source-form.tsxandsettings-sidebar.tsx). The@import alias maps toapps/web/src.
{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.
<threat_model>
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) |
| </threat_model> |
<success_criteria>
- 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 </success_criteria>