docs(07): create phase 7 execution plans for DKV fleet module
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 38s
Tessera CI/CD / Tests (push) Waiting to run

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>
This commit is contained in:
2026-06-26 18:56:59 +02:00
parent e9f4f2dcad
commit de06794e67
9 changed files with 2180 additions and 19 deletions
@@ -0,0 +1,196 @@
---
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"
---
<objective>
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.
</objective>
## 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>
<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.tsx
</context>
## 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`.
<tasks>
<task type="auto" tdd="true">
<name>Task 1: settings-api client + SmtpSettingsForm + SMTP page (DKV-05)</name>
<files>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</files>
<read_first>
- 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)
</read_first>
<behavior>
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)
</behavior>
<action>
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 `<h1 class="mb-6 text-lg font-semibold text-foreground">{t('smtp.title')}</h1>` (matches settings/dashboard/page.tsx) then `<SmtpSettingsForm />`. The route sits under the existing settings layout (SettingsSidebar + back link already wrap it).
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- `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 `<SmtpSettingsForm />`
- `pnpm --filter @tessera/web test` (smtp-settings) and web type-check pass
</acceptance_criteria>
<done>The administrator can view, save, and connection-test the shared SMTP configuration from /settings/general/smtp, covered by a passing Vitest test.</done>
</task>
<task type="auto">
<name>Task 2: Extend SettingsSidebar with the Allgemein > SMTP category</name>
<files>apps/web/src/components/settings/settings-sidebar.tsx</files>
<read_first>
- apps/web/src/components/settings/settings-sidebar.tsx — current sidebar: `items` array, `isActive` helper, category header `<h2 class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">`, 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)
</read_first>
<action>
Extend `settings-sidebar.tsx` to render a new "Allgemein" category ABOVE the existing "Dashboard" category. Add a category header `<h2>` using `t('categoryGeneral')` with the exact `text-xs font-semibold uppercase tracking-wider text-muted-foreground` class, and a nav list containing a single `<Link href="/settings/general/smtp">{t('categorySmtp')}</Link>` 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).
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>Users can navigate to the SMTP settings page from the settings sub-sidebar's new Allgemein category.</done>
</task>
</tasks>
<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>
<verification>
- `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
</verification>
<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>
<output>
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.
</output>