74c00166e2
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>
106 lines
6.0 KiB
Markdown
106 lines
6.0 KiB
Markdown
---
|
|
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."
|
|
---
|
|
|
|
<objective>
|
|
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).
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: getTender liefert sources[] via include (Read-Surface)</name>
|
|
<files>apps/api/src/tenders/tenders.controller.ts, apps/api/src/tenders/tenders.controller.spec.ts</files>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api test -- tenders.controller</automated>
|
|
</verify>
|
|
<done>getTender include sources; Response enthaelt sources[]; Route-Order unveraendert; Spec gruen.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: API-Client-Typ + TenderDetail Multi-Source-Liste</name>
|
|
<files>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</files>
|
|
<action>
|
|
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 `<ul>` mit einem `<a>` 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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd apps/web && npx tsc --noEmit && pnpm --filter web test -- TenderDetail</automated>
|
|
</verify>
|
|
<done>TenderDetail zeigt Liste aller Quell-Links; Fallback bei fehlendem sources; tsc + component test gruen.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<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>
|
|
|
|
<verification>
|
|
- `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.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
Read-Endpoint liefert sources[]; Detailansicht rendert alle Quell-Links; Fallback fuer Alt-Tender. TrAegt Erfolgskriterium 3 (Anzeige aller Quell-Links).
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/13-scraping-adapters-cross-source-dedup/13-06-SUMMARY.md` when done
|
|
</output>
|