Files
tessera-ctl/.planning/phases/07-dkv-fleet-module/07-05-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

245 lines
26 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: 05
type: execute
wave: 2
depends_on: ["07-01", "07-02", "07-03"]
files_modified:
- apps/web/src/lib/dkv-api.ts
- apps/web/src/app/(portal)/modules/dkv-fleet/page.tsx
- apps/web/src/app/(portal)/modules/dkv-fleet/components/StatusBadge.tsx
- apps/web/src/app/(portal)/modules/dkv-fleet/components/InvoiceHistoryTable.tsx
- apps/web/src/app/(portal)/modules/dkv-fleet/components/ExportFileList.tsx
- apps/web/src/app/(portal)/modules/dkv-fleet/settings/page.tsx
- apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx
- apps/web/src/app/(portal)/modules/dkv-fleet/vehicles/page.tsx
- apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.tsx
- apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/CsvImportButton.tsx
- apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.test.tsx
autonomous: true
requirements: [DKV-01, DKV-03, DKV-04]
user_setup: []
must_haves:
truths:
- "User can open /modules/dkv-fleet and see the processing history (Verarbeitungshistorie) and the last export files"
- "User can click 'Jetzt prüfen' to trigger an inbox poll and the history refetches afterward"
- "User can download an export file from the history row / export list"
- "User can configure the inbox (protocol, host, port, encryption, folder, sender filter, interval, credentials, recipient, format string, active) and save it"
- "User can test the inbox connection and see inline success/error feedback"
- "User can list, add, inline-edit, and delete vehicles in the vehicle master table"
- "User can import a CSV of vehicles in merge or replace mode (replace asks for destructive confirmation)"
artifacts:
- path: "apps/web/src/lib/dkv-api.ts"
provides: "Typed fetch client for all /dkv/* routes (config, check-now, history, exports, vehicles CRUD, CSV import, test-connection)"
contains: "credentials: 'include'"
- path: "apps/web/src/app/(portal)/modules/dkv-fleet/page.tsx"
provides: "Surface A — module main page (history + export list + Jetzt prüfen)"
min_lines: 30
- path: "apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx"
provides: "Inbox config form (Surface B inbox tab)"
min_lines: 40
- path: "apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.tsx"
provides: "Vehicle master CRUD table with inline edit"
min_lines: 40
- path: "apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.test.tsx"
provides: "Vitest coverage for vehicle CRUD table (Wave 0 / VALIDATION DKV-03)"
contains: "vi.mock"
key_links:
- from: "apps/web/src/app/(portal)/modules/dkv-fleet/page.tsx"
to: "apps/web/src/lib/dkv-api.ts (checkNow + fetchHistory)"
via: "Jetzt prüfen button calls checkNow then refetches history"
pattern: "checkNow|fetchHistory"
- from: "apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx"
to: "apps/web/src/lib/dkv-api.ts (fetchConfig/saveConfig/testConnection)"
via: "form load + save + test"
pattern: "saveConfig|testConnection"
- from: "apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.tsx"
to: "apps/web/src/lib/dkv-api.ts (vehicles CRUD)"
via: "list/create/update/delete"
pattern: "createVehicle|updateVehicle|deleteVehicle"
---
<objective>
Deliver the DKV Fleet Module frontend (Surfaces A and B from 07-UI-SPEC.md): the typed API client, the module main page (processing history + export download list + "Jetzt prüfen" trigger), the module settings inbox-config form, and the vehicle master CRUD table with CSV import. Consumes the `/dkv/*` REST surface built in Plan 04.
Purpose: This is the user-facing half of DKV-01 (inbox config + manual check), DKV-03 (vehicle mapping CSV + manual edit), and DKV-04 (export download + processing history). Each task is a vertical slice: after it, a user can do something concrete in the browser.
Output: dkv-api client, main page (+ StatusBadge, InvoiceHistoryTable, ExportFileList), settings page (+ InboxConfigForm), vehicles page (+ VehicleTable, CsvImportButton) and a VehicleTable test.
</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 makes the pipeline operable from the browser: configure the inbox, manage vehicle→driver mapping, trigger checks, and download exports.
> **Requirement mapping note:** The checker brief listed `DKV-03, DKV-05` for this plan. DKV-05 (SMTP in general settings) has no UI here — it is delivered by Plan 07-06. This plan's tasks address DKV-01 (inbox config + check-now UI), DKV-03 (vehicle CRUD + CSV), and DKV-04 (export download + history UI), which is the accurate mapping.
<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/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx
@apps/web/src/app/(portal)/modules/domaincheck/page.tsx
</context>
## Artifacts this phase produces (Plan 05 portion)
New symbols (exclude from drift verification):
- File `apps/web/src/lib/dkv-api.ts`: types `DkvConfig`, `DkvHistoryEntry`, `DkvVehicle`, `CreateVehiclePayload`, `UpdateVehiclePayload`; functions `fetchConfig`, `saveConfig`, `testConnection`, `checkNow`, `fetchHistory`, `fetchVehicles`, `createVehicle`, `updateVehicle`, `deleteVehicle`, `importVehiclesCsv`, `exportFileUrl`
- Components `DkvFleetPage` (default), `StatusBadge`, `InvoiceHistoryTable`, `ExportFileList`, `DkvFleetSettingsPage` (default), `InboxConfigForm`, `DkvFleetVehiclesPage` (default), `VehicleTable`, `CsvImportButton`
> **Path note:** The existing API-client convention in this repo is `apps/web/src/lib/<domain>-api.ts` (see `apps/web/src/lib/calendar-api.ts`). The checker brief referenced `lib/api/dkv.ts` with analog `lib/api/calendar.ts`, but that path does not exist in the repo. This plan uses `apps/web/src/lib/dkv-api.ts` to match the real, existing analog. The `@` import alias maps to `apps/web/src`.
<tasks>
<task type="auto">
<name>Task 1: dkv-api client + StatusBadge + module main page (Surface A — DKV-04/DKV-01)</name>
<files>apps/web/src/lib/dkv-api.ts, apps/web/src/app/(portal)/modules/dkv-fleet/components/StatusBadge.tsx, apps/web/src/app/(portal)/modules/dkv-fleet/components/InvoiceHistoryTable.tsx, apps/web/src/app/(portal)/modules/dkv-fleet/components/ExportFileList.tsx, apps/web/src/app/(portal)/modules/dkv-fleet/page.tsx</files>
<read_first>
- apps/web/src/lib/calendar-api.ts — API-client convention: `API_URL` const, `credentials: 'include'`, exported typed functions, throw on non-ok (mirror this file exactly for structure)
- apps/web/src/app/(portal)/modules/domaincheck/page.tsx — page shell: `'use client'`, `useTranslations('dkvFleet')`, `mx-auto max-w-5xl space-y-6 p-6`, card panel
- 07-UI-SPEC.md "Surface A: Module Main Page" — full layout, InvoiceHistoryTable column spec (6 columns + widths), ExportFileList spec, "Jetzt prüfen" button states, skeleton rows, pagination > 25 rows
- 07-UI-SPEC.md "StatusBadge Component" — exact OKLCH inline styles per status, rounded-full px-2 py-0.5 text-xs font-medium
- 07-UI-SPEC.md "Color" + "Typography" + "Spacing Scale" — token usage; accent reserved list
- 07-PATTERNS.md "dkv-fleet/page.tsx" — analog domaincheck/page.tsx; layout note (max-w-5xl)
- 07-CONTEXT.md D-20 (history columns), D-15 (last 10 export files)
</read_first>
<action>
Create `apps/web/src/lib/dkv-api.ts` following the calendar-api.ts structure: `const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';`, all calls use `credentials: 'include'` and throw `new Error(...)` on non-ok. Export TypeScript types mirroring the Plan 04 backend contract: `DkvConfig` (protocol, host, port, encryption, folder, senderFilter, pollIntervalMin, isActive, exportRecipient, vehicleFormatString, username, hasPassword boolean — never a password), `DkvHistoryEntry` (id, datumZeit string, rechnungsnummer, anzahlFahrzeuge number, anzahlTransaktionen number, status 'Verarbeitet'|'Fehler'|'Versand fehlgeschlagen', errorMessage?, exportFilename?), `DkvVehicle` (id, kennzeichen, marke, modell, fahrer), `CreateVehiclePayload`, `UpdateVehiclePayload`. Export functions: `fetchConfig()` GET /dkv/config; `saveConfig(payload)` PUT /dkv/config; `testConnection(payload)` POST /dkv/test-connection → `{ success: boolean }`; `checkNow()` POST /dkv/check-now; `fetchHistory(page?, limit?)` GET /dkv/history with URLSearchParams; `fetchVehicles()` GET /dkv/vehicles; `createVehicle(payload)` POST /dkv/vehicles; `updateVehicle(id, payload)` PUT /dkv/vehicles/:id; `deleteVehicle(id)` DELETE /dkv/vehicles/:id; `importVehiclesCsv(file, mode)` POST /dkv/vehicles/import as `multipart/form-data` (FormData with `file` and `mode`, do NOT set Content-Type header manually); and a pure helper `exportFileUrl(filename)` returning `${API_URL}/dkv/exports/${encodeURIComponent(filename)}`.
Create `StatusBadge.tsx`: props `{ status, errorMessage? }`. Render `<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium">` with the exact inline OKLCH color/background per 07-UI-SPEC "StatusBadge Component" for each of the three statuses; add `title={errorMessage}` for Fehler / Versand fehlgeschlagen when provided. Labels come from `t('status.processed'|'status.error'|'status.sendFailed')`.
Create `InvoiceHistoryTable.tsx`: fetches via `fetchHistory` in useEffect, renders the 6-column table exactly per 07-UI-SPEC (column header style `text-xs font-semibold uppercase tracking-wider text-muted-foreground`, widths and alignments as specified, row hover `hover:bg-muted/50`, container `rounded border border-border overflow-hidden`). Show 5 skeleton rows while loading (`h-4 rounded bg-muted animate-pulse`). Render StatusBadge in the STATUS column and, in the EXPORTDATEI column, a download `<a href={exportFileUrl(filename)} download>` with inline SVG download icon (`text-primary text-sm hover:underline`) when present else an em-dash `<span class="text-muted-foreground">–</span>`. Empty state uses `t('emptyHistory')` heading + `t('emptyHistoryBody')`. Add pagination controls only when total > 25 (per UI-SPEC), wiring `fetchHistory(page, 25)`.
Create `ExportFileList.tsx`: lists up to 10 export files (derive filenames from history entries' `exportFilename`, newest first, dedup) each as an inline SVG download icon + `<a>` (`text-sm text-primary hover:underline`) using `exportFileUrl`; empty state `t('emptyExports')`.
Create `page.tsx` (`'use client'`, default export `DkvFleetPage`): `mx-auto max-w-5xl space-y-6 p-6`. Header row: `<h1 class="text-2xl font-semibold tracking-tight">{t('pageTitle')}</h1>` with the "Jetzt prüfen" button right-aligned and the "Zuletzt geprüft" text (`text-sm text-muted-foreground`). "Jetzt prüfen" button uses the exact idle/loading classes from UI-SPEC; on click set loading, call `checkNow()`, on success clear loading and trigger an InvoiceHistoryTable refetch (lift a refresh key or expose a ref/callback), on error show the destructive error banner above the table (`border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive` with a × close button) using `t('errors.pollFailed')`. Render `<h2>Verarbeitungshistorie</h2>` + `<InvoiceHistoryTable />`, then `<h2>Letzte Exportdateien</h2>` + `<ExportFileList />`.
All copy via `useTranslations('dkvFleet')` keys defined in 07-UI-SPEC (Plan 04 ships these keys). Dark + light mode must both work (use CSS tokens only). Do not introduce any external icon package — inline SVG only.
</action>
<verify>
<automated>pnpm --filter @tessera/web exec tsc --noEmit -p tsconfig.json && grep -q "credentials: 'include'" apps/web/src/lib/dkv-api.ts && grep -q "check-now" apps/web/src/lib/dkv-api.ts && grep -q "useTranslations('dkvFleet')" "apps/web/src/app/(portal)/modules/dkv-fleet/page.tsx"</automated>
</verify>
<acceptance_criteria>
- `dkv-api.ts` exports `checkNow`, `fetchHistory`, `fetchConfig`, `saveConfig`, `testConnection`, vehicle CRUD functions, `importVehiclesCsv`, and `exportFileUrl`; all use `credentials: 'include'`
- `dkv-api.ts` never declares a returned `password` field on `DkvConfig` (only `hasPassword`)
- Main page renders an `<h1>` with `t('pageTitle')` and a "Jetzt prüfen" button that calls `checkNow` then refetches history
- InvoiceHistoryTable renders the 6 columns from UI-SPEC and uses StatusBadge for status
- Export download links use `exportFileUrl(...)` (server-validated filename) — no client-built path traversal
- Web type-check / build passes (`tsc --noEmit`)
</acceptance_criteria>
<done>The module main page lists processing history + export files and the user can trigger an inbox check from the browser.</done>
</task>
<task type="auto">
<name>Task 2: Module settings page + InboxConfigForm (Surface B inbox — DKV-01)</name>
<files>apps/web/src/app/(portal)/modules/dkv-fleet/settings/page.tsx, apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx</files>
<read_first>
- apps/web/src/components/settings/calendar-source-form.tsx — form field pattern: `'use client'`, 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 pattern, password input, actions row `flex gap-3`, submit `data-testid` + disabled styling, show/hide not present here (add it per UI-SPEC)
- apps/web/src/app/(portal)/settings/dashboard/page.tsx — settings page heading `mb-6 text-lg font-semibold text-foreground`
- apps/web/src/lib/dkv-api.ts (Task 1) — fetchConfig / saveConfig / testConnection signatures
- 07-UI-SPEC.md "Surface B: Module Settings Page" → "Tab 1: Posteingang → InboxConfigForm" — exact field list/order, password show/hide toggle, Active toggle switch, form actions row, inline test feedback colors
- 07-UI-SPEC.md "Copywriting Contract" + form i18n keys (`form.*`)
- 07-CONTEXT.md D-01/D-02/D-03 (inbox fields), D-07 (export recipient per module), D-19 (vehicle format string)
</read_first>
<action>
Create `settings/page.tsx` (`'use client'`, default export `DkvFleetSettingsPage`, `useTranslations('dkvFleet')`): heading `<h1 class="text-2xl font-semibold tracking-tight mb-6">{t('settingsTitle')}</h1>`, then render `<InboxConfigForm />`. (Per the checker's route split, vehicle management lives at its own `/modules/dkv-fleet/vehicles` route built in Task 3; add a simple inline link/tab to it here, e.g. a secondary-styled `<Link href="/modules/dkv-fleet/vehicles">{t('tabs.vehicles')}</Link>` styled per the UI-SPEC tab pattern.)
Create `InboxConfigForm.tsx` (`'use client'`): on mount `fetchConfig()` to populate controlled state; render the fields in the exact order from UI-SPEC Tab 1 — Protokoll (select IMAP/Exchange), Host (text), Port (number), Verschlüsselung (select Keine/STARTTLS/SSL-TLS → values none/starttls/ssl-tls), Ordner (text default "INBOX"), Absenderfilter (email, optional, placeholder rechnung@dkv.com), Abrufintervall (number min=5, suffix "Minuten"), Benutzername (text optional), Passwort (password optional with show/hide toggle button — `type="button"`, inline eye/eye-off SVG, toggles input type between password/text), Export-Empfänger (email required), Fahrzeug-Formatstring (text default `{Marke}/{Modell}/{Kennzeichen}` with help text listing placeholders), Aktiv (toggle switch `role="switch" aria-checked`). Use the calendar-source-form label/input classes. Form actions row `flex gap-3 pt-4 border-t border-border mt-6`: "Verbindung testen" (secondary) → calls `testConnection(currentFormPayload)` and shows inline green/destructive feedback per UI-SPEC; "Einstellungen speichern" (primary) → calls `saveConfig`, disabling the button while saving and showing inline error on failure. Never render the existing password back from the server (config returns `hasPassword`, not the password) — leave the password field blank and only send it when the user types a new one. All copy via `t('form.*')`. Both color themes must work.
</action>
<verify>
<automated>pnpm --filter @tessera/web exec tsc --noEmit -p tsconfig.json && grep -q "saveConfig" "apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx" && grep -q "testConnection" "apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx" && grep -q "role=\"switch\"" "apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx"</automated>
</verify>
<acceptance_criteria>
- Settings page renders `t('settingsTitle')` heading and the InboxConfigForm
- InboxConfigForm renders all UI-SPEC Tab-1 fields in order, with the show/hide password toggle and the Active `role="switch"` toggle
- Abrufintervall input enforces `min=5`
- "Verbindung testen" calls `testConnection`; "Einstellungen speichern" calls `saveConfig`
- Password field is never pre-filled from server data (only `hasPassword` is known)
- Web type-check / build passes
</acceptance_criteria>
<done>The user can configure and test the inbox connection and save module settings from the browser.</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: Vehicles page + VehicleTable + CsvImportButton (Surface B vehicles — DKV-03)</name>
<files>apps/web/src/app/(portal)/modules/dkv-fleet/vehicles/page.tsx, apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.tsx, apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/CsvImportButton.tsx, apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.test.tsx</files>
<read_first>
- apps/web/src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx — Vitest+Testing-Library pattern: `vi.mock('next-intl', ...)`, `vi.mock('@/lib/calendar-api', ...)`, dynamic `await import(...)`, `render`, `screen`, `waitFor`, `fireEvent`, `afterEach` cleanup. Mirror this exactly for VehicleTable.test.tsx (mock `@/lib/dkv-api`)
- apps/web/src/lib/dkv-api.ts (Task 1) — fetchVehicles / createVehicle / updateVehicle / deleteVehicle / importVehiclesCsv
- apps/web/src/components/settings/calendar-source-form.tsx — input/label/button classes to reuse
- 07-UI-SPEC.md "Tab 2: Fahrzeuge → VehicleTable + CsvImportButton" — toolbar, columns (KENNZEICHEN/MARKE/MODELL/FAHRER/AKTIONEN), view vs edit row modes, icon button aria-labels + SVG sizes, add-vehicle row, CSV import dialog (merge/replace), destructive replace confirm, success toast
- 07-UI-SPEC.md "Copywriting Contract" — delete dialog + CSV replace dialog copy; "col.*" header keys
- apps/web/vitest.config.ts — confirm jsdom env + setup (tests run via `pnpm --filter @tessera/web test`)
</read_first>
<behavior>
VehicleTable.test.tsx (write FIRST — RED):
- Renders a list of two mocked vehicles (Kennzeichen, Marke, Modell, Fahrer) returned by mocked `fetchVehicles`
- Shows the empty-state heading (`t('emptyVehicles')` → mocked string) when `fetchVehicles` resolves `[]`
- Clicking the delete (trash) icon button opens a confirm dialog, and confirming calls `deleteVehicle` with the vehicle id
- Clicking the edit (pencil) icon button switches the row to inputs; saving calls `updateVehicle` with the id and changed fields
- Icon-only action buttons expose the UI-SPEC `aria-label`s ("Fahrzeug bearbeiten", "Fahrzeug löschen", "Änderungen speichern", "Bearbeitung abbrechen")
</behavior>
<action>
Write `VehicleTable.test.tsx` first and confirm it fails (RED) before implementing — mirror the calendar-settings.test.tsx mocking approach exactly (mock `next-intl` to echo keys/short strings, mock `@/lib/dkv-api`).
Create `VehicleTable.tsx` (`'use client'`): on mount `fetchVehicles()`; toolbar `flex justify-between items-center mb-3` with a left count badge (`t('vehicleCount', { count })`) and right `[CsvImportButton]` + "Fahrzeug hinzufügen" (primary) buttons. Table columns per UI-SPEC with the shared header style. Each row has view mode (plain cells + pencil + trash icon buttons) and edit mode (text inputs, Kennzeichen `uppercase`, + checkmark/× icon buttons). All icon-only buttons carry the exact UI-SPEC `aria-label`s and SVG sizes (16×16, inline SVG). "Fahrzeug hinzufügen" appends a new empty edit-mode row. Delete uses an inline confirm dialog with the UI-SPEC delete copy (`deleteVehicle`/`deleteVehicleBody`), confirm button destructive-styled; on confirm call `deleteVehicle(id)` and refetch. Save row calls `createVehicle` (new) or `updateVehicle(id, ...)` (existing); validation: Kennzeichen required, inline `text-destructive text-xs` on error. Wire CsvImportButton's `onImported` to refetch + show a success toast (`importSuccess`).
Create `CsvImportButton.tsx` (`'use client'`): hidden `<input type="file" accept=".csv">`; the visible "CSV importieren" button triggers it. After a file is selected, show the inline import-mode dialog (Zusammenführen default / Ersetzen) per UI-SPEC; "Ersetzen" → destructive confirm dialog before importing. On confirm call `importVehiclesCsv(file, mode)` then call the `onImported(count)` callback. Validate the selected file is `.csv` client-side; show parse/import errors as `text-destructive text-sm` in the dialog.
Create `vehicles/page.tsx` (`'use client'`, default export `DkvFleetVehiclesPage`, `useTranslations('dkvFleet')`): heading `<h1 class="text-2xl font-semibold tracking-tight mb-6">{t('tabs.vehicles')}</h1>` (or settingsTitle + subheading), a back link to `/modules/dkv-fleet/settings`, then `<VehicleTable />`. Import VehicleTable/CsvImportButton from `../settings/components/` (UI-SPEC component file map). Both color themes must work; inline SVG icons only.
</action>
<verify>
<automated>pnpm --filter @tessera/web test -- --run VehicleTable && pnpm --filter @tessera/web exec tsc --noEmit -p tsconfig.json</automated>
</verify>
<acceptance_criteria>
- `VehicleTable.test.tsx` exists, mocks `@/lib/dkv-api` + `next-intl`, and passes (was RED before VehicleTable existed)
- VehicleTable lists vehicles, supports inline edit (calls `updateVehicle`), create (`createVehicle`), and delete with confirm dialog (`deleteVehicle`)
- All icon-only action buttons carry the UI-SPEC `aria-label`s
- CsvImportButton offers merge/replace modes and shows a destructive confirm before replace, calling `importVehiclesCsv(file, mode)`
- `vehicles/page.tsx` renders VehicleTable under a heading with a back link to settings
- `pnpm --filter @tessera/web test` (VehicleTable) and web type-check pass
</acceptance_criteria>
<done>The user can manage the vehicle→driver master list (CRUD + CSV merge/replace) from the browser, covered by a passing Vitest test.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| browser form → /dkv/* API | Admin-entered inbox config, credentials, and vehicle/CSV data cross to the backend |
| API response → React render | Backend-supplied history/vehicle/export data is rendered in the DOM |
| user file → CSV import | User-selected file uploaded for bulk vehicle import |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-07-12 | Information Disclosure | InboxConfigForm / dkv-api.ts | mitigate | Password field never pre-filled from server (config returns `hasPassword` only, never the secret — mirrors calendar T-05-09); password sent only when user types a new one; transport is cookie auth via `credentials: 'include'` (V3) |
| T-07-13 | Tampering | ExportFileList / InvoiceHistoryTable | mitigate | Download URLs built with `exportFileUrl()` + `encodeURIComponent`; the server enforces the `DKV_*.xlsx` filename format and path-traversal guard (Plan 04) — client never constructs raw paths |
| T-07-14 | Injection (XSS) | InvoiceHistoryTable / VehicleTable | mitigate | All backend strings rendered through React text nodes (auto-escaped); no `dangerouslySetInnerHTML`; StatusBadge tooltip uses the `title` attribute, not raw HTML |
| T-07-15 | Tampering | CsvImportButton | mitigate | Client restricts selection to `.csv` (`accept=".csv"`) and the replace path requires an explicit destructive confirmation; server-side validation/size limits remain authoritative (Plan 04) |
| T-07-SC | — | npm/pnpm installs | n/a | No new frontend dependencies are added in this plan (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 VehicleTable` passes
- All copy resolves through `useTranslations('dkvFleet')` keys defined in 07-UI-SPEC (shipped by Plan 04)
- Components honor the 07-UI-SPEC color/typography/spacing tokens in both light and dark mode
</verification>
<success_criteria>
- DKV-01 inbox-config UI + manual "Jetzt prüfen" trigger operable in the browser
- DKV-03 vehicle master CRUD + CSV merge/replace import operable, covered by a Vitest test
- DKV-04 export download links + processing-history display operable
- All three surfaces follow the 07-UI-SPEC visual contract
</success_criteria>
<output>
Create `.planning/phases/07-dkv-fleet-module/07-05-SUMMARY.md` when done. Record the InvoiceHistoryTable refetch mechanism chosen, how the password-blank-on-load behavior is implemented, and any UI-SPEC deviations.
</output>