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-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.
+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
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| |
+11 -8
View File
@@ -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
@@ -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`