---
phase: 13-scraping-adapters-cross-source-dedup
plan: 06
type: execute
wave: 2
depends_on: ["13-01"]
files_modified:
- 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/TenderDetail.tsx
- apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.test.tsx
autonomous: true
requirements: [SCHEMA-03]
must_haves:
truths:
- "GET /modules/tender-radar/:id liefert tender.sources[] (sourcePortal, sourceUrl, sourceNoticeId) via Prisma include mit."
- "TenderDetail rendert die Liste ALLER Quell-Links eines deduplizierten Tenders (Erfolgskriterium 3, Anzeigeseite)."
- "Fallback: fehlt sources[], wird auf den bestehenden Einzel-sourceUrl-Block zurueckgefallen."
artifacts:
- apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.tsx
key_links:
- "getTender include: { sources } -> API-Client-Typ Tender.sources[] -> TenderDetail Multi-Source-Liste."
---
Macht die Multi-Source-Deduplizierung sichtbar (D-03, Erfolgskriterium 3 Anzeigeteil): der Read-Endpoint `getTender` liefert die `TenderSource`-Liste via Prisma-`include` mit, und `TenderDetail` rendert alle Quell-Portale mit Links statt eines einzelnen `sourceUrl`. Hardcodiertes Deutsch (i18n = Phase 14, NICHT hier).
Purpose: Ein deduplizierter Tender erscheint EINMAL in der Trefferliste und zeigt in der Detailansicht Links zu allen seinen Quell-Portalen.
Output: getTender mit sources-include (+ spec), API-Client-Typ + TenderDetail-Liste (+ component test).
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/13-scraping-adapters-cross-source-dedup/13-CONTEXT.md
@.planning/phases/13-scraping-adapters-cross-source-dedup/13-RESEARCH.md
@apps/api/src/tenders/tenders.controller.ts
@apps/web/src/lib/tender-radar-api.ts
@apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.tsx
Task 1: getTender liefert sources[] via include (Read-Surface)
apps/api/src/tenders/tenders.controller.ts, apps/api/src/tenders/tenders.controller.spec.ts
Erweitere `getTender` (aktuell `prisma.tender.findUnique({ where: { id } })`) um `include: { sources: { select: { sourcePortal: true, sourceUrl: true, sourceNoticeId: true } } }` (13-RESEARCH.md "Read-Endpoint mit Quell-Links"). Gating bleibt `@UseModule('tender-radar')`, global (kein Tenant-Scoping, T-10-13). ACHTUNG Route-Order-Pitfall (MEMORY / bestehender Kommentar): der `@Get(':id')` bleibt NACH allen statischen Routen — nichts an der Reihenfolge aendern, nur den findUnique-Aufruf. Erweitere `tenders.controller.spec.ts`: getTender liefert das sources-Array mit (gemockter Prisma-Return mit sources[]); NotFound-Fall bleibt.
pnpm --filter @tessera/api test -- tenders.controller
getTender include sources; Response enthaelt sources[]; Route-Order unveraendert; Spec gruen.
Task 2: API-Client-Typ + TenderDetail Multi-Source-Liste
apps/web/src/lib/tender-radar-api.ts, apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.tsx, apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.test.tsx
Ergaenze im API-Client-Typ (`tender-radar-api.ts`) das `Tender` um `sources?: { sourcePortal: string; sourceUrl: string | null; sourceNoticeId: string }[]`. In `TenderDetail.tsx` ersetze den einzelnen sourceUrl-Block durch eine Liste (13-RESEARCH.md "TenderDetail: Multi-Source-Liste"): wenn `tender.sources?.length`, rendere `
cd apps/web && npx tsc --noEmit && pnpm --filter web test -- TenderDetail
TenderDetail zeigt Liste aller Quell-Links; Fallback bei fehlendem sources; tsc + component test gruen.
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Portal-HTML (Titel/Buyer/URL) -> Frontend | Extern gescrapte Strings landen in der Detailansicht (untrusted). |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-13-06-01 | Tampering (Stored XSS) | Portal-Strings in TenderDetail | medium | mitigate | React rendert als Text (default-escaped), kein dangerouslySetInnerHTML (V5). |
| T-13-06-02 | Tampering | externer Quell-Link | low | mitigate | rel="noopener noreferrer" + target="_blank" (bestehendes Muster). |
| T-13-06-03 | Info Disclosure | Tenant-Leak ueber Read-Endpoint | low | mitigate | @UseModule('tender-radar'), Tender global, kein Tenant-Scoping-Wechsel. |
- `pnpm --filter @tessera/api test -- tenders.controller` gruen (sources im Response).
- `cd apps/web && npx tsc --noEmit && pnpm --filter web test -- TenderDetail` gruen.
- Manuell (13-VALIDATION.md): Detailansicht eines Tenders mit >1 TenderSource zeigt mehrere Links.
Read-Endpoint liefert sources[]; Detailansicht rendert alle Quell-Links; Fallback fuer Alt-Tender. TrAegt Erfolgskriterium 3 (Anzeige aller Quell-Links).