Files
tessera-ctl/.planning/phases/07-dkv-fleet-module/07-06-PLAN.md
T
schalli de06794e67
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
docs(07): create phase 7 execution plans for DKV fleet module
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>
2026-06-26 18:56:59 +02:00

197 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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>