--- phase: 10-ausschreibungs-radar-foundation-d-e-ingestion plan: 06 type: execute wave: 6 depends_on: ["10-02", "10-05"] files_modified: - apps/web/src/lib/tender-radar-api.ts - apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx - apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.tsx - apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx autonomous: true requirements: [INGEST-06] must_haves: truths: - "Admin can read and change the shared DÖE poll interval / active state from a web form in the module settings, not only via the raw API (INGEST-06: 'Intervall pro Quelle im Admin-Bereich konfigurierbar')" - "Saving the form calls PUT /modules/tender-radar/source-config and reflects the persisted value on reload" artifacts: - apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx - apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.tsx - apps/web/src/lib/tender-radar-api.ts key_links: - "SourceConfigForm ↔ tender-radar-api.ts (fetchSourceConfig/saveSourceConfig) ↔ GET/PUT /modules/tender-radar/source-config (Plan 05 controller)" - "Admin poll-interval config UI — the Next.js frontend tier assigned in 10-RESEARCH.md Architectural Responsibility Map, mirroring DKV InboxConfigForm" --- Deliver the admin-facing configuration UI for the shared DÖE poll — the frontend half of INGEST-06 that REQUIREMENTS.md ("Intervall pro Quelle im Admin-Bereich konfigurierbar") and the RESEARCH Architectural Responsibility Map ("Admin poll-interval config UI" → Next.js frontend, mirroring DKV `InboxConfigForm`) both require. Plan 05 built the backend endpoint; this plan gives an admin a real form to drive it. ## Phase Goal (user story) **As a** Tessera-Administrator, **I want to** das DÖE-Poll-Intervall und den Aktiv-Status ueber ein Formular in den Modul-Einstellungen aendern, **so that** ich den gemeinsamen Zeitplan ohne API-Aufrufe steuern kann (INGEST-06). Purpose: Closes the INGEST-06 gap flagged by the plan checker — the requirement is admin-configurable "im Admin-Bereich", i.e. a UI, not only a REST surface. Thin slice: API client + settings form → the existing Plan 05 endpoint. Output: `tender-radar-api.ts` client, `settings/page.tsx`, `SourceConfigForm` + its test. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-PATTERNS.md @apps/web/src/lib/dkv-api.ts @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/settings/components/VehicleTable.test.tsx Task 1: tender-radar-api client + admin source-config settings form - apps/web/src/lib/dkv-api.ts (client conventions: API_URL, credentials:'include', typed config interface, fetchConfig/saveConfig) - apps/web/src/app/(portal)/modules/dkv-fleet/settings/page.tsx (settings route shell pattern) - apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx (load-on-mount + form-state + save flow to mirror; the tender form is much simpler — only interval + active) apps/web/src/lib/tender-radar-api.ts, apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx, apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.tsx Create `tender-radar-api.ts` mirroring `dkv-api.ts` conventions: `API_URL` from `NEXT_PUBLIC_API_URL`, all calls `credentials: 'include'`. Export a `SourceConfig` interface (`pollIntervalMin: number`, `isActive: boolean`, and read-only display fields `sourceType: string`, `lastIngestedDay?: string | null`) plus `fetchSourceConfig()` (GET `/modules/tender-radar/source-config`) and `saveSourceConfig(payload)` (PUT same path with `{ pollIntervalMin, isActive }`). Create `SourceConfigForm.tsx` (`'use client'`), a much simpler analog of `InboxConfigForm` — no credentials/password. On mount, `fetchSourceConfig()` to populate form state. Fields: a numeric `pollIntervalMin` input (min 5, max 1440 — mirror the backend `SourceConfigDto` bounds so client and server agree) and an `isActive` toggle. A "Speichern" button calls `saveSourceConfig()` and shows a success/error state. Optionally display `sourceType` (doe-opendata) and `lastIngestedDay` read-only so the admin sees the day-cursor state. Note in a comment that `pollIntervalMin` is the cron-tick frequency (D-04 default 60), decoupled from the day-granularity DÖE fetch. Create `settings/page.tsx` (`'use client'`, default export) rendering a title + ``. Use hardcoded German strings for now (full i18n is CONFIG-03, Phase 14) and note the intentional MVP stub in the summary. No module-loader whitelist change is needed — `settings/page.tsx` is a standard Next App Router route. cd apps/web && test -f "src/app/(portal)/modules/tender-radar/settings/page.tsx" && grep -c "saveSourceConfig" src/lib/tender-radar-api.ts && pnpm exec tsc --noEmit 2>&1 | tail -5 - `tender-radar-api.ts` exports `fetchSourceConfig` and `saveSourceConfig` hitting `/modules/tender-radar/source-config`. - `SourceConfigForm` loads config on mount and has a numeric interval input (min 5 / max 1440) + isActive toggle + save action. - `settings/page.tsx` exists and renders the form; web `tsc --noEmit` passes. Admin can view and change the shared poll interval/active state from the module settings UI. Task 2: SourceConfigForm component test - apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.test.tsx (existing web Vitest + Testing Library component-test style) - apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.tsx (component under test) - Test: on mount the form fetches config and renders the returned pollIntervalMin value in the interval input. - Test: editing the interval and clicking Speichern calls saveSourceConfig with the new pollIntervalMin and the isActive value. - Test: an interval below 5 or above 1440 is rejected client-side (no save call fired). apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx Write a Vitest + Testing Library test mirroring `VehicleTable.test.tsx`. Mock `tender-radar-api` (`fetchSourceConfig` resolving a known config, `saveSourceConfig` spied). Assert the load-on-mount renders the interval, that Speichern calls `saveSourceConfig` with the edited payload, and that out-of-bounds intervals do not trigger a save. This is the automated proof for the INGEST-06 admin-UI slice. cd apps/web && pnpm test -- SourceConfigForm 2>&1 | tail -15 - Test asserts fetch-on-mount populates the interval input. - Test asserts Speichern calls `saveSourceConfig` with the edited interval + isActive. - Out-of-bounds interval does not call `saveSourceConfig`. - `pnpm --filter @tessera/web test -- SourceConfigForm` green. Admin config form behaviour has automated coverage; green. ## Trust Boundaries | Boundary | Description | |----------|-------------| | admin browser → PUT /source-config | Privileged mutation of the platform-wide poll schedule via the settings form | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-10-16 | Elevation of Privilege | `SourceConfigForm` save path | high | mitigate | The form calls the Plan 05 `PUT /source-config` route, which is `@Roles(Role.ADMIN, Role.SUPER_ADMIN)`-guarded server-side — a non-admin cannot mutate the schedule even if the form is reachable. Client bounds (min 5 / max 1440) mirror the server DTO; the server remains the authority | | T-10-17 | Input Validation | `pollIntervalMin` input | medium | mitigate | Client-side min/max clamp to 5..1440 matches `SourceConfigDto` server bounds (DoS floor on poll frequency); server re-validates regardless of client | | T-10-18 | Information Disclosure | config fetch response | low | accept | The DÖE source config holds no secrets (auth-free API, no credentials) — unlike DKV there is no password field to protect in this form | - `pnpm --filter @tessera/web test -- SourceConfigForm` green. - Settings route renders the form; save round-trips to the Plan 05 endpoint. - Web `tsc --noEmit` clean. - INGEST-06 (admin UI): the shared DÖE poll interval is configurable from a form in the module's admin settings, satisfying "Intervall pro Quelle im Admin-Bereich konfigurierbar". Create `.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-06-SUMMARY.md` when done.