6 plans (INGEST-02/03/07, SCHEMA-03) + Nyquist validation.
Core (registry, fingerprint, dedup, TenderSource, backfill) lands
first and is green independent of live scraping (D-01).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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).
@.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 `
` mit einem `` je Quelle (`target="_blank" rel="noopener noreferrer"` — Sicherheits-Muster, kein dangerouslySetInnerHTML, React escaped Text), Label ueber eine kleine `portalLabel(sourcePortal)`-Map (hardcodiertes Deutsch: z.B. 'doe-opendata'->'DÖE', 'tender24'->'tender24', 'cosinex-dtvp'->'DTVP'). Fallback: fehlt/leer sources[], bestehender Einzel-sourceUrl-Block. Erweitere `TenderDetail.test.tsx`: rendert mehrere Quell-Links bei sources[]; Fallback bei fehlendem sources.
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.
<threat_model>
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).
@UseModule('tender-radar'), Tender global, kein Tenant-Scoping-Wechsel.
</threat_model>
- `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.
<success_criteria>
Read-Endpoint liefert sources[]; Detailansicht rendert alle Quell-Links; Fallback fuer Alt-Tender. TrAegt Erfolgskriterium 3 (Anzeige aller Quell-Links).
</success_criteria>
Create `.planning/phases/13-scraping-adapters-cross-source-dedup/13-06-SUMMARY.md` when done