From b52a0a796b7d28ed7e12613b535132f0f8a29b6a Mon Sep 17 00:00:00 2001 From: Schalli Date: Tue, 21 Jul 2026 10:08:40 +0200 Subject: [PATCH] docs(10): add admin config UI plan (INGEST-06 gap), resolve research open questions --- .planning/ROADMAP.md | 10 +- .../10-06-PLAN.md | 137 ++++++++++++++++++ .../10-RESEARCH.md | 5 +- .../10-VALIDATION.md | 5 +- 4 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 .planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-06-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 08d8e78..bf9c0f5 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -344,7 +344,7 @@ Plans: 4. DÖE is polled once on a shared, admin-configurable interval regardless of how many tenants have the module active -- never once per tenant (poll-once-fan-out-many, not the DKV single-tenant `findFirst()` pattern) 5. Activating the module for a second tenant does not duplicate ingestion, re-trigger a redundant DÖE poll, or interfere with the first tenant's data -**Plans**: 5 plans +**Plans**: 6 plans **Wave 1** @@ -366,7 +366,11 @@ Plans: - [ ] 10-05-PLAN.md — Controller + admin source-config: global ModuleGuard-gated read, admin-configurable poll interval applied live to scheduler (INGEST-06) -**UI hint**: no (backend-foundation phase; results UI is Phase 11) +**Wave 6** *(blocked on Wave 5 completion)* + +- [ ] 10-06-PLAN.md — Admin config UI: tender-radar-api client + settings page SourceConfigForm reading/writing GET/PUT source-config, mirroring DKV InboxConfigForm (INGEST-06 admin-UI) + +**UI hint**: yes (admin source-config settings form; results UI is Phase 11) ### Phase 11: Filter Engine, Results UI & Saved Searches @@ -449,7 +453,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 | 7. DKV Fleet Module | 6/6 | Complete | 2026-06-27 | | 8. Dashboard Widgets Vollimplementierung | 4/4 | Complete | 2026-07-01 | | 9. Cert Manager Module | 6/6 | Complete | 2026-07-02 | -| 10. Ausschreibungs-Radar Foundation & DÖE Ingestion | 0/5 | Not started | - | +| 10. Ausschreibungs-Radar Foundation & DÖE Ingestion | 0/6 | Not started | - | | 11. Filter Engine, Results UI & Saved Searches | 0/TBD | Not started | - | | 12. Tender Notifications | 0/TBD | Not started | - | | 13. Scraping Adapters & Cross-Source Deduplication | 0/TBD | Not started | - | diff --git a/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-06-PLAN.md b/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-06-PLAN.md new file mode 100644 index 0000000..b99bfdc --- /dev/null +++ b/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-06-PLAN.md @@ -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" +--- + + +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. + diff --git a/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-RESEARCH.md b/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-RESEARCH.md index b8db88a..87404eb 100644 --- a/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-RESEARCH.md +++ b/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-RESEARCH.md @@ -321,7 +321,10 @@ function isOpenTenderNotice(release: { tag?: string[]; awards?: unknown[]; contr | A2 | Fetching both `eforms.zip` and `ocds.zip` per poll cycle (two HTTP calls) is acceptable overhead vs. a single-format MVP | Pattern 3 | Low — at day-granularity polling (at most 1-2 real fetches/day per the day-cursor gate), two requests instead of one is negligible; if the planner descopes to single-format for MVP speed, document that deadline/value coverage will be measurably worse (per Pattern 4's 100%-null spot check) as an explicit MVP trade-off, not a silent gap | | A3 | The 105-notice, 12-notice, and 11-notice samples (all from 2026-07-20) are representative of typical daily volume/tag distribution, not an anomalous day | Pattern 2, Pattern 4 | Medium — a single day's sample is directional, not statistically proven; if actual production volume/tag ratios differ significantly, the D-02 filter logic itself is still correct (tag-based), only the *magnitude* estimates (70% tender / 16% award / etc.) may shift | -## Open Questions +## Open Questions (RESOLVED) + +> **Q1 (RESOLVED):** rate-limit tolerance is mitigated, not left open — Plan 04 (`TenderIngestionService`) inserts a ~1-2s polite delay between successive catch-up day-fetches, so the catch-up loop cannot rapid-fire the DÖE host even after extended downtime. +> **Q2 (RESOLVED):** eForms-vs-OCDS deadline-coverage rate is addressed by the coverage-metric logging recommendation planned into Plan 04 (log `% of ingested tender-tagged notices with non-null deadlineAt`), so the aggregate rate is measured empirically in production instead of re-guessed. 1. **Exact rate-limit tolerance of the DÖE API (undocumented)** - What we know: no `X-RateLimit-*` headers observed on a real response; a `cache-control: max-age=120` + `etag` suggest a caching/CDN layer that can absorb repeated identical requests cheaply. diff --git a/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-VALIDATION.md b/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-VALIDATION.md index af70e6c..ff56b11 100644 --- a/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-VALIDATION.md +++ b/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-VALIDATION.md @@ -53,6 +53,8 @@ created: 2026-07-21 | 10-05-01 | 05 | 5 | INGEST-06 | T-10-15 | DTO bounds (interval floor, limit cap) | build | `tsc --noEmit` | ❌ W0 | ⬜ pending | | 10-05-02 | 05 | 5 | INGEST-06 | T-10-13/14 | ModuleGuard read (not tenant-scoped) + admin Roles | build | `tsc --noEmit`; `grep -c "UseModule" tenders.controller.ts` | ❌ W0 | ⬜ pending | | 10-05-03 | 05 | 5 | INGEST-06 | T-10-14 | read where has no tenantId; save drives scheduler | unit | `pnpm --filter @tessera/api test -- tenders.controller` | ❌ W0 | ⬜ pending | +| 10-06-01 | 06 | 6 | INGEST-06 | T-10-16/17 | admin form clamps interval 5..1440; server Roles-guarded | build | web `tsc --noEmit`; `grep -c "saveSourceConfig" tender-radar-api.ts` | ❌ W0 | ⬜ pending | +| 10-06-02 | 06 | 6 | INGEST-06 | T-10-16 | fetch-on-mount + save payload; out-of-bounds no save | unit | `pnpm --filter @tessera/web test -- SourceConfigForm` | ❌ W0 | ⬜ pending | *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* @@ -65,8 +67,9 @@ created: 2026-07-21 - [ ] `apps/api/src/tenders/adapters/doe-opendata.adapter.spec.ts`, `tender-normalizer.service.spec.ts` (Plan 03 Task 1 — RED first) - [ ] `apps/api/src/tenders/tender-ingestion.service.spec.ts`, `tender-scheduler.service.spec.ts` (Plan 04) - [ ] `apps/api/src/tenders/tenders.controller.spec.ts` (Plan 05 Task 3) +- [ ] `apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx` (Plan 06 Task 2) -Vitest framework already exists — no framework install needed. +Vitest framework already exists (api + web) — no framework install needed. ---