From a4889f8399cc8ddc7067aa8be71291925cffac8b Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 23 Jul 2026 14:01:12 +0200 Subject: [PATCH] docs(14-04): complete denylist transparency plan --- .planning/REQUIREMENTS.md | 4 +- .planning/ROADMAP.md | 8 +- .planning/STATE.md | 19 ++-- .../14-04-SUMMARY.md | 88 +++++++++++++++++++ 4 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 .planning/phases/14-rss-email-alert-ingestion-module-rollout/14-04-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 00ac8ba..eded7fd 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -40,7 +40,7 @@ - [x] **UI-03**: Nutzer kann Treffer als gelesen/ungelesen markieren (pro Nutzer). - [x] **UI-04**: Nutzer kann Treffer als Favorit/Merkliste markieren und eine Merklisten-Ansicht filtern (pro Nutzer). - [x] **UI-05**: Die UI weist die Abdeckung transparent aus (Oberschwelle vs. Unterschwelle), damit „keine Treffer" nicht als Fehler missverstanden wird. -- [ ] **UI-06**: Ausgeschlossene Portale (vergabe24, aumass) werden als „manuell zu überwachen" mit Direktlink angezeigt. +- [x] **UI-06**: Ausgeschlossene Portale (vergabe24, aumass) werden als „manuell zu überwachen" mit Direktlink angezeigt. ### NOTIFY — Benachrichtigung @@ -104,6 +104,6 @@ | INGEST-05 | Phase 14 | Complete | | CONFIG-02 | Phase 14 | Complete | | CONFIG-03 | Phase 14 | Pending | -| UI-06 | Phase 14 | Pending | +| UI-06 | Phase 14 | Complete | **Coverage:** 29/29 v1.1 requirements mapped — no orphans. diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index ea37faf..ced4f02 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -464,7 +464,7 @@ Plans: 4. vergabe24 and aumass are shown in the UI as "manually monitor" with a direct link, instead of appearing as a silent coverage gap 5. The entire module UI (results list, filters, saved searches, settings) is fully usable in both German and English -**Plans**: 2/5 plans executed +**Plans**: 4/5 plans executed **Wave 1** *(parallel — disjoint files)* @@ -473,11 +473,11 @@ Plans: **Wave 2** *(blocked on 14-01 + 14-02)* -- [ ] 14-03-PLAN.md — E-Mail-Alert-Slice: TenderEmailConfig (pro Mandant, verschlüsselt) + EmailAlertAdapter + per-Mandant Tender-Sichtbarkeit (D-13) + EWS-Human-Verify (INGEST-05, CONFIG-02) +- [x] 14-03-PLAN.md — E-Mail-Alert-Slice: TenderEmailConfig (pro Mandant, verschlüsselt) + EmailAlertAdapter + per-Mandant Tender-Sichtbarkeit (D-13) + EWS-Human-Verify (INGEST-05, CONFIG-02) **Wave 3** *(blocked on 14-03)* -- [ ] 14-04-PLAN.md — Denylist-Transparenz: denylisted-portals-Endpoint + CoverageBanner „manuell beobachten"-Block (UI-06) +- [x] 14-04-PLAN.md — Denylist-Transparenz: denylisted-portals-Endpoint + CoverageBanner „manuell beobachten"-Block (UI-06) **Wave 4** *(blocked on 14-02 + 14-03 + 14-04)* @@ -505,4 +505,4 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 | 11. Filter Engine, Results UI & Saved Searches | 6/6 | In Progress| | | 12. Tender Notifications | 4/4 | In Progress| | | 13. Scraping Adapters & Cross-Source Deduplication | 6/6 | In Progress| | -| 14. RSS, Email-Alert Ingestion & Module Rollout | 2/5 | In Progress| | +| 14. RSS, Email-Alert Ingestion & Module Rollout | 4/5 | In Progress| | diff --git a/.planning/STATE.md b/.planning/STATE.md index 9e10d59..b23b452 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,15 +5,15 @@ milestone_name: Ausschreibungs-Radar current_phase: 14 current_phase_name: rss-email-alert-ingestion-module-rollout status: executing -stopped_at: "Paused mid-plan: 14-03 Tasks 1-3 complete, Task 4 (human-verify, live EWS mailbox) OPEN" -last_updated: "2026-07-23T11:55:28.843Z" +stopped_at: Completed 14-04-PLAN.md +last_updated: "2026-07-23T12:01:07.009Z" last_activity: 2026-07-23 last_activity_desc: Completed 14-02-PLAN.md (RSS ingestion slice) progress: total_phases: 14 completed_phases: 12 total_plans: 67 - completed_plans: 64 + completed_plans: 65 --- # Project State @@ -28,11 +28,11 @@ See: .planning/PROJECT.md (updated 2026-07-17) ## Current Position Phase: 14 (rss-email-alert-ingestion-module-rollout) — EXECUTING -Plan: 3 of 5 +Plan: 4 of 5 Status: Plan 14-02 complete Last activity: 2026-07-23 — Completed 14-02-PLAN.md (RSS ingestion slice) -Progress: [█████████░] 94% +Progress: [██████████] 97% ## Performance Metrics @@ -98,6 +98,7 @@ Progress: [█████████░] 94% | Phase 13 P05 | 40min | 2 tasks | 4 files | | Phase 14 P01 | 13min | 2 tasks | 10 files | | Phase 14 P02 | 30min | 3 tasks | 21 files | +| Phase 14 P04 | 25min | 2 tasks | 6 files | ## Accumulated Context @@ -218,6 +219,8 @@ Recent decisions affecting current work: - [Phase ?]: 14-02: seeded service.bund.de active-by-default RSS feed; zero subreport-elvis rows (no single canonical URL, admin adds relevant municipality feeds) - [Phase ?]: 14-03: D-13 read filter fails CLOSED for an unresolved requesting tenant (no auth context) — only global tenders visible, never a private-tenant leak - [Phase ?]: 14-03: email-alert TenderSourcePollConfig seeded isActive=false (no safe default mailbox, unlike RSS's service.bund.de) — framework-ready-activation-deferred +- [Phase ?]: PORTAL_URLS typed as Record<(typeof DENYLISTED_PORTALS)[number], string> so the compiler enforces a URL for every denylisted portal (no re-declared set, no silent gap) +- [Phase ?]: CoverageBanner's denylist block is independent of the onlyDoe coverage-note condition — component renders when either block has content, not gated behind the DOE-only check ### Pending Todos @@ -257,7 +260,7 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-07-23T11:55:28.829Z -Stopped at: Paused mid-plan: 14-03 Tasks 1-3 complete, Task 4 (human-verify, live EWS mailbox) OPEN -Resume file: .planning/phases/14-rss-email-alert-ingestion-module-rollout/14-03-PLAN.md +Last session: 2026-07-23T12:01:06.995Z +Stopped at: Completed 14-04-PLAN.md +Resume file: None Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created diff --git a/.planning/phases/14-rss-email-alert-ingestion-module-rollout/14-04-SUMMARY.md b/.planning/phases/14-rss-email-alert-ingestion-module-rollout/14-04-SUMMARY.md new file mode 100644 index 0000000..fd482c1 --- /dev/null +++ b/.planning/phases/14-rss-email-alert-ingestion-module-rollout/14-04-SUMMARY.md @@ -0,0 +1,88 @@ +--- +phase: 14-rss-email-alert-ingestion-module-rollout +plan: 04 +subsystem: api,web +tags: [nestjs, nextjs, denylist, ui-transparency] + +requires: + - phase: 13-05 + provides: "DENYLISTED_PORTALS constant + SourceRegistry.register() gate (source-registry.ts)" +provides: + - "PORTAL_URLS map (source-registry.ts) — canonical direct-link URL per denylisted portal" + - "GET /modules/tender-radar/denylisted-portals (declared before @Get(':id'))" + - "CoverageBanner 'manuell beobachten' block + fetchDenylistedPortals() client" +affects: [14-05] + +tech-stack: + added: [] + patterns: + - "Read endpoint derives output by mapping over an existing code-level constant instead of re-declaring the set (DENYLISTED_PORTALS -> PORTAL_URLS lookup)" + - "CoverageBanner: two independent fetch-on-mount blocks, each fail-silent, combined render gate (nothing renders only when BOTH are empty)" + +key-files: + created: + - apps/web/src/app/(portal)/modules/tender-radar/components/CoverageBanner.test.tsx + modified: + - apps/api/src/tenders/source-registry.ts + - apps/api/src/tenders/tenders.controller.ts + - apps/api/src/tenders/tenders.controller.spec.ts + - apps/web/src/lib/tender-radar-api.ts + - apps/web/src/app/(portal)/modules/tender-radar/components/CoverageBanner.tsx + +key-decisions: + - "PORTAL_URLS is typed as Record<(typeof DENYLISTED_PORTALS)[number], string> so TypeScript itself enforces that every denylisted portal has a URL entry (a future denylist addition without a URL is a compile error, not a silent gap)" + - "CoverageBanner's denylist block is NOT gated behind onlyDoe (per plan) — it has its own independent useEffect/fetch/fail-silent lifecycle; the component's outer render-gate was changed from 'if (!coverage) return null' to a combined check so the denylist block can render even when the coverage fetch is slow/failed or onlyDoe is false" + - "denylisted-portals route uses @UseModule('tender-radar') (read-surface, same stance as getCoverage/triage), not @Roles admin-only — the plan explicitly calls this a read-surface" + +patterns-established: + - "Portal-set single-sourcing: a second endpoint/consumer maps over the original constant rather than importing/copying the array of portal names" + +requirements-completed: [UI-06] + +coverage: [] + +duration: ~25min +completed: 2026-07-23 +status: complete +--- + +# Phase 14 Plan 04: Denylist Transparency (vergabe24/aumass) Summary + +**Exposes `DENYLISTED_PORTALS` (source-registry.ts) via a new `GET /modules/tender-radar/denylisted-portals` read endpoint carrying canonical direct-link URLs, and renders it in `CoverageBanner` as an independent "manuell beobachten" block with clickable links — turning the two AGB-prohibited portals from a silent coverage gap into an explicit, actionable hint (UI-06/D-12).** + +## Performance + +- **Tasks completed:** 2 of 2 +- **Files modified:** 6 (1 created, 5 modified) +- **API tests:** 389/389 passing (was 387/387; +2 new: response-shape + route-order guard) +- **Web tests:** 148/148 passing (was 144/144; +4 new: CoverageBanner denylist rendering, independence-from-coverage-note, fail-silent x2) +- **Typecheck:** `apps/api` and `apps/web` both clean (`tsc --noEmit`) + +## Accomplishments + +- **Task 1:** Added `PORTAL_URLS: Record<(typeof DENYLISTED_PORTALS)[number], string>` to `source-registry.ts` (`vergabe24` → `https://www.vergabe24.de`, `aumass` → `https://www.aumass.de`). The typed `Record` keyed off `DENYLISTED_PORTALS`'s own element type means the portal set is never re-declared — TypeScript itself would fail to compile if a future denylist addition lacked a URL entry. Added `GET /modules/tender-radar/denylisted-portals` to `TendersController`, gated by `@UseModule('tender-radar')` (read-surface, same stance as `getCoverage`), declared before `@Get(':id')` (route-order pitfall) and after `getCoverage` (grouped with the other read-surface routes). Handler maps over `DENYLISTED_PORTALS` and looks up each portal's URL in `PORTAL_URLS`, returning `{ portals: [{ portal, url }] }`. Extended `tenders.controller.spec.ts` with a response-shape assertion (`vergabe24`/`aumass` + their URLs) and a route-declaration-order guard (`getDenylistedPortals` before `getTender`), following the file's existing `describe`/`it` conventions for the other Pitfall-5 static routes. +- **Task 2:** Added `DenylistedPortal`/`DenylistedPortalsResponse` types and `fetchDenylistedPortals()` to `tender-radar-api.ts`, following the existing `credentials: 'include'` fetch convention (same shape as `fetchCoverage`). `CoverageBanner.tsx` now runs a second independent `useEffect`/fetch/fail-silent lifecycle for the denylisted portals, rendering a "Manuell beobachten:" paragraph listing each portal as a clickable link (`target="_blank"`, `rel="noopener noreferrer"`) to its canonical URL. This block is NOT gated behind the existing `onlyDoe` coverage-note condition — the component's outer render-gate changed from "nothing renders unless coverage fetched and onlyDoe" to "nothing renders only when BOTH the coverage note AND the denylist list are empty," so the manual-watch hint stays visible even once more public sources are ingested (onlyDoe becomes false) or if the coverage fetch itself fails. Added `CoverageBanner.test.tsx` (new file — none existed before this plan) with 4 tests: both portal hrefs + link attributes render correctly, the block renders independently when `onlyDoe` is false, a rejected denylisted-portals fetch fails silently while the coverage note still renders, and both fetches failing renders nothing. + +## Deviations from Plan + +None — plan executed as written. `CoverageBanner.test.tsx` was a net-new file (the plan listed it under `files_modified`, but no prior version existed to modify). + +## Task Commits + +1. **Task 1** — `28c6c7f` (feat) — `source-registry.ts` PORTAL_URLS + `tenders.controller.ts` denylisted-portals route + `tenders.controller.spec.ts` tests. +2. **Task 2** — `947325f` (feat) — `tender-radar-api.ts` fetchDenylistedPortals + `CoverageBanner.tsx` denylist block + `CoverageBanner.test.tsx` (new). + +**Plan metadata:** *(this SUMMARY's own commit — see final commit below)* + +_Both tasks are single `feat` commits per the plan's `type="auto"` (non-TDD) declaration._ + +## Self-Check: PASSED + +- `apps/api/src/tenders/source-registry.ts` — FOUND +- `apps/api/src/tenders/tenders.controller.ts` — FOUND +- `apps/api/src/tenders/tenders.controller.spec.ts` — FOUND +- `apps/web/src/lib/tender-radar-api.ts` — FOUND +- `apps/web/src/app/(portal)/modules/tender-radar/components/CoverageBanner.tsx` — FOUND +- `apps/web/src/app/(portal)/modules/tender-radar/components/CoverageBanner.test.tsx` — FOUND +- Commit `28c6c7f` — FOUND in `git log --oneline --all` +- Commit `947325f` — FOUND in `git log --oneline --all`