docs(14-04): complete denylist transparency plan

This commit is contained in:
2026-07-23 14:01:12 +02:00
parent 947325f6ad
commit a4889f8399
4 changed files with 105 additions and 14 deletions
+2 -2
View File
@@ -40,7 +40,7 @@
- [x] **UI-03**: Nutzer kann Treffer als gelesen/ungelesen markieren (pro Nutzer). - [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-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. - [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 ### NOTIFY — Benachrichtigung
@@ -104,6 +104,6 @@
| INGEST-05 | Phase 14 | Complete | | INGEST-05 | Phase 14 | Complete |
| CONFIG-02 | Phase 14 | Complete | | CONFIG-02 | Phase 14 | Complete |
| CONFIG-03 | Phase 14 | Pending | | CONFIG-03 | Phase 14 | Pending |
| UI-06 | Phase 14 | Pending | | UI-06 | Phase 14 | Complete |
**Coverage:** 29/29 v1.1 requirements mapped — no orphans. **Coverage:** 29/29 v1.1 requirements mapped — no orphans.
+4 -4
View File
@@ -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 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 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)* **Wave 1** *(parallel — disjoint files)*
@@ -473,11 +473,11 @@ Plans:
**Wave 2** *(blocked on 14-01 + 14-02)* **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)* **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)* **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| | | 11. Filter Engine, Results UI & Saved Searches | 6/6 | In Progress| |
| 12. Tender Notifications | 4/4 | In Progress| | | 12. Tender Notifications | 4/4 | In Progress| |
| 13. Scraping Adapters & Cross-Source Deduplication | 6/6 | 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| |
+11 -8
View File
@@ -5,15 +5,15 @@ milestone_name: Ausschreibungs-Radar
current_phase: 14 current_phase: 14
current_phase_name: rss-email-alert-ingestion-module-rollout current_phase_name: rss-email-alert-ingestion-module-rollout
status: executing status: executing
stopped_at: "Paused mid-plan: 14-03 Tasks 1-3 complete, Task 4 (human-verify, live EWS mailbox) OPEN" stopped_at: Completed 14-04-PLAN.md
last_updated: "2026-07-23T11:55:28.843Z" last_updated: "2026-07-23T12:01:07.009Z"
last_activity: 2026-07-23 last_activity: 2026-07-23
last_activity_desc: Completed 14-02-PLAN.md (RSS ingestion slice) last_activity_desc: Completed 14-02-PLAN.md (RSS ingestion slice)
progress: progress:
total_phases: 14 total_phases: 14
completed_phases: 12 completed_phases: 12
total_plans: 67 total_plans: 67
completed_plans: 64 completed_plans: 65
--- ---
# Project State # Project State
@@ -28,11 +28,11 @@ See: .planning/PROJECT.md (updated 2026-07-17)
## Current Position ## Current Position
Phase: 14 (rss-email-alert-ingestion-module-rollout) — EXECUTING Phase: 14 (rss-email-alert-ingestion-module-rollout) — EXECUTING
Plan: 3 of 5 Plan: 4 of 5
Status: Plan 14-02 complete Status: Plan 14-02 complete
Last activity: 2026-07-23 — Completed 14-02-PLAN.md (RSS ingestion slice) Last activity: 2026-07-23 — Completed 14-02-PLAN.md (RSS ingestion slice)
Progress: [█████████░] 94% Progress: [██████████] 97%
## Performance Metrics ## Performance Metrics
@@ -98,6 +98,7 @@ Progress: [█████████░] 94%
| Phase 13 P05 | 40min | 2 tasks | 4 files | | Phase 13 P05 | 40min | 2 tasks | 4 files |
| Phase 14 P01 | 13min | 2 tasks | 10 files | | Phase 14 P01 | 13min | 2 tasks | 10 files |
| Phase 14 P02 | 30min | 3 tasks | 21 files | | Phase 14 P02 | 30min | 3 tasks | 21 files |
| Phase 14 P04 | 25min | 2 tasks | 6 files |
## Accumulated Context ## 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-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: 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 ?]: 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 ### Pending Todos
@@ -257,7 +260,7 @@ Items acknowledged and carried forward from previous milestone close:
## Session Continuity ## Session Continuity
Last session: 2026-07-23T11:55:28.829Z Last session: 2026-07-23T12:01:06.995Z
Stopped at: Paused mid-plan: 14-03 Tasks 1-3 complete, Task 4 (human-verify, live EWS mailbox) OPEN Stopped at: Completed 14-04-PLAN.md
Resume file: .planning/phases/14-rss-email-alert-ingestion-module-rollout/14-03-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 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
@@ -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`