docs(14-04): complete denylist transparency plan
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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`
|
||||
Reference in New Issue
Block a user