docs(10): add admin config UI plan (INGEST-06 gap), resolve research open questions

This commit is contained in:
2026-07-21 10:08:40 +02:00
parent e7b59d16af
commit b52a0a796b
4 changed files with 152 additions and 5 deletions
@@ -0,0 +1,137 @@
---
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"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<tasks>
<task type="auto">
<name>Task 1: tender-radar-api client + admin source-config settings form</name>
<read_first>
- 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)
</read_first>
<files>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</files>
<action>
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 + `<SourceConfigForm />`. 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.
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>Admin can view and change the shared poll interval/active state from the module settings UI.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: SourceConfigForm component test</name>
<read_first>
- 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)
</read_first>
<behavior>
- 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).
</behavior>
<files>apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx</files>
<action>
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.
</action>
<verify>
<automated>cd apps/web && pnpm test -- SourceConfigForm 2>&1 | tail -15</automated>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>Admin config form behaviour has automated coverage; green.</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
- `pnpm --filter @tessera/web test -- SourceConfigForm` green.
- Settings route renders the form; save round-trips to the Plan 05 endpoint.
- Web `tsc --noEmit` clean.
</verification>
<success_criteria>
- 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".
</success_criteria>
<output>
Create `.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-06-SUMMARY.md` when done.
</output>