Files
tessera-ctl/.planning/phases/13-scraping-adapters-cross-source-dedup/13-06-PLAN.md
T
schalli 74c00166e2 docs(13): create phase plan — scraping adapters + cross-source dedup
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>
2026-07-23 08:24:16 +02:00

6.0 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
13-scraping-adapters-cross-source-dedup 06 execute 2
13-01
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
true
SCHEMA-03
truths artifacts key_links
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.
apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.tsx
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).

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.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).
    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.
    </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