feat: Fehler-melden + Datenherkunft auf Kontakte & Würfe erweitern

- Kontakt-Detailseite: „Fehler melden"- und „Datenherkunft"-Button.
- Wurf-Ansicht: „Datenherkunft"-Button (Feedback war bereits vorhanden).
- Feedback-Entity um loses, nullable ContactId erweitert (kein FK → übersteht
  Ingest-Wipe); Migration AddFeedbackContactId.
- Contact.Provenance + Litter.Provenance (nullable text); Migration
  AddContactLitterProvenance; im Ingest gemappt und in den DTOs zurückgegeben.
- Import: build_entity_provenance() generalisiert; Kontakte (sourceFiles,
  Züchter/Abnehmer-Hinweise) und Würfe (Wurfchronik vs. Diagramm-rekonstruiert,
  Geschwister-Merge) erhalten Herkunftsdaten in resolved_import.json.
- Frontend: ProvenanceDialog generalisiert (EntityProvenance + entityLabel).

Tests erweitert (Ingest-Round-trip Kontakt/Wurf, contact-scoped Feedback
übersteht Wipe). dotnet(212)/vitest(129)/playwright(36)/tsc/eslint grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 16:02:11 +02:00
parent 9c98c37d2a
commit 438c816820
27 changed files with 3474 additions and 34 deletions

View File

@@ -56,6 +56,24 @@ test('Wurf-Ansicht: "Fehler melden"-Button öffnet den Dialog und sendet mit Wur
expect(reports[0]).toMatchObject({ context: 'litter-detail', litterId: 'w-kruemel' })
})
test('Kontakt-Detail: "Fehler melden"-Button sendet mit Kontakt-Kontext', async ({ page, mockDb }) => {
skipUnlessMock()
await page.goto('/kontakte/con-meier')
await page.getByRole('button', { name: f.button }).click()
const dialog = page.getByRole('dialog', { name: f.dialogTitle })
await expect(dialog).toBeVisible()
await expect(dialog).toContainText(f.contexts['contact-detail'])
await dialog.getByLabel(f.label).fill('Die Adresse stimmt nicht.')
await dialog.getByRole('button', { name: f.submit }).click()
await expect(page.getByText(f.success)).toBeVisible()
const reports = mockDb!.feedback
expect(reports.length).toBe(1)
expect(reports[0]).toMatchObject({ context: 'contact-detail', contactId: 'con-meier' })
})
test('Stammbaum: Rechtsklick auf eine Karte zeigt "ID kopieren" + "Fehler melden"', async ({ page, mockDb }) => {
skipUnlessMock()
await page.goto('/rennmaeuse/kruemel/stammbaum')