docs(10): add admin config UI plan (INGEST-06 gap), resolve research open questions
This commit is contained in:
@@ -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)
|
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
|
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**
|
**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)
|
- [ ] 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
|
### 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 |
|
| 7. DKV Fleet Module | 6/6 | Complete | 2026-06-27 |
|
||||||
| 8. Dashboard Widgets Vollimplementierung | 4/4 | Complete | 2026-07-01 |
|
| 8. Dashboard Widgets Vollimplementierung | 4/4 | Complete | 2026-07-01 |
|
||||||
| 9. Cert Manager Module | 6/6 | Complete | 2026-07-02 |
|
| 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 | - |
|
| 11. Filter Engine, Results UI & Saved Searches | 0/TBD | Not started | - |
|
||||||
| 12. Tender Notifications | 0/TBD | Not started | - |
|
| 12. Tender Notifications | 0/TBD | Not started | - |
|
||||||
| 13. Scraping Adapters & Cross-Source Deduplication | 0/TBD | Not started | - |
|
| 13. Scraping Adapters & Cross-Source Deduplication | 0/TBD | Not started | - |
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 |
|
| 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 |
|
| 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)**
|
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.
|
- 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.
|
||||||
|
|||||||
@@ -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-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-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-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*
|
*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/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/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/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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user