Files
tessera-ctl/.planning/phases/14-rss-email-alert-ingestion-module-rollout/14-04-SUMMARY.md
T

6.7 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
14-rss-email-alert-ingestion-module-rollout 04 api,web
nestjs
nextjs
denylist
ui-transparency
phase provides
13-05 DENYLISTED_PORTALS constant + SourceRegistry.register() gate (source-registry.ts)
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
14-05
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)
created modified
apps/web/src/app/(portal)/modules/tender-radar/components/CoverageBanner.test.tsx
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
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
Portal-set single-sourcing: a second endpoint/consumer maps over the original constant rather than importing/copying the array of portal names
UI-06
~25min 2026-07-23 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