--- phase: 10-ausschreibungs-radar-foundation-d-e-ingestion plan: 06 subsystem: ui tags: [next.js, react, vitest, testing-library, tender-radar, admin-settings, tdd] # Dependency graph requires: - phase: 10-02 (marketplace registration) provides: TendersModule seeded, tender-radar/page.tsx placeholder + module-loader whitelist entry - phase: 10-05 (controller/DTOs) provides: "GET/PUT /modules/tender-radar/source-config (Roles-guarded, singleton doe-opendata config, live scheduler apply)" provides: - "tender-radar-api.ts web client — fetchSourceConfig/saveSourceConfig against the Plan 05 endpoint" - "SourceConfigForm — admin form for the shared DÖE poll interval (5-1440 min) + isActive toggle, with client-side bounds mirroring the backend DTO" - "settings/page.tsx — standard App Router route rendering the form under the already-whitelisted tender-radar module" - "Component test coverage: fetch-on-mount, save payload, out-of-bounds client-side rejection" affects: [phase 11 saved searches/filter UI (may extend this settings surface with per-search-profile config later)] # Tech tracking tech-stack: added: [] patterns: - "Admin config form without secrets: SourceConfigForm mirrors DKV's InboxConfigForm load-on-mount/save flow but omits all credential fields, since the DÖE source is auth-free (T-10-18)" - "Client-side bound mirroring: pollIntervalMin clamp (5-1440) duplicated on the client purely as UX/DoS-floor guidance — the server's SourceConfigDto class-validator bounds remain the sole authority" key-files: created: - 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 modified: [] key-decisions: - "No next-intl in SourceConfigForm/settings/page.tsx — hardcoded German strings per plan instruction, matching the existing tender-radar/page.tsx placeholder's documented MVP-stub convention (full i18n is CONFIG-03, Phase 14)" - "No module-loader whitelist change — settings/page.tsx is a standard nested Next.js App Router route under the already-whitelisted tender-radar module page (Plan 10-02), unlike the top-level module entry itself" - "Read-only sourceType/lastIngestedDay display fields added beyond the plan's minimum ask (interval + toggle) so the admin can see the day-cursor state, as the plan's action text explicitly suggested (\"Optionally display...\")" requirements-completed: [INGEST-06] coverage: - id: D1 description: "Admin can read and change the shared DÖE poll interval / active state from a web form in module settings (not only via the raw API)" requirement: "INGEST-06" verification: - kind: automated_ui ref: "SourceConfigForm.test.tsx#'fetches config on mount and renders the returned pollIntervalMin in the interval input'" status: pass - kind: automated_ui ref: "SourceConfigForm.test.tsx#'editing the interval and clicking Speichern calls saveSourceConfig with the new pollIntervalMin and isActive'" status: pass human_judgment: false - id: D2 description: "Client-side interval bounds (5-1440 min) mirror the backend SourceConfigDto and block out-of-bounds saves before they reach the server" requirement: "INGEST-06" verification: - kind: automated_ui ref: "SourceConfigForm.test.tsx#'rejects an interval below 5 client-side without calling saveSourceConfig'" status: pass - kind: automated_ui ref: "SourceConfigForm.test.tsx#'rejects an interval above 1440 client-side without calling saveSourceConfig'" status: pass human_judgment: false - id: D3 description: "Settings route renders the form and round-trips a real GET/PUT to the Plan 05 endpoint in a live browser session (not just component-test mocks)" requirement: "INGEST-06" verification: [] human_judgment: true rationale: "Component tests mock tender-radar-api; an actual browser round-trip against the running API (login as admin, open /modules/tender-radar/settings, change interval, reload, confirm persistence) was not exercised in this run — recommend a quick UAT pass once the local stack is rebuilt with these frontend changes." # Metrics duration: 3min completed: 2026-07-21 status: complete --- # Phase 10 Plan 06: Ausschreibungs-Radar Admin Settings UI Summary **`SourceConfigForm` + `tender-radar-api.ts` client deliver the missing admin-facing half of INGEST-06 — a settings form (interval 5-1440 min + active toggle) that reads and writes the Plan 05 `GET`/`PUT /modules/tender-radar/source-config` endpoint, closing the plan-review gap that flagged the requirement as REST-only.** ## Performance - **Duration:** 3 min - **Started:** 2026-07-21T09:23:22Z - **Completed:** 2026-07-21T09:25:48Z - **Tasks:** 2 (Task 1 auto, Task 2 auto with tdd="true") - **Files modified:** 4 (all created, none modified) ## Accomplishments - `tender-radar-api.ts` — `SourceConfig`/`SaveSourceConfigPayload` types plus `fetchSourceConfig()`/`saveSourceConfig()` hitting `GET`/`PUT /modules/tender-radar/source-config`, mirroring `dkv-api.ts` conventions (`NEXT_PUBLIC_API_URL`, `credentials: 'include'`). - `SourceConfigForm` — loads the singleton config on mount, exposes a numeric `pollIntervalMin` input (client-side clamp 5-1440, mirroring the backend `SourceConfigDto` bounds) and an `isActive` toggle switch (same visual pattern as `InboxConfigForm`'s Aktiv switch), plus read-only `sourceType`/`lastIngestedDay` display so the admin can see the day-cursor state. Save success/error and validation-error feedback states, no credentials fields (the DÖE source has none). - `settings/page.tsx` — standard nested App Router route rendering the form; no `module-loader.ts` whitelist change needed since the parent `tender-radar` slug is already whitelisted (Plan 10-02). - Component test (`SourceConfigForm.test.tsx`, 4 tests): fetch-on-mount populates the interval input, editing + Speichern calls `saveSourceConfig` with the edited `{ pollIntervalMin, isActive }` payload, and both out-of-bounds directions (below 5, above 1440) are rejected client-side with zero `saveSourceConfig` calls. - Full web Vitest suite green (111/111 across 20 files), `tsc --noEmit` clean for `apps/web`. ## Task Commits Each task committed atomically: 1. **Task 1: tender-radar-api client + admin source-config settings form** - `4360bc0` (feat) 2. **Task 2: SourceConfigForm component test** - `8e3dfda` (test) **Plan metadata:** see final `docs(10-06)` commit. ## TDD Gate Compliance - Task 2 (`tdd="true"`): the test suite's first draft ran red on two of its four cases — not because the target behavior was unimplemented, but because a test-harness bug (mock call-history leaking across `it()` blocks, see Deviations) caused a later "not.toHaveBeenCalled()" assertion to see a prior test's captured call. This is a test-infrastructure fix (Rule 1), not evidence of a missing feature: Task 1's implementation already had the correct out-of-bounds guard. After adding `mockReset()` in `afterEach`, all four cases passed against the already-correct Task 1 implementation on the very next run. This plan's frontmatter is `type: execute` with a per-task `tdd="true"` flag (not a full `type: tdd` plan), matching the exact precedent documented in Plan 10-05's Task 3 — the strict "investigate a passing RED" rule applies to full-plan TDD gates, not this per-task flag usage. - No REFACTOR commit was needed beyond the inline mock-hygiene fix folded into the Task 2 test commit. ## Files Created/Modified - `apps/web/src/lib/tender-radar-api.ts` - `SourceConfig`/`SaveSourceConfigPayload` types, `fetchSourceConfig()`, `saveSourceConfig()` - `apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx` - settings route rendering `` - `apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.tsx` - interval input (5-1440 min) + isActive toggle + read-only sourceType/lastIngestedDay + save flow - `apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx` - 4 Vitest + Testing Library tests ## Decisions Made - **No next-intl** — hardcoded German strings in both new components, per plan instruction and matching the existing `tender-radar/page.tsx` placeholder's documented MVP-stub convention (full i18n is CONFIG-03, Phase 14). - **No module-loader.ts change** — `settings/page.tsx` is a standard nested App Router route under the already-whitelisted `tender-radar` slug; the whitelist gate only applies to the top-level module entry (Plan 10-02), not sub-routes. - **Read-only `sourceType`/`lastIngestedDay` display** — added beyond the plan's minimum ask (interval + toggle), per the plan's own "Optionally display..." suggestion, so an admin can see the day-cursor state without a separate API call. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Test mock call-history leaked across `it()` blocks in `SourceConfigForm.test.tsx`** - **Found during:** Task 2 (initial test run) - **Issue:** `afterEach` only called `vi.restoreAllMocks()`, which does not clear `vi.fn()` call history for the module-level `mockFetchSourceConfig`/`mockSaveSourceConfig` mocks (only restores spies to their original implementation). The "rejects an interval below 5" and "above 1440" tests' `expect(mockSaveSourceConfig).not.toHaveBeenCalled()` assertions saw the *previous* test's captured `saveSourceConfig(120, true)` call and failed. - **Fix:** Added `mockFetchSourceConfig.mockReset()` / `mockSaveSourceConfig.mockReset()` to `afterEach`, run before `vi.restoreAllMocks()`. - **Files modified:** `apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx` - **Verification:** All 4 tests pass on rerun; full web suite (111/111) still green. - **Committed in:** `8e3dfda` (Task 2 commit, caught and fixed before commit) --- **Total deviations:** 1 auto-fixed (1 Bug — test-infrastructure only, no production code change) **Impact on plan:** Zero scope creep; the fix is entirely within the new test file's own hygiene and does not touch `SourceConfigForm.tsx` or `tender-radar-api.ts`. ## Issues Encountered None beyond the one documented deviation above. Local Docker stack (per environment context) was not needed for this frontend-only plan — no live DÖE calls, no live API round-trip exercised (see coverage D3's `human_judgment: true` rationale). ## User Setup Required None - no external service configuration required. The local Docker stack was left running unmodified; rebuilding the web container to pick up these frontend changes (so `/modules/tender-radar/settings` is reachable in a live browser session) is left to the user per project convention. ## Next Phase Readiness - INGEST-06 is now fully complete on both halves: Plan 05 delivered the `@Roles`-guarded `GET`/`PUT /modules/tender-radar/source-config` endpoint with live scheduler apply, and this plan delivers the admin-facing form driving it — closing the plan-review gap that the requirement needed a UI, not only a REST surface. - This closes out Phase 10 (`ausschreibungs-radar-foundation-d-e-ingestion`) — all 6 plans complete. Phase 11 (saved searches/filter UI) can build on the `GET /modules/tender-radar` / `GET /modules/tender-radar/:id` read surface (Plan 05) and, if needed, extend this settings surface with additional per-search-profile config. - No blockers for Phase 11. Recommended follow-up (non-blocking): a quick live-browser UAT pass confirming the settings form round-trips against the real API once the local stack is rebuilt (coverage D3). ## Self-Check: PASSED - FOUND: apps/web/src/lib/tender-radar-api.ts - FOUND: apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx - FOUND: apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.tsx - FOUND: apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx - FOUND commit: 4360bc0 - FOUND commit: 8e3dfda --- *Phase: 10-ausschreibungs-radar-foundation-d-e-ingestion* *Completed: 2026-07-21*