From 45b8533f1813c5785fd858c56c5db4de729b8688 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sun, 19 Jul 2026 09:19:11 +0200 Subject: [PATCH] feat(deploy): TrueNAS Custom-App + Auto-Deploy, plus aufgelaufene Arbeit Deployment: - custom-app.compose.yaml: self-contained Compose fuer TrueNAS "Custom App" (absolute Host-Bind-Pfade, postgres:18, pull_policy always, Port 8090) - scripts/truenas-deploy.sh: Host-Skript create/redeploy via midclt (App bleibt unter Apps sichtbar) inkl. Image-Pull + Health-Check - ci.yml Deploy-Job: laeuft auf ubuntu-latest-Runner, kopiert Deploy-Dateien per SSH auf den NAS-Host und triggert truenas-deploy.sh (statt runs-on goldeye) - compose.yaml/.env.example: postgres:18 (Locale-Match zur Quell-DB), Port 8090 - .gitignore: .agents/, tools/rag/, deploy/truenas/.env (Secrets/Scratch) Aufgelaufene Feature-Arbeit (verified/Freeze, Migrationen, Import-Triage): - GerbilOverride/VerifiedGerbil-Endpoints + GerbilSnapshotService + Tests - EF-Migrationen (ShowInChronicle, Stillborn, BirthOrder, ManualFlag, DSGVO) - Frontend VerifizierteTierePage + verified-API + e2e-Spec - diverse Import-/Triage-Skripte und -Tests Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/ci.yml | 49 +- .gitignore | 27 +- CLAUDE.md | 94 +- GerbilManager.AppHost/AppHost.cs | 18 + GerbilManager.Tests/Rpro3ImportTests.cs | 7 +- GerbilManager.Tests/VerifiedGerbilTests.cs | 339 +++ GerbilManager.Tests/VerifiedGoldenTests.cs | 151 ++ GerbilManagerWebAPI/ApplicationContext.cs | 24 +- GerbilManagerWebAPI/Dockerfile | 5 + GerbilManagerWebAPI/Dtos/ApiDtos.cs | 36 +- .../Endpoints/ContactEndpoints.cs | 2 +- .../Endpoints/GerbilEndpoints.cs | 52 +- .../Endpoints/LitterEndpoints.cs | 11 +- .../Endpoints/VerifiedGerbilEndpoints.cs | 136 ++ .../Import/GerbilSnapshotService.cs | 307 +++ .../Import/IngestResolvedService.cs | 342 +-- .../Import/Rpro3/Rpro3Decisions.cs | 5 + .../Import/Rpro3/Rpro3ImportService.cs | 1 + .../Import/Rpro3/rpro3-decisions.json | 1091 +++++++++- ...71249_AddLitterShowInChronicle.Designer.cs | 1861 ++++++++++++++++ ...20260624071249_AddLitterShowInChronicle.cs | 32 + ...24082334_UpdateLegalPagesDsgvo.Designer.cs | 1861 ++++++++++++++++ .../20260624082334_UpdateLegalPagesDsgvo.cs | 47 + ...84747_UpdateImpressumHobbyMStV.Designer.cs | 1861 ++++++++++++++++ ...20260624084747_UpdateImpressumHobbyMStV.cs | 33 + ...AddVerifiedGerbilAndManualFlag.Designer.cs | 1921 ++++++++++++++++ ...12142631_AddVerifiedGerbilAndManualFlag.cs | 82 + ...60713173625_AddLitterStillborn.Designer.cs | 1924 ++++++++++++++++ .../20260713173625_AddLitterStillborn.cs | 28 + ...0714055648_AddGerbilBirthOrder.Designer.cs | 1927 +++++++++++++++++ .../20260714055648_AddGerbilBirthOrder.cs | 28 + .../ApplicationContextModelSnapshot.cs | 73 +- GerbilManagerWebAPI/Models/Contact.cs | 5 + GerbilManagerWebAPI/Models/Gerbil.cs | 10 + GerbilManagerWebAPI/Models/GerbilOverride.cs | 62 + GerbilManagerWebAPI/Models/Litter.cs | 17 + GerbilManagerWebAPI/Program.cs | 1 + deploy/truenas/.env.example | 5 +- deploy/truenas/compose.yaml | 10 +- deploy/truenas/custom-app.compose.yaml | 97 + deploy/truenas/nginx/publicsite.conf | 19 +- deploy/truenas/scripts/deploy.sh | 91 + deploy/truenas/scripts/truenas-deploy.sh | 103 + deploy/truenas/vhost-snippet.conf | 76 +- docs/active-triage-summary.md | 615 ++++++ docs/ticket-triage.md | 470 +++- gerbil-manager-web/e2e/mock-api.ts | 38 + gerbil-manager-web/e2e/mock-data.ts | 25 +- gerbil-manager-web/e2e/verified.spec.ts | 36 + gerbil-manager-web/src/App.tsx | 3 + gerbil-manager-web/src/api/types.ts | 48 + gerbil-manager-web/src/api/verified.ts | 37 + gerbil-manager-web/src/format/gerbilName.ts | 5 +- gerbil-manager-web/src/genetics/catalog.ts | 15 + gerbil-manager-web/src/genetics/index.ts | 1 + .../src/pages/BeckenDetailPage.tsx | 3 +- .../src/pages/GerbilDetailPage.tsx | 253 ++- gerbil-manager-web/src/pages/GerbilsPage.tsx | Bin 15281 -> 15312 bytes gerbil-manager-web/src/pages/HilfePage.tsx | 6 + .../src/pages/KontaktDetailPage.tsx | 3 +- .../src/pages/StammbaumPage.tsx | 4 +- .../src/pages/StatistikPage.tsx | 4 +- .../src/pages/VerifizierteTierePage.tsx | 115 + .../src/pages/WuerfeListPage.tsx | 19 +- .../src/pages/WurfDetailPage.tsx | 39 +- gerbil-manager-web/src/pages/hilfe.css | 18 + gerbil-manager-web/src/pages/rennmausakte.css | 2 + gerbil-manager-web/src/strings/de.ts | 110 +- tools/import/ask_breeder_krumel.py | 24 + tools/import/conflict-decisions.json | 1492 ++++++++++++- tools/import/extract.py | 16 +- tools/import/extract_docx.py | 29 + tools/import/merge_and_resolve.py | 911 +++++++- tools/import/output/review-report.md | 21 +- tools/import/remaining.md | 627 ++++++ tools/import/research_tickets.py | 68 + tools/import/resolve_blue_ticket.py | 26 + tools/import/resolve_gender_filter_ticket.py | 26 + tools/import/resolve_missing_alvin_ticket.py | 26 + tools/import/resolve_sort_ticket.py | 26 + tools/import/resolve_ticket_bali.py | 25 + tools/import/resolve_ticket_lotta.py | 25 + tools/import/resolve_tickets_23.py | 34 + tools/import/resolve_tickets_ancestors.py | 42 + tools/import/resolve_tickets_jackjack.py | 34 + tools/import/resolve_tickets_not_my_litter.py | 34 + tools/import/resolve_tickets_taro.py | 34 + tools/import/rpro3_lookup.py | 13 +- tools/import/rpro3_mxp.py | 139 ++ tools/import/rpro3_pedigree.py | 82 + tools/import/test_extract.py | 20 + tools/import/test_merge_resolve.py | 265 ++- tools/triage_active_tickets.py | 130 ++ 93 files changed, 20477 insertions(+), 432 deletions(-) create mode 100644 GerbilManager.Tests/VerifiedGerbilTests.cs create mode 100644 GerbilManager.Tests/VerifiedGoldenTests.cs create mode 100644 GerbilManagerWebAPI/Endpoints/VerifiedGerbilEndpoints.cs create mode 100644 GerbilManagerWebAPI/Import/GerbilSnapshotService.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260624071249_AddLitterShowInChronicle.Designer.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260624071249_AddLitterShowInChronicle.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260624082334_UpdateLegalPagesDsgvo.Designer.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260624082334_UpdateLegalPagesDsgvo.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260624084747_UpdateImpressumHobbyMStV.Designer.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260624084747_UpdateImpressumHobbyMStV.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260712142631_AddVerifiedGerbilAndManualFlag.Designer.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260712142631_AddVerifiedGerbilAndManualFlag.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260713173625_AddLitterStillborn.Designer.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260713173625_AddLitterStillborn.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260714055648_AddGerbilBirthOrder.Designer.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260714055648_AddGerbilBirthOrder.cs create mode 100644 GerbilManagerWebAPI/Models/GerbilOverride.cs create mode 100644 deploy/truenas/custom-app.compose.yaml create mode 100644 deploy/truenas/scripts/deploy.sh create mode 100644 deploy/truenas/scripts/truenas-deploy.sh create mode 100644 docs/active-triage-summary.md create mode 100644 gerbil-manager-web/e2e/verified.spec.ts create mode 100644 gerbil-manager-web/src/api/verified.ts create mode 100644 gerbil-manager-web/src/pages/VerifizierteTierePage.tsx create mode 100644 tools/import/ask_breeder_krumel.py create mode 100644 tools/import/remaining.md create mode 100644 tools/import/research_tickets.py create mode 100644 tools/import/resolve_blue_ticket.py create mode 100644 tools/import/resolve_gender_filter_ticket.py create mode 100644 tools/import/resolve_missing_alvin_ticket.py create mode 100644 tools/import/resolve_sort_ticket.py create mode 100644 tools/import/resolve_ticket_bali.py create mode 100644 tools/import/resolve_ticket_lotta.py create mode 100644 tools/import/resolve_tickets_23.py create mode 100644 tools/import/resolve_tickets_ancestors.py create mode 100644 tools/import/resolve_tickets_jackjack.py create mode 100644 tools/import/resolve_tickets_not_my_litter.py create mode 100644 tools/import/resolve_tickets_taro.py create mode 100644 tools/import/rpro3_mxp.py create mode 100644 tools/import/rpro3_pedigree.py create mode 100644 tools/triage_active_tickets.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index dc62e87..2c84991 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -23,6 +23,10 @@ on: pull_request: branches: - main + # Manueller „Run workflow"-Button in Gitea: testet, baut, pusht und deployt frisch. + # Fuer einen Rollback auf ein altes Image stattdessen auf der NAS: + # TAG= sh /opt/gerbilmanager/deploy/truenas/scripts/deploy.sh + workflow_dispatch: env: REGISTRY: git.rismer.de @@ -100,7 +104,7 @@ jobs: name: Docker Build & Push runs-on: ubuntu-latest needs: [test-backend, test-frontend] - if: github.ref == 'refs/heads/main' && github.event_name == 'push' + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') steps: - uses: actions/checkout@v4 @@ -133,3 +137,46 @@ jobs: tags: | ${{ env.REGISTRY }}/${{ env.REGISTRY_OWNER }}/gerbilmanager-frontend:latest ${{ env.REGISTRY }}/${{ env.REGISTRY_OWNER }}/gerbilmanager-frontend:${{ github.sha }} + + deploy: + name: Deploy auf TrueNAS (Custom App) + # Laeuft auf dem vorhandenen containerisierten Gitea-Runner (Label ubuntu-latest). + # Der Runner hat KEINEN Host-Dateisystem-/midclt-Zugriff, daher: per SSH zum NAS-Host + # verbinden, die Deploy-Dateien nach /opt/gerbilmanager kopieren und dort + # truenas-deploy.sh ausfuehren (legt die TrueNAS "Custom App" an bzw. rollt sie neu + # aus via `midclt` — die App bleibt unter Apps sichtbar). + # + # Benoetigte Repo-Secrets (Gitea -> Einstellungen -> Actions -> Secrets): + # NAS_SSH_HOST z. B. 192.168.2.115 (Host-LAN-IP der NAS) + # NAS_SSH_KEY privater SSH-Deploy-Key (Pubkey liegt in /root/.ssh/authorized_keys) + runs-on: ubuntu-latest + needs: [build-and-push] + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + steps: + - uses: actions/checkout@v4 + + - name: SSH-Client + Key einrichten + run: | + command -v ssh >/dev/null 2>&1 || (apt-get update && apt-get install -y --no-install-recommends openssh-client rsync) + install -m 700 -d ~/.ssh + printf '%s\n' "${{ secrets.NAS_SSH_KEY }}" > ~/.ssh/id_deploy + chmod 600 ~/.ssh/id_deploy + ssh-keyscan -H "${{ secrets.NAS_SSH_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || true + + - name: Deploy-Dateien auf den NAS-Host kopieren + run: | + NAS="root@${{ secrets.NAS_SSH_HOST }}" + SSH="ssh -i ~/.ssh/id_deploy -o StrictHostKeyChecking=accept-new" + $SSH "$NAS" 'mkdir -p /opt/gerbilmanager/deploy/truenas/scripts' + rsync -az -e "$SSH" \ + deploy/truenas/custom-app.compose.yaml \ + "$NAS":/opt/gerbilmanager/deploy/truenas/ + rsync -az -e "$SSH" \ + deploy/truenas/scripts/ \ + "$NAS":/opt/gerbilmanager/deploy/truenas/scripts/ + + - name: Custom App neu ausrollen (midclt create/redeploy + Health-Check) + run: | + NAS="root@${{ secrets.NAS_SSH_HOST }}" + ssh -i ~/.ssh/id_deploy -o StrictHostKeyChecking=accept-new "$NAS" \ + "sh /opt/gerbilmanager/deploy/truenas/scripts/truenas-deploy.sh" diff --git a/.gitignore b/.gitignore index 9fbf6f5..fba693f 100644 --- a/.gitignore +++ b/.gitignore @@ -140,9 +140,24 @@ GerbilManagerWebAPI/.data-protection-keys/ # node_modules — never commit, installed locally per npm install node_modules/ - -# Runtime: gespeicherte Vertrags-Dateien -GerbilManagerWebAPI/contract-storage/ - -# Agent-Worktrees (lokal, nicht versionieren) -.claude/worktrees/ + +# Runtime: gespeicherte Vertrags-Dateien +GerbilManagerWebAPI/contract-storage/ + +# Agent-Worktrees (lokal, nicht versionieren) +.claude/worktrees/ + +# Lokaler Publish-Output der öffentlichen Seite (Aspire dev) +deploy/.local-publicsite/ + +# RennmausPro III SQLite-Quelle (lokal, nicht committen) +tools/import/_rpro3.db +tools/import/_rpro3_extract/ +tools/import/_rpro3_backup.zip + +# Lokale Scratch-/Werkzeug-Verzeichnisse (nicht versionieren) +.agents/ +tools/rag/ + +# Produktions-Secrets (nur auf dem NAS, nie im Repo) +deploy/truenas/.env diff --git a/CLAUDE.md b/CLAUDE.md index bb011ef..6142970 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,20 @@ dotnet run --project GerbilManager.AppHost Die DB wird **nicht** direkt von Python beschrieben. Nach jeder Import-Code-Änderung neu generieren UND neu einspielen, sonst zeigt die App den alten Stand. +> **Arbeitsweise (verbindlich, Owner-Regel — aktualisiert 2026-07-12):** Für **variable** +> (von der Züchterin **nicht** angefasste) Import-Tiere gilt weiter: Datenfehler IMMER **im +> Importierer** beheben (Pipeline-Code + `conflict-decisions.json` / `rpro3-decisions.json`) +> und **neu importieren** — für diese Tiere **nicht** die Live-DB patchen. Jeder Importer-Fix +> muss **re-ingest-stabil** sein (data-driven, deterministische GUIDs). +> +> **Neu:** Das Ziel ist **nicht** mehr „Importer perfekt, dann einmal final", sondern +> **Tier für Tier konvergieren**. Die Züchterin **darf** über die UI kuratieren, und das +> **überlebt den Ingest** (der wischt **nicht** mehr alles): (a) **manuell angelegte Tiere/ +> Würfe/Kontakte** (`IsManual=true`) und (b) **Overrides** (`GerbilOverride`) für als +> **„vollständig korrekt"** markierte bzw. **von Hand bearbeitete** Tiere. Deren eigene Felder +> werden nach jedem Ingest **eingefroren** (Freeze). Siehe „Geprüfte Tiere / Freeze" unten. +> Zusätzlich zu diesen Overrides überleben weiterhin **Tickets/Feedback**. + ```bash # 1. Stammbäume + Wurfchronik → animals.json/litters.json (+ Fotos) python tools/import/extract.py @@ -44,16 +58,22 @@ python tools/import/extract.py python tools/import/extract_contracts.py # 3. Dedup, Eltern-Resolver, Provenance, Vertrags-Anreicherung → resolved_import.json python tools/import/merge_and_resolve.py -# 4. In die laufende DB laden (API muss laufen): WISCHT + lädt neu +# 4. In die laufende DB laden (API muss laufen): UPSERT + Freeze curl -X POST http://localhost:5179/import/ingest-resolved ``` -- **`IngestResolvedService` wischt** Gerbils/Litters/Contacts/Photos/Health/Weights/ - **SaleContracts** und lädt aus `resolved_import.json` neu. Importierte SaleContracts - sind Teil des Payloads → werden bei jedem Ingest neu erzeugt (idempotent, IDs deterministisch). -- **Feedback überlebt den Ingest** (wird nicht gewischt) — daher hat `Feedback` nur - **lose, nullable Guid-Spalten ohne FK** (GerbilId/LitterId/ContactId). Dieses Muster - für alles nutzen, was Re-Ingests überleben soll. +- **`IngestResolvedService` (seit 2026-07-12: UPSERT statt Total-Wipe).** Importierte Zeilen + (Gerbils/Litters/Contacts) werden **per deterministischer Id gematcht und in place + aktualisiert**; **stale** importierte Zeilen (nicht mehr im Payload) werden entfernt (dangling + Referenzen manueller Zeilen werden vorher genullt + im Rückgabestring gewarnt). **Nie gelöscht/ + überschrieben:** Zeilen mit `IsManual=true` sowie **user-eingegebene Sub-Records** + (Weights/Health) und Fotos manueller Tiere. **SaleContracts** werden weiter aus dem Payload + neu erzeugt (manuelle Verträge sind noch **nicht** geschützt — Folgeaufgabe). Am Ende werden + alle **`GerbilOverride`** angewandt (**Freeze** der eigenen Felder) und für verifizierte Tiere + der Rohimport-Stand vorher als Drift-Snapshot erfasst. +- **Feedback + GerbilOverride überleben den Ingest** (werden nicht gewischt) — daher haben sie nur + **lose, nullable Guid-Spalten ohne FK** (`Feedback`: GerbilId/LitterId/ContactId; `GerbilOverride`: + GerbilId). Dieses Muster für alles nutzen, was Re-Ingests überleben soll. - `resolved_import.json` ist **gitignored** (Runtime-Output); `review-report.md` ist getrackt. - Deterministische GUIDs via `generate_guid("...")` → stabile IDs über Re-Ingests. @@ -108,6 +128,26 @@ python test_extract.py test_extract_docx.py test_genotype.py test_merge_resol Indiz). UI: Diagramm führt den Vorfahren-Ast zusammen (Verweis-Knoten); Akte zeigt „⚭ Geschwisterverpaarung"-Chip. - **Box-Farbe im Stammbaum-xlsx = Geschlecht**: weiß = weiblich, blau = männlich. +- **Bestandstier (`isResident`) vs. reiner Vorfahre** (Regel der Züchterin, Ticket 09bcac78): + Ein Tier ist **ihr Zuchttier** ⇔ es hatte bei ihr Nachzucht mit Herkunft **„von den + Kleinen Chaoten"** (nur dann konnten die Jungtiere ihren Zuchtnamen tragen). Trägt die + Nachzucht einen **fremden** Herkunftsnamen (z. B. „of Black Forest", „of Topolino"), ist + das Tier nur **Vorfahre** (`isResident=false`, keine Würfe/Charakter-Bereich). **Ausnahme: + zugekaufte Zuchttiere** (fremder Herkunftsname, aber bei ihr gezüchtet → Nachzucht „von den + Kleinen Chaoten") bleiben **Bestandstiere** (z. B. Echo/Elia of Black Forest). + **Sweep umgesetzt (2026-06-27, Ticket 381f7e51):** Der Wurfchronik-Import-Default ist + **nicht mehr `is_resident=True`**. Neue Regel in `merge_and_resolve.py` (später Sweep + nach Eltern-/`addLitters`-Verknüpfung): `isResident=true` **genau dann**, wenn (1) ein + expliziter `conflict-decisions.json`-`isResident`-Override greift (gewinnt immer — `true` + wie Echo/Elia, `false` wie Akane/Naémi/Bentley), **oder** (2) das Tier **Elternteil eines + eigenen Wurfs** ist (nicht-virtuell ODER aus Wurfchronik ODER Clan-Nachzucht ODER mit + residentem Elternteil ODER `addLitter` mit `ShowInChronicle`). Reine virtuelle + Ahnen-Verpaarungen machen **nicht** resident (z. B. Antares bleibt `false`). Sonst + `false`. `ShowInChronicle` wird **nach** dem Sweep berechnet (nutzt die finale Residenz). + Effekt: resident **1711 → 442**. UI-Label (`de.ts` `isResidentLabel`) = **„Zuchtanwärter/ + innen"** — die Züchterin markiert künftige Zuchttiere selbst. **Offene Folgeaufgabe:** + manuelle UI-Residenz-Markierungen überleben den Re-Ingest noch **nicht** (bräuchten eine + ingest-stabile, FK-freie Override-Ablage analog `Feedback`). - **Genotyp/Farbschlag**: 8-Locus-Notation (siehe `tools/import/genotype.py`, `gerbil-manager-web/src/genetics`). Unbekanntes Allel = `-` (nicht `?`). E-Locus: `ee`=Fuchs, `eef`=Fuchsschimmel, `efef`=Schimmel. Rezessiver Fuchs ist @@ -149,11 +189,51 @@ python test_extract.py test_extract_docx.py test_genotype.py test_merge_resol attribuierte `history[]`, inkl. „⚠ … verworfen — Grund; … verwendet"). - **Fehler melden (Feedback)**: `src/components/ReportErrorDialog.tsx`, `src/api/feedback.ts`, Backend `Endpoints/FeedbackEndpoints.cs` + `Models/Feedback.cs`. +- **Geprüfte Tiere / „vollständig korrekt"-Freeze**: Backend `Models/GerbilOverride.cs` (FK-frei, + ingest-überlebend), `Import/GerbilSnapshotService.cs` (Akten-Snapshot + Freeze-Map + Diff, von App + **und** Test geteilt), `Endpoints/VerifiedGerbilEndpoints.cs` (`/verified-gerbils`: POST markieren, + DELETE entfernen/Toggle, GET Liste+Drift, GET `/export` = Golden-Fixture). Freeze wird in + `IngestResolvedService` angewandt; `GerbilEndpoints` setzt `IsManual=true` bei POST und legt bei + Edit eines importierten Tiers einen per-field-Override an (verifiziert → Golden auto-aktualisiert). + Frontend: `src/api/verified.ts`, Badge/Toggle/Drift-Karte in `pages/GerbilDetailPage.tsx`, + Übersicht `pages/VerifizierteTierePage.tsx` (`/hilfe/verifizierte-tiere`); Texte in `strings/de.ts` + (`verified`-Block). **Modell:** `OverrideJson` = Feld→Wert-Map; `IsVerified=true` = zertifiziert + + voller Freeze + Regression/Export, `false` = „geschützt" (nur geänderte Felder). Status/Gehege und + Abstammung/Nachzucht werden **nicht** eingefroren (nur erkannt/gemeldet). Regressionstest: + `GerbilManager.Tests/VerifiedGoldenTests.cs` gegen `tools/import/verified-golden.json` (via + `GET /verified-gerbils/export` erzeugen + committen; skippt, wenn Fixture/`resolved_import.json` fehlt). + **Scope-Grenze:** Freeze/Override gelten für **Gerbils**; Edits an importierten Würfen/Kontakten und + manuelle SaleContracts sind noch **nicht** geschützt (Folgeaufgaben). - **Verträge**: `Models/SaleContract.cs`, `Endpoints/ContractEndpoints.cs`, `ContractGenerator.cs`, Frontend `src/pages/Vertraege*`; Import: `extract_contracts.py` + `enrich_from_contracts` in `merge_and_resolve.py`. - **Import-Kernlogik**: `tools/import/merge_and_resolve.py` (Dedup, Eltern-Resolver mit Gender-/Alters-Plausibilität, Litter-/Sibling-Dedup, Provenance, Vertrags-Anreicherung). +- **RennmausPro-III-Import (rpro3)**: Backend-Importer `GerbilManagerWebAPI/Import/Rpro3/*` + (`Rpro3ImportService` Analyze/Execute, `Rpro3Dedup` = automatischer Dubletten-Merge, Port von + `tools/import/compare_rpro3.py`). **Manuelle Dubletten-Entscheidungen der Züchterin** (aus den + „rpro3-import"-Tickets) liegen in `GerbilManagerWebAPI/Import/Rpro3/rpro3-decisions.json` und + werden von `Rpro3Dedup.Run(animals, decisions)` honoriert: `same` = Force-Merge (eine rid zieht + den Auto-Cluster mit), `different` = Force-Split, `fields` = Feld-Overrides (Farbe/DOB/Herkunft/ + `genotype` (roher Fcode)/`resident`/Notiz) am Cluster. Der `genotype`-Override ist nötig, wenn + Varianten sich im Gencode unterscheiden (z. B. C-Locus c[h] vs. c[chm]) und der Auto-Merge sonst + den falschen Gencode (erstes Cluster-Mitglied) zöge — die Züchterin gibt den korrekten vor + (z. B. MilkyWay → `aa Cc[h] DD ee Gg P- spsp`). **Schlüssel ist die RPRO3-Nummer (rid), NIE der Variantenbuchstabe** + (Buchstaben verschieben sich bei geänderter Clusterbildung) — rids aus dem Ticket-`agentContext`. + Triage-Helfer `tools/import/rpro3_lookup.py` (Name- oder `--rids`-Modus) zieht Farbe/Gencode/ + Eltern/Nachzucht+Partner aus `_rpro3.db` für die Rückfragen an die Züchterin. +- **Manuell ergänzte Würfe / versteckte Würfe**: `conflict-decisions.json` kennt neben + `resolutions`/`addAnimals` auch **`addLitters`** [{name, mother(+motherDob), father, children[], + showInChronicle, totalBorn, date, notes}] — materialisiert in `merge_and_resolve.py` als + **späten** Schritt (nach Dedup/Parent-Roles, vor json.dump): Wurf mit deterministischer + `generate_guid("decision-litter-")`, verknüpft Mutter/Vater/Kinder per Name(+dob), + setzt Kinder-`LitterId`. Die name+dob-Resolutions können zusätzlich `isResident`, `notes`, + `receiver` (Kontaktname→ReceiverContactId) auf bestehende Tiere setzen (läuft NACH der + Residenz-Propagation). `Litter.ShowInChronicle` (bool, Default true; Migration + `AddLitterShowInChronicle`): `false` blendet den Wurf aus der Wurfchronik + (`WuerfeListPage`, Filter `showInChronicle==true`) aus, zeigt ihn aber weiter auf der + Tier-Akte (parentLitters-Query, ungefiltert). Use-Case: extern entstandener Wurf eines + nicht-residenten Tieres (z. B. Akane × Bonaparte). ## Verifikations-Checkliste vor „fertig" diff --git a/GerbilManager.AppHost/AppHost.cs b/GerbilManager.AppHost/AppHost.cs index 5a9eea6..05b8e92 100644 --- a/GerbilManager.AppHost/AppHost.cs +++ b/GerbilManager.AppHost/AppHost.cs @@ -4,6 +4,13 @@ using System.Net.Sockets; var builder = DistributedApplication.CreateBuilder(args); +// WEB-2: lokales Verzeichnis für die veröffentlichte öffentliche Seite. Die API schreibt +// nach /live (POST /api/publish), der publicsite-nginx serviert genau dieses live/ +// — dieselbe Form wie in Produktion (deploy/truenas), nur lokal zum Testen. +var publicSiteRoot = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", "deploy", ".local-publicsite")); +Directory.CreateDirectory(Path.Combine(publicSiteRoot, "live")); // leeres live/, damit nginx sofort startet +var publicSiteConf = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", "deploy", "truenas", "nginx", "publicsite.conf")); + var postgres = builder.AddPostgres("postgres") .WithDataVolume(); @@ -19,6 +26,8 @@ var webapi = builder.AddProject("webapi") // Proxyless so the app binds the port directly — Program.cs then widens the // host to 0.0.0.0 for LAN access (the DCP proxy binds localhost only). .WithEndpoint("http", e => { e.Port = ApiHttpPort; e.IsProxied = false; }, createIfNotExists: false) + // WEB-2: Publish-Ziel der öffentlichen Seite (sonst gibt POST /api/publish 503). + .WithEnvironment("PublicSite__RootPath", publicSiteRoot) .WithExternalHttpEndpoints(); // The machine's LAN IP — the SPA runs in the phone's browser, so it must reach @@ -40,6 +49,15 @@ builder.AddViteApp("frontend", "../gerbil-manager-web") context.EnvironmentVariables["VITE_API_BASE_URL"] = $"http://{lanIp}:{ApiHttpPort}"; }); +// WEB-2: publicsite-nginx — serviert die veröffentlichte öffentliche Seite lokal (gleiche +// nginx-Config wie in Produktion). Liest das live/-Verzeichnis aus dem Publish-Output der API. +// Test: in der App veröffentlichen (oder POST /api/publish), dann http://localhost:8081 öffnen. +builder.AddContainer("publicsite", "nginx", "alpine") + .WithBindMount(publicSiteRoot, "/usr/share/nginx/html", isReadOnly: true) + .WithBindMount(publicSiteConf, "/etc/nginx/conf.d/default.conf", isReadOnly: true) + .WithHttpEndpoint(port: 8081, targetPort: 80, name: "http") + .WithExternalHttpEndpoints(); + builder.Build().Run(); // Picks the IPv4 address of the active physical LAN adapter (Wi-Fi/Ethernet), diff --git a/GerbilManager.Tests/Rpro3ImportTests.cs b/GerbilManager.Tests/Rpro3ImportTests.cs index 6b0a0d4..ea77955 100644 --- a/GerbilManager.Tests/Rpro3ImportTests.cs +++ b/GerbilManager.Tests/Rpro3ImportTests.cs @@ -145,13 +145,16 @@ namespace GerbilManager.Tests { var path = Path.Combine(Path.GetTempPath(), "rpro3-dec-" + Guid.NewGuid().ToString("N") + ".json"); File.WriteAllText(path, - "{\"decisions\":[{\"name\":\"Bura\",\"same\":[[\"228\",\"u2128\"]],\"fields\":{\"228\":{\"origin\":\"Sarah Wörz\"}}}]}"); + "{\"decisions\":[{\"name\":\"Bura\",\"same\":[[\"228\",\"u2128\"]],\"fields\":{\"228\":{\"origin\":\"Sarah Wörz\"}}}," + + "{\"name\":\"MilkyWay\",\"same\":[[\"u1031\",\"u1019\"]],\"fields\":{\"u1031\":{\"genotype\":\"aa Cc[h] DD ee Gg P- spsp\",\"color\":\"Kohlfuchs-Hell\"}}}]}"); try { var d = Rpro3Decisions.Load(path); - Assert.Single(d.Decisions); + Assert.Equal(2, d.Decisions.Count); Assert.Equal("Sarah Wörz", d.BuildFieldIndex()["228"].Origin); Assert.Equal(new[] { "228", "u2128" }, d.SameGroups().First()); + // Gencode-Override (z. B. MilkyWay: korrektes C-Locus c[h]) muss durch den JSON-Roundtrip kommen. + Assert.Equal("aa Cc[h] DD ee Gg P- spsp", d.BuildFieldIndex()["u1031"].Genotype); } finally { File.Delete(path); } } diff --git a/GerbilManager.Tests/VerifiedGerbilTests.cs b/GerbilManager.Tests/VerifiedGerbilTests.cs new file mode 100644 index 0000000..023f7b9 --- /dev/null +++ b/GerbilManager.Tests/VerifiedGerbilTests.cs @@ -0,0 +1,339 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using GerbilManagerWebAPI.Import; +using GerbilManagerWebAPI.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace GerbilManager.Tests +{ + /// + /// Regression tests for the "Freeze/Override" mechanism (verified key animals + manual work + /// survive the re-ingest). Exercised at the service + ingest level with an InMemory DB, mirroring + /// the harness in . + /// + public class VerifiedGerbilTests : IDisposable + { + private static readonly Guid Schwarz = new("00000000-0000-0000-0000-000000000006"); + private readonly List _dirs = new(); + + public void Dispose() + { + foreach (var d in _dirs) + try { Directory.Delete(d, recursive: true); } catch { } + } + + // ── Harness helpers ────────────────────────────────────────────────── + + private string NewDir() + { + var dir = Path.Combine(Path.GetTempPath(), "verified-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + _dirs.Add(dir); + return dir; + } + + private static ApplicationContext NewDb() + { + var opts = new DbContextOptionsBuilder() + .UseInMemoryDatabase("verified-" + Guid.NewGuid().ToString("N")) + .Options; + var db = new ApplicationContext(opts); + db.Database.EnsureCreated(); + return db; + } + + private static IngestResolvedService Service(ApplicationContext db, string dir) + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "Import:SourcePath", dir }, + { "Photos:RootPath", dir }, + }) + .Build(); + return new IngestResolvedService(db, config, null!); + } + + private static void WritePayload(string dir, object[] gerbils, object[]? litters = null, object[]? contacts = null) => + File.WriteAllText(Path.Combine(dir, "resolved_import.json"), JsonSerializer.Serialize(new + { + Contacts = contacts ?? Array.Empty(), + Litters = litters ?? Array.Empty(), + Gerbils = gerbils, + GerbilPhotos = Array.Empty(), + SaleContracts = Array.Empty(), + })); + + private static object ImportGerbil(Guid id, string name, string genotype, string gender = "female", string dob = "2022-01-01") => new + { + Id = id, + Name = name, + Gender = gender, + Status = "Breeding", + DateOfBirth = dob, + Genotype = genotype, + ColorVarietyId = Schwarz, + IsResident = true, + OriginBreeder = "Test Zucht", + }; + + // ── 1. Manual animal survives the ingest (never wiped) ─────────────── + + [Fact] + public async Task Manual_animal_survives_ingest_that_omits_it() + { + using var db = NewDb(); + var dir = NewDir(); + + var manualId = Guid.NewGuid(); + db.Gerbils.Add(new Gerbil { Id = manualId, Name = "Handmaus", Gender = Gender.female, IsManual = true }); + await db.SaveChangesAsync(); + + var importedId = Guid.NewGuid(); + WritePayload(dir, new[] { ImportGerbil(importedId, "Importiert", "aa CC DD EE GG PP spsp") }); + + var result = await Service(db, dir).RunAsync(); + + Assert.StartsWith("Ingestion successful!", result); + // Manual row is still there, unchanged. + var manual = await db.Gerbils.FindAsync(manualId); + Assert.NotNull(manual); + Assert.True(manual!.IsManual); + Assert.Equal("Handmaus", manual.Name); + // Imported row was loaded. + Assert.NotNull(await db.Gerbils.FindAsync(importedId)); + Assert.Equal(2, await db.Gerbils.CountAsync()); + } + + // ── 2. Verified freeze forces the golden values over a drifted import ─ + + [Fact] + public async Task Verified_override_forces_golden_and_records_drift() + { + using var db = NewDb(); + var dir = NewDir(); + var id = new Guid("11111111-1111-1111-1111-111111111111"); + + // First ingest: import brings X in with the WRONG genotype/name. + WritePayload(dir, new[] { ImportGerbil(id, "Falschname", "aa dd DD EE GG PP spsp") }); + var svc = Service(db, dir); + await svc.RunAsync(); + + // The breeder corrects X and marks it "vollständig korrekt". + var g = await db.Gerbils.FindAsync(id); + Assert.NotNull(g); + g!.Name = "Korrekt"; + g.Genotype = "aa CC DD EE GG PP spsp"; + await db.SaveChangesAsync(); + + var golden = await GerbilSnapshotService.BuildSnapshotAsync(db, id); + db.GerbilOverrides.Add(new GerbilOverride + { + Id = Guid.NewGuid(), + GerbilId = id, + EntityName = g.Name, + IsVerified = true, + OverrideJson = GerbilSnapshotService.BuildFreezeJson(g), + SnapshotJson = GerbilSnapshotService.SerializeSnapshot(golden!), + VerifiedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + }); + await db.SaveChangesAsync(); + + // A later import DRIFTS back to wrong values. + WritePayload(dir, new[] { ImportGerbil(id, "Wiederfalsch", "aa dd DD EE GG PP spsp") }); + await svc.RunAsync(); + + // Golden wins (freeze re-applied on top of the fresh import). + var after = await db.Gerbils.FindAsync(id); + Assert.Equal("Korrekt", after!.Name); + Assert.Equal("aa CC DD EE GG PP spsp", after.Genotype); + + // Drift was detected and recorded, naming the offending fields. + var ov = await db.GerbilOverrides.SingleAsync(); + Assert.False(string.IsNullOrEmpty(ov.LastImportDiffJson)); + Assert.Contains("genotype", ov.LastImportDiffJson); + Assert.Contains("name", ov.LastImportDiffJson); + Assert.NotNull(ov.LastImportSnapshotJson); + } + + // ── 3. No drift → LastImportDiffJson stays null ────────────────────── + + [Fact] + public async Task Verified_override_without_drift_leaves_diff_null() + { + using var db = NewDb(); + var dir = NewDir(); + var id = new Guid("22222222-2222-2222-2222-222222222222"); + + WritePayload(dir, new[] { ImportGerbil(id, "Stabil", "aa CC DD EE GG PP spsp") }); + var svc = Service(db, dir); + await svc.RunAsync(); + + var g = await db.Gerbils.FindAsync(id); + var golden = await GerbilSnapshotService.BuildSnapshotAsync(db, id); + db.GerbilOverrides.Add(new GerbilOverride + { + Id = Guid.NewGuid(), + GerbilId = id, + EntityName = g!.Name, + IsVerified = true, + OverrideJson = GerbilSnapshotService.BuildFreezeJson(g), + SnapshotJson = GerbilSnapshotService.SerializeSnapshot(golden!), + VerifiedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + }); + await db.SaveChangesAsync(); + + // Re-ingest with the IDENTICAL payload (== golden). + await svc.RunAsync(); + + var ov = await db.GerbilOverrides.SingleAsync(); + Assert.Null(ov.LastImportDiffJson); + } + + // ── 4. Removing the override → import-driven again ─────────────────── + + [Fact] + public async Task Removing_override_restores_import_driven_values() + { + using var db = NewDb(); + var dir = NewDir(); + var id = new Guid("33333333-3333-3333-3333-333333333333"); + + WritePayload(dir, new[] { ImportGerbil(id, "Import-Wert", "aa CC DD EE GG PP spsp") }); + var svc = Service(db, dir); + await svc.RunAsync(); + + var g = await db.Gerbils.FindAsync(id); + g!.Genotype = "aa cc DD EE GG PP spsp"; // "golden" + await db.SaveChangesAsync(); + var golden = await GerbilSnapshotService.BuildSnapshotAsync(db, id); + var ov = new GerbilOverride + { + Id = Guid.NewGuid(), + GerbilId = id, + IsVerified = true, + OverrideJson = GerbilSnapshotService.BuildFreezeJson(g), + SnapshotJson = GerbilSnapshotService.SerializeSnapshot(golden!), + UpdatedAt = DateTimeOffset.UtcNow, + }; + db.GerbilOverrides.Add(ov); + await db.SaveChangesAsync(); + + // Freeze wins while the override exists. + await svc.RunAsync(); + Assert.Equal("aa cc DD EE GG PP spsp", (await db.Gerbils.FindAsync(id))!.Genotype); + + // Remove the override → the animal follows the import again. + db.GerbilOverrides.Remove(ov); + await db.SaveChangesAsync(); + await svc.RunAsync(); + Assert.Equal("aa CC DD EE GG PP spsp", (await db.Gerbils.FindAsync(id))!.Genotype); + } + + // ── 5. Per-field protection (pure service level) ───────────────────── + + [Fact] + public void PerField_change_pins_only_the_changed_field() + { + var g = new Gerbil { Name = "Quelle", Notes = "alte Notiz", Genotype = "aa CC DD EE GG PP spsp" }; + + var before = GerbilSnapshotService.BuildFreezeObject(g); + g.Notes = "neue Notiz"; + var after = GerbilSnapshotService.BuildFreezeObject(g); + + var changed = GerbilSnapshotService.ChangedFields(before, after); + var only = Assert.Single(changed); // ONLY notes changed + Assert.Equal("notes", only.Key); + + var merged = GerbilSnapshotService.MergeOverrideJson("{}", changed); + var mergedObj = JsonNode.Parse(merged)!.AsObject(); + Assert.True(mergedObj.ContainsKey("notes")); + Assert.False(mergedObj.ContainsKey("genotype")); // untouched fields not pinned + + // Applying it onto a different animal overwrites ONLY notes, not the genotype. + var target = new Gerbil { Name = "Ziel", Notes = "Ziel-Notiz", Genotype = "zz ZZ dd ee gg pp spsp" }; + GerbilSnapshotService.ApplyOverride(target, merged); + Assert.Equal("neue Notiz", target.Notes); + Assert.Equal("zz ZZ dd ee gg pp spsp", target.Genotype); + } + + // ── 6. Dangling reference from a manual row is nulled + warned ──────── + + [Fact] + public async Task Stale_parent_reference_from_manual_litter_is_nulled_with_warning() + { + using var db = NewDb(); + var dir = NewDir(); + var fatherId = new Guid("44444444-4444-4444-4444-444444444444"); + + // Ingest 1 brings in the (imported) father. + WritePayload(dir, new[] { ImportGerbil(fatherId, "Vater", "aa CC DD EE GG PP spsp", gender: "male") }); + var svc = Service(db, dir); + await svc.RunAsync(); + + // The breeder wires a MANUAL litter to that imported father. + var litterId = Guid.NewGuid(); + db.Litters.Add(new Litter { Id = litterId, Name = "Manueller Wurf", FatherId = fatherId, IsManual = true }); + await db.SaveChangesAsync(); + + // Ingest 2 no longer contains the father → it becomes stale and is removed. + WritePayload(dir, Array.Empty()); + var result = await svc.RunAsync(); + + // The manual litter survives, but its dangling parent link was nulled... + var litter = await db.Litters.FindAsync(litterId); + Assert.NotNull(litter); + Assert.True(litter!.IsManual); + Assert.Null(litter.FatherId); + // ...and a warning was surfaced. + Assert.Contains("warning", result); + Assert.Contains("nicht mehr existiert", result); + // The stale imported father is gone. + Assert.Null(await db.Gerbils.FindAsync(fatherId)); + } + + // ── 7. Diff detects ancestry / offspring differences ───────────────── + + [Fact] + public void Diff_detects_parent_and_children_differences() + { + var self = new SnapshotSelf( + "Proband", "female", "2022-01-01", null, null, null, "aa CC DD EE GG PP spsp", + null, "Schwarz", true, null, false, null, null, null, null, + new List(), null, null); + + var golden = new GerbilSnapshot( + self, + new SnapshotParent("Papa Alt", "2020-01-01", "aa CC"), + null, + new List + { + new("Wurf1", "2023-01-01", "mother", 3, true, new List + { + new("Kind1", "2023-01-01", "female"), + }), + }); + + var other = new GerbilSnapshot( + self, + new SnapshotParent("Papa Neu", "2020-01-01", "aa CC"), // different father + null, + new List + { + new("Wurf1", "2023-01-01", "mother", 3, true, new List + { + new("Kind1", "2023-01-01", "female"), + new("Kind2", "2023-01-02", "male"), // extra child + }), + }); + + var diffs = GerbilSnapshotService.Diff(golden, other); + Assert.NotEmpty(diffs); + Assert.Contains(diffs, d => d.Path == "father"); + Assert.Contains(diffs, d => d.Path.StartsWith("wurf[")); + } + } +} diff --git a/GerbilManager.Tests/VerifiedGoldenTests.cs b/GerbilManager.Tests/VerifiedGoldenTests.cs new file mode 100644 index 0000000..9ffb063 --- /dev/null +++ b/GerbilManager.Tests/VerifiedGoldenTests.cs @@ -0,0 +1,151 @@ +using System.Text.Json; +using GerbilManagerWebAPI.Import; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Xunit.Abstractions; + +namespace GerbilManager.Tests +{ + /// + /// Golden regression fixture: ingests the REAL tools/import/output/resolved_import.json + /// (gitignored, local only) and asserts that every verified key animal's Akte still matches its + /// frozen golden snapshot in tools/import/verified-golden.json (committed fixture). + /// + /// Both files are optional: if EITHER is missing the test skips cleanly (no failure), so it is a + /// no-op on CI / fresh clones but a real guard on the breeder's machine once she has verified + /// animals and exported the fixture. + /// + public class VerifiedGoldenTests + { + private readonly ITestOutputHelper _out; + public VerifiedGoldenTests(ITestOutputHelper output) => _out = output; + + private sealed record GoldenEntry(Guid GerbilId, string? Name, DateTimeOffset? VerifiedAt, GerbilSnapshot? Snapshot); + + private static readonly JsonSerializerOptions Web = new(JsonSerializerDefaults.Web); + + private static string? FindRepoRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null) + { + if (Directory.Exists(Path.Combine(dir.FullName, "tools", "import"))) + return dir.FullName; + dir = dir.Parent; + } + return null; + } + + [Fact] + public async Task Verified_animals_still_match_their_golden_snapshot() + { + var repoRoot = FindRepoRoot(); + if (repoRoot is null) + { + _out.WriteLine("SKIP: repo root (tools/import) not found from the test host."); + return; + } + + var goldenPath = Path.Combine(repoRoot, "tools", "import", "verified-golden.json"); + var outputDir = Path.Combine(repoRoot, "tools", "import", "output"); + var resolvedPath = Path.Combine(outputDir, "resolved_import.json"); + + if (!File.Exists(goldenPath)) + { + _out.WriteLine($"SKIP: golden fixture not present ({goldenPath})."); + return; + } + if (!File.Exists(resolvedPath)) + { + _out.WriteLine($"SKIP: resolved_import.json not present ({resolvedPath}) — run the import pipeline."); + return; + } + + List? golden; + try + { + golden = JsonSerializer.Deserialize>(await File.ReadAllTextAsync(goldenPath), Web); + } + catch (Exception ex) + { + _out.WriteLine($"SKIP: could not parse golden fixture: {ex.Message}"); + return; + } + if (golden is null || golden.Count == 0) + { + _out.WriteLine("SKIP: golden fixture is empty."); + return; + } + + // Ingest the real resolved_import.json into an InMemory DB. + var opts = new DbContextOptionsBuilder() + .UseInMemoryDatabase("verified-golden-" + Guid.NewGuid().ToString("N")) + .Options; + using var db = new ApplicationContext(opts); + db.Database.EnsureCreated(); + + var photoDir = Path.Combine(Path.GetTempPath(), "verified-golden-photos-" + Guid.NewGuid().ToString("N")); + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "Import:SourcePath", outputDir }, + { "Photos:RootPath", photoDir }, + }) + .Build(); + + try + { + var result = await new IngestResolvedService(db, config, null!).RunAsync(); + Assert.StartsWith("Ingestion successful!", result); + } + catch (Exception ex) + { + _out.WriteLine($"SKIP: ingest of the real resolved_import.json failed: {ex.Message}"); + return; + } + finally + { + try { Directory.Delete(photoDir, recursive: true); } catch { } + } + + var mismatches = new List(); + int checkedCount = 0; + foreach (var entry in golden) + { + if (entry.Snapshot is null) continue; + checkedCount++; + GerbilSnapshot? actual; + try + { + actual = await GerbilSnapshotService.BuildSnapshotAsync(db, entry.GerbilId); + } + catch (Exception ex) + { + mismatches.Add($"• {entry.Name ?? entry.GerbilId.ToString()}: Akte konnte nicht gebaut werden — {ex.Message}"); + continue; + } + + if (actual is null) + { + mismatches.Add($"• {entry.Name ?? entry.GerbilId.ToString()}: fehlt im Import (verifiziertes Tier verschwunden)."); + continue; + } + + var diffs = GerbilSnapshotService.Diff(entry.Snapshot, actual); + if (diffs.Count > 0) + { + var fields = string.Join("; ", diffs.Select(d => $"{d.Path}: golden='{d.GoldenValue}' ≠ import='{d.OtherValue}'")); + mismatches.Add($"• {entry.Name ?? entry.GerbilId.ToString()}: {diffs.Count} Abweichung(en) — {fields}"); + } + } + + _out.WriteLine($"Golden-Fixture geprüft: {checkedCount} verifizierte Tiere, {mismatches.Count} abweichend."); + if (mismatches.Count > 0) + { + var report = $"Golden-Regression fehlgeschlagen — {mismatches.Count} von {checkedCount} verifizierten Tieren weichen vom eingefrorenen Snapshot ab:\n" + + string.Join("\n", mismatches); + Assert.Fail(report); + } + } + } +} diff --git a/GerbilManagerWebAPI/ApplicationContext.cs b/GerbilManagerWebAPI/ApplicationContext.cs index 7b080a2..a1a2415 100644 --- a/GerbilManagerWebAPI/ApplicationContext.cs +++ b/GerbilManagerWebAPI/ApplicationContext.cs @@ -32,6 +32,7 @@ public class ApplicationContext : DbContext public DbSet WaitingListEntries => Set(); public DbSet ReturnRecords => Set(); public DbSet ExhibitionResults => Set(); + public DbSet GerbilOverrides => Set(); // Keep Gerbil.NameSearch in sync on every save (separator-insensitive search key), // so it can never drift from Name regardless of which code path mutates the entity. @@ -74,6 +75,10 @@ public class ApplicationContext : DbContext // Residency defaults to true (own stock unless explicitly marked external). e.Property(g => g.IsResident).HasDefaultValue(true); + // Manual-flag defaults false: every pre-existing / imported row is import-driven. + // Only POST /gerbils sets it true, so the ingest wipe never touches manual animals. + e.Property(g => g.IsManual).HasDefaultValue(false); + // DB-4: German collation on searched/sorted name fields (Npgsql-only). if (isNpgsql) { @@ -125,6 +130,8 @@ public class ApplicationContext : DbContext e.HasIndex(l => l.ExternalRef) .IsUnique() .HasFilter("\"ExternalRef\" IS NOT NULL"); + + e.Property(l => l.IsManual).HasDefaultValue(false); }); modelBuilder.Entity(e => @@ -268,6 +275,19 @@ public class ApplicationContext : DbContext e.HasIndex(x => x.GerbilId); }); + // VERIFIED/PROTECTED OVERRIDE: deliberately relationship-free (same pattern as Feedback). + // GerbilId is a plain Guid column (no navigation property → EF creates NO foreign key), so + // the import re-ingest wipe of Gerbils never cascades into — or breaks — override rows. They + // survive re-ingest and are re-applied on top of the fresh import (the freeze), which is the + // whole point. + modelBuilder.Entity(e => + { + e.HasIndex(o => o.GerbilId).IsUnique(); + }); + + // Manual-flag default false for Contact (imported/pre-existing rows are import-driven). + modelBuilder.Entity().Property(c => c.IsManual).HasDefaultValue(false); + // DB-4: German collation on remaining searched/sorted text columns (Npgsql-only). if (isNpgsql) { @@ -304,9 +324,9 @@ public class ApplicationContext : DbContext (int n, string Slug, string Title, string BodyMd)[] legalPages = { (7, "impressum", "Impressum", - "**Angaben gemäß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*"), + "## Angaben gemäß § 18 Abs. 1 MStV\\n\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\n\\n## Kontakt\\n\\nE-Mail: [E-Mail-Adresse]\\nTelefon: [Telefonnummer]\\n\\n---\\n\\nDiese Seite stellt eine private Hobbyzucht mongolischer Rennmäuse ohne Gewinnerzielungsabsicht vor.\\n\\n*Hinweis für den Seitenbetreiber: Name und ladungsfähige Anschrift (kein Postfach) sind Pflicht (§ 18 Abs. 1 MStV). Sollte die Zucht jemals gewerblich bzw. geschäftsmäßig betrieben werden, gelten zusätzlich § 5 DDG (vollständiges Anbieter-Impressum) und ggf. die Erlaubnispflicht nach § 11 TierSchG — dann zuständige Behörde und Aktenzeichen hier ergänzen. Kein Hinweis auf die EU-Streitbeilegungsplattform: diese wurde zum 20.07.2025 abgeschaltet (ein fortbestehender Link wäre irreführend).*"), (8, "datenschutz", "Datenschutz", - "**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*"), + "## Datenschutzerklärung\\n\\n## 1. Verantwortlicher\\n\\nVerantwortlich für die Datenverarbeitung auf dieser Webseite ist:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\nE-Mail: [E-Mail-Adresse]\\n\\n## 2. Hosting und Server-Logfiles\\n\\nDiese Webseite wird auf einem eigenen Server betrieben. Beim Aufruf der Seite verarbeitet der Server automatisch Informationen, die dein Browser übermittelt, in sogenannten Server-Logfiles: IP-Adresse, Datum und Uhrzeit des Zugriffs, die aufgerufene Seite bzw. Datei, die übertragene Datenmenge sowie Browser und Betriebssystem.\\nDiese Verarbeitung ist technisch erforderlich, um die Webseite auszuliefern und ihren sicheren und stabilen Betrieb zu gewährleisten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse am störungsfreien Betrieb und an der Sicherheit). Die Logfiles werden nach spätestens [z. B. 7] Tagen gelöscht und nicht mit anderen Daten zusammengeführt.\\n\\n## 3. Cookies, Tracking und externe Dienste\\n\\nDiese Webseite verwendet keine Cookies, kein Tracking und keine Analyse- oder Marketing-Dienste. Es werden keine externen Inhalte Dritter (z. B. Schriftarten, Karten, Videos, CDNs) nachgeladen.\\n\\n## 4. Kontaktaufnahme\\n\\nWenn du uns per E-Mail kontaktierst, verarbeiten wir deine Angaben (z. B. Name, E-Mail-Adresse, Inhalt der Nachricht) ausschließlich zur Bearbeitung deiner Anfrage. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO bzw. Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden gelöscht, sobald sie nicht mehr benötigt werden und keine gesetzlichen Aufbewahrungspflichten entgegenstehen.\\n\\n## 5. Empfänger der Daten\\n\\nEine Weitergabe deiner Daten an Dritte findet nicht statt. Eine Datenübermittlung in Länder außerhalb der EU bzw. des EWR erfolgt nicht.\\n\\n## 6. Deine Rechte\\n\\nDu hast jederzeit das Recht auf:\\n- Auskunft über die zu deiner Person gespeicherten Daten (Art. 15 DSGVO)\\n- Berichtigung unrichtiger Daten (Art. 16 DSGVO)\\n- Löschung (Art. 17 DSGVO)\\n- Einschränkung der Verarbeitung (Art. 18 DSGVO)\\n- Datenübertragbarkeit (Art. 20 DSGVO)\\n- Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)\\n\\nZur Ausübung genügt eine formlose Nachricht an die oben genannte E-Mail-Adresse.\\n\\n## 7. Beschwerderecht bei der Aufsichtsbehörde\\n\\nDu hast das Recht, dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO). Zuständig ist die Aufsichtsbehörde deines Bundeslandes bzw. des Seitenbetreibers: [zuständige Landesdatenschutzbehörde eintragen].\\n\\n## 8. Keine automatisierte Entscheidungsfindung\\n\\nEine automatisierte Entscheidungsfindung einschließlich Profiling nach Art. 22 DSGVO findet nicht statt.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Platzhalter in eckigen Klammern ausfüllen. Aufbewahrungsdauer der Logfiles an deinen Server bzw. Reverse-Proxy anpassen. Sobald externe Dienste oder ein Kontaktformular eingebunden werden, diesen Text ergänzen. Stand: [Datum eintragen].*"), }; var pageRows = new List(); diff --git a/GerbilManagerWebAPI/Dockerfile b/GerbilManagerWebAPI/Dockerfile index 3f78f6e..2162622 100644 --- a/GerbilManagerWebAPI/Dockerfile +++ b/GerbilManagerWebAPI/Dockerfile @@ -17,6 +17,11 @@ RUN dotnet publish GerbilManagerWebAPI.csproj -c Release -o /app/publish /p:UseA FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final WORKDIR /app +# curl für den Container-Healthcheck (das Basis-Image bringt weder curl noch wget mit; +# ohne dieses würde der compose-Healthcheck dauerhaft fehlschlagen und api nie "healthy"). +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* # .NET 10 containers default to ASPNETCORE_HTTP_PORTS=8080 bound on all interfaces. EXPOSE 8080 COPY --from=build /app/publish . diff --git a/GerbilManagerWebAPI/Dtos/ApiDtos.cs b/GerbilManagerWebAPI/Dtos/ApiDtos.cs index 1f0d36c..9ba4702 100644 --- a/GerbilManagerWebAPI/Dtos/ApiDtos.cs +++ b/GerbilManagerWebAPI/Dtos/ApiDtos.cs @@ -33,7 +33,8 @@ namespace GerbilManagerWebAPI.Dtos bool IsResident, string? ProfilePhotoUrl, bool IsCastrated, - string? Provenance); + string? Provenance, + int? BirthOrder = null); public record LitterDto( Guid Id, @@ -41,11 +42,13 @@ namespace GerbilManagerWebAPI.Dtos DateOnly? Date, int? TotalBorn, int? DeathsWithin8Weeks, + int? Stillborn, Guid? FatherId, Guid? MotherId, DateOnly? ExpectedGoHomeDate, string? Notes, - string? Provenance); + string? Provenance, + bool ShowInChronicle); public record ContactDto(Guid Id, string Name, string? Email, string? Phone, string? Address, string? Notes, bool IsBreeder, bool IsReceiver, string? NameSuffix, string? Provenance); @@ -65,8 +68,33 @@ namespace GerbilManagerWebAPI.Dtos public record PhotoDto(Guid Id, string FileName, string? Caption, int SortOrder, string Url); + // VERIFIED/PROTECTED OVERRIDE ------------------------------------------- + // Status: "unchanged" = raw import agrees with the golden state; "drifted" = the raw import + // differs from the golden state (see ImportDiff — a signal for what the importer might still + // get wrong); "missing" = the verified animal no longer exists in the imported data. + public record VerifiedGerbilDto( + Guid GerbilId, + string? EntityName, + bool IsVerified, + string Status, + DateTimeOffset? VerifiedAt, + DateTimeOffset UpdatedAt, + string? Note, + IReadOnlyList ProtectedFields, + IReadOnlyList ImportDiff); + + /// One field difference between the golden state and the raw import. + public record SnapshotDiffDto(string Path, string? GoldenValue, string? ImportValue); + + /// Export entry for the committed regression fixture (verified-golden.json). + public record VerifiedGoldenEntry( + Guid GerbilId, string? Name, DateTimeOffset? VerifiedAt, Import.GerbilSnapshot? Snapshot); + // Request DTOs ----------------------------------------------------------- + /// Body for POST /verified-gerbils/{id} — optional free-text note. + public record VerifyGerbilInput(string? Note); + public record GerbilInput( string? Name, Gender? Gender, @@ -97,10 +125,12 @@ namespace GerbilManagerWebAPI.Dtos DateOnly? Date, int? TotalBorn, int? DeathsWithin8Weeks, + int? Stillborn, Guid? FatherId, Guid? MotherId, DateOnly? ExpectedGoHomeDate, - string? Notes); + string? Notes, + bool? ShowInChronicle = null); public record ContactInput(string Name, string? Email, string? Phone, string? Address, string? Notes, bool IsBreeder, bool IsReceiver, string? NameSuffix); diff --git a/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs b/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs index a4a5a34..4c65c0e 100644 --- a/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs @@ -24,7 +24,7 @@ namespace GerbilManagerWebAPI.Endpoints group.MapPost("/", async (ContactInput input, ApplicationContext db) => { - var c = new Contact { Id = Guid.NewGuid(), Name = input.Name, Email = input.Email, Phone = input.Phone, Address = input.Address, Notes = input.Notes, IsBreeder = input.IsBreeder, IsReceiver = input.IsReceiver, NameSuffix = input.NameSuffix }; + var c = new Contact { Id = Guid.NewGuid(), Name = input.Name, Email = input.Email, Phone = input.Phone, Address = input.Address, Notes = input.Notes, IsBreeder = input.IsBreeder, IsReceiver = input.IsReceiver, NameSuffix = input.NameSuffix, IsManual = true }; db.Contacts.Add(c); await db.SaveChangesAsync(); return TypedResults.Created($"/contacts/{c.Id}", ToDto(c)); diff --git a/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs b/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs index 4829453..51d29d1 100644 --- a/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs @@ -1,5 +1,6 @@ using GerbilManagerWebAPI.Common; using GerbilManagerWebAPI.Dtos; +using GerbilManagerWebAPI.Import; using GerbilManagerWebAPI.Models; using GerbilManagerWebAPI.Services; using Gridify; @@ -61,7 +62,9 @@ namespace GerbilManagerWebAPI.Endpoints if (string.IsNullOrWhiteSpace(input.Name)) return TypedResults.ValidationProblem(new Dictionary { ["name"] = ["Name is required."] }); - var g = new Gerbil { Id = Guid.NewGuid(), Name = input.Name! }; + // Manually created in the UI → IsManual=true, so the import re-ingest never wipes or + // overwrites this animal (and its sub-records). + var g = new Gerbil { Id = Guid.NewGuid(), Name = input.Name!, IsManual = true }; Apply(g, input, isCreate: true); db.Gerbils.Add(g); await db.SaveChangesAsync(); @@ -73,8 +76,17 @@ namespace GerbilManagerWebAPI.Endpoints { var g = await db.Gerbils.FirstOrDefaultAsync(x => x.Id == id); if (g is null) return TypedResults.NotFound(); + + // Capture the own-field values BEFORE the edit so manual changes to an IMPORTED animal + // can be pinned per-field (survive the next ingest). Manual animals need no override — + // they survive the ingest wholesale. + var before = GerbilSnapshotService.BuildFreezeObject(g); Apply(g, input, isCreate: false); await db.SaveChangesAsync(); + + if (!g.IsManual) + await RecordEditOverrideAsync(db, g, before); + return TypedResults.NoContent(); }); @@ -133,11 +145,47 @@ namespace GerbilManagerWebAPI.Endpoints GerbilStatusService.Apply(g, DateOnly.FromDateTime(DateTime.UtcNow)); } + // EDIT PROTECTION: after a manual edit to an IMPORTED animal, pin the change so the next + // ingest can't overwrite it. A VERIFIED animal keeps its certification and its whole golden + // is refreshed to the edited state (Frage 5). An unverified animal gets/updates a "geschützt" + // override carrying ONLY the changed fields (per-field) — untouched fields keep receiving + // import improvements. Called after the edit is already persisted. + private static async Task RecordEditOverrideAsync(ApplicationContext db, Gerbil g, System.Text.Json.Nodes.JsonObject before) + { + var ov = await db.GerbilOverrides.FirstOrDefaultAsync(o => o.GerbilId == g.Id); + if (ov is { IsVerified: true }) + { + ov.EntityName = g.Name; + ov.OverrideJson = GerbilSnapshotService.BuildFreezeJson(g); + var snap = await GerbilSnapshotService.BuildSnapshotAsync(db, g.Id); + if (snap is not null) ov.SnapshotJson = GerbilSnapshotService.SerializeSnapshot(snap); + ov.LastImportSnapshotJson = null; + ov.LastImportDiffJson = null; + ov.UpdatedAt = DateTimeOffset.UtcNow; + await db.SaveChangesAsync(); + return; + } + + var after = GerbilSnapshotService.BuildFreezeObject(g); + var changed = GerbilSnapshotService.ChangedFields(before, after); + if (changed.Count == 0) return; + + if (ov is null) + { + ov = new GerbilOverride { Id = Guid.NewGuid(), GerbilId = g.Id, IsVerified = false, OverrideJson = "{}" }; + db.GerbilOverrides.Add(ov); + } + ov.EntityName = g.Name; + ov.OverrideJson = GerbilSnapshotService.MergeOverrideJson(ov.OverrideJson, changed); + ov.UpdatedAt = DateTimeOffset.UtcNow; + await db.SaveChangesAsync(); + } + internal static GerbilDto ToDto(Gerbil g, string? profilePhotoUrl = null) => new( g.Id, g.Name, g.Gender, g.Status, g.LitterId, g.OriginContactId, g.ReceiverContactId, g.EnclosureId, g.ColorVarietyId, g.DateOfBirth, g.DateOfDeath, g.CauseOfDeath, g.GoHomeDate, g.Genotype, g.SpottingType, g.Notes, g.ImportSource, g.ExternalRef, g.OriginBreeder, g.CharacterTraits, g.CharacterNote, g.IsDeaf, g.IsResident, profilePhotoUrl, g.IsCastrated, - g.Provenance); + g.Provenance, g.BirthOrder); } } diff --git a/GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs b/GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs index db77499..b8b1262 100644 --- a/GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs @@ -30,7 +30,7 @@ namespace GerbilManagerWebAPI.Endpoints var mortalityErr = ValidateMortality(input.DeathsWithin8Weeks, input.TotalBorn); if (mortalityErr is not null) return TypedResults.BadRequest(mortalityErr); - var l = new Litter { Id = Guid.NewGuid(), Name = input.Name, Date = input.Date }; + var l = new Litter { Id = Guid.NewGuid(), Name = input.Name, Date = input.Date, IsManual = true }; Apply(l, input); db.Litters.Add(l); await db.SaveChangesAsync(); @@ -81,10 +81,15 @@ namespace GerbilManagerWebAPI.Endpoints { l.TotalBorn = i.TotalBorn; l.DeathsWithin8Weeks = i.DeathsWithin8Weeks; + l.Stillborn = i.Stillborn; l.FatherId = i.FatherId; l.MotherId = i.MotherId; l.ExpectedGoHomeDate = i.ExpectedGoHomeDate; l.Notes = i.Notes; + // Nur überschreiben, wenn explizit gesetzt — sonst bestehenden Wert / Modell-Default + // (true) behalten, damit ein Edit ohne dieses Feld einen versteckten Wurf nicht + // versehentlich wieder in die Wurfchronik holt. + if (i.ShowInChronicle is bool show) l.ShowInChronicle = show; } private static string? ValidateMortality(int? deaths, int? totalBorn) @@ -97,8 +102,8 @@ namespace GerbilManagerWebAPI.Endpoints } private static LitterDto ToDto(Litter l) => new( - l.Id, l.Name, l.Date, l.TotalBorn, l.DeathsWithin8Weeks, - l.FatherId, l.MotherId, l.ExpectedGoHomeDate, l.Notes, l.Provenance); + l.Id, l.Name, l.Date, l.TotalBorn, l.DeathsWithin8Weeks, l.Stillborn, + l.FatherId, l.MotherId, l.ExpectedGoHomeDate, l.Notes, l.Provenance, l.ShowInChronicle); } /// 400 body for a father×mother gender mismatch; frontend localises by Code. diff --git a/GerbilManagerWebAPI/Endpoints/VerifiedGerbilEndpoints.cs b/GerbilManagerWebAPI/Endpoints/VerifiedGerbilEndpoints.cs new file mode 100644 index 0000000..bef288d --- /dev/null +++ b/GerbilManagerWebAPI/Endpoints/VerifiedGerbilEndpoints.cs @@ -0,0 +1,136 @@ +using System.Text.Json; +using GerbilManagerWebAPI.Dtos; +using GerbilManagerWebAPI.Import; +using GerbilManagerWebAPI.Models; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.EntityFrameworkCore; + +namespace GerbilManagerWebAPI.Endpoints +{ + /// + /// VERIFIED / PROTECTED OVERRIDES — the breeder's hand-curated "this animal is correct" layer. + /// POST /verified-gerbils/{gerbilId} -> mark "vollständig korrekt": snapshot the full Akte, + /// freeze all own fields, apply the freeze immediately. Toggle-on. Optional {note}. + /// DELETE /verified-gerbils/{gerbilId} -> remove the override (un-flag / release protection). + /// Non-destructive: current values stay; the next ingest makes the animal import-driven again. + /// GET /verified-gerbils -> all overrides (verified + protected) with drift status. + /// GET /verified-gerbils/{gerbilId} -> single status (badge on the Rennmausakte). + /// GET /verified-gerbils/export -> canonical golden dump for the committed regression fixture. + /// Overrides are decoupled from gerbils (loose GerbilId, no FK), so they survive the import + /// re-ingest wipe and are re-applied on top of the fresh import (the "freeze"). + /// + public static class VerifiedGerbilEndpoints + { + private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web); + + public static IEndpointRouteBuilder MapVerifiedGerbilEndpoints(this IEndpointRouteBuilder app) + { + var group = app.MapGroup("/verified-gerbils").WithTags("VerifiedGerbils"); + + group.MapPost("/{gerbilId:guid}", async Task, NotFound>> ( + Guid gerbilId, VerifyGerbilInput? input, ApplicationContext db) => + { + var g = await db.Gerbils.FirstOrDefaultAsync(x => x.Id == gerbilId); + if (g is null) return TypedResults.NotFound(); + + var snapshot = await GerbilSnapshotService.BuildSnapshotAsync(db, gerbilId); + + var ov = await db.GerbilOverrides.FirstOrDefaultAsync(o => o.GerbilId == gerbilId); + if (ov is null) + { + ov = new GerbilOverride { Id = Guid.NewGuid(), GerbilId = gerbilId, OverrideJson = "{}" }; + db.GerbilOverrides.Add(ov); + } + ov.EntityName = g.Name; + ov.IsVerified = true; + ov.OverrideJson = GerbilSnapshotService.BuildFreezeJson(g); + ov.SnapshotJson = snapshot is null ? null : GerbilSnapshotService.SerializeSnapshot(snapshot); + // Reset the drift cache — recomputed on the next ingest against the fresh import. + ov.LastImportSnapshotJson = null; + ov.LastImportDiffJson = null; + ov.Note = string.IsNullOrWhiteSpace(input?.Note) ? ov.Note : input!.Note!.Trim(); + ov.VerifiedAt ??= DateTimeOffset.UtcNow; + ov.UpdatedAt = DateTimeOffset.UtcNow; + await db.SaveChangesAsync(); + return TypedResults.Ok(await ToDtoAsync(db, ov)); + }); + + group.MapDelete("/{gerbilId:guid}", async Task> ( + Guid gerbilId, ApplicationContext db) => + { + var ov = await db.GerbilOverrides.FirstOrDefaultAsync(o => o.GerbilId == gerbilId); + if (ov is null) return TypedResults.NotFound(); + db.GerbilOverrides.Remove(ov); + await db.SaveChangesAsync(); + return TypedResults.NoContent(); + }); + + group.MapGet("/", async (ApplicationContext db) => + { + var rows = await db.GerbilOverrides.ToListAsync(); + var existingIds = await db.Gerbils.Select(g => g.Id).ToHashSetAsync(); + var list = new List(); + foreach (var ov in rows.OrderByDescending(o => o.UpdatedAt)) + list.Add(await ToDtoAsync(db, ov, existingIds)); + return TypedResults.Ok(list); + }); + + group.MapGet("/export", async (ApplicationContext db) => + { + var rows = await db.GerbilOverrides.Where(o => o.IsVerified).ToListAsync(); + var export = rows + .OrderBy(o => o.EntityName, StringComparer.Ordinal) + .ThenBy(o => o.GerbilId) + .Select(o => new VerifiedGoldenEntry( + o.GerbilId, o.EntityName, o.VerifiedAt, + GerbilSnapshotService.DeserializeSnapshot(o.SnapshotJson))) + .ToList(); + return TypedResults.Ok(export); + }); + + group.MapGet("/{gerbilId:guid}", async Task, NotFound>> ( + Guid gerbilId, ApplicationContext db) => + { + var ov = await db.GerbilOverrides.FirstOrDefaultAsync(o => o.GerbilId == gerbilId); + if (ov is null) return TypedResults.NotFound(); + return TypedResults.Ok(await ToDtoAsync(db, ov)); + }); + + return app; + } + + private static async Task ToDtoAsync( + ApplicationContext db, GerbilOverride ov, HashSet? existingIds = null) + { + var exists = existingIds?.Contains(ov.GerbilId) ?? await db.Gerbils.AnyAsync(g => g.Id == ov.GerbilId); + var diffs = DeserializeDiff(ov.LastImportDiffJson); + var status = !exists ? "missing" : (diffs.Count > 0 ? "drifted" : "unchanged"); + return new VerifiedGerbilDto( + ov.GerbilId, ov.EntityName, ov.IsVerified, status, ov.VerifiedAt, ov.UpdatedAt, ov.Note, + ExtractFieldKeys(ov.OverrideJson), diffs); + } + + private static IReadOnlyList DeserializeDiff(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return Array.Empty(); + try + { + var raw = JsonSerializer.Deserialize>(json, Json) ?? new(); + return raw.Select(d => new SnapshotDiffDto(d.Path, d.GoldenValue, d.OtherValue)).ToList(); + } + catch (JsonException) { return Array.Empty(); } + } + + private static IReadOnlyList ExtractFieldKeys(string? overrideJson) + { + if (string.IsNullOrWhiteSpace(overrideJson)) return Array.Empty(); + try + { + using var doc = JsonDocument.Parse(overrideJson); + if (doc.RootElement.ValueKind != JsonValueKind.Object) return Array.Empty(); + return doc.RootElement.EnumerateObject().Select(p => p.Name).ToList(); + } + catch (JsonException) { return Array.Empty(); } + } + } +} diff --git a/GerbilManagerWebAPI/Import/GerbilSnapshotService.cs b/GerbilManagerWebAPI/Import/GerbilSnapshotService.cs new file mode 100644 index 0000000..8ead711 --- /dev/null +++ b/GerbilManagerWebAPI/Import/GerbilSnapshotService.cs @@ -0,0 +1,307 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using GerbilManagerWebAPI.Models; +using Microsoft.EntityFrameworkCore; + +namespace GerbilManagerWebAPI.Import +{ + // ── Canonical, human-readable "Akte" snapshot ──────────────────────────── + // Names (not ids) are stored so the golden state stays human-verifiable and robust to + // deterministic-id reseeds. Used for the drift report, the export fixture and the + // regression test. Lists are sorted canonically by the builder so equality is stable. + + public sealed record GerbilSnapshot( + SnapshotSelf Self, + SnapshotParent? Father, + SnapshotParent? Mother, + List Litters); + + public sealed record SnapshotSelf( + string Name, string Gender, string? DateOfBirth, string? DateOfDeath, string? CauseOfDeath, + string? GoHomeDate, string? Genotype, string? SpottingType, string? ColorName, + bool IsResident, bool? IsDeaf, bool IsCastrated, string? OriginBreeder, + string? OriginContactName, string? ReceiverContactName, string? Notes, + List CharacterTraits, string? CharacterNote, string? BirthLitterName); + + public sealed record SnapshotParent(string Name, string? DateOfBirth, string? Genotype); + + public sealed record SnapshotLitter( + string Name, string? Date, string Role, int? TotalBorn, bool ShowInChronicle, + List Children); + + public sealed record SnapshotChild(string Name, string? DateOfBirth, string Gender); + + /// One field-level difference between two snapshots (golden ↔ current/raw import). + public sealed record SnapshotDiff(string Path, string? GoldenValue, string? OtherValue); + + /// + /// Builds the canonical Akte snapshot for a gerbil, the machine-readable freeze map (own fields + /// that "vollständig korrekt" pins), applies an override onto a gerbil, and diffs two snapshots. + /// SHARED by the app (endpoints + ingest freeze) and the regression test so the comparison logic + /// can never drift between the two. + /// + public static class GerbilSnapshotService + { + private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web); + + /// Own-field keys that the "vollständig korrekt" freeze pins. Deliberately excludes + /// Status (lifecycle, derived) and EnclosureId (current location) and LitterId/lineage + /// (abstammung is detect-only). Keys match the JSON serialised in . + public static readonly IReadOnlyList FreezeFields = new[] + { + "name", "gender", "dateOfBirth", "dateOfDeath", "causeOfDeath", "goHomeDate", + "genotype", "spottingType", "colorVarietyId", "isResident", "isDeaf", "isCastrated", + "originBreeder", "originContactId", "receiverContactId", "notes", + "characterTraits", "characterNote", + }; + + // ── Snapshot (human-readable Akte) ─────────────────────────────────── + + public static async Task BuildSnapshotAsync(ApplicationContext db, Guid gerbilId) + { + var g = await db.Gerbils.AsNoTracking().FirstOrDefaultAsync(x => x.Id == gerbilId); + if (g is null) return null; + + var colorName = g.ColorVarietyId is { } cv + ? await db.ColorVarieties.AsNoTracking().Where(c => c.Id == cv).Select(c => c.Name).FirstOrDefaultAsync() + : null; + var originName = g.OriginContactId is { } oc + ? await db.Contacts.AsNoTracking().Where(c => c.Id == oc).Select(c => c.Name).FirstOrDefaultAsync() + : null; + var receiverName = g.ReceiverContactId is { } rc + ? await db.Contacts.AsNoTracking().Where(c => c.Id == rc).Select(c => c.Name).FirstOrDefaultAsync() + : null; + + // Birth litter → parents. + Litter? birthLitter = g.LitterId is { } lid + ? await db.Litters.AsNoTracking().FirstOrDefaultAsync(l => l.Id == lid) + : null; + var father = await BuildParentAsync(db, birthLitter?.FatherId); + var mother = await BuildParentAsync(db, birthLitter?.MotherId); + + // Own litters (as parent) + children. + var ownLitters = await db.Litters.AsNoTracking() + .Where(l => l.FatherId == gerbilId || l.MotherId == gerbilId) + .ToListAsync(); + var ownLitterIds = ownLitters.Select(l => l.Id).ToList(); + var children = ownLitterIds.Count == 0 + ? new List() + : await db.Gerbils.AsNoTracking().Where(c => c.LitterId != null && ownLitterIds.Contains(c.LitterId.Value)).ToListAsync(); + + var litterSnaps = ownLitters + .Select(l => new SnapshotLitter( + l.Name, + Iso(l.Date), + l.FatherId == gerbilId ? "father" : "mother", + l.TotalBorn, + l.ShowInChronicle, + children.Where(c => c.LitterId == l.Id) + .Select(c => new SnapshotChild(c.Name, Iso(c.DateOfBirth), c.Gender.ToString())) + .OrderBy(c => c.DateOfBirth ?? "", StringComparer.Ordinal) + .ThenBy(c => c.Name, StringComparer.Ordinal) + .ThenBy(c => c.Gender, StringComparer.Ordinal) + .ToList())) + .OrderBy(l => l.Date ?? "", StringComparer.Ordinal) + .ThenBy(l => l.Name, StringComparer.Ordinal) + .ToList(); + + var self = new SnapshotSelf( + g.Name, g.Gender.ToString(), Iso(g.DateOfBirth), Iso(g.DateOfDeath), g.CauseOfDeath, + Iso(g.GoHomeDate), g.Genotype, g.SpottingType, colorName, + g.IsResident, g.IsDeaf, g.IsCastrated, g.OriginBreeder, + originName, receiverName, g.Notes, + (g.CharacterTraits ?? new List()).OrderBy(t => t, StringComparer.Ordinal).ToList(), + g.CharacterNote, birthLitter?.Name); + + return new GerbilSnapshot(self, father, mother, litterSnaps); + } + + private static async Task BuildParentAsync(ApplicationContext db, Guid? parentId) + { + if (parentId is not { } pid) return null; + var p = await db.Gerbils.AsNoTracking().Where(x => x.Id == pid) + .Select(x => new { x.Name, x.DateOfBirth, x.Genotype }).FirstOrDefaultAsync(); + return p is null ? null : new SnapshotParent(p.Name, Iso(p.DateOfBirth), p.Genotype); + } + + public static string SerializeSnapshot(GerbilSnapshot snap) => JsonSerializer.Serialize(snap, Json); + + public static GerbilSnapshot? DeserializeSnapshot(string? json) => + string.IsNullOrWhiteSpace(json) ? null : JsonSerializer.Deserialize(json, Json); + + // ── Diff (golden ↔ other) ──────────────────────────────────────────── + + /// Flat, human-readable field diff between a golden snapshot and another (current + /// DB or raw import) snapshot. Empty = identical. + public static List Diff(GerbilSnapshot? golden, GerbilSnapshot? other) + { + var g = golden is null ? new Dictionary() : Flatten(golden); + var o = other is null ? new Dictionary() : Flatten(other); + var diffs = new List(); + foreach (var key in g.Keys.Union(o.Keys).OrderBy(k => k, StringComparer.Ordinal)) + { + var gv = g.GetValueOrDefault(key); + var ov = o.GetValueOrDefault(key); + if (!string.Equals(gv, ov, StringComparison.Ordinal)) + diffs.Add(new SnapshotDiff(key, gv, ov)); + } + return diffs; + } + + private static Dictionary Flatten(GerbilSnapshot s) + { + var d = new Dictionary(StringComparer.Ordinal) + { + ["name"] = s.Self.Name, + ["gender"] = s.Self.Gender, + ["dateOfBirth"] = s.Self.DateOfBirth, + ["dateOfDeath"] = s.Self.DateOfDeath, + ["causeOfDeath"] = s.Self.CauseOfDeath, + ["goHomeDate"] = s.Self.GoHomeDate, + ["genotype"] = s.Self.Genotype, + ["spottingType"] = s.Self.SpottingType, + ["colorName"] = s.Self.ColorName, + ["isResident"] = s.Self.IsResident.ToString(), + ["isDeaf"] = s.Self.IsDeaf?.ToString(), + ["isCastrated"] = s.Self.IsCastrated.ToString(), + ["originBreeder"] = s.Self.OriginBreeder, + ["originContact"] = s.Self.OriginContactName, + ["receiverContact"] = s.Self.ReceiverContactName, + ["notes"] = s.Self.Notes, + ["characterTraits"] = string.Join(", ", s.Self.CharacterTraits), + ["characterNote"] = s.Self.CharacterNote, + ["birthLitter"] = s.Self.BirthLitterName, + ["father"] = FormatParent(s.Father), + ["mother"] = FormatParent(s.Mother), + }; + foreach (var l in s.Litters) + { + var lk = $"wurf[{l.Name} {l.Date}]"; + d[$"{lk}.rolle"] = l.Role; + d[$"{lk}.geboren"] = l.TotalBorn?.ToString(); + d[$"{lk}.inChronik"] = l.ShowInChronicle.ToString(); + foreach (var c in l.Children) + d[$"{lk}.kind[{c.Name} {c.DateOfBirth}]"] = c.Gender; + } + return d; + } + + private static string? FormatParent(SnapshotParent? p) => + p is null ? null : $"{p.Name} (*{p.DateOfBirth}) {p.Genotype}".Trim(); + + // ── Freeze map (machine-readable own-field override) ───────────────── + + /// Builds the full own-field freeze object (all ) from a + /// gerbil's current values. Used by verify (store as OverrideJson) and by the per-field + /// edit-protection diff. + public static JsonObject BuildFreezeObject(Gerbil g) + { + var o = new JsonObject + { + ["name"] = g.Name, + ["gender"] = g.Gender.ToString(), + ["dateOfBirth"] = Iso(g.DateOfBirth), + ["dateOfDeath"] = Iso(g.DateOfDeath), + ["causeOfDeath"] = g.CauseOfDeath, + ["goHomeDate"] = Iso(g.GoHomeDate), + ["genotype"] = g.Genotype, + ["spottingType"] = g.SpottingType, + ["colorVarietyId"] = g.ColorVarietyId?.ToString(), + ["isResident"] = g.IsResident, + ["isDeaf"] = g.IsDeaf, + ["isCastrated"] = g.IsCastrated, + ["originBreeder"] = g.OriginBreeder, + ["originContactId"] = g.OriginContactId?.ToString(), + ["receiverContactId"] = g.ReceiverContactId?.ToString(), + ["notes"] = g.Notes, + ["characterTraits"] = new JsonArray((g.CharacterTraits ?? new List()).Select(t => (JsonNode?)JsonValue.Create(t)).ToArray()), + ["characterNote"] = g.CharacterNote, + }; + return o; + } + + public static string BuildFreezeJson(Gerbil g) => BuildFreezeObject(g).ToJsonString(Json); + + /// Per-field change detection for the PUT edit-protection path. Returns the subset of + /// freeze keys whose value differs between and , + /// as a JsonObject carrying the AFTER values (the ones to pin). Empty if nothing relevant changed. + public static JsonObject ChangedFields(JsonObject before, JsonObject after) + { + var changed = new JsonObject(); + foreach (var key in FreezeFields) + { + var b = before[key]?.ToJsonString() ?? "null"; + var a = after[key]?.ToJsonString() ?? "null"; + if (!string.Equals(b, a, StringComparison.Ordinal)) + changed[key] = after[key] is { } node ? node.DeepClone() : null; + } + return changed; + } + + /// Merge field values into an existing override JSON + /// object (incoming wins). Returns the merged JSON string. + public static string MergeOverrideJson(string? existingJson, JsonObject incoming) + { + var baseObj = ParseObject(existingJson) ?? new JsonObject(); + foreach (var kv in incoming) + baseObj[kv.Key] = kv.Value is { } n ? n.DeepClone() : null; + return baseObj.ToJsonString(Json); + } + + /// Applies a stored override JSON object onto a gerbil, writing only the keys present + /// (the "freeze"). Unknown keys are ignored. + public static void ApplyOverride(Gerbil g, string? overrideJson) + { + var o = ParseObject(overrideJson); + if (o is null) return; + foreach (var kv in o) + { + var v = kv.Value; + switch (kv.Key) + { + case "name": if (Str(v) is { Length: > 0 } nm) g.Name = nm; break; + case "gender": if (Str(v) is { } gd && Enum.TryParse(gd, out var gender)) g.Gender = gender; break; + case "dateOfBirth": g.DateOfBirth = Date(v); break; + case "dateOfDeath": g.DateOfDeath = Date(v); break; + case "causeOfDeath": g.CauseOfDeath = Str(v); break; + case "goHomeDate": g.GoHomeDate = Date(v); break; + case "genotype": g.Genotype = Str(v); break; + case "spottingType": g.SpottingType = Str(v); break; + case "colorVarietyId": g.ColorVarietyId = Guid_(v); break; + case "isResident": g.IsResident = Bool(v) ?? g.IsResident; break; + case "isDeaf": g.IsDeaf = Bool(v); break; + case "isCastrated": g.IsCastrated = Bool(v) ?? g.IsCastrated; break; + case "originBreeder": g.OriginBreeder = Str(v); break; + case "originContactId": g.OriginContactId = Guid_(v); break; + case "receiverContactId": g.ReceiverContactId = Guid_(v); break; + case "notes": g.Notes = Str(v); break; + case "characterTraits": g.CharacterTraits = StrList(v); break; + case "characterNote": g.CharacterNote = Str(v); break; + } + } + } + + // ── JSON helpers ───────────────────────────────────────────────────── + + private static JsonObject? ParseObject(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return null; + try { return JsonNode.Parse(json) as JsonObject; } + catch (JsonException) { return null; } + } + + private static string? Str(JsonNode? n) => n?.GetValueKind() == JsonValueKind.String ? n.GetValue() : null; + private static bool? Bool(JsonNode? n) => n?.GetValueKind() is JsonValueKind.True or JsonValueKind.False ? n!.GetValue() : null; + private static Guid? Guid_(JsonNode? n) => Guid.TryParse(Str(n), out var id) ? id : null; + private static DateOnly? Date(JsonNode? n) => DateOnly.TryParse(Str(n), out var d) ? d : null; + + private static List StrList(JsonNode? n) + { + if (n is JsonArray arr) + return arr.Where(x => x is not null).Select(x => x!.GetValue()).ToList(); + return new List(); + } + + private static string? Iso(DateOnly? d) => d?.ToString("yyyy-MM-dd"); + } +} diff --git a/GerbilManagerWebAPI/Import/IngestResolvedService.cs b/GerbilManagerWebAPI/Import/IngestResolvedService.cs index 04ed41b..8172a3c 100644 --- a/GerbilManagerWebAPI/Import/IngestResolvedService.cs +++ b/GerbilManagerWebAPI/Import/IngestResolvedService.cs @@ -13,6 +13,8 @@ namespace GerbilManagerWebAPI.Import private readonly string _sourceDir; private readonly string _photoRoot; + private static readonly JsonSerializerOptions DiffJson = new(JsonSerializerDefaults.Web); + public IngestResolvedService(ApplicationContext db, IConfiguration config, IWebHostEnvironment? env) { _db = db; @@ -32,11 +34,8 @@ namespace GerbilManagerWebAPI.Import } string jsonContent = await File.ReadAllTextAsync(_resolvedJsonPath); - var jsonOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }; - jsonOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); + var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + jsonOptions.Converters.Add(new JsonStringEnumConverter()); var data = JsonSerializer.Deserialize(jsonContent, jsonOptions); if (data == null) @@ -44,163 +43,185 @@ namespace GerbilManagerWebAPI.Import return "Error: Failed to deserialize resolved_import.json."; } - // 1. Provider-agnostic clean wipe of dependent tables to prevent duplicate keys - if (_db.Database.ProviderName == "Microsoft.EntityFrameworkCore.InMemory") - { - // Break circular references first - foreach (var l in _db.Litters) - { - l.FatherId = null; - l.MotherId = null; - } - foreach (var g in _db.Gerbils) - { - g.LitterId = null; - } - await _db.SaveChangesAsync(); + // ── UPSERT-based ingest (no total wipe) ────────────────────────────────────── + // The ingest NO LONGER wipes everything. Imported rows are matched by their + // (deterministic) id and updated in place; manually-created rows (IsManual=true) and + // user-entered sub-records (weights/health/photos) are never deleted, so the breeder's + // work survives the re-import. Only STALE imported rows (IsManual=false, absent from the + // new payload) are removed — after nulling any manual references to them (with a warning). + // Finally, hand-curated GerbilOverrides are re-applied on top (the "freeze"). + var payloadContactIds = data.Contacts.Select(c => c.Id).ToHashSet(); + var payloadLitterIds = data.Litters.Select(l => l.Id).ToHashSet(); + var payloadGerbilIds = data.Gerbils.Select(g => g.Id).ToHashSet(); + var warnings = new List(); - _db.GerbilPhotos.RemoveRange(_db.GerbilPhotos); - _db.WeightRecords.RemoveRange(_db.WeightRecords); - _db.HealthRecords.RemoveRange(_db.HealthRecords); - _db.SaleContracts.RemoveRange(_db.SaleContracts); - _db.Gerbils.RemoveRange(_db.Gerbils); - _db.Litters.RemoveRange(_db.Litters); - _db.Contacts.RemoveRange(_db.Contacts); - await _db.SaveChangesAsync(); - } - else - { - // Break circular references first using ExecuteUpdateAsync - await _db.Litters.ExecuteUpdateAsync(s => s - .SetProperty(l => l.FatherId, (Guid?)null) - .SetProperty(l => l.MotherId, (Guid?)null)); - - await _db.Gerbils.ExecuteUpdateAsync(s => s - .SetProperty(g => g.LitterId, (Guid?)null)); - - await _db.GerbilPhotos.ExecuteDeleteAsync(); - await _db.WeightRecords.ExecuteDeleteAsync(); - await _db.HealthRecords.ExecuteDeleteAsync(); - await _db.SaleContracts.ExecuteDeleteAsync(); - await _db.Gerbils.ExecuteDeleteAsync(); - await _db.Litters.ExecuteDeleteAsync(); - await _db.Contacts.ExecuteDeleteAsync(); - } - - // 2. Import Contacts (Add new ones, and update roles/details of existing ones) + // 1. Upsert contacts (preserve manual rows + their IsManual flag). var existingContacts = await _db.Contacts.ToDictionaryAsync(c => c.Id); - var addedContactIds = new HashSet(); - int contactsAdded = 0; - int contactsUpdated = 0; + int contactsAdded = 0, contactsUpdated = 0; foreach (var c in data.Contacts) { - if (existingContacts.TryGetValue(c.Id, out var existingContact)) + if (existingContacts.TryGetValue(c.Id, out var ec)) { - existingContact.Name = c.Name; - existingContact.Email = c.Email; - existingContact.Phone = c.Phone; - existingContact.Address = c.Address; - existingContact.Notes = c.Notes; - existingContact.IsBreeder = c.IsBreeder; - existingContact.IsReceiver = c.IsReceiver; - existingContact.NameSuffix = c.NameSuffix; - existingContact.Provenance = c.Provenance; + ec.Name = c.Name; ec.Email = c.Email; ec.Phone = c.Phone; ec.Address = c.Address; + ec.Notes = c.Notes; ec.IsBreeder = c.IsBreeder; ec.IsReceiver = c.IsReceiver; + ec.NameSuffix = c.NameSuffix; ec.Provenance = c.Provenance; contactsUpdated++; } - else if (addedContactIds.Add(c.Id)) + else { + c.IsManual = false; _db.Contacts.Add(c); contactsAdded++; } } await _db.SaveChangesAsync(); - // 3. Pass 1: Insert all Litters with parent references set to null to avoid FK errors - var littersToInsert = new List(); + // 2. Upsert litters (parent FKs reset to null; set in pass 2 to avoid ordering issues). + var existingLitters = await _db.Litters.ToDictionaryAsync(l => l.Id); foreach (var l in data.Litters) { - littersToInsert.Add(new Litter + if (existingLitters.TryGetValue(l.Id, out var el)) { - Id = l.Id, - Name = l.Name, - Date = l.Date, - TotalBorn = l.TotalBorn, - DeathsWithin8Weeks = l.DeathsWithin8Weeks, - FatherId = null, // Set in Pass 2 - MotherId = null, // Set in Pass 2 - ExpectedGoHomeDate = l.ExpectedGoHomeDate, - Notes = l.Notes, - PairingCode = l.PairingCode, - ExternalRef = l.ExternalRef, - LitterLetter = l.LitterLetter, - Provenance = l.Provenance - }); + el.Name = l.Name; el.Date = l.Date; el.TotalBorn = l.TotalBorn; + el.DeathsWithin8Weeks = l.DeathsWithin8Weeks; el.Stillborn = l.Stillborn; + el.ExpectedGoHomeDate = l.ExpectedGoHomeDate; el.Notes = l.Notes; + el.PairingCode = l.PairingCode; el.ExternalRef = l.ExternalRef; + el.LitterLetter = l.LitterLetter; el.Provenance = l.Provenance; + el.ShowInChronicle = l.ShowInChronicle; + el.FatherId = null; el.MotherId = null; + } + else + { + _db.Litters.Add(new Litter + { + Id = l.Id, Name = l.Name, Date = l.Date, TotalBorn = l.TotalBorn, + DeathsWithin8Weeks = l.DeathsWithin8Weeks, Stillborn = l.Stillborn, FatherId = null, MotherId = null, + ExpectedGoHomeDate = l.ExpectedGoHomeDate, Notes = l.Notes, PairingCode = l.PairingCode, + ExternalRef = l.ExternalRef, LitterLetter = l.LitterLetter, Provenance = l.Provenance, + ShowInChronicle = l.ShowInChronicle, IsManual = false, + }); + } } - _db.Litters.AddRange(littersToInsert); await _db.SaveChangesAsync(); - // 4. Pass 1: Insert all Gerbils with LitterId set to null to avoid FK errors - var gerbilsToInsert = new List(); + // 3. Upsert gerbils (LitterId reset to null; set in pass 2). Manual rows keep IsManual=true + // and are never matched here (their random ids never collide with deterministic import ids). + var existingGerbils = await _db.Gerbils.ToDictionaryAsync(g => g.Id); foreach (var g in data.Gerbils) { - var importedGerbil = new Gerbil + Gerbil target; + if (existingGerbils.TryGetValue(g.Id, out var eg)) { - Id = g.Id, - Name = g.Name, - Gender = g.Gender, - Status = g.Status, - LitterId = null, // Set in Pass 2 - OriginContactId = g.OriginContactId, - ReceiverContactId = g.ReceiverContactId, - EnclosureId = g.EnclosureId, - ColorVarietyId = g.ColorVarietyId, - DateOfBirth = g.DateOfBirth, - DateOfDeath = g.DateOfDeath, - CauseOfDeath = g.CauseOfDeath, - GoHomeDate = g.GoHomeDate, - Genotype = g.Genotype, - Notes = g.Notes, - ImportSource = g.ImportSource, - ExternalRef = g.ExternalRef, - RawImportData = g.RawImportData, - Provenance = g.Provenance, - OriginBreeder = g.OriginBreeder, - NameSearch = g.NameSearch, - CharacterTraits = g.CharacterTraits, - CharacterNote = g.CharacterNote, - IsDeaf = g.IsDeaf, - IsResident = g.IsResident - }; - GerbilStatusService.Apply(importedGerbil, DateOnly.FromDateTime(DateTime.UtcNow)); - gerbilsToInsert.Add(importedGerbil); + eg.Name = g.Name; eg.Gender = g.Gender; eg.Status = g.Status; + eg.OriginContactId = g.OriginContactId; eg.ReceiverContactId = g.ReceiverContactId; + eg.EnclosureId = g.EnclosureId; eg.ColorVarietyId = g.ColorVarietyId; + eg.DateOfBirth = g.DateOfBirth; eg.DateOfDeath = g.DateOfDeath; eg.CauseOfDeath = g.CauseOfDeath; + eg.GoHomeDate = g.GoHomeDate; eg.Genotype = g.Genotype; eg.Notes = g.Notes; + eg.ImportSource = g.ImportSource; eg.ExternalRef = g.ExternalRef; eg.RawImportData = g.RawImportData; + eg.Provenance = g.Provenance; eg.OriginBreeder = g.OriginBreeder; + eg.CharacterTraits = g.CharacterTraits; eg.CharacterNote = g.CharacterNote; + eg.IsDeaf = g.IsDeaf; eg.IsResident = g.IsResident; eg.LitterId = null; + eg.BirthOrder = g.BirthOrder; + target = eg; + } + else + { + var ng = new Gerbil + { + Id = g.Id, Name = g.Name, Gender = g.Gender, Status = g.Status, LitterId = null, + OriginContactId = g.OriginContactId, ReceiverContactId = g.ReceiverContactId, + EnclosureId = g.EnclosureId, ColorVarietyId = g.ColorVarietyId, + DateOfBirth = g.DateOfBirth, DateOfDeath = g.DateOfDeath, CauseOfDeath = g.CauseOfDeath, + GoHomeDate = g.GoHomeDate, Genotype = g.Genotype, Notes = g.Notes, + ImportSource = g.ImportSource, ExternalRef = g.ExternalRef, RawImportData = g.RawImportData, + Provenance = g.Provenance, OriginBreeder = g.OriginBreeder, NameSearch = g.NameSearch, + CharacterTraits = g.CharacterTraits, CharacterNote = g.CharacterNote, + IsDeaf = g.IsDeaf, IsResident = g.IsResident, IsManual = false, + BirthOrder = g.BirthOrder, + }; + _db.Gerbils.Add(ng); + target = ng; + } + GerbilStatusService.Apply(target, DateOnly.FromDateTime(DateTime.UtcNow)); } - _db.Gerbils.AddRange(gerbilsToInsert); await _db.SaveChangesAsync(); - // 5. Pass 2: Set the actual foreign key relationships - // Update Litters with their parent animal IDs + // 4. Pass 2: set the payload FK relationships (only for payload rows; manual rows untouched). var littersInDb = await _db.Litters.ToDictionaryAsync(l => l.Id); foreach (var l in data.Litters) - { - if (littersInDb.TryGetValue(l.Id, out var dbLitter)) - { - dbLitter.FatherId = l.FatherId; - dbLitter.MotherId = l.MotherId; - } - } - - // Update Gerbils with their actual LitterId + if (littersInDb.TryGetValue(l.Id, out var dl)) { dl.FatherId = l.FatherId; dl.MotherId = l.MotherId; } var gerbilsInDb = await _db.Gerbils.ToDictionaryAsync(g => g.Id); foreach (var g in data.Gerbils) + if (gerbilsInDb.TryGetValue(g.Id, out var dg)) dg.LitterId = g.LitterId; + await _db.SaveChangesAsync(); + + // 5. Remove STALE imported rows (IsManual=false, no longer in the payload). Null any + // dangling references from surviving rows first; warn when a MANUAL row is affected. + var allGerbils = await _db.Gerbils.ToListAsync(); + var allLitters = await _db.Litters.ToListAsync(); + var allContacts = await _db.Contacts.ToListAsync(); + var gerbilNameById = allGerbils.ToDictionary(x => x.Id, x => x.Name); + var litterNameById = allLitters.ToDictionary(x => x.Id, x => x.Name); + var contactNameById = allContacts.ToDictionary(x => x.Id, x => x.Name); + + var staleGerbilIds = allGerbils.Where(g => !g.IsManual && !payloadGerbilIds.Contains(g.Id)).Select(g => g.Id).ToHashSet(); + var staleLitterIds = allLitters.Where(l => !l.IsManual && !payloadLitterIds.Contains(l.Id)).Select(l => l.Id).ToHashSet(); + var staleContactIds = allContacts.Where(c => !c.IsManual && !payloadContactIds.Contains(c.Id)).Select(c => c.Id).ToHashSet(); + + if (staleGerbilIds.Count > 0) { - if (gerbilsInDb.TryGetValue(g.Id, out var dbGerbil)) + foreach (var l in allLitters) { - dbGerbil.LitterId = g.LitterId; + if (l.FatherId is { } f && staleGerbilIds.Contains(f)) + { + if (l.IsManual) warnings.Add($"Manueller Wurf „{l.Name}\" verwies auf importiertes Elterntier „{gerbilNameById.GetValueOrDefault(f, "?")}\" (Vater), das im Import nicht mehr existiert — Elternteil entfernt."); + l.FatherId = null; + } + if (l.MotherId is { } m && staleGerbilIds.Contains(m)) + { + if (l.IsManual) warnings.Add($"Manueller Wurf „{l.Name}\" verwies auf importiertes Elterntier „{gerbilNameById.GetValueOrDefault(m, "?")}\" (Mutter), das im Import nicht mehr existiert — Elternteil entfernt."); + l.MotherId = null; + } } } + if (staleContactIds.Count > 0) + { + foreach (var g in allGerbils) + { + if (g.OriginContactId is { } o && staleContactIds.Contains(o)) + { + if (g.IsManual) warnings.Add($"Manuelles Tier „{g.Name}\" verwies auf Kontakt „{contactNameById.GetValueOrDefault(o, "?")}\" (Herkunft), der im Import nicht mehr existiert — Zuordnung entfernt."); + g.OriginContactId = null; + } + if (g.ReceiverContactId is { } r && staleContactIds.Contains(r)) + { + if (g.IsManual) warnings.Add($"Manuelles Tier „{g.Name}\" verwies auf Kontakt „{contactNameById.GetValueOrDefault(r, "?")}\" (Abnehmer), der im Import nicht mehr existiert — Zuordnung entfernt."); + g.ReceiverContactId = null; + } + } + } + if (staleLitterIds.Count > 0) + { + // Gerbil.LitterId FK is SetNull on delete → nulled automatically when the litter is + // removed; we only surface a warning for affected MANUAL animals. + foreach (var g in allGerbils) + if (g.IsManual && g.LitterId is { } lid && staleLitterIds.Contains(lid)) + warnings.Add($"Manuelles Tier „{g.Name}\" war im Wurf „{litterNameById.GetValueOrDefault(lid, "?")}\" geboren, der im Import nicht mehr existiert — Wurf-Zuordnung entfernt."); + } + await _db.SaveChangesAsync(); + + int gerbilsDeleted = staleGerbilIds.Count, littersDeleted = staleLitterIds.Count, contactsDeleted = staleContactIds.Count; + if (staleGerbilIds.Count > 0) _db.Gerbils.RemoveRange(allGerbils.Where(g => staleGerbilIds.Contains(g.Id))); + if (staleLitterIds.Count > 0) _db.Litters.RemoveRange(allLitters.Where(l => staleLitterIds.Contains(l.Id))); + if (staleContactIds.Count > 0) _db.Contacts.RemoveRange(allContacts.Where(c => staleContactIds.Contains(c.Id))); + await _db.SaveChangesAsync(); + + // 6. Photos: replace photos of IMPORTED gerbils (manual gerbils' photos survive). Copy files. + var manualGerbilIds = allGerbils.Where(g => g.IsManual).Select(g => g.Id).ToHashSet(); + var allPhotos = await _db.GerbilPhotos.ToListAsync(); + _db.GerbilPhotos.RemoveRange(allPhotos.Where(p => !manualGerbilIds.Contains(p.GerbilId))); await _db.SaveChangesAsync(); - // 6. Insert all Photos & copy physical files Directory.CreateDirectory(_photoRoot); foreach (var p in data.GerbilPhotos) { @@ -211,14 +232,8 @@ namespace GerbilManagerWebAPI.Import if (File.Exists(src)) { var dest = Path.Combine(_photoRoot, p.FileName); - try - { - File.Copy(src, dest, overwrite: true); - } - catch (Exception ex) - { - Console.WriteLine($"Warning: Failed to copy photo {src} to {dest}: {ex.Message}"); - } + try { File.Copy(src, dest, overwrite: true); } + catch (Exception ex) { Console.WriteLine($"Warning: Failed to copy photo {src} to {dest}: {ex.Message}"); } } else { @@ -233,24 +248,18 @@ namespace GerbilManagerWebAPI.Import FileName = p.FileName, Caption = p.Caption, SortOrder = p.SortOrder, - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = DateTimeOffset.UtcNow, }); } await _db.SaveChangesAsync(); - // 7. Insert imported Abgabeverträge (SaleContract + join rows). - // The table was wiped above, so these are recreated on every ingest - // (they survive only by being part of the payload). FK-safe: contacts - // and gerbils are already inserted. We skip rows whose buyer contact - // is unknown and skip individual animal links to unknown gerbils - // (defensive — the resolver should never emit those). - // No .docx is copied: the source files live on the network share and - // are not staged into Contracts:RootPath, so the Word download 404s - // gracefully (the endpoint already returns NotFound) while the PDF is - // regenerated from the data. The DTO exposes HasFile=false for these - // so the frontend hides the Word button. - int contractsAdded = 0; - int contractAnimalsSkipped = 0; + // 7. Imported Abgabeverträge: recreate from payload (unchanged behaviour — the imported set + // is part of the payload). Manual sale-contract protection is a separate follow-up. + var scToRemove = await _db.SaleContracts.ToListAsync(); + _db.SaleContracts.RemoveRange(scToRemove); + await _db.SaveChangesAsync(); + + int contractsAdded = 0, contractAnimalsSkipped = 0; if (data.SaleContracts.Count > 0) { var contactIds = await _db.Contacts.Select(c => c.Id).ToHashSetAsync(); @@ -258,8 +267,8 @@ namespace GerbilManagerWebAPI.Import var seenContractIds = new HashSet(); foreach (var sc in data.SaleContracts) { - if (!seenContractIds.Add(sc.Id)) continue; // dedupe within payload - if (!contactIds.Contains(sc.ContactId)) continue; // unknown buyer + if (!seenContractIds.Add(sc.Id)) continue; + if (!contactIds.Contains(sc.ContactId)) continue; var animals = new List(); foreach (var gid in (sc.Animals ?? new List()).Distinct()) @@ -284,7 +293,36 @@ namespace GerbilManagerWebAPI.Import await _db.SaveChangesAsync(); } - return $"Ingestion successful! Imported {contactsAdded} contacts ({contactsUpdated} updated), {data.Litters.Count} litters, {data.Gerbils.Count} gerbils, {data.GerbilPhotos.Count} photos, and {contractsAdded} sale contracts ({contractAnimalsSkipped} animal links to unknown gerbils skipped)."; + // 8. FREEZE: capture the raw-import snapshot for verified overrides (BEFORE applying the + // freeze), then re-apply every override on top of the fresh import so the breeder's + // curated values win. Overrides survive re-ingest by being their own (FK-free) table. + var overrides = await _db.GerbilOverrides.ToListAsync(); + foreach (var ov in overrides.Where(o => o.IsVerified)) + { + var raw = await GerbilSnapshotService.BuildSnapshotAsync(_db, ov.GerbilId); + if (raw is null) continue; // verified animal vanished from import → surfaced as "missing" in the UI + ov.LastImportSnapshotJson = GerbilSnapshotService.SerializeSnapshot(raw); + var golden = GerbilSnapshotService.DeserializeSnapshot(ov.SnapshotJson); + var diff = GerbilSnapshotService.Diff(golden, raw); + ov.LastImportDiffJson = diff.Count == 0 ? null : JsonSerializer.Serialize(diff, DiffJson); + } + var gerbilsById = await _db.Gerbils.ToDictionaryAsync(g => g.Id); + int overridesApplied = 0; + foreach (var ov in overrides) + { + if (gerbilsById.TryGetValue(ov.GerbilId, out var gg)) + { + GerbilSnapshotService.ApplyOverride(gg, ov.OverrideJson); + overridesApplied++; + } + } + await _db.SaveChangesAsync(); + + var warnPart = warnings.Count > 0 ? $" {warnings.Count} warning(s): {string.Join(" | ", warnings)}" : ""; + return $"Ingestion successful! Contacts +{contactsAdded}/~{contactsUpdated} (−{contactsDeleted} stale), " + + $"{data.Litters.Count} litters (−{littersDeleted} stale), {data.Gerbils.Count} gerbils (−{gerbilsDeleted} stale), " + + $"{data.GerbilPhotos.Count} photos, {contractsAdded} sale contracts ({contractAnimalsSkipped} animal links skipped), " + + $"{overridesApplied} override(s) applied (freeze).{warnPart}"; } } @@ -319,7 +357,7 @@ namespace GerbilManagerWebAPI.Import public required string FileName { get; set; } public string? Caption { get; set; } public int SortOrder { get; set; } - + [JsonPropertyName("_source_path")] public string? SourcePath { get; set; } } diff --git a/GerbilManagerWebAPI/Import/Rpro3/Rpro3Decisions.cs b/GerbilManagerWebAPI/Import/Rpro3/Rpro3Decisions.cs index 119f76a..d89eb86 100644 --- a/GerbilManagerWebAPI/Import/Rpro3/Rpro3Decisions.cs +++ b/GerbilManagerWebAPI/Import/Rpro3/Rpro3Decisions.cs @@ -105,6 +105,11 @@ namespace GerbilManagerWebAPI.Import.Rpro3 public string? Color { get; set; } public string? Dob { get; set; } // ISO yyyy-MM-dd public string? Origin { get; set; } + /// Roher Gencode (Fcode, z. B. „aa Cc[h] DD ee Gg P- spsp"). Überschreibt den + /// automatisch aus den Cluster-Mitgliedern gewählten Gencode — nötig, wenn sich die + /// Varianten im Gencode unterscheiden (z. B. C-Locus c[h] vs. c[chm]) und die Züchterin + /// den korrekten Genotyp vorgibt. + public string? Genotype { get; set; } public bool? Resident { get; set; } /// Zusatz, der an die Notizen des Tiers angehängt wird (z. B. abweichendes DOB). public string? Note { get; set; } diff --git a/GerbilManagerWebAPI/Import/Rpro3/Rpro3ImportService.cs b/GerbilManagerWebAPI/Import/Rpro3/Rpro3ImportService.cs index e65cc05..be95a1c 100644 --- a/GerbilManagerWebAPI/Import/Rpro3/Rpro3ImportService.cs +++ b/GerbilManagerWebAPI/Import/Rpro3/Rpro3ImportService.cs @@ -427,6 +427,7 @@ namespace GerbilManagerWebAPI.Import.Rpro3 { if (!string.IsNullOrWhiteSpace(ov.Color)) farbe = ov.Color; if (!string.IsNullOrWhiteSpace(ov.Origin)) origin = ov.Origin; + if (!string.IsNullOrWhiteSpace(ov.Genotype)) fcode = ov.Genotype; if (!string.IsNullOrWhiteSpace(ov.Dob) && DateOnly.TryParse(ov.Dob, out var od)) dob = od; } diff --git a/GerbilManagerWebAPI/Import/Rpro3/rpro3-decisions.json b/GerbilManagerWebAPI/Import/Rpro3/rpro3-decisions.json index 7975b8a..f211e73 100644 --- a/GerbilManagerWebAPI/Import/Rpro3/rpro3-decisions.json +++ b/GerbilManagerWebAPI/Import/Rpro3/rpro3-decisions.json @@ -256,17 +256,14 @@ { "name": "Pinto", "ticket": "08d00bca-70c0-4c14-88c8-0d3181313ba3", - "same": [ + "different": [ + [ + "385" + ], [ - "385", "u3709" ] - ], - "fields": { - "385": { - "note": "Angaben/Schenkung von Variante A (eigenes Tier) übernehmen." - } - } + ] }, { "name": "Jasper", @@ -298,6 +295,1084 @@ "u2634" ] ] + }, + { + "name": "Fred", + "ticket": "da9899f0-0947-406f-8997-8ba2b4fafed5", + "different": [ + [ + "u2151" + ], + [ + "u2122" + ] + ] + }, + { + "name": "Sonja", + "ticket": "ea3e7032-3e42-4da0-aa33-97ec0961c763", + "different": [ + [ + "u2133" + ], + [ + "u1101" + ] + ] + }, + { + "name": "MilkyWay", + "ticket": "6d86e7a3-870e-4fc7-92ad-8419e6fecc15", + "same": [ + [ + "u1031", + "u1019" + ] + ], + "fields": { + "u1031": { + "genotype": "aa Cc[h] DD ee Gg P- spsp", + "color": "Kohlfuchs-Hell", + "origin": "Claudia Stettler", + "note": "Beide Einträge sind dasselbe Tier; korrekter Gencode ist Cc[h] (Variante mit Herkunft Claudia Stettler). Nachzucht Amy + Ultra zusammengeführt." + } + } + }, + { + "name": "Fijou", + "ticket": "36864756-3a21-4119-aca1-d2f6bb6cd2aa", + "same": [ + [ + "u1389", + "u3379" + ] + ], + "fields": { + "u1389": { + "color": "Agouti-Schecke", + "note": "Alle Fijou-Einträge sind dasselbe Tier (männlich, Vater von Ilvy, Partner BB3). Farbe Agouti-Schecke." + } + } + }, + { + "name": "Sherin", + "ticket": "8c69d21e-2cd6-4017-b4a1-89827f6f1f04", + "same": [ + [ + "u2233", + "u2605" + ] + ] + }, + { + "name": "Trixie", + "ticket": "a2469977-fa01-44af-8fd0-9e349f7aa03d", + "same": [ + [ + "u618", + "u2967" + ] + ] + }, + { + "name": "Daisy (Mutter von Trixie)", + "ticket": "a2469977-fa01-44af-8fd0-9e349f7aa03d", + "same": [ + [ + "u619", + "u2968" + ] + ], + "fields": { + "u619": { + "color": "Platin", + "genotype": "aa Cc[chm] DD Ee GG pp spsp", + "note": "Laut Züchterin: Platin mit C-Locus Cc[chm] (Trixies Mutter, *2010-02-15). Betrifft NICHT die anderen Daisy-Einträge (u667/u6367)." + } + } + }, + { + "name": "Cerato", + "ticket": "a86ecf19-b69d-478e-9581-de4ee16bdc96", + "same": [ + [ + "u377", + "u3704" + ] + ], + "fields": { + "u377": { + "color": "Algierfuchs", + "genotype": "Aa CC DD ee Gg P- spsp", + "note": "Beide Cerato-Einträge sind dasselbe Tier; Farbe von Variante A (Algierfuchs, *2007-04-02). Auch die beiden Lua-Einträge (Partner) sind dasselbe Tier — beim RennmausPro-Import ebenfalls zusammenführen (Lua-rids dann via rpro3_lookup 'Lua' bestimmen)." + } + } + }, + { + "name": "Lina", + "ticket": "5f3b7c30-4687-4273-82a5-689a27426001", + "different": [ + [ + "u1382" + ], + [ + "u520" + ] + ], + "fields": { + "u520": { + "resident": true, + "note": "Eigenes, ehemaliges Zuchttier (Lina von den kleinen Chaoten, *14.06.2012 Kohlfuchs, Eltern Eddy/Vee); verstorben 25.08.2015 — als eigenes verstorbenes Zuchttier führen. Variante A (u1382, *15.06.2012 Marder, von Privat) ist ein getrenntes Fremdtier." + } + } + }, + { + "name": "Caruso", + "ticket": "abfa9599-8c9c-46d2-82fb-6fb4642f7978", + "same": [ + [ + "u2806", + "u6885", + "u1001" + ] + ], + "fields": { + "u2806": { + "color": "Colourpoint-Kohlfuchs (CP-Kohlfuchs)", + "origin": "Clan of Panthera", + "note": "Alle Caruso-Einträge sind dasselbe Tier (gleiche Eltern Magnus/Morigane, gleicher Geburtstag, gemeinsamer Wurf 25.05.2011 mit Joy). Herkunft Clan of Panthera; Farbe CP-Kohlfuchs (c[chm]c[chm])." + } + } + }, + { + "name": "Ketchup", + "ticket": "b6432938-e2b4-423e-b387-748b2d7fa34c", + "same": [ + [ + "u2430", + "u3127", + "u3287", + "u3909", + "u587" + ] + ] + }, + { + "name": "Tony", + "ticket": "ac889eca-297c-406e-a719-63f2b99b541a", + "different": [ + [ + "u6434" + ], + [ + "u270" + ] + ], + "fields": { + "u6434": { + "resident": true, + "note": "Eigenes, ehemaliges Zuchttier (Tony v.d. Kleinen Chaoten, *16.09.2015, Partner Cooky, Nachzucht Porenta); verstorben 29.04.2019 — als eigenes verstorbenes Zuchttier führen. Variante B (u270, Herkunft Privatzucht) ist ein getrenntes Fremdtier." + } + } + }, + { + "name": "Luke", + "ticket": "1ca7bfc5-86d8-4911-b164-c51a0176b8c8", + "different": [ + [ + "u6815" + ], + [ + "u19" + ] + ], + "fields": { + "u6815": { + "resident": true, + "note": "Eigenes, ehemaliges (verstorbenes) Zuchttier (Luke, *19.06.2016, Polarfuchs, von den Kleinen Chaoten, Eltern Piet/Violet). Variante B (u19, *17.04.2005, Algierfuchsschimmel) ist ein getrenntes Fremdtier." + } + } + }, + { + "name": "Pan", + "ticket": "d5f34d99-c036-4054-95a7-4271e70f9e39", + "same": [ + [ + "u2346", + "u2420", + "u2735", + "u3874", + "u6014" + ] + ], + "different": [ + [ + "u2346" + ], + [ + "u6926" + ] + ], + "fields": { + "u2346": { + "origin": "Sarah Schramm", + "note": "Varianten A und B sind dasselbe Tier (WFNZ-Agouti, Herkunft Sarah Schramm). C (u6926, Schwarz, von den Kleinen Chaoten) ist ein anderes Tier." + }, + "u6926": { + "resident": true, + "note": "Eigenes, ehemaliges (verstorbenes) Zuchttier (Pan, *09.12.2014, Schwarz, von den Kleinen Chaoten, Eltern Oliver/Mia). Getrennt von den WFNZ-Agouti-Einträgen (Sarah Schramm)." + } + } + }, + { + "name": "Bärchen", + "ticket": "da1b84fd-9246-466d-aad1-fef9584f913f", + "same": [ + [ + "u3355", + "u6924", + "u1408" + ] + ], + "different": [ + [ + "u3355" + ], + [ + "u2900" + ] + ], + "fields": { + "u3355": { + "note": "Varianten A (u3355, Agouti) und C (u1408, WFNZ Agouti) sind dasselbe Tier (*01.03.2011). B (u2900, *06.04.2012) ist ein anderes Tier." + } + } + }, + { + "name": "Romina", + "ticket": "0bea63f5-964c-4e59-b296-6aff0dfdc631", + "same": [ + [ + "u2897", + "u3348", + "u6789", + "u1405", + "u6917" + ] + ], + "fields": { + "u2897": { + "dob": "2011-05-01", + "note": "Alle Romina-Einträge sind dasselbe Tier. Geburtstag: 01.05.2011. Eine Variante (C, u6917) war in RennmausPro mit abweichendem Geburtsdatum 01.03.2011 gespeichert." + } + } + }, + { + "name": "Herakles", + "ticket": "ccbffb08-a25a-4041-8e21-8f2723274e8c", + "same": [ + [ + "u2909", + "u6801", + "u1377" + ] + ], + "different": [ + [ + "u2909" + ], + [ + "u2115" + ] + ], + "fields": { + "u2909": { + "note": "Varianten B (u2909, Schimmel) und C (u1377, Orangeschimmel) sind dasselbe Tier (*14.08.2012, Clan of sweet little mouse). A (u2115, *04.01.2005, Zobel-Schecke, Herkunft Stephan) ist ein anderes Tier." + } + } + }, + { + "name": "Molly", + "ticket": "f6874415-96b3-4201-95b8-bb74ccf17bab", + "same": [ + [ + "u4013", + "u1367" + ] + ], + "different": [ + [ + "u4013" + ], + [ + "u7418" + ] + ], + "fields": { + "u4013": { + "note": "Varianten A (u4013, Marder-Hell-Schecke) und C (u1367, Siam-Schecke) sind dasselbe Tier (*01.08.2009, Clan of little Snugglers). B (u7418, *21.11.2010, Colourpoint-Agouti-Hell, Gerbilol) ist ein anderes Tier." + } + } + }, + { + "name": "Flora", + "ticket": "783821d9-f38e-4cd3-ae1b-0446398e035d", + "same": [ + [ + "u2889", + "u3343", + "u6781", + "u4006" + ] + ], + "different": [ + [ + "u2889" + ], + [ + "u1198" + ] + ], + "fields": { + "u2889": { + "note": "Varianten A (u2889) und C (u4006) sind dasselbe Tier (Anthrazit, Svenja Müller, ca. *30.12.2010). B (u1198, *23.07.2004, dilute-Agouti-Schecke, Clan of Refuge) ist ein anderes Tier." + } + } + }, + { + "name": "Willow", + "ticket": "0b8f1e1f-f768-42e9-94c7-be844978888a", + "same": [ + [ + "u2185", + "u2377", + "u2552", + "u2818", + "u1073", + "u1612", + "u1957", + "u3514" + ] + ], + "different": [ + [ + "u2185" + ], + [ + "u6941" + ] + ], + "fields": { + "u2185": { + "color": "Zobel-Hell-Schecke", + "note": "Varianten A und B sind dasselbe Tier (Zobel-Hell-Schecke). C (u6941, *16.01.2017, Schwarz-Kragenschecke DP, Clan of Black Forest = Willow of Black Forest) ist ein anderes Tier und bleibt reiner Zuchtvorfahre." + }, + "u6941": { + "resident": false, + "note": "Reiner Zuchtvorfahre (Willow of Black Forest); laut Züchterin kein eigenes Zuchttier." + } + } + }, + { + "name": "Olaf", + "ticket": "0d5d388c-2951-4900-a3cb-a9421bc4bdb5", + "same": [ + [ + "u2856", + "u2928", + "u6748", + "u335" + ] + ], + "different": [ + [ + "u2856" + ], + [ + "u7342" + ] + ], + "fields": { + "u2856": { + "dob": "2008-02-24", + "color": "Colourpoint-Kohlfuchs-Hell", + "origin": "Miris Renner", + "note": "Varianten A und B sind dasselbe Tier (gemeinsame Tochter Odette *23.03.2010 mit Lilli). Abweichendes Geburtsdatum in RennmausPro: 24.12.2008 (laut Variante B). C (u7342, Anthrazit, Nachzucht Polly mit Nikita) ist ein anderes Tier." + } + } + }, + { + "name": "Aphrodite", + "ticket": "82091dc8-4c30-40c9-b515-461e96b3f318", + "different": [ + [ + "u421" + ], + [ + "u2114" + ], + [ + "u155" + ] + ] + }, + { + "name": "Joy", + "ticket": "f41cee14-8c90-467b-b6ae-01e51fc2580e", + "same": [ + [ + "u1425", + "u6914" + ] + ], + "different": [ + [ + "u1506" + ], + [ + "u2103" + ], + [ + "u1425" + ] + ], + "fields": { + "u1425": { + "note": "Varianten C (u1425) und D (u6914) sind dasselbe Tier (Marder-Hell-Schecke, *22.12.2008, Freiberger Rennmauszucht). A (Schwarz-Schecke) und B (Platin) sind jeweils andere Tiere." + } + } + }, + { + "name": "Aisha", + "ticket": "0c1cd459-7783-4495-8438-515731fd0cde", + "same": [ + [ + "u2137", + "u2272", + "u2490", + "u2659", + "u479", + "u1171", + "u1191", + "u2055" + ] + ], + "different": [ + [ + "u2137" + ], + [ + "u896" + ], + [ + "u2630" + ] + ], + "fields": { + "u2137": { + "note": "Varianten A (WFNZ-Agouti, Christine Enders) und C (Agouti, Rastatter Fellnasen) sind dasselbe Tier. B (u896, Blau, von Alicia) und D (u2630, Marder-Hell-Schecke DP) sind jeweils andere Tiere." + } + } + }, + { + "name": "Balu", + "ticket": "f0edd31c-eaa5-4cb6-9a42-4f2d54cd8ae5", + "same": [ + [ + "u413", + "u2955", + "u3702" + ] + ], + "different": [ + [ + "u413" + ], + [ + "u3376" + ], + [ + "u211" + ] + ], + "fields": { + "u413": { + "dob": "2007-03-20", + "note": "Varianten B, C und D sind dasselbe Tier (Marder-Hell, Nachzucht mit Aphrodite). Geburtstag: 20.03.2007. Abweichende Geburtsdaten in RennmausPro: 20.03.2008 (B) und 29.03.2007 (D). A (u3376, Schwarz) und E (u211, Blaufuchs) sind jeweils andere Tiere." + } + } + }, + { + "name": "Luna", + "ticket": "e1452ca1-92de-4070-9c7e-8e2e0d37d266", + "different": [ + [ + "u43" + ], + [ + "u624" + ], + [ + "u959" + ], + [ + "u283" + ], + [ + "u7532" + ], + [ + "u241" + ] + ] + }, + { + "name": "Lilo", + "ticket": "6456ac2c-d64e-4747-95ae-712c14b7342d", + "same": [ + [ + "381", + "u6944", + "u6959" + ] + ], + "fields": { + "381": { + "resident": false, + "note": "Lilo (*04.11.2018, Algierfuchs) und ihre Zuchtpartner (Stich, Rubeus Hagrid) sowie deren Vorfahren sind reine Zuchtvorfahren — kein eigenes Zuchttier. Ausnahme: wo „von den Kleinen Chaoten“ steht, waren die jeweiligen Eltern eigene Zuchttiere. Lilos vier Jungtiere Dorie, Marlin, Giulietta (Giulie) und Jimniny sind dagegen eigene, ehemalige (verstorbene) Zuchttiere und entsprechend als Bestandstiere zu führen." + } + } + }, + { + "name": "Ria", + "ticket": "831a9f5f-55b0-4ecf-b0d9-199697d555d9", + "same": [ + [ + "u2925", + "u326" + ] + ], + "fields": { + "u2925": { + "color": "Marder", + "dob": "2010-11-05", + "note": "Beide Ria-Einträge sind dasselbe Tier (gleiche Eltern Arthur/Odette, Partner Romeo). Korrekte Farbe: Marder; Geburtstag: 05.11.2010. Abweichende Angaben in RennmausPro (Marder-Schecke / 05.11.2011) nicht übernehmen." + } + } + }, + { + "name": "Zorro", + "ticket": "9fb78478-aa20-4530-8fba-d1663c28b8d1", + "same": [ + [ + "u2251", + "u2392", + "u2604", + "u3052", + "u1110", + "u1653", + "u1994", + "u3551" + ] + ], + "different": [ + [ + "u2251" + ], + [ + "u6442" + ] + ], + "fields": { + "u2251": { + "note": "Varianten A und B sind dasselbe Tier. C (u6442, *10.08.2014, Anthrazit-Schecke, von den Kleinen Chaoten) ist ein anderes Tier." + }, + "u6442": { + "resident": true, + "note": "Eigenes, ehemaliges (verstorbenes) Zuchttier (Zorro, *10.08.2014, Anthrazit-Schecke, von den Kleinen Chaoten); verstorben 21.11.2017. Getrennt von den Varianten A/B." + } + } + }, + { + "name": "WFNZ", + "ticket": "af5d505a-77ef-4971-9c43-02e0c320a8a0", + "same": [ + [ + "u2249", + "u2390", + "u2602", + "u3048", + "u7485", + "u7486" + ] + ], + "fields": { + "u2249": { + "excludeFromInbreeding": true, + "note": "Sammelbegriff/Sonderfall (wie „unbekannt“ oder „privat“) für eine Tiergruppe — kein einzelnes Tier. Einmal in der Datenbank führen, aber von der Inzuchtberechnung ausschließen." + } + } + }, + { + "name": "Cloud", + "ticket": "d6ab5a8b-2bb1-4d8a-95f0-181bb29f4721", + "same": [ + [ + "u3446", + "u2644" + ] + ] + }, + { + "name": "Zuleika", + "ticket": "8005f184-1465-4e10-a0c8-216b93b7ed4b", + "same": [ + [ + "u2852", + "u710" + ] + ], + "fields": { + "u6479": { + "father": "Xtra Bounty" + } + } + }, + { + "name": "Sasu", + "ticket": "f71900b4-e339-42e9-bf65-a456cd04d956", + "same": [ + [ + "u77", + "u391", + "u927", + "u1550" + ] + ], + "fields": { + "u77": { + "origin": "Clan of Melody (Züchterin: Birmie)" + } + } + }, + { + "name": "Caspar", + "ticket": "f71900b4-e339-42e9-bf65-a456cd04d956", + "same": [ + [ + "u76", + "u928", + "u1542" + ] + ] + }, + { + "name": "Daisy / Goofy", + "ticket": "cc837f69-906d-4720-94d0-ea6cf8091ecf", + "same": [ + [ + "u2968", + "u619" + ], + [ + "u620", + "u2969" + ] + ], + "fields": { + "u620": { + "color": "Schwarz", + "note": "Züchterin bestätigte die Farbe Schwarz; Anmerkung zu Schwarz-Schecke." + } + } + }, + { + "name": "Quentin", + "ticket": "8d50f668-96ea-439d-b8d9-2916b9b32a26", + "same": [ + [ + "u2208", + "u261" + ] + ] + }, + { + "name": "Anton", + "ticket": "33af41eb-ffac-47bc-8467-561dba513c06", + "same": [ + [ + "u2899", + "u6791", + "u6921", + "u3352", + "u1407" + ], + [ + "u1064", + "u1605", + "u1948", + "u2246", + "u2387", + "u2599", + "u3041", + "u3199", + "u3505" + ], + [ + "u1405", + "u2897", + "u3348", + "u6789", + "u6917" + ], + [ + "u1408", + "u2900", + "u3355", + "u6792", + "u6924" + ], + [ + "u1243", + "u933" + ] + ], + "fields": { + "u2899": { + "dob": "2007-07-31", + "color": "WFNZ Agouti", + "note": "Geburtsdatum laut Züchterin 31.07.2007 (andere Quellen nannten 30.07.2007 oder 21.03.2007)." + }, + "u1064": { + "color": "Anthrazit-Schecke", + "genotype": "aa C- D- Ee[f] gg P- Spsp" + }, + "u1405": { + "dob": "2011-03-01", + "color": "WFNZ Agouti", + "note": "Geburtsdatum laut Züchterin 01.03.2011 (andere Quellen nannten 01.05.2011)." + }, + "u1408": { + "dob": "2011-03-01", + "color": "WFNZ Agouti", + "note": "Geburtsdatum laut Züchterin 01.03.2011 (andere Quellen nannten 06.04.2012)." + } + } + }, + { + "name": "Pegasus", + "ticket": "b362f68e-28bb-46fc-a328-396057a42258", + "same": [ + [ + "u2448", + "u3147", + "u3307", + "u3929", + "u1062", + "u1603", + "u1946", + "u3503", + "u1180", + "u1710", + "u2064", + "u690", + "u756" + ] + ] + }, + { + "name": "Pünktchen", + "ticket": "12440e00-829e-4209-b3d9-a3d28e7879e7", + "same": [ + [ + "u1065", + "u1124", + "u1606", + "u1667", + "u2245", + "u2386", + "u2598", + "u3040" + ], + [ + "u560", + "u1460" + ] + ] + }, + { + "name": "Kira", + "ticket": "18da8456-eb93-4717-b7bf-83e4a5407511", + "same": [ + [ + "u1225", + "u6837", + "u1863" + ] + ], + "different": [ + [ + "u1826", + "u2618" + ], + [ + "u385" + ], + [ + "u1739" + ] + ] + }, + { + "name": "Paula", + "ticket": "5931f460-b39d-4ab4-913b-c29f18df0f13", + "different": [ + [ + "u1481" + ], + [ + "u608" + ], + [ + "u352" + ] + ] + }, + { + "name": "Felix", + "ticket": "2836b684-9622-437a-a3b0-d1ab44cd0b40", + "same": [ + [ + "u48", + "u934" + ], + [ + "u902", + "u1251", + "u1527", + "u3451", + "u4843", + "u5019", + "u5178", + "u6845", + "u7250" + ], + [ + "u897", + "u1524", + "u3448", + "u6891" + ] + ], + "fields": { + "u48": { + "note": "Geburtsdatum 21.04.2009. Ein zweiter Eintrag unter u934 nannte abweichend den 21.02.2009 und Herkunft Clan of Happy Mice." + } + } + }, + { + "name": "Ruby", + "ticket": "c70ab118-7999-4070-8293-182c5079d3ba", + "same": [ + [ + "u1757", + "u1854", + "u2950", + "u315" + ], + [ + "u314", + "u1756", + "u1853", + "u2949" + ] + ], + "fields": { + "u1757": { + "dob": "2009-03-19", + "note": "Geburtsdatum laut Züchterin 19.03.2009 (andere Quelle nannte 10.03.2009)." + } + } + }, + { + "name": "Charly", + "ticket": "d1129f5f-21a9-4e6b-8c6c-6b7c32927cbf", + "same": [ + [ + "u1818", + "u3365", + "u2912", + "u6804" + ], + [ + "u3407", + "u6999", + "u752", + "u1176", + "u1193", + "u2060" + ] + ], + "fields": { + "u6441": { + "resident": true, + "note": "Ehemaliges Zuchttier, verstorben." + } + } + }, + { + "name": "Ceres", + "ticket": "ec1b047e-987c-46f3-b5cb-f17557e95a6a", + "different": [ + [ + "276" + ], + [ + "u2649", + "u2758", + "u6374" + ] + ], + "fields": { + "276": { + "resident": false, + "note": "Reiner Zuchtvorfahre (laut Züchterin kein eigenes Zuchttier)." + } + } + }, + { + "name": "Ben", + "ticket": "be6e1446-4eef-46a3-8b86-a2fc9e41a699", + "different": [ + [ + "105" + ], + [ + "u3354", + "u6923" + ], + [ + "u1409" + ] + ], + "fields": { + "u3354": { + "color": "WFNZ Agouti" + } + } + }, + { + "name": "Sammy", + "ticket": "20a6d069-ed37-4a52-8d75-51b478c3b333", + "different": [ + [ + "287" + ], + [ + "u5246", + "u5463", + "u5587", + "u5759" + ] + ] + }, + { + "name": "Tinkerbell", + "ticket": "73664c57-6646-4975-a97e-2084c0b5a11c", + "different": [ + [ + "265" + ], + [ + "u7447" + ] + ], + "fields": { + "265": { + "resident": false, + "note": "Tinkerbell (Variant A, rid 265) war nie in der eigenen Zucht, sondern ist reiner Zuchtvorfahre." + } + } + }, + { + "name": "Filou", + "ticket": "3576d000-e4a0-4e84-81fb-eb3734c25a44", + "different": [ + [ + "u552" + ], + [ + "u3598" + ], + [ + "u3599" + ], + [ + "u6428" + ], + [ + "u7000" + ] + ] + }, + { + "name": "Fiona", + "ticket": "f0d678f0-99a0-4fb8-bccc-726226afba5d", + "same": [ + [ + "u264", + "u2205", + "u2298", + "u2569", + "u2685", + "u3069", + "u3229", + "u3939", + "u4164", + "u4359", + "u4520", + "u4581", + "u4678", + "u4828", + "u5004", + "u5163", + "u5306", + "u5401", + "u5525", + "u5729", + "u5905", + "u6158", + "u6333", + "u6672", + "u7235" + ] + ], + "different": [ + [ + "u264" + ], + [ + "u494" + ], + [ + "u1338" + ] + ] + }, + { + "name": "Bailey", + "ticket": "7d1b2704-c69c-4e93-8b17-912203b6e845", + "different": [ + [ + "u121" + ], + [ + "u414" + ], + [ + "u6397" + ] + ] } ] } \ No newline at end of file diff --git a/GerbilManagerWebAPI/Migrations/20260624071249_AddLitterShowInChronicle.Designer.cs b/GerbilManagerWebAPI/Migrations/20260624071249_AddLitterShowInChronicle.Designer.cs new file mode 100644 index 0000000..2d6b9eb --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260624071249_AddLitterShowInChronicle.Designer.cs @@ -0,0 +1,1861 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20260624071249_AddLitterShowInChronicle")] + partial class AddLitterShowInChronicle + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.AcquisitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("SourceContactId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("AcquisitionRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PageId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Blocks"); + + b.HasData( + new + { + Id = new Guid("51720002-0000-0000-0000-000000000001"), + Data = "{\"text\":\"Startseite\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000001"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000002"), + Data = "{\"text\":\"Über die Zucht\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000002"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000003"), + Data = "{\"text\":\"Abgabetiere\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000004"), + Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000004"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000005"), + Data = "{\"text\":\"Farben & Genetik\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000005"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000006"), + Data = "{\"text\":\"Kontakt\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000006"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000010"), + Data = "{\"mode\":\"auto\",\"intro\":\"\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "AbgabetiereList" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000007"), + Data = "{\"text\":\"Impressum\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000070"), + Data = "{\"markdown\":\"**Angaben gemäß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "RichText" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000008"), + Data = "{\"text\":\"Datenschutz\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000080"), + Data = "{\"markdown\":\"**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "RichText" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Homepage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameSuffix") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ZuchtName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BreederSettings"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-000000000001"), + Address = "", + City = "", + Email = "", + Homepage = "", + Name = "", + NameSuffix = "", + Phone = "", + ZuchtName = "" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanonicalGenotype") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ColorVarieties"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0000-000000000001"), + CanonicalGenotype = "AA chch DD EE GG pp spsp rere", + Name = "REW", + SortOrder = 0 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000002"), + CanonicalGenotype = "aa chch DD EE GG PP spsp rere", + Name = "Hermelin", + SortOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000003"), + CanonicalGenotype = "AA chch DD EE GG PP spsp rere", + Name = "Himalaya", + SortOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000004"), + CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere", + Name = "Zobel", + SortOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000005"), + CanonicalGenotype = "AA CC DD efef GG pp spsp rere", + Name = "Rotaugenschimmel", + SortOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000006"), + CanonicalGenotype = "AA CC DD EE GG PP spsp rere", + Name = "Agouti", + SortOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000007"), + CanonicalGenotype = "aa CC DD EE GG PP spsp rere", + Name = "Schwarz", + SortOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000008"), + CanonicalGenotype = "AA CC DD EE gg PP spsp rere", + Name = "Silberagouti", + SortOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000009"), + CanonicalGenotype = "aa CC DD EE gg PP spsp rere", + Name = "Anthrazit", + SortOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000010"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs", + SortOrder = 9 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000011"), + CanonicalGenotype = "aa CC dd EE GG PP spsp rere", + Name = "Blau", + SortOrder = 10 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000012"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold", + SortOrder = 11 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000013"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin", + SortOrder = 12 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000014"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs", + SortOrder = 13 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000015"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs", + SortOrder = 14 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000016"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Gold", + SortOrder = 15 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000017"), + CanonicalGenotype = "aa CC dd EE GG pp spsp rere", + Name = "Dilute Platin", + SortOrder = 16 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000018"), + CanonicalGenotype = "aa CC DD EE gg pp spsp rere", + Name = "Altweiss (REW)", + SortOrder = 17 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000019"), + CanonicalGenotype = "AA CC DD ee gg pp spsp rere", + Name = "Apricot (Blassfuchs)", + SortOrder = 18 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000020"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs", + SortOrder = 19 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000021"), + CanonicalGenotype = "aa CC DD ee gg pp spsp rere", + Name = "C-Separator", + SortOrder = 20 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000022"), + CanonicalGenotype = "AA CC DD EE gg pp spsp rere", + Name = "Elfenbein", + SortOrder = 21 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000023"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs", + SortOrder = 22 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000024"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs", + SortOrder = 23 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000025"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Saphir", + SortOrder = 24 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000026"), + CanonicalGenotype = "AA CC DD efef GG PP spsp rere", + Name = "Orangeschimmel", + SortOrder = 25 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000027"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Topas", + SortOrder = 26 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000028"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin-Hell", + SortOrder = 27 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000029"), + CanonicalGenotype = "AA CC dd EE GG PP spsp rere", + Name = "Dilute Agouti", + SortOrder = 28 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000030"), + CanonicalGenotype = "AA CC dd EE gg PP spsp rere", + Name = "Dilute Silberagouti", + SortOrder = 29 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000031"), + CanonicalGenotype = "aa CC dd ee GG PP spsp rere", + Name = "Dilute Kohlfuchs", + SortOrder = 30 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000032"), + CanonicalGenotype = "aa CC dd EE gg PP spsp rere", + Name = "Dilute Anthrazit", + SortOrder = 31 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000033"), + CanonicalGenotype = "AA CC DD efef gg PP spsp rere", + Name = "Silberschimmel", + SortOrder = 36 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000034"), + CanonicalGenotype = "AA CC DD efe gg PP spsp rere", + Name = "Polarfuchsschimmel", + SortOrder = 37 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000035"), + CanonicalGenotype = "AA CC DD efe GG PP spsp rere", + Name = "Algierfuchsschimmel", + SortOrder = 38 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000036"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel", + SortOrder = 39 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000037"), + CanonicalGenotype = "aa CC DD efe gg PP spsp rere", + Name = "Blaufuchsschimmel", + SortOrder = 40 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000038"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs, hell", + SortOrder = 41 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000039"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs, hell", + SortOrder = 42 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000040"), + CanonicalGenotype = "AA CC DD efe GG pp spsp rere", + Name = "Goldfuchsschimmel", + SortOrder = 43 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000041"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold-Hell", + SortOrder = 44 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000042"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs, hell", + SortOrder = 45 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000043"), + CanonicalGenotype = "aa CC DD efe GG pp spsp rere", + Name = "Rotfuchsschimmel", + SortOrder = 46 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000044"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs, hell", + SortOrder = 47 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000045"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel, hell", + SortOrder = 48 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000046"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs, hell", + SortOrder = 49 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000047"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs-Hell", + SortOrder = 50 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000048"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs, hell", + SortOrder = 51 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000049"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Topas", + SortOrder = 52 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000050"), + CanonicalGenotype = "aa CC dd ee gg PP spsp rere", + Name = "Dilute Blaufuchs", + SortOrder = 53 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000051"), + CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere", + Name = "Marder", + SortOrder = 54 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000052"), + CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere", + Name = "Siam", + SortOrder = 55 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000053"), + CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere", + Name = "Zobel-Hell", + SortOrder = 56 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000054"), + CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere", + Name = "CP-Agouti", + SortOrder = 57 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000055"), + CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere", + Name = "CP-Silberagouti", + SortOrder = 59 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000056"), + CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere", + Name = "CP-Algierfuchs", + SortOrder = 61 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000057"), + CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere", + Name = "CP-Polarfuchs", + SortOrder = 63 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000058"), + CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere", + Name = "CP-Fuchs", + SortOrder = 65 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000059"), + CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere", + Name = "CP-Fuchs-Hell", + SortOrder = 66 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000060"), + CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere", + Name = "CP-Blaufuchs", + SortOrder = 67 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000061"), + CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel", + SortOrder = 68 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000062"), + CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere", + Name = "CP-Agouti-Hell", + SortOrder = 58 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000063"), + CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere", + Name = "CP-Silberagouti-Hell", + SortOrder = 60 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000064"), + CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere", + Name = "CP-Algierfuchs-Hell", + SortOrder = 62 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000065"), + CanonicalGenotype = "AA cchmch DD ee gg PP spsp rere", + Name = "CP-Polarfuchs-Hell", + SortOrder = 64 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000066"), + CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel-Hell", + SortOrder = 69 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000067"), + CanonicalGenotype = "AA CC dd ee GG PP spsp rere", + Name = "Dilute Algierfuchs", + SortOrder = 32 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000068"), + CanonicalGenotype = "AA CC dd ee GG pp spsp rere", + Name = "Dilute Goldfuchs", + SortOrder = 33 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000069"), + CanonicalGenotype = "aa CC dd ee GG pp spsp rere", + Name = "Dilute Rotfuchs", + SortOrder = 34 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000070"), + CanonicalGenotype = "AA CC dd ee gg PP spsp rere", + Name = "Dilute Polarfuchs", + SortOrder = 35 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("IsBreeder") + .HasColumnType("boolean"); + + b.Property("IsReceiver") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSuffix") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CleaningCycleDays") + .HasColumnType("integer"); + + b.Property("LastCleanedDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Enclosures"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EnclosureId"); + + b.ToTable("EnclosurePhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ExhibitionResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Award") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("EventName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Placement") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("ExhibitionResults"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentContext") + .HasColumnType("text"); + + b.Property("Answer") + .HasColumnType("text"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ClientTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("Context") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("FixNote") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Helpful") + .HasColumnType("boolean"); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Question") + .HasColumnType("text"); + + b.Property("ReopenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Thread") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.FeedbackAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FeedbackId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("FeedbackAttachments"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CauseOfDeath") + .HasColumnType("text"); + + b.Property("CharacterNote") + .HasColumnType("text"); + + b.Property("CharacterTraits") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorVarietyId") + .HasColumnType("uuid"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DateOfDeath") + .HasColumnType("date"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + + b.Property("Genotype") + .HasColumnType("text"); + + b.Property("GoHomeDate") + .HasColumnType("date"); + + b.Property("ImportSource") + .HasColumnType("text"); + + b.Property("IsCastrated") + .HasColumnType("boolean"); + + b.Property("IsDeaf") + .HasColumnType("boolean"); + + b.Property("IsResident") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSearch") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OriginBreeder") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("OriginContactId") + .HasColumnType("uuid"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("RawImportData") + .HasColumnType("text"); + + b.Property("ReceiverContactId") + .HasColumnType("uuid"); + + b.Property("SpottingType") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ColorVarietyId"); + + b.HasIndex("EnclosureId"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("LitterId"); + + b.HasIndex("OriginContactId"); + + b.HasIndex("ReceiverContactId"); + + b.ToTable("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("GerbilPhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Veterinarian") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("HealthRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeathsWithin8Weeks") + .HasColumnType("integer"); + + b.Property("ExpectedGoHomeDate") + .HasColumnType("date"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("FatherId") + .HasColumnType("uuid"); + + b.Property("LitterLetter") + .HasColumnType("text"); + + b.Property("MotherId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PairingCode") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("ShowInChronicle") + .HasColumnType("boolean"); + + b.Property("TotalBorn") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("FatherId"); + + b.HasIndex("MotherId"); + + b.ToTable("Litters"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppPasswordProtected") + .HasColumnType("text"); + + b.Property("BackgroundPollEnabled") + .HasColumnType("boolean"); + + b.Property("Folder") + .IsRequired() + .HasColumnType("text"); + + b.Property("GmailAddress") + .HasColumnType("text"); + + b.Property("LastUid") + .HasColumnType("bigint"); + + b.Property("PollIntervalMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MailSettings"); + + b.HasData( + new + { + Id = new Guid("ab0c0000-0000-0000-0000-000000000001"), + BackgroundPollEnabled = false, + Folder = "INBOX", + LastUid = 0L, + PollIntervalMinutes = 15 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alt") + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SeoDescription") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pages"); + + b.HasData( + new + { + Id = new Guid("51720001-0000-0000-0000-000000000001"), + Slug = "start", + Status = "Published", + Title = "Startseite" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000002"), + Slug = "ueber-die-zucht", + Status = "Published", + Title = "Über die Zucht" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000003"), + Slug = "abgabetiere", + Status = "Published", + Title = "Abgabetiere" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000004"), + Slug = "abgabebedingungen", + Status = "Published", + Title = "Abgabebedingungen" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000005"), + Slug = "farben-genetik", + Status = "Published", + Title = "Farben & Genetik" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000006"), + Slug = "kontakt", + Status = "Published", + Title = "Kontakt" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000007"), + Slug = "impressum", + Status = "Published", + Title = "Impressum" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000008"), + Slug = "datenschutz", + Status = "Published", + Title = "Datenschutz" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedContactId") + .HasColumnType("uuid"); + + b.Property("BodyText") + .HasColumnType("text"); + + b.Property("DraftReply") + .HasColumnType("text"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromName") + .HasColumnType("text"); + + b.Property("GmailMessageId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InReplyToMessageId") + .HasColumnType("text"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferencesHeader") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AssignedContactId"); + + b.HasIndex("GmailMessageId") + .IsUnique(); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ReturnRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FromContactId") + .HasColumnType("uuid"); + + b.Property("FromContactName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OriginalPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("OriginalSaleDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("GerbilId"); + + b.ToTable("ReturnRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContractDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HandoverDate") + .HasColumnType("date"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("SaleContracts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.Property("SaleContractId") + .HasColumnType("uuid"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("PhotoId") + .HasColumnType("uuid"); + + b.HasKey("SaleContractId", "GerbilId"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleContractAnimal"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppointmentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("HandedOverDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("ReservedForContactId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleReservations"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultLocale") + .IsRequired() + .HasColumnType("text"); + + b.Property("NavOrder") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sites"); + + b.HasData( + new + { + Id = new Guid("5172e000-0000-0000-0000-000000000001"), + DefaultLocale = "de", + NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WaitingListEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("WishColor") + .HasColumnType("text"); + + b.Property("WishGender") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("WaitingListEntries"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WebPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("P256dh") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("WebPushSubscriptions"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("WeightGrams") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("WeightRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Page", null) + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", null) + .WithMany() + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety") + .WithMany() + .HasForeignKey("ColorVarietyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure") + .WithMany("Gerbils") + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter") + .WithMany() + .HasForeignKey("LitterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact") + .WithMany() + .HasForeignKey("OriginContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact") + .WithMany() + .HasForeignKey("ReceiverContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ColorVariety"); + + b.Navigation("Enclosure"); + + b.Navigation("Litter"); + + b.Navigation("OriginContact"); + + b.Navigation("ReceiverContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father") + .WithMany() + .HasForeignKey("FatherId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother") + .WithMany() + .HasForeignKey("MotherId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Father"); + + b.Navigation("Mother"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact") + .WithMany() + .HasForeignKey("AssignedContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignedContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil") + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null) + .WithMany("Animals") + .HasForeignKey("SaleContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gerbil"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Navigation("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Navigation("Animals"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260624071249_AddLitterShowInChronicle.cs b/GerbilManagerWebAPI/Migrations/20260624071249_AddLitterShowInChronicle.cs new file mode 100644 index 0000000..634f620 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260624071249_AddLitterShowInChronicle.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class AddLitterShowInChronicle : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ShowInChronicle", + table: "Litters", + type: "boolean", + nullable: false, + // Bestehende Würfe bleiben in der Wurfchronik sichtbar; nur explizit als + // „nicht in Chronik" importierte Würfe (z. B. Akanes extern entstandener Wurf) + // werden beim Ingest auf false gesetzt. + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ShowInChronicle", + table: "Litters"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260624082334_UpdateLegalPagesDsgvo.Designer.cs b/GerbilManagerWebAPI/Migrations/20260624082334_UpdateLegalPagesDsgvo.Designer.cs new file mode 100644 index 0000000..aa34a3b --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260624082334_UpdateLegalPagesDsgvo.Designer.cs @@ -0,0 +1,1861 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20260624082334_UpdateLegalPagesDsgvo")] + partial class UpdateLegalPagesDsgvo + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.AcquisitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("SourceContactId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("AcquisitionRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PageId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Blocks"); + + b.HasData( + new + { + Id = new Guid("51720002-0000-0000-0000-000000000001"), + Data = "{\"text\":\"Startseite\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000001"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000002"), + Data = "{\"text\":\"Über die Zucht\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000002"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000003"), + Data = "{\"text\":\"Abgabetiere\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000004"), + Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000004"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000005"), + Data = "{\"text\":\"Farben & Genetik\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000005"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000006"), + Data = "{\"text\":\"Kontakt\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000006"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000010"), + Data = "{\"mode\":\"auto\",\"intro\":\"\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "AbgabetiereList" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000007"), + Data = "{\"text\":\"Impressum\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000070"), + Data = "{\"markdown\":\"## Angaben gemäß § 5 DDG (Digitale-Dienste-Gesetz)\\n\\nSeitenbetreiber:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\n\\n## Kontakt\\n\\nTelefon: [Telefonnummer]\\nE-Mail: [E-Mail-Adresse]\\n\\n## Verantwortlich für den Inhalt gemäß § 18 Abs. 2 MStV\\n\\n[Vor- und Nachname], Anschrift wie oben\\n\\n## Verbraucherstreitbeilegung\\n\\nWir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Bitte alle Platzhalter in eckigen Klammern durch deine echten Angaben ersetzen. Pflicht ist eine ladungsfähige Anschrift (kein Postfach). Wird die Zucht gewerblich betrieben oder besteht eine Erlaubnis nach § 11 TierSchG, ergänze hier die zuständige Behörde und das Aktenzeichen.*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "RichText" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000008"), + Data = "{\"text\":\"Datenschutz\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000080"), + Data = "{\"markdown\":\"## Datenschutzerklärung\\n\\n## 1. Verantwortlicher\\n\\nVerantwortlich für die Datenverarbeitung auf dieser Webseite ist:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\nE-Mail: [E-Mail-Adresse]\\n\\n## 2. Hosting und Server-Logfiles\\n\\nDiese Webseite wird auf einem eigenen Server betrieben. Beim Aufruf der Seite verarbeitet der Server automatisch Informationen, die dein Browser übermittelt, in sogenannten Server-Logfiles: IP-Adresse, Datum und Uhrzeit des Zugriffs, die aufgerufene Seite bzw. Datei, die übertragene Datenmenge sowie Browser und Betriebssystem.\\nDiese Verarbeitung ist technisch erforderlich, um die Webseite auszuliefern und ihren sicheren und stabilen Betrieb zu gewährleisten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse am störungsfreien Betrieb und an der Sicherheit). Die Logfiles werden nach spätestens [z. B. 7] Tagen gelöscht und nicht mit anderen Daten zusammengeführt.\\n\\n## 3. Cookies, Tracking und externe Dienste\\n\\nDiese Webseite verwendet keine Cookies, kein Tracking und keine Analyse- oder Marketing-Dienste. Es werden keine externen Inhalte Dritter (z. B. Schriftarten, Karten, Videos, CDNs) nachgeladen.\\n\\n## 4. Kontaktaufnahme\\n\\nWenn du uns per E-Mail kontaktierst, verarbeiten wir deine Angaben (z. B. Name, E-Mail-Adresse, Inhalt der Nachricht) ausschließlich zur Bearbeitung deiner Anfrage. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO bzw. Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden gelöscht, sobald sie nicht mehr benötigt werden und keine gesetzlichen Aufbewahrungspflichten entgegenstehen.\\n\\n## 5. Empfänger der Daten\\n\\nEine Weitergabe deiner Daten an Dritte findet nicht statt. Eine Datenübermittlung in Länder außerhalb der EU bzw. des EWR erfolgt nicht.\\n\\n## 6. Deine Rechte\\n\\nDu hast jederzeit das Recht auf:\\n- Auskunft über die zu deiner Person gespeicherten Daten (Art. 15 DSGVO)\\n- Berichtigung unrichtiger Daten (Art. 16 DSGVO)\\n- Löschung (Art. 17 DSGVO)\\n- Einschränkung der Verarbeitung (Art. 18 DSGVO)\\n- Datenübertragbarkeit (Art. 20 DSGVO)\\n- Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)\\n\\nZur Ausübung genügt eine formlose Nachricht an die oben genannte E-Mail-Adresse.\\n\\n## 7. Beschwerderecht bei der Aufsichtsbehörde\\n\\nDu hast das Recht, dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO). Zuständig ist die Aufsichtsbehörde deines Bundeslandes bzw. des Seitenbetreibers: [zuständige Landesdatenschutzbehörde eintragen].\\n\\n## 8. Keine automatisierte Entscheidungsfindung\\n\\nEine automatisierte Entscheidungsfindung einschließlich Profiling nach Art. 22 DSGVO findet nicht statt.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Platzhalter in eckigen Klammern ausfüllen. Aufbewahrungsdauer der Logfiles an deinen Server bzw. Reverse-Proxy anpassen. Sobald externe Dienste oder ein Kontaktformular eingebunden werden, diesen Text ergänzen. Stand: [Datum eintragen].*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "RichText" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Homepage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameSuffix") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ZuchtName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BreederSettings"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-000000000001"), + Address = "", + City = "", + Email = "", + Homepage = "", + Name = "", + NameSuffix = "", + Phone = "", + ZuchtName = "" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanonicalGenotype") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ColorVarieties"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0000-000000000001"), + CanonicalGenotype = "AA chch DD EE GG pp spsp rere", + Name = "REW", + SortOrder = 0 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000002"), + CanonicalGenotype = "aa chch DD EE GG PP spsp rere", + Name = "Hermelin", + SortOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000003"), + CanonicalGenotype = "AA chch DD EE GG PP spsp rere", + Name = "Himalaya", + SortOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000004"), + CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere", + Name = "Zobel", + SortOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000005"), + CanonicalGenotype = "AA CC DD efef GG pp spsp rere", + Name = "Rotaugenschimmel", + SortOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000006"), + CanonicalGenotype = "AA CC DD EE GG PP spsp rere", + Name = "Agouti", + SortOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000007"), + CanonicalGenotype = "aa CC DD EE GG PP spsp rere", + Name = "Schwarz", + SortOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000008"), + CanonicalGenotype = "AA CC DD EE gg PP spsp rere", + Name = "Silberagouti", + SortOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000009"), + CanonicalGenotype = "aa CC DD EE gg PP spsp rere", + Name = "Anthrazit", + SortOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000010"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs", + SortOrder = 9 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000011"), + CanonicalGenotype = "aa CC dd EE GG PP spsp rere", + Name = "Blau", + SortOrder = 10 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000012"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold", + SortOrder = 11 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000013"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin", + SortOrder = 12 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000014"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs", + SortOrder = 13 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000015"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs", + SortOrder = 14 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000016"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Gold", + SortOrder = 15 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000017"), + CanonicalGenotype = "aa CC dd EE GG pp spsp rere", + Name = "Dilute Platin", + SortOrder = 16 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000018"), + CanonicalGenotype = "aa CC DD EE gg pp spsp rere", + Name = "Altweiss (REW)", + SortOrder = 17 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000019"), + CanonicalGenotype = "AA CC DD ee gg pp spsp rere", + Name = "Apricot (Blassfuchs)", + SortOrder = 18 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000020"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs", + SortOrder = 19 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000021"), + CanonicalGenotype = "aa CC DD ee gg pp spsp rere", + Name = "C-Separator", + SortOrder = 20 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000022"), + CanonicalGenotype = "AA CC DD EE gg pp spsp rere", + Name = "Elfenbein", + SortOrder = 21 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000023"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs", + SortOrder = 22 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000024"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs", + SortOrder = 23 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000025"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Saphir", + SortOrder = 24 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000026"), + CanonicalGenotype = "AA CC DD efef GG PP spsp rere", + Name = "Orangeschimmel", + SortOrder = 25 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000027"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Topas", + SortOrder = 26 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000028"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin-Hell", + SortOrder = 27 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000029"), + CanonicalGenotype = "AA CC dd EE GG PP spsp rere", + Name = "Dilute Agouti", + SortOrder = 28 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000030"), + CanonicalGenotype = "AA CC dd EE gg PP spsp rere", + Name = "Dilute Silberagouti", + SortOrder = 29 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000031"), + CanonicalGenotype = "aa CC dd ee GG PP spsp rere", + Name = "Dilute Kohlfuchs", + SortOrder = 30 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000032"), + CanonicalGenotype = "aa CC dd EE gg PP spsp rere", + Name = "Dilute Anthrazit", + SortOrder = 31 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000033"), + CanonicalGenotype = "AA CC DD efef gg PP spsp rere", + Name = "Silberschimmel", + SortOrder = 36 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000034"), + CanonicalGenotype = "AA CC DD efe gg PP spsp rere", + Name = "Polarfuchsschimmel", + SortOrder = 37 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000035"), + CanonicalGenotype = "AA CC DD efe GG PP spsp rere", + Name = "Algierfuchsschimmel", + SortOrder = 38 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000036"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel", + SortOrder = 39 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000037"), + CanonicalGenotype = "aa CC DD efe gg PP spsp rere", + Name = "Blaufuchsschimmel", + SortOrder = 40 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000038"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs, hell", + SortOrder = 41 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000039"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs, hell", + SortOrder = 42 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000040"), + CanonicalGenotype = "AA CC DD efe GG pp spsp rere", + Name = "Goldfuchsschimmel", + SortOrder = 43 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000041"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold-Hell", + SortOrder = 44 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000042"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs, hell", + SortOrder = 45 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000043"), + CanonicalGenotype = "aa CC DD efe GG pp spsp rere", + Name = "Rotfuchsschimmel", + SortOrder = 46 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000044"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs, hell", + SortOrder = 47 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000045"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel, hell", + SortOrder = 48 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000046"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs, hell", + SortOrder = 49 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000047"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs-Hell", + SortOrder = 50 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000048"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs, hell", + SortOrder = 51 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000049"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Topas", + SortOrder = 52 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000050"), + CanonicalGenotype = "aa CC dd ee gg PP spsp rere", + Name = "Dilute Blaufuchs", + SortOrder = 53 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000051"), + CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere", + Name = "Marder", + SortOrder = 54 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000052"), + CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere", + Name = "Siam", + SortOrder = 55 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000053"), + CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere", + Name = "Zobel-Hell", + SortOrder = 56 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000054"), + CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere", + Name = "CP-Agouti", + SortOrder = 57 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000055"), + CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere", + Name = "CP-Silberagouti", + SortOrder = 59 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000056"), + CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere", + Name = "CP-Algierfuchs", + SortOrder = 61 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000057"), + CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere", + Name = "CP-Polarfuchs", + SortOrder = 63 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000058"), + CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere", + Name = "CP-Fuchs", + SortOrder = 65 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000059"), + CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere", + Name = "CP-Fuchs-Hell", + SortOrder = 66 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000060"), + CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere", + Name = "CP-Blaufuchs", + SortOrder = 67 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000061"), + CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel", + SortOrder = 68 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000062"), + CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere", + Name = "CP-Agouti-Hell", + SortOrder = 58 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000063"), + CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere", + Name = "CP-Silberagouti-Hell", + SortOrder = 60 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000064"), + CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere", + Name = "CP-Algierfuchs-Hell", + SortOrder = 62 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000065"), + CanonicalGenotype = "AA cchmch DD ee gg PP spsp rere", + Name = "CP-Polarfuchs-Hell", + SortOrder = 64 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000066"), + CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel-Hell", + SortOrder = 69 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000067"), + CanonicalGenotype = "AA CC dd ee GG PP spsp rere", + Name = "Dilute Algierfuchs", + SortOrder = 32 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000068"), + CanonicalGenotype = "AA CC dd ee GG pp spsp rere", + Name = "Dilute Goldfuchs", + SortOrder = 33 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000069"), + CanonicalGenotype = "aa CC dd ee GG pp spsp rere", + Name = "Dilute Rotfuchs", + SortOrder = 34 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000070"), + CanonicalGenotype = "AA CC dd ee gg PP spsp rere", + Name = "Dilute Polarfuchs", + SortOrder = 35 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("IsBreeder") + .HasColumnType("boolean"); + + b.Property("IsReceiver") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSuffix") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CleaningCycleDays") + .HasColumnType("integer"); + + b.Property("LastCleanedDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Enclosures"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EnclosureId"); + + b.ToTable("EnclosurePhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ExhibitionResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Award") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("EventName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Placement") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("ExhibitionResults"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentContext") + .HasColumnType("text"); + + b.Property("Answer") + .HasColumnType("text"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ClientTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("Context") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("FixNote") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Helpful") + .HasColumnType("boolean"); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Question") + .HasColumnType("text"); + + b.Property("ReopenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Thread") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.FeedbackAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FeedbackId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("FeedbackAttachments"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CauseOfDeath") + .HasColumnType("text"); + + b.Property("CharacterNote") + .HasColumnType("text"); + + b.Property("CharacterTraits") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorVarietyId") + .HasColumnType("uuid"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DateOfDeath") + .HasColumnType("date"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + + b.Property("Genotype") + .HasColumnType("text"); + + b.Property("GoHomeDate") + .HasColumnType("date"); + + b.Property("ImportSource") + .HasColumnType("text"); + + b.Property("IsCastrated") + .HasColumnType("boolean"); + + b.Property("IsDeaf") + .HasColumnType("boolean"); + + b.Property("IsResident") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSearch") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OriginBreeder") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("OriginContactId") + .HasColumnType("uuid"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("RawImportData") + .HasColumnType("text"); + + b.Property("ReceiverContactId") + .HasColumnType("uuid"); + + b.Property("SpottingType") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ColorVarietyId"); + + b.HasIndex("EnclosureId"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("LitterId"); + + b.HasIndex("OriginContactId"); + + b.HasIndex("ReceiverContactId"); + + b.ToTable("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("GerbilPhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Veterinarian") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("HealthRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeathsWithin8Weeks") + .HasColumnType("integer"); + + b.Property("ExpectedGoHomeDate") + .HasColumnType("date"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("FatherId") + .HasColumnType("uuid"); + + b.Property("LitterLetter") + .HasColumnType("text"); + + b.Property("MotherId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PairingCode") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("ShowInChronicle") + .HasColumnType("boolean"); + + b.Property("TotalBorn") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("FatherId"); + + b.HasIndex("MotherId"); + + b.ToTable("Litters"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppPasswordProtected") + .HasColumnType("text"); + + b.Property("BackgroundPollEnabled") + .HasColumnType("boolean"); + + b.Property("Folder") + .IsRequired() + .HasColumnType("text"); + + b.Property("GmailAddress") + .HasColumnType("text"); + + b.Property("LastUid") + .HasColumnType("bigint"); + + b.Property("PollIntervalMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MailSettings"); + + b.HasData( + new + { + Id = new Guid("ab0c0000-0000-0000-0000-000000000001"), + BackgroundPollEnabled = false, + Folder = "INBOX", + LastUid = 0L, + PollIntervalMinutes = 15 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alt") + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SeoDescription") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pages"); + + b.HasData( + new + { + Id = new Guid("51720001-0000-0000-0000-000000000001"), + Slug = "start", + Status = "Published", + Title = "Startseite" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000002"), + Slug = "ueber-die-zucht", + Status = "Published", + Title = "Über die Zucht" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000003"), + Slug = "abgabetiere", + Status = "Published", + Title = "Abgabetiere" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000004"), + Slug = "abgabebedingungen", + Status = "Published", + Title = "Abgabebedingungen" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000005"), + Slug = "farben-genetik", + Status = "Published", + Title = "Farben & Genetik" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000006"), + Slug = "kontakt", + Status = "Published", + Title = "Kontakt" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000007"), + Slug = "impressum", + Status = "Published", + Title = "Impressum" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000008"), + Slug = "datenschutz", + Status = "Published", + Title = "Datenschutz" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedContactId") + .HasColumnType("uuid"); + + b.Property("BodyText") + .HasColumnType("text"); + + b.Property("DraftReply") + .HasColumnType("text"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromName") + .HasColumnType("text"); + + b.Property("GmailMessageId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InReplyToMessageId") + .HasColumnType("text"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferencesHeader") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AssignedContactId"); + + b.HasIndex("GmailMessageId") + .IsUnique(); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ReturnRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FromContactId") + .HasColumnType("uuid"); + + b.Property("FromContactName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OriginalPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("OriginalSaleDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("GerbilId"); + + b.ToTable("ReturnRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContractDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HandoverDate") + .HasColumnType("date"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("SaleContracts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.Property("SaleContractId") + .HasColumnType("uuid"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("PhotoId") + .HasColumnType("uuid"); + + b.HasKey("SaleContractId", "GerbilId"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleContractAnimal"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppointmentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("HandedOverDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("ReservedForContactId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleReservations"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultLocale") + .IsRequired() + .HasColumnType("text"); + + b.Property("NavOrder") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sites"); + + b.HasData( + new + { + Id = new Guid("5172e000-0000-0000-0000-000000000001"), + DefaultLocale = "de", + NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WaitingListEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("WishColor") + .HasColumnType("text"); + + b.Property("WishGender") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("WaitingListEntries"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WebPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("P256dh") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("WebPushSubscriptions"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("WeightGrams") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("WeightRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Page", null) + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", null) + .WithMany() + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety") + .WithMany() + .HasForeignKey("ColorVarietyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure") + .WithMany("Gerbils") + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter") + .WithMany() + .HasForeignKey("LitterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact") + .WithMany() + .HasForeignKey("OriginContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact") + .WithMany() + .HasForeignKey("ReceiverContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ColorVariety"); + + b.Navigation("Enclosure"); + + b.Navigation("Litter"); + + b.Navigation("OriginContact"); + + b.Navigation("ReceiverContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father") + .WithMany() + .HasForeignKey("FatherId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother") + .WithMany() + .HasForeignKey("MotherId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Father"); + + b.Navigation("Mother"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact") + .WithMany() + .HasForeignKey("AssignedContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignedContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil") + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null) + .WithMany("Animals") + .HasForeignKey("SaleContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gerbil"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Navigation("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Navigation("Animals"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260624082334_UpdateLegalPagesDsgvo.cs b/GerbilManagerWebAPI/Migrations/20260624082334_UpdateLegalPagesDsgvo.cs new file mode 100644 index 0000000..53fea9e --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260624082334_UpdateLegalPagesDsgvo.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class UpdateLegalPagesDsgvo : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "Blocks", + keyColumn: "Id", + keyValue: new Guid("51720002-0000-0000-0000-000000000070"), + column: "Data", + value: "{\"markdown\":\"## Angaben gemäß § 5 DDG (Digitale-Dienste-Gesetz)\\n\\nSeitenbetreiber:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\n\\n## Kontakt\\n\\nTelefon: [Telefonnummer]\\nE-Mail: [E-Mail-Adresse]\\n\\n## Verantwortlich für den Inhalt gemäß § 18 Abs. 2 MStV\\n\\n[Vor- und Nachname], Anschrift wie oben\\n\\n## Verbraucherstreitbeilegung\\n\\nWir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Bitte alle Platzhalter in eckigen Klammern durch deine echten Angaben ersetzen. Pflicht ist eine ladungsfähige Anschrift (kein Postfach). Wird die Zucht gewerblich betrieben oder besteht eine Erlaubnis nach § 11 TierSchG, ergänze hier die zuständige Behörde und das Aktenzeichen.*\"}"); + + migrationBuilder.UpdateData( + table: "Blocks", + keyColumn: "Id", + keyValue: new Guid("51720002-0000-0000-0000-000000000080"), + column: "Data", + value: "{\"markdown\":\"## Datenschutzerklärung\\n\\n## 1. Verantwortlicher\\n\\nVerantwortlich für die Datenverarbeitung auf dieser Webseite ist:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\nE-Mail: [E-Mail-Adresse]\\n\\n## 2. Hosting und Server-Logfiles\\n\\nDiese Webseite wird auf einem eigenen Server betrieben. Beim Aufruf der Seite verarbeitet der Server automatisch Informationen, die dein Browser übermittelt, in sogenannten Server-Logfiles: IP-Adresse, Datum und Uhrzeit des Zugriffs, die aufgerufene Seite bzw. Datei, die übertragene Datenmenge sowie Browser und Betriebssystem.\\nDiese Verarbeitung ist technisch erforderlich, um die Webseite auszuliefern und ihren sicheren und stabilen Betrieb zu gewährleisten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse am störungsfreien Betrieb und an der Sicherheit). Die Logfiles werden nach spätestens [z. B. 7] Tagen gelöscht und nicht mit anderen Daten zusammengeführt.\\n\\n## 3. Cookies, Tracking und externe Dienste\\n\\nDiese Webseite verwendet keine Cookies, kein Tracking und keine Analyse- oder Marketing-Dienste. Es werden keine externen Inhalte Dritter (z. B. Schriftarten, Karten, Videos, CDNs) nachgeladen.\\n\\n## 4. Kontaktaufnahme\\n\\nWenn du uns per E-Mail kontaktierst, verarbeiten wir deine Angaben (z. B. Name, E-Mail-Adresse, Inhalt der Nachricht) ausschließlich zur Bearbeitung deiner Anfrage. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO bzw. Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden gelöscht, sobald sie nicht mehr benötigt werden und keine gesetzlichen Aufbewahrungspflichten entgegenstehen.\\n\\n## 5. Empfänger der Daten\\n\\nEine Weitergabe deiner Daten an Dritte findet nicht statt. Eine Datenübermittlung in Länder außerhalb der EU bzw. des EWR erfolgt nicht.\\n\\n## 6. Deine Rechte\\n\\nDu hast jederzeit das Recht auf:\\n- Auskunft über die zu deiner Person gespeicherten Daten (Art. 15 DSGVO)\\n- Berichtigung unrichtiger Daten (Art. 16 DSGVO)\\n- Löschung (Art. 17 DSGVO)\\n- Einschränkung der Verarbeitung (Art. 18 DSGVO)\\n- Datenübertragbarkeit (Art. 20 DSGVO)\\n- Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)\\n\\nZur Ausübung genügt eine formlose Nachricht an die oben genannte E-Mail-Adresse.\\n\\n## 7. Beschwerderecht bei der Aufsichtsbehörde\\n\\nDu hast das Recht, dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO). Zuständig ist die Aufsichtsbehörde deines Bundeslandes bzw. des Seitenbetreibers: [zuständige Landesdatenschutzbehörde eintragen].\\n\\n## 8. Keine automatisierte Entscheidungsfindung\\n\\nEine automatisierte Entscheidungsfindung einschließlich Profiling nach Art. 22 DSGVO findet nicht statt.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Platzhalter in eckigen Klammern ausfüllen. Aufbewahrungsdauer der Logfiles an deinen Server bzw. Reverse-Proxy anpassen. Sobald externe Dienste oder ein Kontaktformular eingebunden werden, diesen Text ergänzen. Stand: [Datum eintragen].*\"}"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "Blocks", + keyColumn: "Id", + keyValue: new Guid("51720002-0000-0000-0000-000000000070"), + column: "Data", + value: "{\"markdown\":\"**Angaben gemäß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}"); + + migrationBuilder.UpdateData( + table: "Blocks", + keyColumn: "Id", + keyValue: new Guid("51720002-0000-0000-0000-000000000080"), + column: "Data", + value: "{\"markdown\":\"**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260624084747_UpdateImpressumHobbyMStV.Designer.cs b/GerbilManagerWebAPI/Migrations/20260624084747_UpdateImpressumHobbyMStV.Designer.cs new file mode 100644 index 0000000..c27746a --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260624084747_UpdateImpressumHobbyMStV.Designer.cs @@ -0,0 +1,1861 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20260624084747_UpdateImpressumHobbyMStV")] + partial class UpdateImpressumHobbyMStV + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.AcquisitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("SourceContactId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("AcquisitionRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PageId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Blocks"); + + b.HasData( + new + { + Id = new Guid("51720002-0000-0000-0000-000000000001"), + Data = "{\"text\":\"Startseite\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000001"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000002"), + Data = "{\"text\":\"Über die Zucht\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000002"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000003"), + Data = "{\"text\":\"Abgabetiere\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000004"), + Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000004"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000005"), + Data = "{\"text\":\"Farben & Genetik\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000005"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000006"), + Data = "{\"text\":\"Kontakt\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000006"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000010"), + Data = "{\"mode\":\"auto\",\"intro\":\"\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "AbgabetiereList" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000007"), + Data = "{\"text\":\"Impressum\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000070"), + Data = "{\"markdown\":\"## Angaben gemäß § 18 Abs. 1 MStV\\n\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\n\\n## Kontakt\\n\\nE-Mail: [E-Mail-Adresse]\\nTelefon: [Telefonnummer]\\n\\n---\\n\\nDiese Seite stellt eine private Hobbyzucht mongolischer Rennmäuse ohne Gewinnerzielungsabsicht vor.\\n\\n*Hinweis für den Seitenbetreiber: Name und ladungsfähige Anschrift (kein Postfach) sind Pflicht (§ 18 Abs. 1 MStV). Sollte die Zucht jemals gewerblich bzw. geschäftsmäßig betrieben werden, gelten zusätzlich § 5 DDG (vollständiges Anbieter-Impressum) und ggf. die Erlaubnispflicht nach § 11 TierSchG — dann zuständige Behörde und Aktenzeichen hier ergänzen. Kein Hinweis auf die EU-Streitbeilegungsplattform: diese wurde zum 20.07.2025 abgeschaltet (ein fortbestehender Link wäre irreführend).*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "RichText" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000008"), + Data = "{\"text\":\"Datenschutz\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000080"), + Data = "{\"markdown\":\"## Datenschutzerklärung\\n\\n## 1. Verantwortlicher\\n\\nVerantwortlich für die Datenverarbeitung auf dieser Webseite ist:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\nE-Mail: [E-Mail-Adresse]\\n\\n## 2. Hosting und Server-Logfiles\\n\\nDiese Webseite wird auf einem eigenen Server betrieben. Beim Aufruf der Seite verarbeitet der Server automatisch Informationen, die dein Browser übermittelt, in sogenannten Server-Logfiles: IP-Adresse, Datum und Uhrzeit des Zugriffs, die aufgerufene Seite bzw. Datei, die übertragene Datenmenge sowie Browser und Betriebssystem.\\nDiese Verarbeitung ist technisch erforderlich, um die Webseite auszuliefern und ihren sicheren und stabilen Betrieb zu gewährleisten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse am störungsfreien Betrieb und an der Sicherheit). Die Logfiles werden nach spätestens [z. B. 7] Tagen gelöscht und nicht mit anderen Daten zusammengeführt.\\n\\n## 3. Cookies, Tracking und externe Dienste\\n\\nDiese Webseite verwendet keine Cookies, kein Tracking und keine Analyse- oder Marketing-Dienste. Es werden keine externen Inhalte Dritter (z. B. Schriftarten, Karten, Videos, CDNs) nachgeladen.\\n\\n## 4. Kontaktaufnahme\\n\\nWenn du uns per E-Mail kontaktierst, verarbeiten wir deine Angaben (z. B. Name, E-Mail-Adresse, Inhalt der Nachricht) ausschließlich zur Bearbeitung deiner Anfrage. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO bzw. Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden gelöscht, sobald sie nicht mehr benötigt werden und keine gesetzlichen Aufbewahrungspflichten entgegenstehen.\\n\\n## 5. Empfänger der Daten\\n\\nEine Weitergabe deiner Daten an Dritte findet nicht statt. Eine Datenübermittlung in Länder außerhalb der EU bzw. des EWR erfolgt nicht.\\n\\n## 6. Deine Rechte\\n\\nDu hast jederzeit das Recht auf:\\n- Auskunft über die zu deiner Person gespeicherten Daten (Art. 15 DSGVO)\\n- Berichtigung unrichtiger Daten (Art. 16 DSGVO)\\n- Löschung (Art. 17 DSGVO)\\n- Einschränkung der Verarbeitung (Art. 18 DSGVO)\\n- Datenübertragbarkeit (Art. 20 DSGVO)\\n- Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)\\n\\nZur Ausübung genügt eine formlose Nachricht an die oben genannte E-Mail-Adresse.\\n\\n## 7. Beschwerderecht bei der Aufsichtsbehörde\\n\\nDu hast das Recht, dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO). Zuständig ist die Aufsichtsbehörde deines Bundeslandes bzw. des Seitenbetreibers: [zuständige Landesdatenschutzbehörde eintragen].\\n\\n## 8. Keine automatisierte Entscheidungsfindung\\n\\nEine automatisierte Entscheidungsfindung einschließlich Profiling nach Art. 22 DSGVO findet nicht statt.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Platzhalter in eckigen Klammern ausfüllen. Aufbewahrungsdauer der Logfiles an deinen Server bzw. Reverse-Proxy anpassen. Sobald externe Dienste oder ein Kontaktformular eingebunden werden, diesen Text ergänzen. Stand: [Datum eintragen].*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "RichText" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Homepage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameSuffix") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ZuchtName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BreederSettings"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-000000000001"), + Address = "", + City = "", + Email = "", + Homepage = "", + Name = "", + NameSuffix = "", + Phone = "", + ZuchtName = "" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanonicalGenotype") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ColorVarieties"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0000-000000000001"), + CanonicalGenotype = "AA chch DD EE GG pp spsp rere", + Name = "REW", + SortOrder = 0 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000002"), + CanonicalGenotype = "aa chch DD EE GG PP spsp rere", + Name = "Hermelin", + SortOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000003"), + CanonicalGenotype = "AA chch DD EE GG PP spsp rere", + Name = "Himalaya", + SortOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000004"), + CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere", + Name = "Zobel", + SortOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000005"), + CanonicalGenotype = "AA CC DD efef GG pp spsp rere", + Name = "Rotaugenschimmel", + SortOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000006"), + CanonicalGenotype = "AA CC DD EE GG PP spsp rere", + Name = "Agouti", + SortOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000007"), + CanonicalGenotype = "aa CC DD EE GG PP spsp rere", + Name = "Schwarz", + SortOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000008"), + CanonicalGenotype = "AA CC DD EE gg PP spsp rere", + Name = "Silberagouti", + SortOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000009"), + CanonicalGenotype = "aa CC DD EE gg PP spsp rere", + Name = "Anthrazit", + SortOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000010"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs", + SortOrder = 9 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000011"), + CanonicalGenotype = "aa CC dd EE GG PP spsp rere", + Name = "Blau", + SortOrder = 10 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000012"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold", + SortOrder = 11 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000013"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin", + SortOrder = 12 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000014"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs", + SortOrder = 13 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000015"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs", + SortOrder = 14 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000016"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Gold", + SortOrder = 15 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000017"), + CanonicalGenotype = "aa CC dd EE GG pp spsp rere", + Name = "Dilute Platin", + SortOrder = 16 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000018"), + CanonicalGenotype = "aa CC DD EE gg pp spsp rere", + Name = "Altweiss (REW)", + SortOrder = 17 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000019"), + CanonicalGenotype = "AA CC DD ee gg pp spsp rere", + Name = "Apricot (Blassfuchs)", + SortOrder = 18 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000020"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs", + SortOrder = 19 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000021"), + CanonicalGenotype = "aa CC DD ee gg pp spsp rere", + Name = "C-Separator", + SortOrder = 20 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000022"), + CanonicalGenotype = "AA CC DD EE gg pp spsp rere", + Name = "Elfenbein", + SortOrder = 21 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000023"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs", + SortOrder = 22 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000024"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs", + SortOrder = 23 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000025"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Saphir", + SortOrder = 24 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000026"), + CanonicalGenotype = "AA CC DD efef GG PP spsp rere", + Name = "Orangeschimmel", + SortOrder = 25 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000027"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Topas", + SortOrder = 26 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000028"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin-Hell", + SortOrder = 27 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000029"), + CanonicalGenotype = "AA CC dd EE GG PP spsp rere", + Name = "Dilute Agouti", + SortOrder = 28 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000030"), + CanonicalGenotype = "AA CC dd EE gg PP spsp rere", + Name = "Dilute Silberagouti", + SortOrder = 29 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000031"), + CanonicalGenotype = "aa CC dd ee GG PP spsp rere", + Name = "Dilute Kohlfuchs", + SortOrder = 30 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000032"), + CanonicalGenotype = "aa CC dd EE gg PP spsp rere", + Name = "Dilute Anthrazit", + SortOrder = 31 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000033"), + CanonicalGenotype = "AA CC DD efef gg PP spsp rere", + Name = "Silberschimmel", + SortOrder = 36 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000034"), + CanonicalGenotype = "AA CC DD efe gg PP spsp rere", + Name = "Polarfuchsschimmel", + SortOrder = 37 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000035"), + CanonicalGenotype = "AA CC DD efe GG PP spsp rere", + Name = "Algierfuchsschimmel", + SortOrder = 38 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000036"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel", + SortOrder = 39 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000037"), + CanonicalGenotype = "aa CC DD efe gg PP spsp rere", + Name = "Blaufuchsschimmel", + SortOrder = 40 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000038"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs, hell", + SortOrder = 41 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000039"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs, hell", + SortOrder = 42 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000040"), + CanonicalGenotype = "AA CC DD efe GG pp spsp rere", + Name = "Goldfuchsschimmel", + SortOrder = 43 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000041"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold-Hell", + SortOrder = 44 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000042"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs, hell", + SortOrder = 45 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000043"), + CanonicalGenotype = "aa CC DD efe GG pp spsp rere", + Name = "Rotfuchsschimmel", + SortOrder = 46 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000044"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs, hell", + SortOrder = 47 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000045"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel, hell", + SortOrder = 48 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000046"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs, hell", + SortOrder = 49 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000047"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs-Hell", + SortOrder = 50 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000048"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs, hell", + SortOrder = 51 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000049"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Topas", + SortOrder = 52 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000050"), + CanonicalGenotype = "aa CC dd ee gg PP spsp rere", + Name = "Dilute Blaufuchs", + SortOrder = 53 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000051"), + CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere", + Name = "Marder", + SortOrder = 54 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000052"), + CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere", + Name = "Siam", + SortOrder = 55 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000053"), + CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere", + Name = "Zobel-Hell", + SortOrder = 56 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000054"), + CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere", + Name = "CP-Agouti", + SortOrder = 57 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000055"), + CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere", + Name = "CP-Silberagouti", + SortOrder = 59 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000056"), + CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere", + Name = "CP-Algierfuchs", + SortOrder = 61 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000057"), + CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere", + Name = "CP-Polarfuchs", + SortOrder = 63 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000058"), + CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere", + Name = "CP-Fuchs", + SortOrder = 65 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000059"), + CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere", + Name = "CP-Fuchs-Hell", + SortOrder = 66 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000060"), + CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere", + Name = "CP-Blaufuchs", + SortOrder = 67 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000061"), + CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel", + SortOrder = 68 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000062"), + CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere", + Name = "CP-Agouti-Hell", + SortOrder = 58 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000063"), + CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere", + Name = "CP-Silberagouti-Hell", + SortOrder = 60 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000064"), + CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere", + Name = "CP-Algierfuchs-Hell", + SortOrder = 62 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000065"), + CanonicalGenotype = "AA cchmch DD ee gg PP spsp rere", + Name = "CP-Polarfuchs-Hell", + SortOrder = 64 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000066"), + CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel-Hell", + SortOrder = 69 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000067"), + CanonicalGenotype = "AA CC dd ee GG PP spsp rere", + Name = "Dilute Algierfuchs", + SortOrder = 32 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000068"), + CanonicalGenotype = "AA CC dd ee GG pp spsp rere", + Name = "Dilute Goldfuchs", + SortOrder = 33 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000069"), + CanonicalGenotype = "aa CC dd ee GG pp spsp rere", + Name = "Dilute Rotfuchs", + SortOrder = 34 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000070"), + CanonicalGenotype = "AA CC dd ee gg PP spsp rere", + Name = "Dilute Polarfuchs", + SortOrder = 35 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("IsBreeder") + .HasColumnType("boolean"); + + b.Property("IsReceiver") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSuffix") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CleaningCycleDays") + .HasColumnType("integer"); + + b.Property("LastCleanedDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Enclosures"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EnclosureId"); + + b.ToTable("EnclosurePhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ExhibitionResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Award") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("EventName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Placement") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("ExhibitionResults"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentContext") + .HasColumnType("text"); + + b.Property("Answer") + .HasColumnType("text"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ClientTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("Context") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("FixNote") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Helpful") + .HasColumnType("boolean"); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Question") + .HasColumnType("text"); + + b.Property("ReopenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Thread") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.FeedbackAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FeedbackId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("FeedbackAttachments"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CauseOfDeath") + .HasColumnType("text"); + + b.Property("CharacterNote") + .HasColumnType("text"); + + b.Property("CharacterTraits") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorVarietyId") + .HasColumnType("uuid"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DateOfDeath") + .HasColumnType("date"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + + b.Property("Genotype") + .HasColumnType("text"); + + b.Property("GoHomeDate") + .HasColumnType("date"); + + b.Property("ImportSource") + .HasColumnType("text"); + + b.Property("IsCastrated") + .HasColumnType("boolean"); + + b.Property("IsDeaf") + .HasColumnType("boolean"); + + b.Property("IsResident") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSearch") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OriginBreeder") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("OriginContactId") + .HasColumnType("uuid"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("RawImportData") + .HasColumnType("text"); + + b.Property("ReceiverContactId") + .HasColumnType("uuid"); + + b.Property("SpottingType") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ColorVarietyId"); + + b.HasIndex("EnclosureId"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("LitterId"); + + b.HasIndex("OriginContactId"); + + b.HasIndex("ReceiverContactId"); + + b.ToTable("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("GerbilPhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Veterinarian") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("HealthRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeathsWithin8Weeks") + .HasColumnType("integer"); + + b.Property("ExpectedGoHomeDate") + .HasColumnType("date"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("FatherId") + .HasColumnType("uuid"); + + b.Property("LitterLetter") + .HasColumnType("text"); + + b.Property("MotherId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PairingCode") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("ShowInChronicle") + .HasColumnType("boolean"); + + b.Property("TotalBorn") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("FatherId"); + + b.HasIndex("MotherId"); + + b.ToTable("Litters"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppPasswordProtected") + .HasColumnType("text"); + + b.Property("BackgroundPollEnabled") + .HasColumnType("boolean"); + + b.Property("Folder") + .IsRequired() + .HasColumnType("text"); + + b.Property("GmailAddress") + .HasColumnType("text"); + + b.Property("LastUid") + .HasColumnType("bigint"); + + b.Property("PollIntervalMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MailSettings"); + + b.HasData( + new + { + Id = new Guid("ab0c0000-0000-0000-0000-000000000001"), + BackgroundPollEnabled = false, + Folder = "INBOX", + LastUid = 0L, + PollIntervalMinutes = 15 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alt") + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SeoDescription") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pages"); + + b.HasData( + new + { + Id = new Guid("51720001-0000-0000-0000-000000000001"), + Slug = "start", + Status = "Published", + Title = "Startseite" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000002"), + Slug = "ueber-die-zucht", + Status = "Published", + Title = "Über die Zucht" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000003"), + Slug = "abgabetiere", + Status = "Published", + Title = "Abgabetiere" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000004"), + Slug = "abgabebedingungen", + Status = "Published", + Title = "Abgabebedingungen" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000005"), + Slug = "farben-genetik", + Status = "Published", + Title = "Farben & Genetik" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000006"), + Slug = "kontakt", + Status = "Published", + Title = "Kontakt" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000007"), + Slug = "impressum", + Status = "Published", + Title = "Impressum" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000008"), + Slug = "datenschutz", + Status = "Published", + Title = "Datenschutz" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedContactId") + .HasColumnType("uuid"); + + b.Property("BodyText") + .HasColumnType("text"); + + b.Property("DraftReply") + .HasColumnType("text"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromName") + .HasColumnType("text"); + + b.Property("GmailMessageId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InReplyToMessageId") + .HasColumnType("text"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferencesHeader") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AssignedContactId"); + + b.HasIndex("GmailMessageId") + .IsUnique(); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ReturnRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FromContactId") + .HasColumnType("uuid"); + + b.Property("FromContactName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OriginalPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("OriginalSaleDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("GerbilId"); + + b.ToTable("ReturnRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContractDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HandoverDate") + .HasColumnType("date"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("SaleContracts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.Property("SaleContractId") + .HasColumnType("uuid"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("PhotoId") + .HasColumnType("uuid"); + + b.HasKey("SaleContractId", "GerbilId"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleContractAnimal"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppointmentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("HandedOverDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("ReservedForContactId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleReservations"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultLocale") + .IsRequired() + .HasColumnType("text"); + + b.Property("NavOrder") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sites"); + + b.HasData( + new + { + Id = new Guid("5172e000-0000-0000-0000-000000000001"), + DefaultLocale = "de", + NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WaitingListEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("WishColor") + .HasColumnType("text"); + + b.Property("WishGender") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("WaitingListEntries"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WebPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("P256dh") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("WebPushSubscriptions"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("WeightGrams") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("WeightRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Page", null) + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", null) + .WithMany() + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety") + .WithMany() + .HasForeignKey("ColorVarietyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure") + .WithMany("Gerbils") + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter") + .WithMany() + .HasForeignKey("LitterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact") + .WithMany() + .HasForeignKey("OriginContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact") + .WithMany() + .HasForeignKey("ReceiverContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ColorVariety"); + + b.Navigation("Enclosure"); + + b.Navigation("Litter"); + + b.Navigation("OriginContact"); + + b.Navigation("ReceiverContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father") + .WithMany() + .HasForeignKey("FatherId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother") + .WithMany() + .HasForeignKey("MotherId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Father"); + + b.Navigation("Mother"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact") + .WithMany() + .HasForeignKey("AssignedContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignedContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil") + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null) + .WithMany("Animals") + .HasForeignKey("SaleContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gerbil"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Navigation("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Navigation("Animals"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260624084747_UpdateImpressumHobbyMStV.cs b/GerbilManagerWebAPI/Migrations/20260624084747_UpdateImpressumHobbyMStV.cs new file mode 100644 index 0000000..4d3ab06 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260624084747_UpdateImpressumHobbyMStV.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class UpdateImpressumHobbyMStV : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "Blocks", + keyColumn: "Id", + keyValue: new Guid("51720002-0000-0000-0000-000000000070"), + column: "Data", + value: "{\"markdown\":\"## Angaben gemäß § 18 Abs. 1 MStV\\n\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\n\\n## Kontakt\\n\\nE-Mail: [E-Mail-Adresse]\\nTelefon: [Telefonnummer]\\n\\n---\\n\\nDiese Seite stellt eine private Hobbyzucht mongolischer Rennmäuse ohne Gewinnerzielungsabsicht vor.\\n\\n*Hinweis für den Seitenbetreiber: Name und ladungsfähige Anschrift (kein Postfach) sind Pflicht (§ 18 Abs. 1 MStV). Sollte die Zucht jemals gewerblich bzw. geschäftsmäßig betrieben werden, gelten zusätzlich § 5 DDG (vollständiges Anbieter-Impressum) und ggf. die Erlaubnispflicht nach § 11 TierSchG — dann zuständige Behörde und Aktenzeichen hier ergänzen. Kein Hinweis auf die EU-Streitbeilegungsplattform: diese wurde zum 20.07.2025 abgeschaltet (ein fortbestehender Link wäre irreführend).*\"}"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "Blocks", + keyColumn: "Id", + keyValue: new Guid("51720002-0000-0000-0000-000000000070"), + column: "Data", + value: "{\"markdown\":\"## Angaben gemäß § 5 DDG (Digitale-Dienste-Gesetz)\\n\\nSeitenbetreiber:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\n\\n## Kontakt\\n\\nTelefon: [Telefonnummer]\\nE-Mail: [E-Mail-Adresse]\\n\\n## Verantwortlich für den Inhalt gemäß § 18 Abs. 2 MStV\\n\\n[Vor- und Nachname], Anschrift wie oben\\n\\n## Verbraucherstreitbeilegung\\n\\nWir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Bitte alle Platzhalter in eckigen Klammern durch deine echten Angaben ersetzen. Pflicht ist eine ladungsfähige Anschrift (kein Postfach). Wird die Zucht gewerblich betrieben oder besteht eine Erlaubnis nach § 11 TierSchG, ergänze hier die zuständige Behörde und das Aktenzeichen.*\"}"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260712142631_AddVerifiedGerbilAndManualFlag.Designer.cs b/GerbilManagerWebAPI/Migrations/20260712142631_AddVerifiedGerbilAndManualFlag.Designer.cs new file mode 100644 index 0000000..c68730c --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260712142631_AddVerifiedGerbilAndManualFlag.Designer.cs @@ -0,0 +1,1921 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20260712142631_AddVerifiedGerbilAndManualFlag")] + partial class AddVerifiedGerbilAndManualFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.AcquisitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("SourceContactId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("AcquisitionRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PageId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Blocks"); + + b.HasData( + new + { + Id = new Guid("51720002-0000-0000-0000-000000000001"), + Data = "{\"text\":\"Startseite\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000001"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000002"), + Data = "{\"text\":\"Über die Zucht\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000002"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000003"), + Data = "{\"text\":\"Abgabetiere\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000004"), + Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000004"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000005"), + Data = "{\"text\":\"Farben & Genetik\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000005"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000006"), + Data = "{\"text\":\"Kontakt\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000006"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000010"), + Data = "{\"mode\":\"auto\",\"intro\":\"\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "AbgabetiereList" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000007"), + Data = "{\"text\":\"Impressum\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000070"), + Data = "{\"markdown\":\"## Angaben gemäß § 18 Abs. 1 MStV\\n\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\n\\n## Kontakt\\n\\nE-Mail: [E-Mail-Adresse]\\nTelefon: [Telefonnummer]\\n\\n---\\n\\nDiese Seite stellt eine private Hobbyzucht mongolischer Rennmäuse ohne Gewinnerzielungsabsicht vor.\\n\\n*Hinweis für den Seitenbetreiber: Name und ladungsfähige Anschrift (kein Postfach) sind Pflicht (§ 18 Abs. 1 MStV). Sollte die Zucht jemals gewerblich bzw. geschäftsmäßig betrieben werden, gelten zusätzlich § 5 DDG (vollständiges Anbieter-Impressum) und ggf. die Erlaubnispflicht nach § 11 TierSchG — dann zuständige Behörde und Aktenzeichen hier ergänzen. Kein Hinweis auf die EU-Streitbeilegungsplattform: diese wurde zum 20.07.2025 abgeschaltet (ein fortbestehender Link wäre irreführend).*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "RichText" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000008"), + Data = "{\"text\":\"Datenschutz\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000080"), + Data = "{\"markdown\":\"## Datenschutzerklärung\\n\\n## 1. Verantwortlicher\\n\\nVerantwortlich für die Datenverarbeitung auf dieser Webseite ist:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\nE-Mail: [E-Mail-Adresse]\\n\\n## 2. Hosting und Server-Logfiles\\n\\nDiese Webseite wird auf einem eigenen Server betrieben. Beim Aufruf der Seite verarbeitet der Server automatisch Informationen, die dein Browser übermittelt, in sogenannten Server-Logfiles: IP-Adresse, Datum und Uhrzeit des Zugriffs, die aufgerufene Seite bzw. Datei, die übertragene Datenmenge sowie Browser und Betriebssystem.\\nDiese Verarbeitung ist technisch erforderlich, um die Webseite auszuliefern und ihren sicheren und stabilen Betrieb zu gewährleisten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse am störungsfreien Betrieb und an der Sicherheit). Die Logfiles werden nach spätestens [z. B. 7] Tagen gelöscht und nicht mit anderen Daten zusammengeführt.\\n\\n## 3. Cookies, Tracking und externe Dienste\\n\\nDiese Webseite verwendet keine Cookies, kein Tracking und keine Analyse- oder Marketing-Dienste. Es werden keine externen Inhalte Dritter (z. B. Schriftarten, Karten, Videos, CDNs) nachgeladen.\\n\\n## 4. Kontaktaufnahme\\n\\nWenn du uns per E-Mail kontaktierst, verarbeiten wir deine Angaben (z. B. Name, E-Mail-Adresse, Inhalt der Nachricht) ausschließlich zur Bearbeitung deiner Anfrage. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO bzw. Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden gelöscht, sobald sie nicht mehr benötigt werden und keine gesetzlichen Aufbewahrungspflichten entgegenstehen.\\n\\n## 5. Empfänger der Daten\\n\\nEine Weitergabe deiner Daten an Dritte findet nicht statt. Eine Datenübermittlung in Länder außerhalb der EU bzw. des EWR erfolgt nicht.\\n\\n## 6. Deine Rechte\\n\\nDu hast jederzeit das Recht auf:\\n- Auskunft über die zu deiner Person gespeicherten Daten (Art. 15 DSGVO)\\n- Berichtigung unrichtiger Daten (Art. 16 DSGVO)\\n- Löschung (Art. 17 DSGVO)\\n- Einschränkung der Verarbeitung (Art. 18 DSGVO)\\n- Datenübertragbarkeit (Art. 20 DSGVO)\\n- Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)\\n\\nZur Ausübung genügt eine formlose Nachricht an die oben genannte E-Mail-Adresse.\\n\\n## 7. Beschwerderecht bei der Aufsichtsbehörde\\n\\nDu hast das Recht, dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO). Zuständig ist die Aufsichtsbehörde deines Bundeslandes bzw. des Seitenbetreibers: [zuständige Landesdatenschutzbehörde eintragen].\\n\\n## 8. Keine automatisierte Entscheidungsfindung\\n\\nEine automatisierte Entscheidungsfindung einschließlich Profiling nach Art. 22 DSGVO findet nicht statt.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Platzhalter in eckigen Klammern ausfüllen. Aufbewahrungsdauer der Logfiles an deinen Server bzw. Reverse-Proxy anpassen. Sobald externe Dienste oder ein Kontaktformular eingebunden werden, diesen Text ergänzen. Stand: [Datum eintragen].*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "RichText" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Homepage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameSuffix") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ZuchtName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BreederSettings"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-000000000001"), + Address = "", + City = "", + Email = "", + Homepage = "", + Name = "", + NameSuffix = "", + Phone = "", + ZuchtName = "" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanonicalGenotype") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ColorVarieties"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0000-000000000001"), + CanonicalGenotype = "AA chch DD EE GG pp spsp rere", + Name = "REW", + SortOrder = 0 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000002"), + CanonicalGenotype = "aa chch DD EE GG PP spsp rere", + Name = "Hermelin", + SortOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000003"), + CanonicalGenotype = "AA chch DD EE GG PP spsp rere", + Name = "Himalaya", + SortOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000004"), + CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere", + Name = "Zobel", + SortOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000005"), + CanonicalGenotype = "AA CC DD efef GG pp spsp rere", + Name = "Rotaugenschimmel", + SortOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000006"), + CanonicalGenotype = "AA CC DD EE GG PP spsp rere", + Name = "Agouti", + SortOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000007"), + CanonicalGenotype = "aa CC DD EE GG PP spsp rere", + Name = "Schwarz", + SortOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000008"), + CanonicalGenotype = "AA CC DD EE gg PP spsp rere", + Name = "Silberagouti", + SortOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000009"), + CanonicalGenotype = "aa CC DD EE gg PP spsp rere", + Name = "Anthrazit", + SortOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000010"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs", + SortOrder = 9 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000011"), + CanonicalGenotype = "aa CC dd EE GG PP spsp rere", + Name = "Blau", + SortOrder = 10 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000012"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold", + SortOrder = 11 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000013"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin", + SortOrder = 12 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000014"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs", + SortOrder = 13 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000015"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs", + SortOrder = 14 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000016"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Gold", + SortOrder = 15 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000017"), + CanonicalGenotype = "aa CC dd EE GG pp spsp rere", + Name = "Dilute Platin", + SortOrder = 16 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000018"), + CanonicalGenotype = "aa CC DD EE gg pp spsp rere", + Name = "Altweiss (REW)", + SortOrder = 17 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000019"), + CanonicalGenotype = "AA CC DD ee gg pp spsp rere", + Name = "Apricot (Blassfuchs)", + SortOrder = 18 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000020"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs", + SortOrder = 19 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000021"), + CanonicalGenotype = "aa CC DD ee gg pp spsp rere", + Name = "C-Separator", + SortOrder = 20 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000022"), + CanonicalGenotype = "AA CC DD EE gg pp spsp rere", + Name = "Elfenbein", + SortOrder = 21 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000023"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs", + SortOrder = 22 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000024"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs", + SortOrder = 23 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000025"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Saphir", + SortOrder = 24 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000026"), + CanonicalGenotype = "AA CC DD efef GG PP spsp rere", + Name = "Orangeschimmel", + SortOrder = 25 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000027"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Topas", + SortOrder = 26 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000028"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin-Hell", + SortOrder = 27 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000029"), + CanonicalGenotype = "AA CC dd EE GG PP spsp rere", + Name = "Dilute Agouti", + SortOrder = 28 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000030"), + CanonicalGenotype = "AA CC dd EE gg PP spsp rere", + Name = "Dilute Silberagouti", + SortOrder = 29 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000031"), + CanonicalGenotype = "aa CC dd ee GG PP spsp rere", + Name = "Dilute Kohlfuchs", + SortOrder = 30 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000032"), + CanonicalGenotype = "aa CC dd EE gg PP spsp rere", + Name = "Dilute Anthrazit", + SortOrder = 31 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000033"), + CanonicalGenotype = "AA CC DD efef gg PP spsp rere", + Name = "Silberschimmel", + SortOrder = 36 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000034"), + CanonicalGenotype = "AA CC DD efe gg PP spsp rere", + Name = "Polarfuchsschimmel", + SortOrder = 37 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000035"), + CanonicalGenotype = "AA CC DD efe GG PP spsp rere", + Name = "Algierfuchsschimmel", + SortOrder = 38 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000036"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel", + SortOrder = 39 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000037"), + CanonicalGenotype = "aa CC DD efe gg PP spsp rere", + Name = "Blaufuchsschimmel", + SortOrder = 40 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000038"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs, hell", + SortOrder = 41 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000039"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs, hell", + SortOrder = 42 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000040"), + CanonicalGenotype = "AA CC DD efe GG pp spsp rere", + Name = "Goldfuchsschimmel", + SortOrder = 43 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000041"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold-Hell", + SortOrder = 44 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000042"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs, hell", + SortOrder = 45 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000043"), + CanonicalGenotype = "aa CC DD efe GG pp spsp rere", + Name = "Rotfuchsschimmel", + SortOrder = 46 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000044"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs, hell", + SortOrder = 47 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000045"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel, hell", + SortOrder = 48 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000046"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs, hell", + SortOrder = 49 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000047"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs-Hell", + SortOrder = 50 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000048"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs, hell", + SortOrder = 51 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000049"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Topas", + SortOrder = 52 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000050"), + CanonicalGenotype = "aa CC dd ee gg PP spsp rere", + Name = "Dilute Blaufuchs", + SortOrder = 53 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000051"), + CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere", + Name = "Marder", + SortOrder = 54 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000052"), + CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere", + Name = "Siam", + SortOrder = 55 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000053"), + CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere", + Name = "Zobel-Hell", + SortOrder = 56 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000054"), + CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere", + Name = "CP-Agouti", + SortOrder = 57 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000055"), + CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere", + Name = "CP-Silberagouti", + SortOrder = 59 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000056"), + CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere", + Name = "CP-Algierfuchs", + SortOrder = 61 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000057"), + CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere", + Name = "CP-Polarfuchs", + SortOrder = 63 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000058"), + CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere", + Name = "CP-Fuchs", + SortOrder = 65 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000059"), + CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere", + Name = "CP-Fuchs-Hell", + SortOrder = 66 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000060"), + CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere", + Name = "CP-Blaufuchs", + SortOrder = 67 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000061"), + CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel", + SortOrder = 68 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000062"), + CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere", + Name = "CP-Agouti-Hell", + SortOrder = 58 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000063"), + CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere", + Name = "CP-Silberagouti-Hell", + SortOrder = 60 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000064"), + CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere", + Name = "CP-Algierfuchs-Hell", + SortOrder = 62 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000065"), + CanonicalGenotype = "AA cchmch DD ee gg PP spsp rere", + Name = "CP-Polarfuchs-Hell", + SortOrder = 64 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000066"), + CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel-Hell", + SortOrder = 69 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000067"), + CanonicalGenotype = "AA CC dd ee GG PP spsp rere", + Name = "Dilute Algierfuchs", + SortOrder = 32 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000068"), + CanonicalGenotype = "AA CC dd ee GG pp spsp rere", + Name = "Dilute Goldfuchs", + SortOrder = 33 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000069"), + CanonicalGenotype = "aa CC dd ee GG pp spsp rere", + Name = "Dilute Rotfuchs", + SortOrder = 34 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000070"), + CanonicalGenotype = "AA CC dd ee gg PP spsp rere", + Name = "Dilute Polarfuchs", + SortOrder = 35 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("IsBreeder") + .HasColumnType("boolean"); + + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsReceiver") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSuffix") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CleaningCycleDays") + .HasColumnType("integer"); + + b.Property("LastCleanedDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Enclosures"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EnclosureId"); + + b.ToTable("EnclosurePhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ExhibitionResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Award") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("EventName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Placement") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("ExhibitionResults"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentContext") + .HasColumnType("text"); + + b.Property("Answer") + .HasColumnType("text"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ClientTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("Context") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("FixNote") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Helpful") + .HasColumnType("boolean"); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Question") + .HasColumnType("text"); + + b.Property("ReopenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Thread") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.FeedbackAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FeedbackId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("FeedbackAttachments"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CauseOfDeath") + .HasColumnType("text"); + + b.Property("CharacterNote") + .HasColumnType("text"); + + b.Property("CharacterTraits") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorVarietyId") + .HasColumnType("uuid"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DateOfDeath") + .HasColumnType("date"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + + b.Property("Genotype") + .HasColumnType("text"); + + b.Property("GoHomeDate") + .HasColumnType("date"); + + b.Property("ImportSource") + .HasColumnType("text"); + + b.Property("IsCastrated") + .HasColumnType("boolean"); + + b.Property("IsDeaf") + .HasColumnType("boolean"); + + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsResident") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSearch") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OriginBreeder") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("OriginContactId") + .HasColumnType("uuid"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("RawImportData") + .HasColumnType("text"); + + b.Property("ReceiverContactId") + .HasColumnType("uuid"); + + b.Property("SpottingType") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ColorVarietyId"); + + b.HasIndex("EnclosureId"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("LitterId"); + + b.HasIndex("OriginContactId"); + + b.HasIndex("ReceiverContactId"); + + b.ToTable("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LastImportDiffJson") + .HasColumnType("text"); + + b.Property("LastImportSnapshotJson") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OverrideJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SnapshotJson") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId") + .IsUnique(); + + b.ToTable("GerbilOverrides"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("GerbilPhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Veterinarian") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("HealthRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeathsWithin8Weeks") + .HasColumnType("integer"); + + b.Property("ExpectedGoHomeDate") + .HasColumnType("date"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("FatherId") + .HasColumnType("uuid"); + + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LitterLetter") + .HasColumnType("text"); + + b.Property("MotherId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PairingCode") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("ShowInChronicle") + .HasColumnType("boolean"); + + b.Property("TotalBorn") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("FatherId"); + + b.HasIndex("MotherId"); + + b.ToTable("Litters"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppPasswordProtected") + .HasColumnType("text"); + + b.Property("BackgroundPollEnabled") + .HasColumnType("boolean"); + + b.Property("Folder") + .IsRequired() + .HasColumnType("text"); + + b.Property("GmailAddress") + .HasColumnType("text"); + + b.Property("LastUid") + .HasColumnType("bigint"); + + b.Property("PollIntervalMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MailSettings"); + + b.HasData( + new + { + Id = new Guid("ab0c0000-0000-0000-0000-000000000001"), + BackgroundPollEnabled = false, + Folder = "INBOX", + LastUid = 0L, + PollIntervalMinutes = 15 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alt") + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SeoDescription") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pages"); + + b.HasData( + new + { + Id = new Guid("51720001-0000-0000-0000-000000000001"), + Slug = "start", + Status = "Published", + Title = "Startseite" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000002"), + Slug = "ueber-die-zucht", + Status = "Published", + Title = "Über die Zucht" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000003"), + Slug = "abgabetiere", + Status = "Published", + Title = "Abgabetiere" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000004"), + Slug = "abgabebedingungen", + Status = "Published", + Title = "Abgabebedingungen" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000005"), + Slug = "farben-genetik", + Status = "Published", + Title = "Farben & Genetik" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000006"), + Slug = "kontakt", + Status = "Published", + Title = "Kontakt" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000007"), + Slug = "impressum", + Status = "Published", + Title = "Impressum" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000008"), + Slug = "datenschutz", + Status = "Published", + Title = "Datenschutz" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedContactId") + .HasColumnType("uuid"); + + b.Property("BodyText") + .HasColumnType("text"); + + b.Property("DraftReply") + .HasColumnType("text"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromName") + .HasColumnType("text"); + + b.Property("GmailMessageId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InReplyToMessageId") + .HasColumnType("text"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferencesHeader") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AssignedContactId"); + + b.HasIndex("GmailMessageId") + .IsUnique(); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ReturnRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FromContactId") + .HasColumnType("uuid"); + + b.Property("FromContactName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OriginalPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("OriginalSaleDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("GerbilId"); + + b.ToTable("ReturnRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContractDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HandoverDate") + .HasColumnType("date"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("SaleContracts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.Property("SaleContractId") + .HasColumnType("uuid"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("PhotoId") + .HasColumnType("uuid"); + + b.HasKey("SaleContractId", "GerbilId"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleContractAnimal"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppointmentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("HandedOverDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("ReservedForContactId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleReservations"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultLocale") + .IsRequired() + .HasColumnType("text"); + + b.Property("NavOrder") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sites"); + + b.HasData( + new + { + Id = new Guid("5172e000-0000-0000-0000-000000000001"), + DefaultLocale = "de", + NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WaitingListEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("WishColor") + .HasColumnType("text"); + + b.Property("WishGender") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("WaitingListEntries"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WebPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("P256dh") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("WebPushSubscriptions"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("WeightGrams") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("WeightRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Page", null) + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", null) + .WithMany() + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety") + .WithMany() + .HasForeignKey("ColorVarietyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure") + .WithMany("Gerbils") + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter") + .WithMany() + .HasForeignKey("LitterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact") + .WithMany() + .HasForeignKey("OriginContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact") + .WithMany() + .HasForeignKey("ReceiverContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ColorVariety"); + + b.Navigation("Enclosure"); + + b.Navigation("Litter"); + + b.Navigation("OriginContact"); + + b.Navigation("ReceiverContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father") + .WithMany() + .HasForeignKey("FatherId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother") + .WithMany() + .HasForeignKey("MotherId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Father"); + + b.Navigation("Mother"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact") + .WithMany() + .HasForeignKey("AssignedContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignedContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil") + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null) + .WithMany("Animals") + .HasForeignKey("SaleContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gerbil"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Navigation("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Navigation("Animals"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260712142631_AddVerifiedGerbilAndManualFlag.cs b/GerbilManagerWebAPI/Migrations/20260712142631_AddVerifiedGerbilAndManualFlag.cs new file mode 100644 index 0000000..8830916 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260712142631_AddVerifiedGerbilAndManualFlag.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class AddVerifiedGerbilAndManualFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsManual", + table: "Litters", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsManual", + table: "Gerbils", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsManual", + table: "Contacts", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "GerbilOverrides", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + GerbilId = table.Column(type: "uuid", nullable: false), + EntityName = table.Column(type: "text", nullable: true), + IsVerified = table.Column(type: "boolean", nullable: false), + OverrideJson = table.Column(type: "text", nullable: false), + SnapshotJson = table.Column(type: "text", nullable: true), + LastImportSnapshotJson = table.Column(type: "text", nullable: true), + LastImportDiffJson = table.Column(type: "text", nullable: true), + Note = table.Column(type: "text", nullable: true), + VerifiedAt = table.Column(type: "timestamp with time zone", nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GerbilOverrides", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_GerbilOverrides_GerbilId", + table: "GerbilOverrides", + column: "GerbilId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GerbilOverrides"); + + migrationBuilder.DropColumn( + name: "IsManual", + table: "Litters"); + + migrationBuilder.DropColumn( + name: "IsManual", + table: "Gerbils"); + + migrationBuilder.DropColumn( + name: "IsManual", + table: "Contacts"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260713173625_AddLitterStillborn.Designer.cs b/GerbilManagerWebAPI/Migrations/20260713173625_AddLitterStillborn.Designer.cs new file mode 100644 index 0000000..056ac61 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260713173625_AddLitterStillborn.Designer.cs @@ -0,0 +1,1924 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20260713173625_AddLitterStillborn")] + partial class AddLitterStillborn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.AcquisitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("SourceContactId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("AcquisitionRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PageId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Blocks"); + + b.HasData( + new + { + Id = new Guid("51720002-0000-0000-0000-000000000001"), + Data = "{\"text\":\"Startseite\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000001"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000002"), + Data = "{\"text\":\"Über die Zucht\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000002"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000003"), + Data = "{\"text\":\"Abgabetiere\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000004"), + Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000004"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000005"), + Data = "{\"text\":\"Farben & Genetik\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000005"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000006"), + Data = "{\"text\":\"Kontakt\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000006"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000010"), + Data = "{\"mode\":\"auto\",\"intro\":\"\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "AbgabetiereList" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000007"), + Data = "{\"text\":\"Impressum\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000070"), + Data = "{\"markdown\":\"## Angaben gemäß § 18 Abs. 1 MStV\\n\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\n\\n## Kontakt\\n\\nE-Mail: [E-Mail-Adresse]\\nTelefon: [Telefonnummer]\\n\\n---\\n\\nDiese Seite stellt eine private Hobbyzucht mongolischer Rennmäuse ohne Gewinnerzielungsabsicht vor.\\n\\n*Hinweis für den Seitenbetreiber: Name und ladungsfähige Anschrift (kein Postfach) sind Pflicht (§ 18 Abs. 1 MStV). Sollte die Zucht jemals gewerblich bzw. geschäftsmäßig betrieben werden, gelten zusätzlich § 5 DDG (vollständiges Anbieter-Impressum) und ggf. die Erlaubnispflicht nach § 11 TierSchG — dann zuständige Behörde und Aktenzeichen hier ergänzen. Kein Hinweis auf die EU-Streitbeilegungsplattform: diese wurde zum 20.07.2025 abgeschaltet (ein fortbestehender Link wäre irreführend).*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "RichText" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000008"), + Data = "{\"text\":\"Datenschutz\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000080"), + Data = "{\"markdown\":\"## Datenschutzerklärung\\n\\n## 1. Verantwortlicher\\n\\nVerantwortlich für die Datenverarbeitung auf dieser Webseite ist:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\nE-Mail: [E-Mail-Adresse]\\n\\n## 2. Hosting und Server-Logfiles\\n\\nDiese Webseite wird auf einem eigenen Server betrieben. Beim Aufruf der Seite verarbeitet der Server automatisch Informationen, die dein Browser übermittelt, in sogenannten Server-Logfiles: IP-Adresse, Datum und Uhrzeit des Zugriffs, die aufgerufene Seite bzw. Datei, die übertragene Datenmenge sowie Browser und Betriebssystem.\\nDiese Verarbeitung ist technisch erforderlich, um die Webseite auszuliefern und ihren sicheren und stabilen Betrieb zu gewährleisten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse am störungsfreien Betrieb und an der Sicherheit). Die Logfiles werden nach spätestens [z. B. 7] Tagen gelöscht und nicht mit anderen Daten zusammengeführt.\\n\\n## 3. Cookies, Tracking und externe Dienste\\n\\nDiese Webseite verwendet keine Cookies, kein Tracking und keine Analyse- oder Marketing-Dienste. Es werden keine externen Inhalte Dritter (z. B. Schriftarten, Karten, Videos, CDNs) nachgeladen.\\n\\n## 4. Kontaktaufnahme\\n\\nWenn du uns per E-Mail kontaktierst, verarbeiten wir deine Angaben (z. B. Name, E-Mail-Adresse, Inhalt der Nachricht) ausschließlich zur Bearbeitung deiner Anfrage. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO bzw. Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden gelöscht, sobald sie nicht mehr benötigt werden und keine gesetzlichen Aufbewahrungspflichten entgegenstehen.\\n\\n## 5. Empfänger der Daten\\n\\nEine Weitergabe deiner Daten an Dritte findet nicht statt. Eine Datenübermittlung in Länder außerhalb der EU bzw. des EWR erfolgt nicht.\\n\\n## 6. Deine Rechte\\n\\nDu hast jederzeit das Recht auf:\\n- Auskunft über die zu deiner Person gespeicherten Daten (Art. 15 DSGVO)\\n- Berichtigung unrichtiger Daten (Art. 16 DSGVO)\\n- Löschung (Art. 17 DSGVO)\\n- Einschränkung der Verarbeitung (Art. 18 DSGVO)\\n- Datenübertragbarkeit (Art. 20 DSGVO)\\n- Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)\\n\\nZur Ausübung genügt eine formlose Nachricht an die oben genannte E-Mail-Adresse.\\n\\n## 7. Beschwerderecht bei der Aufsichtsbehörde\\n\\nDu hast das Recht, dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO). Zuständig ist die Aufsichtsbehörde deines Bundeslandes bzw. des Seitenbetreibers: [zuständige Landesdatenschutzbehörde eintragen].\\n\\n## 8. Keine automatisierte Entscheidungsfindung\\n\\nEine automatisierte Entscheidungsfindung einschließlich Profiling nach Art. 22 DSGVO findet nicht statt.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Platzhalter in eckigen Klammern ausfüllen. Aufbewahrungsdauer der Logfiles an deinen Server bzw. Reverse-Proxy anpassen. Sobald externe Dienste oder ein Kontaktformular eingebunden werden, diesen Text ergänzen. Stand: [Datum eintragen].*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "RichText" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Homepage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameSuffix") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ZuchtName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BreederSettings"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-000000000001"), + Address = "", + City = "", + Email = "", + Homepage = "", + Name = "", + NameSuffix = "", + Phone = "", + ZuchtName = "" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanonicalGenotype") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ColorVarieties"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0000-000000000001"), + CanonicalGenotype = "AA chch DD EE GG pp spsp rere", + Name = "REW", + SortOrder = 0 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000002"), + CanonicalGenotype = "aa chch DD EE GG PP spsp rere", + Name = "Hermelin", + SortOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000003"), + CanonicalGenotype = "AA chch DD EE GG PP spsp rere", + Name = "Himalaya", + SortOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000004"), + CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere", + Name = "Zobel", + SortOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000005"), + CanonicalGenotype = "AA CC DD efef GG pp spsp rere", + Name = "Rotaugenschimmel", + SortOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000006"), + CanonicalGenotype = "AA CC DD EE GG PP spsp rere", + Name = "Agouti", + SortOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000007"), + CanonicalGenotype = "aa CC DD EE GG PP spsp rere", + Name = "Schwarz", + SortOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000008"), + CanonicalGenotype = "AA CC DD EE gg PP spsp rere", + Name = "Silberagouti", + SortOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000009"), + CanonicalGenotype = "aa CC DD EE gg PP spsp rere", + Name = "Anthrazit", + SortOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000010"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs", + SortOrder = 9 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000011"), + CanonicalGenotype = "aa CC dd EE GG PP spsp rere", + Name = "Blau", + SortOrder = 10 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000012"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold", + SortOrder = 11 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000013"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin", + SortOrder = 12 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000014"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs", + SortOrder = 13 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000015"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs", + SortOrder = 14 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000016"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Gold", + SortOrder = 15 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000017"), + CanonicalGenotype = "aa CC dd EE GG pp spsp rere", + Name = "Dilute Platin", + SortOrder = 16 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000018"), + CanonicalGenotype = "aa CC DD EE gg pp spsp rere", + Name = "Altweiss (REW)", + SortOrder = 17 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000019"), + CanonicalGenotype = "AA CC DD ee gg pp spsp rere", + Name = "Apricot (Blassfuchs)", + SortOrder = 18 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000020"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs", + SortOrder = 19 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000021"), + CanonicalGenotype = "aa CC DD ee gg pp spsp rere", + Name = "C-Separator", + SortOrder = 20 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000022"), + CanonicalGenotype = "AA CC DD EE gg pp spsp rere", + Name = "Elfenbein", + SortOrder = 21 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000023"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs", + SortOrder = 22 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000024"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs", + SortOrder = 23 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000025"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Saphir", + SortOrder = 24 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000026"), + CanonicalGenotype = "AA CC DD efef GG PP spsp rere", + Name = "Orangeschimmel", + SortOrder = 25 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000027"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Topas", + SortOrder = 26 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000028"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin-Hell", + SortOrder = 27 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000029"), + CanonicalGenotype = "AA CC dd EE GG PP spsp rere", + Name = "Dilute Agouti", + SortOrder = 28 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000030"), + CanonicalGenotype = "AA CC dd EE gg PP spsp rere", + Name = "Dilute Silberagouti", + SortOrder = 29 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000031"), + CanonicalGenotype = "aa CC dd ee GG PP spsp rere", + Name = "Dilute Kohlfuchs", + SortOrder = 30 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000032"), + CanonicalGenotype = "aa CC dd EE gg PP spsp rere", + Name = "Dilute Anthrazit", + SortOrder = 31 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000033"), + CanonicalGenotype = "AA CC DD efef gg PP spsp rere", + Name = "Silberschimmel", + SortOrder = 36 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000034"), + CanonicalGenotype = "AA CC DD efe gg PP spsp rere", + Name = "Polarfuchsschimmel", + SortOrder = 37 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000035"), + CanonicalGenotype = "AA CC DD efe GG PP spsp rere", + Name = "Algierfuchsschimmel", + SortOrder = 38 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000036"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel", + SortOrder = 39 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000037"), + CanonicalGenotype = "aa CC DD efe gg PP spsp rere", + Name = "Blaufuchsschimmel", + SortOrder = 40 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000038"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs, hell", + SortOrder = 41 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000039"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs, hell", + SortOrder = 42 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000040"), + CanonicalGenotype = "AA CC DD efe GG pp spsp rere", + Name = "Goldfuchsschimmel", + SortOrder = 43 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000041"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold-Hell", + SortOrder = 44 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000042"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs, hell", + SortOrder = 45 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000043"), + CanonicalGenotype = "aa CC DD efe GG pp spsp rere", + Name = "Rotfuchsschimmel", + SortOrder = 46 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000044"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs, hell", + SortOrder = 47 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000045"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel, hell", + SortOrder = 48 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000046"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs, hell", + SortOrder = 49 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000047"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs-Hell", + SortOrder = 50 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000048"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs, hell", + SortOrder = 51 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000049"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Topas", + SortOrder = 52 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000050"), + CanonicalGenotype = "aa CC dd ee gg PP spsp rere", + Name = "Dilute Blaufuchs", + SortOrder = 53 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000051"), + CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere", + Name = "Marder", + SortOrder = 54 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000052"), + CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere", + Name = "Siam", + SortOrder = 55 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000053"), + CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere", + Name = "Zobel-Hell", + SortOrder = 56 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000054"), + CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere", + Name = "CP-Agouti", + SortOrder = 57 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000055"), + CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere", + Name = "CP-Silberagouti", + SortOrder = 59 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000056"), + CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere", + Name = "CP-Algierfuchs", + SortOrder = 61 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000057"), + CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere", + Name = "CP-Polarfuchs", + SortOrder = 63 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000058"), + CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere", + Name = "CP-Fuchs", + SortOrder = 65 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000059"), + CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere", + Name = "CP-Fuchs-Hell", + SortOrder = 66 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000060"), + CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere", + Name = "CP-Blaufuchs", + SortOrder = 67 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000061"), + CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel", + SortOrder = 68 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000062"), + CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere", + Name = "CP-Agouti-Hell", + SortOrder = 58 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000063"), + CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere", + Name = "CP-Silberagouti-Hell", + SortOrder = 60 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000064"), + CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere", + Name = "CP-Algierfuchs-Hell", + SortOrder = 62 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000065"), + CanonicalGenotype = "AA cchmch DD ee gg PP spsp rere", + Name = "CP-Polarfuchs-Hell", + SortOrder = 64 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000066"), + CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel-Hell", + SortOrder = 69 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000067"), + CanonicalGenotype = "AA CC dd ee GG PP spsp rere", + Name = "Dilute Algierfuchs", + SortOrder = 32 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000068"), + CanonicalGenotype = "AA CC dd ee GG pp spsp rere", + Name = "Dilute Goldfuchs", + SortOrder = 33 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000069"), + CanonicalGenotype = "aa CC dd ee GG pp spsp rere", + Name = "Dilute Rotfuchs", + SortOrder = 34 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000070"), + CanonicalGenotype = "AA CC dd ee gg PP spsp rere", + Name = "Dilute Polarfuchs", + SortOrder = 35 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("IsBreeder") + .HasColumnType("boolean"); + + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsReceiver") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSuffix") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CleaningCycleDays") + .HasColumnType("integer"); + + b.Property("LastCleanedDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Enclosures"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EnclosureId"); + + b.ToTable("EnclosurePhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ExhibitionResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Award") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("EventName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Placement") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("ExhibitionResults"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentContext") + .HasColumnType("text"); + + b.Property("Answer") + .HasColumnType("text"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ClientTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("Context") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("FixNote") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Helpful") + .HasColumnType("boolean"); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Question") + .HasColumnType("text"); + + b.Property("ReopenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Thread") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.FeedbackAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FeedbackId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("FeedbackAttachments"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CauseOfDeath") + .HasColumnType("text"); + + b.Property("CharacterNote") + .HasColumnType("text"); + + b.Property("CharacterTraits") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorVarietyId") + .HasColumnType("uuid"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DateOfDeath") + .HasColumnType("date"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + + b.Property("Genotype") + .HasColumnType("text"); + + b.Property("GoHomeDate") + .HasColumnType("date"); + + b.Property("ImportSource") + .HasColumnType("text"); + + b.Property("IsCastrated") + .HasColumnType("boolean"); + + b.Property("IsDeaf") + .HasColumnType("boolean"); + + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsResident") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSearch") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OriginBreeder") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("OriginContactId") + .HasColumnType("uuid"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("RawImportData") + .HasColumnType("text"); + + b.Property("ReceiverContactId") + .HasColumnType("uuid"); + + b.Property("SpottingType") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ColorVarietyId"); + + b.HasIndex("EnclosureId"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("LitterId"); + + b.HasIndex("OriginContactId"); + + b.HasIndex("ReceiverContactId"); + + b.ToTable("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LastImportDiffJson") + .HasColumnType("text"); + + b.Property("LastImportSnapshotJson") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OverrideJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SnapshotJson") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId") + .IsUnique(); + + b.ToTable("GerbilOverrides"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("GerbilPhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Veterinarian") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("HealthRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeathsWithin8Weeks") + .HasColumnType("integer"); + + b.Property("ExpectedGoHomeDate") + .HasColumnType("date"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("FatherId") + .HasColumnType("uuid"); + + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LitterLetter") + .HasColumnType("text"); + + b.Property("MotherId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PairingCode") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("ShowInChronicle") + .HasColumnType("boolean"); + + b.Property("Stillborn") + .HasColumnType("integer"); + + b.Property("TotalBorn") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("FatherId"); + + b.HasIndex("MotherId"); + + b.ToTable("Litters"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppPasswordProtected") + .HasColumnType("text"); + + b.Property("BackgroundPollEnabled") + .HasColumnType("boolean"); + + b.Property("Folder") + .IsRequired() + .HasColumnType("text"); + + b.Property("GmailAddress") + .HasColumnType("text"); + + b.Property("LastUid") + .HasColumnType("bigint"); + + b.Property("PollIntervalMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MailSettings"); + + b.HasData( + new + { + Id = new Guid("ab0c0000-0000-0000-0000-000000000001"), + BackgroundPollEnabled = false, + Folder = "INBOX", + LastUid = 0L, + PollIntervalMinutes = 15 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alt") + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SeoDescription") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pages"); + + b.HasData( + new + { + Id = new Guid("51720001-0000-0000-0000-000000000001"), + Slug = "start", + Status = "Published", + Title = "Startseite" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000002"), + Slug = "ueber-die-zucht", + Status = "Published", + Title = "Über die Zucht" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000003"), + Slug = "abgabetiere", + Status = "Published", + Title = "Abgabetiere" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000004"), + Slug = "abgabebedingungen", + Status = "Published", + Title = "Abgabebedingungen" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000005"), + Slug = "farben-genetik", + Status = "Published", + Title = "Farben & Genetik" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000006"), + Slug = "kontakt", + Status = "Published", + Title = "Kontakt" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000007"), + Slug = "impressum", + Status = "Published", + Title = "Impressum" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000008"), + Slug = "datenschutz", + Status = "Published", + Title = "Datenschutz" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedContactId") + .HasColumnType("uuid"); + + b.Property("BodyText") + .HasColumnType("text"); + + b.Property("DraftReply") + .HasColumnType("text"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromName") + .HasColumnType("text"); + + b.Property("GmailMessageId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InReplyToMessageId") + .HasColumnType("text"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferencesHeader") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AssignedContactId"); + + b.HasIndex("GmailMessageId") + .IsUnique(); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ReturnRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FromContactId") + .HasColumnType("uuid"); + + b.Property("FromContactName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OriginalPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("OriginalSaleDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("GerbilId"); + + b.ToTable("ReturnRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContractDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HandoverDate") + .HasColumnType("date"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("SaleContracts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.Property("SaleContractId") + .HasColumnType("uuid"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("PhotoId") + .HasColumnType("uuid"); + + b.HasKey("SaleContractId", "GerbilId"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleContractAnimal"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppointmentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("HandedOverDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("ReservedForContactId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleReservations"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultLocale") + .IsRequired() + .HasColumnType("text"); + + b.Property("NavOrder") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sites"); + + b.HasData( + new + { + Id = new Guid("5172e000-0000-0000-0000-000000000001"), + DefaultLocale = "de", + NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WaitingListEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("WishColor") + .HasColumnType("text"); + + b.Property("WishGender") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("WaitingListEntries"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WebPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("P256dh") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("WebPushSubscriptions"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("WeightGrams") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("WeightRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Page", null) + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", null) + .WithMany() + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety") + .WithMany() + .HasForeignKey("ColorVarietyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure") + .WithMany("Gerbils") + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter") + .WithMany() + .HasForeignKey("LitterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact") + .WithMany() + .HasForeignKey("OriginContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact") + .WithMany() + .HasForeignKey("ReceiverContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ColorVariety"); + + b.Navigation("Enclosure"); + + b.Navigation("Litter"); + + b.Navigation("OriginContact"); + + b.Navigation("ReceiverContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father") + .WithMany() + .HasForeignKey("FatherId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother") + .WithMany() + .HasForeignKey("MotherId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Father"); + + b.Navigation("Mother"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact") + .WithMany() + .HasForeignKey("AssignedContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignedContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil") + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null) + .WithMany("Animals") + .HasForeignKey("SaleContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gerbil"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Navigation("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Navigation("Animals"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260713173625_AddLitterStillborn.cs b/GerbilManagerWebAPI/Migrations/20260713173625_AddLitterStillborn.cs new file mode 100644 index 0000000..e73910a --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260713173625_AddLitterStillborn.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class AddLitterStillborn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Stillborn", + table: "Litters", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Stillborn", + table: "Litters"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260714055648_AddGerbilBirthOrder.Designer.cs b/GerbilManagerWebAPI/Migrations/20260714055648_AddGerbilBirthOrder.Designer.cs new file mode 100644 index 0000000..30709c4 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260714055648_AddGerbilBirthOrder.Designer.cs @@ -0,0 +1,1927 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20260714055648_AddGerbilBirthOrder")] + partial class AddGerbilBirthOrder + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.AcquisitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("SourceContactId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("AcquisitionRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PageId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Blocks"); + + b.HasData( + new + { + Id = new Guid("51720002-0000-0000-0000-000000000001"), + Data = "{\"text\":\"Startseite\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000001"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000002"), + Data = "{\"text\":\"Über die Zucht\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000002"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000003"), + Data = "{\"text\":\"Abgabetiere\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000004"), + Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000004"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000005"), + Data = "{\"text\":\"Farben & Genetik\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000005"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000006"), + Data = "{\"text\":\"Kontakt\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000006"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000010"), + Data = "{\"mode\":\"auto\",\"intro\":\"\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "AbgabetiereList" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000007"), + Data = "{\"text\":\"Impressum\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000070"), + Data = "{\"markdown\":\"## Angaben gemäß § 18 Abs. 1 MStV\\n\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\n\\n## Kontakt\\n\\nE-Mail: [E-Mail-Adresse]\\nTelefon: [Telefonnummer]\\n\\n---\\n\\nDiese Seite stellt eine private Hobbyzucht mongolischer Rennmäuse ohne Gewinnerzielungsabsicht vor.\\n\\n*Hinweis für den Seitenbetreiber: Name und ladungsfähige Anschrift (kein Postfach) sind Pflicht (§ 18 Abs. 1 MStV). Sollte die Zucht jemals gewerblich bzw. geschäftsmäßig betrieben werden, gelten zusätzlich § 5 DDG (vollständiges Anbieter-Impressum) und ggf. die Erlaubnispflicht nach § 11 TierSchG — dann zuständige Behörde und Aktenzeichen hier ergänzen. Kein Hinweis auf die EU-Streitbeilegungsplattform: diese wurde zum 20.07.2025 abgeschaltet (ein fortbestehender Link wäre irreführend).*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "RichText" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000008"), + Data = "{\"text\":\"Datenschutz\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000080"), + Data = "{\"markdown\":\"## Datenschutzerklärung\\n\\n## 1. Verantwortlicher\\n\\nVerantwortlich für die Datenverarbeitung auf dieser Webseite ist:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\nE-Mail: [E-Mail-Adresse]\\n\\n## 2. Hosting und Server-Logfiles\\n\\nDiese Webseite wird auf einem eigenen Server betrieben. Beim Aufruf der Seite verarbeitet der Server automatisch Informationen, die dein Browser übermittelt, in sogenannten Server-Logfiles: IP-Adresse, Datum und Uhrzeit des Zugriffs, die aufgerufene Seite bzw. Datei, die übertragene Datenmenge sowie Browser und Betriebssystem.\\nDiese Verarbeitung ist technisch erforderlich, um die Webseite auszuliefern und ihren sicheren und stabilen Betrieb zu gewährleisten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse am störungsfreien Betrieb und an der Sicherheit). Die Logfiles werden nach spätestens [z. B. 7] Tagen gelöscht und nicht mit anderen Daten zusammengeführt.\\n\\n## 3. Cookies, Tracking und externe Dienste\\n\\nDiese Webseite verwendet keine Cookies, kein Tracking und keine Analyse- oder Marketing-Dienste. Es werden keine externen Inhalte Dritter (z. B. Schriftarten, Karten, Videos, CDNs) nachgeladen.\\n\\n## 4. Kontaktaufnahme\\n\\nWenn du uns per E-Mail kontaktierst, verarbeiten wir deine Angaben (z. B. Name, E-Mail-Adresse, Inhalt der Nachricht) ausschließlich zur Bearbeitung deiner Anfrage. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO bzw. Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden gelöscht, sobald sie nicht mehr benötigt werden und keine gesetzlichen Aufbewahrungspflichten entgegenstehen.\\n\\n## 5. Empfänger der Daten\\n\\nEine Weitergabe deiner Daten an Dritte findet nicht statt. Eine Datenübermittlung in Länder außerhalb der EU bzw. des EWR erfolgt nicht.\\n\\n## 6. Deine Rechte\\n\\nDu hast jederzeit das Recht auf:\\n- Auskunft über die zu deiner Person gespeicherten Daten (Art. 15 DSGVO)\\n- Berichtigung unrichtiger Daten (Art. 16 DSGVO)\\n- Löschung (Art. 17 DSGVO)\\n- Einschränkung der Verarbeitung (Art. 18 DSGVO)\\n- Datenübertragbarkeit (Art. 20 DSGVO)\\n- Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)\\n\\nZur Ausübung genügt eine formlose Nachricht an die oben genannte E-Mail-Adresse.\\n\\n## 7. Beschwerderecht bei der Aufsichtsbehörde\\n\\nDu hast das Recht, dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO). Zuständig ist die Aufsichtsbehörde deines Bundeslandes bzw. des Seitenbetreibers: [zuständige Landesdatenschutzbehörde eintragen].\\n\\n## 8. Keine automatisierte Entscheidungsfindung\\n\\nEine automatisierte Entscheidungsfindung einschließlich Profiling nach Art. 22 DSGVO findet nicht statt.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Platzhalter in eckigen Klammern ausfüllen. Aufbewahrungsdauer der Logfiles an deinen Server bzw. Reverse-Proxy anpassen. Sobald externe Dienste oder ein Kontaktformular eingebunden werden, diesen Text ergänzen. Stand: [Datum eintragen].*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "RichText" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Homepage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameSuffix") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ZuchtName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BreederSettings"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-000000000001"), + Address = "", + City = "", + Email = "", + Homepage = "", + Name = "", + NameSuffix = "", + Phone = "", + ZuchtName = "" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanonicalGenotype") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ColorVarieties"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0000-000000000001"), + CanonicalGenotype = "AA chch DD EE GG pp spsp rere", + Name = "REW", + SortOrder = 0 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000002"), + CanonicalGenotype = "aa chch DD EE GG PP spsp rere", + Name = "Hermelin", + SortOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000003"), + CanonicalGenotype = "AA chch DD EE GG PP spsp rere", + Name = "Himalaya", + SortOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000004"), + CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere", + Name = "Zobel", + SortOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000005"), + CanonicalGenotype = "AA CC DD efef GG pp spsp rere", + Name = "Rotaugenschimmel", + SortOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000006"), + CanonicalGenotype = "AA CC DD EE GG PP spsp rere", + Name = "Agouti", + SortOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000007"), + CanonicalGenotype = "aa CC DD EE GG PP spsp rere", + Name = "Schwarz", + SortOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000008"), + CanonicalGenotype = "AA CC DD EE gg PP spsp rere", + Name = "Silberagouti", + SortOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000009"), + CanonicalGenotype = "aa CC DD EE gg PP spsp rere", + Name = "Anthrazit", + SortOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000010"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs", + SortOrder = 9 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000011"), + CanonicalGenotype = "aa CC dd EE GG PP spsp rere", + Name = "Blau", + SortOrder = 10 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000012"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold", + SortOrder = 11 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000013"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin", + SortOrder = 12 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000014"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs", + SortOrder = 13 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000015"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs", + SortOrder = 14 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000016"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Gold", + SortOrder = 15 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000017"), + CanonicalGenotype = "aa CC dd EE GG pp spsp rere", + Name = "Dilute Platin", + SortOrder = 16 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000018"), + CanonicalGenotype = "aa CC DD EE gg pp spsp rere", + Name = "Altweiss (REW)", + SortOrder = 17 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000019"), + CanonicalGenotype = "AA CC DD ee gg pp spsp rere", + Name = "Apricot (Blassfuchs)", + SortOrder = 18 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000020"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs", + SortOrder = 19 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000021"), + CanonicalGenotype = "aa CC DD ee gg pp spsp rere", + Name = "C-Separator", + SortOrder = 20 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000022"), + CanonicalGenotype = "AA CC DD EE gg pp spsp rere", + Name = "Elfenbein", + SortOrder = 21 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000023"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs", + SortOrder = 22 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000024"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs", + SortOrder = 23 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000025"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Saphir", + SortOrder = 24 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000026"), + CanonicalGenotype = "AA CC DD efef GG PP spsp rere", + Name = "Orangeschimmel", + SortOrder = 25 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000027"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Topas", + SortOrder = 26 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000028"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin-Hell", + SortOrder = 27 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000029"), + CanonicalGenotype = "AA CC dd EE GG PP spsp rere", + Name = "Dilute Agouti", + SortOrder = 28 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000030"), + CanonicalGenotype = "AA CC dd EE gg PP spsp rere", + Name = "Dilute Silberagouti", + SortOrder = 29 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000031"), + CanonicalGenotype = "aa CC dd ee GG PP spsp rere", + Name = "Dilute Kohlfuchs", + SortOrder = 30 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000032"), + CanonicalGenotype = "aa CC dd EE gg PP spsp rere", + Name = "Dilute Anthrazit", + SortOrder = 31 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000033"), + CanonicalGenotype = "AA CC DD efef gg PP spsp rere", + Name = "Silberschimmel", + SortOrder = 36 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000034"), + CanonicalGenotype = "AA CC DD efe gg PP spsp rere", + Name = "Polarfuchsschimmel", + SortOrder = 37 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000035"), + CanonicalGenotype = "AA CC DD efe GG PP spsp rere", + Name = "Algierfuchsschimmel", + SortOrder = 38 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000036"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel", + SortOrder = 39 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000037"), + CanonicalGenotype = "aa CC DD efe gg PP spsp rere", + Name = "Blaufuchsschimmel", + SortOrder = 40 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000038"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs, hell", + SortOrder = 41 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000039"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs, hell", + SortOrder = 42 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000040"), + CanonicalGenotype = "AA CC DD efe GG pp spsp rere", + Name = "Goldfuchsschimmel", + SortOrder = 43 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000041"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold-Hell", + SortOrder = 44 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000042"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs, hell", + SortOrder = 45 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000043"), + CanonicalGenotype = "aa CC DD efe GG pp spsp rere", + Name = "Rotfuchsschimmel", + SortOrder = 46 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000044"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs, hell", + SortOrder = 47 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000045"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel, hell", + SortOrder = 48 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000046"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs, hell", + SortOrder = 49 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000047"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs-Hell", + SortOrder = 50 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000048"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs, hell", + SortOrder = 51 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000049"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Topas", + SortOrder = 52 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000050"), + CanonicalGenotype = "aa CC dd ee gg PP spsp rere", + Name = "Dilute Blaufuchs", + SortOrder = 53 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000051"), + CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere", + Name = "Marder", + SortOrder = 54 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000052"), + CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere", + Name = "Siam", + SortOrder = 55 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000053"), + CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere", + Name = "Zobel-Hell", + SortOrder = 56 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000054"), + CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere", + Name = "CP-Agouti", + SortOrder = 57 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000055"), + CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere", + Name = "CP-Silberagouti", + SortOrder = 59 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000056"), + CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere", + Name = "CP-Algierfuchs", + SortOrder = 61 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000057"), + CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere", + Name = "CP-Polarfuchs", + SortOrder = 63 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000058"), + CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere", + Name = "CP-Fuchs", + SortOrder = 65 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000059"), + CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere", + Name = "CP-Fuchs-Hell", + SortOrder = 66 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000060"), + CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere", + Name = "CP-Blaufuchs", + SortOrder = 67 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000061"), + CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel", + SortOrder = 68 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000062"), + CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere", + Name = "CP-Agouti-Hell", + SortOrder = 58 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000063"), + CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere", + Name = "CP-Silberagouti-Hell", + SortOrder = 60 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000064"), + CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere", + Name = "CP-Algierfuchs-Hell", + SortOrder = 62 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000065"), + CanonicalGenotype = "AA cchmch DD ee gg PP spsp rere", + Name = "CP-Polarfuchs-Hell", + SortOrder = 64 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000066"), + CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel-Hell", + SortOrder = 69 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000067"), + CanonicalGenotype = "AA CC dd ee GG PP spsp rere", + Name = "Dilute Algierfuchs", + SortOrder = 32 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000068"), + CanonicalGenotype = "AA CC dd ee GG pp spsp rere", + Name = "Dilute Goldfuchs", + SortOrder = 33 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000069"), + CanonicalGenotype = "aa CC dd ee GG pp spsp rere", + Name = "Dilute Rotfuchs", + SortOrder = 34 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000070"), + CanonicalGenotype = "AA CC dd ee gg PP spsp rere", + Name = "Dilute Polarfuchs", + SortOrder = 35 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("IsBreeder") + .HasColumnType("boolean"); + + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsReceiver") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSuffix") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CleaningCycleDays") + .HasColumnType("integer"); + + b.Property("LastCleanedDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Enclosures"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EnclosureId"); + + b.ToTable("EnclosurePhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ExhibitionResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Award") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("EventName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Placement") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("ExhibitionResults"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentContext") + .HasColumnType("text"); + + b.Property("Answer") + .HasColumnType("text"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ClientTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("Context") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("FixNote") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Helpful") + .HasColumnType("boolean"); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Question") + .HasColumnType("text"); + + b.Property("ReopenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Thread") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.FeedbackAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FeedbackId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("FeedbackAttachments"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BirthOrder") + .HasColumnType("integer"); + + b.Property("CauseOfDeath") + .HasColumnType("text"); + + b.Property("CharacterNote") + .HasColumnType("text"); + + b.Property("CharacterTraits") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorVarietyId") + .HasColumnType("uuid"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DateOfDeath") + .HasColumnType("date"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + + b.Property("Genotype") + .HasColumnType("text"); + + b.Property("GoHomeDate") + .HasColumnType("date"); + + b.Property("ImportSource") + .HasColumnType("text"); + + b.Property("IsCastrated") + .HasColumnType("boolean"); + + b.Property("IsDeaf") + .HasColumnType("boolean"); + + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsResident") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSearch") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OriginBreeder") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("OriginContactId") + .HasColumnType("uuid"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("RawImportData") + .HasColumnType("text"); + + b.Property("ReceiverContactId") + .HasColumnType("uuid"); + + b.Property("SpottingType") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ColorVarietyId"); + + b.HasIndex("EnclosureId"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("LitterId"); + + b.HasIndex("OriginContactId"); + + b.HasIndex("ReceiverContactId"); + + b.ToTable("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LastImportDiffJson") + .HasColumnType("text"); + + b.Property("LastImportSnapshotJson") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OverrideJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SnapshotJson") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId") + .IsUnique(); + + b.ToTable("GerbilOverrides"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("GerbilPhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Veterinarian") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("HealthRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeathsWithin8Weeks") + .HasColumnType("integer"); + + b.Property("ExpectedGoHomeDate") + .HasColumnType("date"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("FatherId") + .HasColumnType("uuid"); + + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LitterLetter") + .HasColumnType("text"); + + b.Property("MotherId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PairingCode") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("ShowInChronicle") + .HasColumnType("boolean"); + + b.Property("Stillborn") + .HasColumnType("integer"); + + b.Property("TotalBorn") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("FatherId"); + + b.HasIndex("MotherId"); + + b.ToTable("Litters"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppPasswordProtected") + .HasColumnType("text"); + + b.Property("BackgroundPollEnabled") + .HasColumnType("boolean"); + + b.Property("Folder") + .IsRequired() + .HasColumnType("text"); + + b.Property("GmailAddress") + .HasColumnType("text"); + + b.Property("LastUid") + .HasColumnType("bigint"); + + b.Property("PollIntervalMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MailSettings"); + + b.HasData( + new + { + Id = new Guid("ab0c0000-0000-0000-0000-000000000001"), + BackgroundPollEnabled = false, + Folder = "INBOX", + LastUid = 0L, + PollIntervalMinutes = 15 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alt") + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SeoDescription") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pages"); + + b.HasData( + new + { + Id = new Guid("51720001-0000-0000-0000-000000000001"), + Slug = "start", + Status = "Published", + Title = "Startseite" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000002"), + Slug = "ueber-die-zucht", + Status = "Published", + Title = "Über die Zucht" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000003"), + Slug = "abgabetiere", + Status = "Published", + Title = "Abgabetiere" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000004"), + Slug = "abgabebedingungen", + Status = "Published", + Title = "Abgabebedingungen" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000005"), + Slug = "farben-genetik", + Status = "Published", + Title = "Farben & Genetik" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000006"), + Slug = "kontakt", + Status = "Published", + Title = "Kontakt" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000007"), + Slug = "impressum", + Status = "Published", + Title = "Impressum" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000008"), + Slug = "datenschutz", + Status = "Published", + Title = "Datenschutz" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedContactId") + .HasColumnType("uuid"); + + b.Property("BodyText") + .HasColumnType("text"); + + b.Property("DraftReply") + .HasColumnType("text"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromName") + .HasColumnType("text"); + + b.Property("GmailMessageId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InReplyToMessageId") + .HasColumnType("text"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferencesHeader") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AssignedContactId"); + + b.HasIndex("GmailMessageId") + .IsUnique(); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ReturnRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FromContactId") + .HasColumnType("uuid"); + + b.Property("FromContactName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OriginalPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("OriginalSaleDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("GerbilId"); + + b.ToTable("ReturnRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContractDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HandoverDate") + .HasColumnType("date"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("SaleContracts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.Property("SaleContractId") + .HasColumnType("uuid"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("PhotoId") + .HasColumnType("uuid"); + + b.HasKey("SaleContractId", "GerbilId"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleContractAnimal"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppointmentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("HandedOverDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("ReservedForContactId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleReservations"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultLocale") + .IsRequired() + .HasColumnType("text"); + + b.Property("NavOrder") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sites"); + + b.HasData( + new + { + Id = new Guid("5172e000-0000-0000-0000-000000000001"), + DefaultLocale = "de", + NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WaitingListEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("WishColor") + .HasColumnType("text"); + + b.Property("WishGender") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("WaitingListEntries"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WebPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("P256dh") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("WebPushSubscriptions"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("WeightGrams") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("WeightRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Page", null) + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", null) + .WithMany() + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety") + .WithMany() + .HasForeignKey("ColorVarietyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure") + .WithMany("Gerbils") + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter") + .WithMany() + .HasForeignKey("LitterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact") + .WithMany() + .HasForeignKey("OriginContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact") + .WithMany() + .HasForeignKey("ReceiverContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ColorVariety"); + + b.Navigation("Enclosure"); + + b.Navigation("Litter"); + + b.Navigation("OriginContact"); + + b.Navigation("ReceiverContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father") + .WithMany() + .HasForeignKey("FatherId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother") + .WithMany() + .HasForeignKey("MotherId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Father"); + + b.Navigation("Mother"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact") + .WithMany() + .HasForeignKey("AssignedContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignedContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil") + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null) + .WithMany("Animals") + .HasForeignKey("SaleContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gerbil"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Navigation("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Navigation("Animals"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260714055648_AddGerbilBirthOrder.cs b/GerbilManagerWebAPI/Migrations/20260714055648_AddGerbilBirthOrder.cs new file mode 100644 index 0000000..491b6b2 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260714055648_AddGerbilBirthOrder.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class AddGerbilBirthOrder : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BirthOrder", + table: "Gerbils", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BirthOrder", + table: "Gerbils"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs index 96e4b77..84acacc 100644 --- a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs +++ b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs @@ -147,7 +147,7 @@ namespace GerbilManagerWebAPI.Migrations new { Id = new Guid("51720002-0000-0000-0000-000000000070"), - Data = "{\"markdown\":\"**Angaben gemäß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}", + Data = "{\"markdown\":\"## Angaben gemäß § 18 Abs. 1 MStV\\n\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\n\\n## Kontakt\\n\\nE-Mail: [E-Mail-Adresse]\\nTelefon: [Telefonnummer]\\n\\n---\\n\\nDiese Seite stellt eine private Hobbyzucht mongolischer Rennmäuse ohne Gewinnerzielungsabsicht vor.\\n\\n*Hinweis für den Seitenbetreiber: Name und ladungsfähige Anschrift (kein Postfach) sind Pflicht (§ 18 Abs. 1 MStV). Sollte die Zucht jemals gewerblich bzw. geschäftsmäßig betrieben werden, gelten zusätzlich § 5 DDG (vollständiges Anbieter-Impressum) und ggf. die Erlaubnispflicht nach § 11 TierSchG — dann zuständige Behörde und Aktenzeichen hier ergänzen. Kein Hinweis auf die EU-Streitbeilegungsplattform: diese wurde zum 20.07.2025 abgeschaltet (ein fortbestehender Link wäre irreführend).*\"}", Order = 1, PageId = new Guid("51720001-0000-0000-0000-000000000007"), Type = "RichText" @@ -163,7 +163,7 @@ namespace GerbilManagerWebAPI.Migrations new { Id = new Guid("51720002-0000-0000-0000-000000000080"), - Data = "{\"markdown\":\"**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}", + Data = "{\"markdown\":\"## Datenschutzerklärung\\n\\n## 1. Verantwortlicher\\n\\nVerantwortlich für die Datenverarbeitung auf dieser Webseite ist:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\nE-Mail: [E-Mail-Adresse]\\n\\n## 2. Hosting und Server-Logfiles\\n\\nDiese Webseite wird auf einem eigenen Server betrieben. Beim Aufruf der Seite verarbeitet der Server automatisch Informationen, die dein Browser übermittelt, in sogenannten Server-Logfiles: IP-Adresse, Datum und Uhrzeit des Zugriffs, die aufgerufene Seite bzw. Datei, die übertragene Datenmenge sowie Browser und Betriebssystem.\\nDiese Verarbeitung ist technisch erforderlich, um die Webseite auszuliefern und ihren sicheren und stabilen Betrieb zu gewährleisten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse am störungsfreien Betrieb und an der Sicherheit). Die Logfiles werden nach spätestens [z. B. 7] Tagen gelöscht und nicht mit anderen Daten zusammengeführt.\\n\\n## 3. Cookies, Tracking und externe Dienste\\n\\nDiese Webseite verwendet keine Cookies, kein Tracking und keine Analyse- oder Marketing-Dienste. Es werden keine externen Inhalte Dritter (z. B. Schriftarten, Karten, Videos, CDNs) nachgeladen.\\n\\n## 4. Kontaktaufnahme\\n\\nWenn du uns per E-Mail kontaktierst, verarbeiten wir deine Angaben (z. B. Name, E-Mail-Adresse, Inhalt der Nachricht) ausschließlich zur Bearbeitung deiner Anfrage. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO bzw. Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden gelöscht, sobald sie nicht mehr benötigt werden und keine gesetzlichen Aufbewahrungspflichten entgegenstehen.\\n\\n## 5. Empfänger der Daten\\n\\nEine Weitergabe deiner Daten an Dritte findet nicht statt. Eine Datenübermittlung in Länder außerhalb der EU bzw. des EWR erfolgt nicht.\\n\\n## 6. Deine Rechte\\n\\nDu hast jederzeit das Recht auf:\\n- Auskunft über die zu deiner Person gespeicherten Daten (Art. 15 DSGVO)\\n- Berichtigung unrichtiger Daten (Art. 16 DSGVO)\\n- Löschung (Art. 17 DSGVO)\\n- Einschränkung der Verarbeitung (Art. 18 DSGVO)\\n- Datenübertragbarkeit (Art. 20 DSGVO)\\n- Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)\\n\\nZur Ausübung genügt eine formlose Nachricht an die oben genannte E-Mail-Adresse.\\n\\n## 7. Beschwerderecht bei der Aufsichtsbehörde\\n\\nDu hast das Recht, dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO). Zuständig ist die Aufsichtsbehörde deines Bundeslandes bzw. des Seitenbetreibers: [zuständige Landesdatenschutzbehörde eintragen].\\n\\n## 8. Keine automatisierte Entscheidungsfindung\\n\\nEine automatisierte Entscheidungsfindung einschließlich Profiling nach Art. 22 DSGVO findet nicht statt.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Platzhalter in eckigen Klammern ausfüllen. Aufbewahrungsdauer der Logfiles an deinen Server bzw. Reverse-Proxy anpassen. Sobald externe Dienste oder ein Kontaktformular eingebunden werden, diesen Text ergänzen. Stand: [Datum eintragen].*\"}", Order = 1, PageId = new Guid("51720001-0000-0000-0000-000000000008"), Type = "RichText" @@ -756,6 +756,11 @@ namespace GerbilManagerWebAPI.Migrations b.Property("IsBreeder") .HasColumnType("boolean"); + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("IsReceiver") .HasColumnType("boolean"); @@ -998,6 +1003,9 @@ namespace GerbilManagerWebAPI.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("BirthOrder") + .HasColumnType("integer"); + b.Property("CauseOfDeath") .HasColumnType("text"); @@ -1042,6 +1050,11 @@ namespace GerbilManagerWebAPI.Migrations b.Property("IsDeaf") .HasColumnType("boolean"); + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("IsResident") .ValueGeneratedOnAdd() .HasColumnType("boolean") @@ -1104,6 +1117,51 @@ namespace GerbilManagerWebAPI.Migrations b.ToTable("Gerbils"); }); + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LastImportDiffJson") + .HasColumnType("text"); + + b.Property("LastImportSnapshotJson") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OverrideJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SnapshotJson") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId") + .IsUnique(); + + b.ToTable("GerbilOverrides"); + }); + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => { b.Property("Id") @@ -1187,6 +1245,11 @@ namespace GerbilManagerWebAPI.Migrations b.Property("FatherId") .HasColumnType("uuid"); + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("LitterLetter") .HasColumnType("text"); @@ -1206,6 +1269,12 @@ namespace GerbilManagerWebAPI.Migrations b.Property("Provenance") .HasColumnType("text"); + b.Property("ShowInChronicle") + .HasColumnType("boolean"); + + b.Property("Stillborn") + .HasColumnType("integer"); + b.Property("TotalBorn") .HasColumnType("integer"); diff --git a/GerbilManagerWebAPI/Models/Contact.cs b/GerbilManagerWebAPI/Models/Contact.cs index a5fb749..81d1d7a 100644 --- a/GerbilManagerWebAPI/Models/Contact.cs +++ b/GerbilManagerWebAPI/Models/Contact.cs @@ -30,5 +30,10 @@ namespace GerbilManagerWebAPI.Models /// notes). Written by the Python merge_and_resolve step and surfaced read-only /// ("Datenherkunft"). Null = manually-added contact / no import data. public string? Provenance { get; set; } + + /// true = manually created in the UI; false = produced by an import path. The ingest + /// re-import wipe deletes ONLY IsManual=false contacts, so a manually-added contact (e.g. the + /// Herkunft/Abnehmer of a manual animal) is never wiped. All import paths leave this false. + public bool IsManual { get; set; } } } diff --git a/GerbilManagerWebAPI/Models/Gerbil.cs b/GerbilManagerWebAPI/Models/Gerbil.cs index 68fc531..520a21d 100644 --- a/GerbilManagerWebAPI/Models/Gerbil.cs +++ b/GerbilManagerWebAPI/Models/Gerbil.cs @@ -90,6 +90,16 @@ namespace GerbilManagerWebAPI.Models /// imported animals. See hive/agents/god/OWNERSHIP-residency.md. Gridify-filterable. public bool IsResident { get; set; } = true; public bool IsCastrated { get; set; } + + /// true = manually created in the UI (POST /gerbils); false = produced by an import + /// path. The ingest re-import wipe deletes ONLY IsManual=false rows, so manually-added animals + /// (and their sub-records) are never wiped or overwritten. All import paths leave this false. + public bool IsManual { get; set; } + + /// Reihenfolge der Jungtiere innerhalb ihres Wurfs (aus der Wurfchronik, sortiert nach + /// Geburtsgewicht). null = unbekannt → alphabetische Sortierung als Fallback. Wird im Import aus + /// conflict-decisions.json (`litterOrder`) gesetzt; von der Wurf- und Akten-Anzeige genutzt. + public int? BirthOrder { get; set; } } /// Shared normalisation for the separator-insensitive name search. diff --git a/GerbilManagerWebAPI/Models/GerbilOverride.cs b/GerbilManagerWebAPI/Models/GerbilOverride.cs new file mode 100644 index 0000000..6d80a1b --- /dev/null +++ b/GerbilManagerWebAPI/Models/GerbilOverride.cs @@ -0,0 +1,62 @@ +using System.ComponentModel.DataAnnotations; + +namespace GerbilManagerWebAPI.Models +{ + /// + /// VERIFIED/PROTECTED OVERRIDE: authoritative, hand-curated per-gerbil field values. Decoupled + /// from the rest of the model on purpose — is a plain Guid column (NOT an + /// enforced foreign key, no navigation property), so the import re-ingest wipe + /// (IngestResolvedService) can delete and reload gerbils without deleting or breaking + /// override rows. After every ingest the override is re-applied on top of the fresh import (the + /// "freeze"), so the breeder's curated values are never lost. Matched to the (deterministic) + /// gerbil id. Same ingest-surviving pattern as . + /// + /// Two flavours, both stored here: + /// - = true → "vollständig korrekt": the whole own-field freeze set is + /// pinned, and a full Akte snapshot () is kept for the drift report, + /// export and regression test. + /// - = false → "geschützt": created by a manual edit; only the changed + /// fields are pinned (per-field) so untouched fields keep receiving import improvements. + /// + public class GerbilOverride + { + [Key] + public Guid Id { get; set; } + + /// Loose reference (no FK) to the gerbil this override curates. + public Guid GerbilId { get; set; } + + /// Captured gerbil name — keeps the row human-readable and survives an ingest wipe. + public string? EntityName { get; set; } + + /// true = "vollständig korrekt" (full freeze + certified); false = "geschützt" (per-field edit). + public bool IsVerified { get; set; } + + /// Frozen own-field values as a JSON object (field name → value). Verified rows carry + /// the full freeze set; protected rows carry only the manually-changed fields. Re-applied on + /// top of the fresh import after every ingest. + public required string OverrideJson { get; set; } + + /// Full human-readable Akte snapshot (self + parents + own litters/children) captured + /// at verify time — feeds the drift report, export and regression test. null for protected-only rows. + public string? SnapshotJson { get; set; } + + /// Raw-import Akte snapshot captured during the LAST ingest, BEFORE the freeze was + /// applied — lets the UI show where the raw import disagrees with the golden state. null until + /// the first ingest after verifying. + public string? LastImportSnapshotJson { get; set; } + + /// Cached diff (golden ↔ last raw import) as a JSON array, computed at ingest time. + /// null/empty = no drift / not yet computed. + public string? LastImportDiffJson { get; set; } + + /// Optional free-text note the breeder attached when verifying. + public string? Note { get; set; } + + /// When the row was marked "vollständig korrekt"; null for protected-only rows. + public DateTimeOffset? VerifiedAt { get; set; } + + /// When the override was last created/updated. + public DateTimeOffset UpdatedAt { get; set; } + } +} diff --git a/GerbilManagerWebAPI/Models/Litter.cs b/GerbilManagerWebAPI/Models/Litter.cs index 70d53c5..ae47c26 100644 --- a/GerbilManagerWebAPI/Models/Litter.cs +++ b/GerbilManagerWebAPI/Models/Litter.cs @@ -17,6 +17,11 @@ namespace GerbilManagerWebAPI.Models /// Null = unbekannt (Altdaten). Must be ≤ TotalBorn if both are set. public int? DeathsWithin8Weeks { get; set; } + /// Totgeburten (TG): tot geborene Jungtiere dieses Wurfs. Getrennt von + /// DeathsWithin8Weeks (in den ersten 8 Wochen verstorben). Aus der xlsx-Wurfchronik + /// (litters.json breakdown) importiert. Null = unbekannt/nicht erfasst. + public int? Stillborn { get; set; } + public Guid? FatherId { get; set; } public Gerbil? Father { get; set; } public Guid? MotherId { get; set; } @@ -45,5 +50,17 @@ namespace GerbilManagerWebAPI.Models /// Written by the Python merge_and_resolve step and surfaced read-only ("Datenherkunft"). /// Null = manually-added litter / no import data. public string? Provenance { get; set; } + + /// Steuert, ob dieser Wurf in der Wurfchronik (Würfe-Liste) erscheint. + /// true = normal sichtbar. false = aus der Wurfchronik ausgeblendet, aber weiterhin + /// auf der Tier-Detailseite des Eltern­tiers sichtbar — z. B. ein extern (bei einer + /// anderen Zucht) entstandener Wurf eines nicht-residenten Tieres, der nur zur + /// Dokumentation der Abstammung gehört. Default true (alle bestehenden Würfe). + public bool ShowInChronicle { get; set; } = true; + + /// true = manually created in the UI; false = produced by an import path. The ingest + /// re-import wipe deletes ONLY IsManual=false litters, so a manually-added litter (e.g. the + /// birth litter of a manual animal) is never wiped. All import paths leave this false. + public bool IsManual { get; set; } } } diff --git a/GerbilManagerWebAPI/Program.cs b/GerbilManagerWebAPI/Program.cs index 55c448b..d6f85d1 100644 --- a/GerbilManagerWebAPI/Program.cs +++ b/GerbilManagerWebAPI/Program.cs @@ -128,6 +128,7 @@ app.MapCmsEndpoints(); app.MapRequestEndpoints(); app.MapNamesEndpoints(); app.MapFeedbackEndpoints(); +app.MapVerifiedGerbilEndpoints(); app.MapPushEndpoints(); app.MapAcquisitionEndpoints(); app.MapSaleReservationEndpoints(); diff --git a/deploy/truenas/.env.example b/deploy/truenas/.env.example index 186122a..024d536 100644 --- a/deploy/truenas/.env.example +++ b/deploy/truenas/.env.example @@ -4,8 +4,9 @@ # Sicheres Datenbankpasswort (mind. 20 Zeichen, keine Anführungszeichen) POSTGRES_PASSWORD=aendere_mich_bitte -# Externer Port fuer das Frontend (Standard: 80) -PORT=80 +# Externer Port fuer das Frontend. NAS-Port 80 ist durch den nginx-Reverse-Proxy +# belegt -> Standard 8090 (Zugriff: http://truenas:8090/). +PORT=8090 # Container Registry (git.rismer.de/gulum) REGISTRY=git.rismer.de/gulum diff --git a/deploy/truenas/compose.yaml b/deploy/truenas/compose.yaml index 23f848c..1c9f03e 100644 --- a/deploy/truenas/compose.yaml +++ b/deploy/truenas/compose.yaml @@ -11,8 +11,9 @@ services: # --- PostgreSQL-Datenbank --- + # postgres:18 (Debian): Locale en_US.utf8 == Quell-DB (Aspire) -> sauberer Dump-Restore. db: - image: postgres:17-alpine + image: postgres:18 restart: unless-stopped environment: POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" @@ -58,7 +59,7 @@ services: db: condition: service_healthy healthcheck: - test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"] + test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"] interval: 30s timeout: 10s retries: 3 @@ -72,7 +73,8 @@ services: dockerfile: gerbil-manager-web/Dockerfile restart: unless-stopped ports: - - "${PORT:-80}:80" + # NAS-Port 80 ist durch den nginx-Reverse-Proxy belegt -> Default 8090. + - "${PORT:-8090}:80" depends_on: api: condition: service_healthy @@ -94,7 +96,7 @@ services: # --- Backup-Sidecar (taeglicher pg_dump + Foto-Archiv + Rotation) --- backup: - image: postgres:17-alpine + image: postgres:18 restart: unless-stopped environment: PGPASSWORD: "${POSTGRES_PASSWORD}" diff --git a/deploy/truenas/custom-app.compose.yaml b/deploy/truenas/custom-app.compose.yaml new file mode 100644 index 0000000..a8be2dd --- /dev/null +++ b/deploy/truenas/custom-app.compose.yaml @@ -0,0 +1,97 @@ +# GerbilManager — TrueNAS SCALE Custom App (Goldeye 25.10) +# ========================================================= +# Diese Datei ist die VORLAGE fuer die TrueNAS-"Custom App" (taucht unter Apps auf). +# Sie wird NICHT direkt mit `docker compose` gestartet, sondern von +# deploy/truenas/scripts/truenas-deploy.sh +# gerendert (Platzhalter aus deploy/truenas/.env eingesetzt) und via +# midclt call app.create {custom_app:true, app_name:gerbilmanager, custom_compose_config_string:} +# an die TrueNAS-Middleware uebergeben. Redeploys laufen ueber `midclt call app.redeploy`. +# +# Unterschiede zur lokalen deploy/truenas/compose.yaml: +# * ABSOLUTE Host-Bind-Pfade (Custom Apps kennen keine relativen ./-Mounts) — +# gleiche Konvention wie die anderen Apps dieser NAS (/mnt/JailStorage/DockerVolumes/...). +# * pull_policy: always -> `app.redeploy` zieht das frische :latest aus der Registry. +# * kein publicsite-Container (oeffentliche Seite ist ein separater Schritt mit Domain/Proxy). +# +# Platzhalter (werden von truenas-deploy.sh aus .env ersetzt): +# __POSTGRES_PASSWORD__ __REGISTRY__ __TAG__ __PORT__ __AI_BASEURL__ __AI_APIKEY__ __AI_MODEL__ + +services: + + # --- PostgreSQL 18 (Debian-Image: Locale en_US.utf8 == Quell-DB) --- + db: + image: postgres:18 + restart: unless-stopped + environment: + POSTGRES_PASSWORD: "__POSTGRES_PASSWORD__" + POSTGRES_DB: gerbilmanager + POSTGRES_USER: postgres + TZ: Europe/Berlin + volumes: + - /mnt/JailStorage/DockerVolumes/gerbilmanager/pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d gerbilmanager"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + # --- .NET API --- + api: + image: "__REGISTRY__/gerbilmanager-api:__TAG__" + pull_policy: always + restart: unless-stopped + environment: + ASPNETCORE_ENVIRONMENT: Production + ConnectionStrings__gerbilmanager: "Host=db;Port=5432;Database=gerbilmanager;Username=postgres;Password=__POSTGRES_PASSWORD__" + Photos__RootPath: /data/photos + DataProtection__KeyRingPath: /data/keys + PublicSite__RootPath: /data/publicsite + AI__BaseUrl: "__AI_BASEURL__" + AI__ApiKey: "__AI_APIKEY__" + AI__Model: "__AI_MODEL__" + TZ: Europe/Berlin + volumes: + - /mnt/JailStorage/DockerVolumes/gerbilmanager/photos:/data/photos + - /mnt/JailStorage/DockerVolumes/gerbilmanager/keys:/data/keys + - /mnt/JailStorage/DockerVolumes/gerbilmanager/publicsite:/data/publicsite + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s + + # --- Frontend (nginx: React-SPA + /api-Proxy) --- + frontend: + image: "__REGISTRY__/gerbilmanager-frontend:__TAG__" + pull_policy: always + restart: unless-stopped + ports: + - "__PORT__:80" + depends_on: + api: + condition: service_healthy + + # --- Backup-Sidecar (taeglicher pg_dump + Foto-Archiv + Rotation) --- + backup: + image: postgres:18 + restart: unless-stopped + environment: + PGPASSWORD: "__POSTGRES_PASSWORD__" + POSTGRES_HOST: db + POSTGRES_USER: postgres + POSTGRES_DB: gerbilmanager + BACKUP_KEEP_DAYS: "7" + TZ: Europe/Berlin + volumes: + - /mnt/JailStorage/DockerVolumes/gerbilmanager/photos:/data/photos:ro + - /mnt/JailStorage/DockerVolumes/gerbilmanager/backups:/backups + - /mnt/JailStorage/DockerVolumes/gerbilmanager/scripts:/scripts:ro + depends_on: + db: + condition: service_healthy + entrypoint: ["/bin/sh", "/scripts/entrypoint.sh"] diff --git a/deploy/truenas/nginx/publicsite.conf b/deploy/truenas/nginx/publicsite.conf index a6bd9d0..0beed1d 100644 --- a/deploy/truenas/nginx/publicsite.conf +++ b/deploy/truenas/nginx/publicsite.conf @@ -1,6 +1,11 @@ # GerbilManager — publicsite nginx (WEB-2) # Serviert die statische oeffentliche Seite aus dem live/-Verzeichnis des Shared Volumes. # SICHERHEIT: Kein Proxy auf die API, kein Zugriff auf den Manager. +# +# Security-Header (Art. 32 DSGVO — technische Massnahmen): MUESSEN pro location wiederholt +# werden, da nginx server-weite add_header verwirft, sobald eine location eigene add_header hat. +# HSTS wird NICHT hier gesetzt (HTTP-only-Container) — sondern am TLS-terminierenden +# Reverse-Proxy (siehe deploy/truenas/vhost-snippet.conf). server { listen 80; root /usr/share/nginx/html/live; @@ -11,15 +16,23 @@ server { location / { try_files $uri $uri/index.html =404; add_header Cache-Control "no-cache, must-revalidate"; - add_header X-Content-Type-Options "nosniff"; - add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "no-referrer" always; + add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'none'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; form-action 'self'" always; } - # CSS/Bilder: kurze TTL (1 Tag) + # CSS/Bilder: kurze TTL (1 Tag) — Security-Header wiederholen (s. o.) location ~* \.(css|png|jpg|jpeg|gif|ico|webp|svg)$ { try_files $uri =404; expires 1d; add_header Cache-Control "public, max-age=86400"; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "no-referrer" always; + add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'none'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; form-action 'self'" always; } # Kein Zugriff auf Staging-Verzeichnisse diff --git a/deploy/truenas/scripts/deploy.sh b/deploy/truenas/scripts/deploy.sh new file mode 100644 index 0000000..479960a --- /dev/null +++ b/deploy/truenas/scripts/deploy.sh @@ -0,0 +1,91 @@ +#!/bin/sh +# GerbilManager Deploy-Skript (laeuft auf der TrueNAS Goldeye) +# =========================================================== +# Holt die neuen Images aus der Gitea-Registry und rollt den Compose-Stack neu aus. +# Wird vom Gitea-Actions 'deploy'-Job (self-hosted Runner auf der NAS) aufgerufen +# UND kann jederzeit manuell auf der NAS ausgefuehrt werden: +# +# sh /opt/gerbilmanager/deploy/truenas/scripts/deploy.sh +# TAG= sh .../deploy.sh # Rollback auf ein bestimmtes Image +# GIT_PULL=0 sh .../deploy.sh # ohne 'git pull' (nur Images neu ziehen) +# +# Konfiguration ueber Umgebungsvariablen (alle optional, sinnvolle Defaults): +# COMPOSE_DIR Installationsverzeichnis (Default: /opt/gerbilmanager) +# TAG Image-Tag (Default: latest; fuer Rollback z. B. ein git-SHA) +# GIT_PULL 1 = vorher 'git pull' (Default), 0 = ueberspringen +# HEALTH_TIMEOUT Sekunden auf api-Healthcheck warten (Default: 180) +# REGISTRY / REGISTRY_USER / REGISTRY_TOKEN optionaler docker login vor dem Pull +set -eu + +COMPOSE_DIR="${COMPOSE_DIR:-/opt/gerbilmanager}" +COMPOSE_FILE="$COMPOSE_DIR/deploy/truenas/compose.yaml" +TAG="${TAG:-latest}" +GIT_PULL="${GIT_PULL:-1}" +HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-180}" +REGISTRY="${REGISTRY:-git.rismer.de}" + +log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"; } +fail() { log "FEHLER: $1"; exit 1; } + +export TAG # compose interpoliert ${TAG} in den Image-Referenzen + +log "=== Deploy gestartet (TAG=$TAG, dir=$COMPOSE_DIR) ===" + +[ -f "$COMPOSE_FILE" ] || fail "compose.yaml nicht gefunden: $COMPOSE_FILE" +command -v docker >/dev/null 2>&1 || fail "docker nicht im PATH des Runners" +docker compose version >/dev/null 2>&1 || fail "'docker compose' (v2) nicht verfuegbar" + +cd "$COMPOSE_DIR" + +# --- 1. Repo aktualisieren (compose.yaml / Skripte / Docs) --- +if [ "$GIT_PULL" = "1" ] && [ -d "$COMPOSE_DIR/.git" ]; then + log "git pull --ff-only ..." + git -C "$COMPOSE_DIR" pull --ff-only || log "WARNUNG: git pull fehlgeschlagen — fahre mit vorhandenem Stand fort" +fi + +# --- 2. Registry-Login (nur wenn Credentials uebergeben wurden) --- +if [ -n "${REGISTRY_USER:-}" ] && [ -n "${REGISTRY_TOKEN:-}" ]; then + log "docker login $REGISTRY ..." + echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin >/dev/null \ + || fail "docker login fehlgeschlagen" +fi + +# --- 3. Neue Images ziehen --- +log "Ziehe Images (Tag: $TAG) ..." +docker compose -f "$COMPOSE_FILE" pull || fail "docker compose pull fehlgeschlagen" + +# --- 4. Stack neu ausrollen (nur geaenderte Container werden neu erstellt) --- +log "Rolle Stack aus (up -d) ..." +docker compose -f "$COMPOSE_FILE" up -d --remove-orphans || fail "docker compose up fehlgeschlagen" + +# --- 5. Auf api-Healthcheck warten (impliziert db gesund) --- +log "Warte auf api-Healthcheck (max ${HEALTH_TIMEOUT}s) ..." +API_CID="$(docker compose -f "$COMPOSE_FILE" ps -q api)" +[ -n "$API_CID" ] || fail "api-Container nicht gefunden" + +elapsed=0 +while :; do + state="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$API_CID" 2>/dev/null || echo unknown)" + case "$state" in + healthy) log "api ist gesund."; break ;; + none|unknown) + # Kein Healthcheck definiert/lesbar -> auf laufenden Zustand zurueckfallen. + running="$(docker inspect --format '{{.State.Running}}' "$API_CID" 2>/dev/null || echo false)" + [ "$running" = "true" ] && { log "api laeuft (kein Healthcheck-Status lesbar)."; break; } + ;; + esac + if [ "$elapsed" -ge "$HEALTH_TIMEOUT" ]; then + log "Letzte api-Logs:"; docker compose -f "$COMPOSE_FILE" logs --tail 40 api || true + fail "api wurde nicht innerhalb ${HEALTH_TIMEOUT}s gesund (Status: $state)" + fi + sleep 5; elapsed=$((elapsed + 5)) +done + +# --- 6. Alte/ungenutzte Images aufraeumen --- +log "Raeume verwaiste Images auf ..." +docker image prune -f >/dev/null 2>&1 || true + +log "Laufende Dienste:" +docker compose -f "$COMPOSE_FILE" ps --format 'table {{.Service}}\t{{.Status}}' 2>/dev/null \ + || docker compose -f "$COMPOSE_FILE" ps +log "=== Deploy erfolgreich abgeschlossen (TAG=$TAG) ===" diff --git a/deploy/truenas/scripts/truenas-deploy.sh b/deploy/truenas/scripts/truenas-deploy.sh new file mode 100644 index 0000000..67aadd8 --- /dev/null +++ b/deploy/truenas/scripts/truenas-deploy.sh @@ -0,0 +1,103 @@ +#!/bin/sh +# GerbilManager — TrueNAS Custom-App Deploy/Redeploy (laeuft auf dem NAS-HOST) +# =========================================================================== +# Legt die App als TrueNAS "Custom App" an (sichtbar unter Apps) bzw. rollt sie +# neu aus. Wird vom Gitea-Actions-Deploy-Job per SSH aufgerufen UND kann jederzeit +# manuell auf dem NAS-Host ausgefuehrt werden: +# +# sh /opt/gerbilmanager/deploy/truenas/scripts/truenas-deploy.sh +# +# Voraussetzungen auf dem Host: +# * /opt/gerbilmanager/deploy/truenas/custom-app.compose.yaml (Vorlage) +# * /opt/gerbilmanager/deploy/truenas/.env (mit POSTGRES_PASSWORD etc. — NICHT im Repo) +# * midclt + docker im PATH (auf TrueNAS SCALE gegeben) +# +# Env-Overrides: COMPOSE_DIR, ENV_FILE, HEALTH_TIMEOUT +set -eu + +APP_NAME=gerbilmanager +COMPOSE_DIR="${COMPOSE_DIR:-/opt/gerbilmanager}" +DEPLOY_DIR="$COMPOSE_DIR/deploy/truenas" +ENV_FILE="${ENV_FILE:-$DEPLOY_DIR/.env}" +TEMPLATE="$DEPLOY_DIR/custom-app.compose.yaml" +HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-300}" +BASE=/mnt/JailStorage/DockerVolumes/gerbilmanager + +log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"; } +fail() { log "FEHLER: $*"; exit 1; } + +[ -f "$TEMPLATE" ] || fail "Vorlage fehlt: $TEMPLATE" +[ -f "$ENV_FILE" ] || fail ".env fehlt: $ENV_FILE (aus .env.example erstellen)" +command -v midclt >/dev/null 2>&1 || fail "midclt nicht im PATH" +command -v docker >/dev/null 2>&1 || fail "docker nicht im PATH" + +# --- .env laden --- +set -a; . "$ENV_FILE"; set +a +: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in .env}" +export REGISTRY="${REGISTRY:-git.rismer.de/gulum}" +export TAG="${TAG:-latest}" +export PORT="${PORT:-8090}" + +log "=== Deploy $APP_NAME (TAG=$TAG, PORT=$PORT) ===" + +# --- Host-Verzeichnisse sicherstellen --- +for d in pgdata photos keys backups publicsite scripts; do mkdir -p "$BASE/$d"; done +chown -R 999:999 "$BASE/pgdata" 2>/dev/null || true +chmod 700 "$BASE/pgdata" 2>/dev/null || true +# Backup-Sidecar-Skripte an den absoluten Mount-Pfad kopieren +cp "$DEPLOY_DIR/scripts/backup.sh" "$DEPLOY_DIR/scripts/entrypoint.sh" \ + "$DEPLOY_DIR/scripts/restore.sh" "$BASE/scripts/" 2>/dev/null || true + +# --- Frische Images ziehen (garantiert :latest, unabhaengig vom pull_policy) --- +log "Ziehe Images ..." +docker pull "$REGISTRY/gerbilmanager-api:$TAG" || fail "docker pull api fehlgeschlagen" +docker pull "$REGISTRY/gerbilmanager-frontend:$TAG" || fail "docker pull frontend fehlgeschlagen" + +# --- Vorlage rendern + midclt-Payload bauen (python: sichere Escapes) --- +PAYLOAD_FILE="$(mktemp)" +trap 'rm -f "$PAYLOAD_FILE"' EXIT +python3 - "$TEMPLATE" > "$PAYLOAD_FILE" <<'PY' +import sys, os, json +tmpl = open(sys.argv[1], encoding="utf-8").read() +repl = { + "__POSTGRES_PASSWORD__": os.environ["POSTGRES_PASSWORD"], + "__REGISTRY__": os.environ.get("REGISTRY", "git.rismer.de/gulum"), + "__TAG__": os.environ.get("TAG", "latest"), + "__PORT__": os.environ.get("PORT", "8090"), + "__AI_BASEURL__": os.environ.get("AI__BaseUrl", ""), + "__AI_APIKEY__": os.environ.get("AI__ApiKey", ""), + "__AI_MODEL__": os.environ.get("AI__Model", "gemini-flash-latest"), +} +for k, v in repl.items(): + tmpl = tmpl.replace(k, v) +print(json.dumps({"custom_app": True, "app_name": "gerbilmanager", + "custom_compose_config_string": tmpl})) +PY + +# --- Existiert die App schon? --- +EXISTS="$(midclt call app.query "[[\"name\",\"=\",\"$APP_NAME\"]]" 2>/dev/null | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))' 2>/dev/null || echo 0)" + +if [ "$EXISTS" -ge 1 ]; then + log "App existiert -> redeploy (frische Images sind bereits gezogen)" + midclt call app.redeploy "$APP_NAME" >/dev/null || fail "app.redeploy fehlgeschlagen" +else + log "App fehlt -> app.create (Custom App)" + midclt call app.create "$(cat "$PAYLOAD_FILE")" >/dev/null || fail "app.create fehlgeschlagen" +fi + +# --- Auf Gesundheit warten (Frontend proxyt /api/health -> API) --- +log "Warte auf /api/health (max ${HEALTH_TIMEOUT}s) ..." +elapsed=0 +while :; do + if curl -fsS "http://localhost:$PORT/api/health" 2>/dev/null | grep -qi 'healthy'; then + log "App ist gesund."; break + fi + [ "$elapsed" -ge "$HEALTH_TIMEOUT" ] && { \ + log "Letzte App-Logs:"; midclt call app.query "[[\"name\",\"=\",\"$APP_NAME\"]]" >/dev/null 2>&1 || true; \ + fail "App wurde nicht innerhalb ${HEALTH_TIMEOUT}s gesund"; } + sleep 5; elapsed=$((elapsed + 5)) +done + +log "Aufraeumen ungenutzter Images ..." +docker image prune -f >/dev/null 2>&1 || true +log "=== Deploy erfolgreich ($APP_NAME, TAG=$TAG) ===" diff --git a/deploy/truenas/vhost-snippet.conf b/deploy/truenas/vhost-snippet.conf index 2be0551..63f6a32 100644 --- a/deploy/truenas/vhost-snippet.conf +++ b/deploy/truenas/vhost-snippet.conf @@ -1,37 +1,63 @@ -# GerbilManager — Externer nginx-Vhost fuer die oeffentliche Webseite (WEB-2) -# In Julians bestehenden nginx-Reverse-Proxy einfuegen. -# ersetzen sobald der Hostname feststeht (Julian liefert ihn). +# GerbilManager — Vhost fuer die oeffentliche Webseite (WEB-2) +# =========================================================== +# Einzufuegen in den bestehenden nginx-Reverse-Proxy (TrueNAS-App "ix-webserver"): +# Datei ablegen unter: /mnt/JailStorage/DockerVolumes/nginx/config/gerbil-public.conf +# (das ist der conf.d-Mount des Proxys; dort liegen auch git/cloud/rismer.de) +# Danach: docker exec ix-webserver-webserver-1 nginx -s reload # -# SICHERHEIT: Dieser Vhost zeigt NUR auf den publicsite-Container (Port 8081). -# Der Manager (API + Frontend, Port 80) ist NICHT erreichbar von aussen — -# er hat keine Authentifizierung und muss LAN-only bleiben. +# WICHTIG (containerisierter Proxy): das Backend MUSS ueber die Host-LAN-IP +# angesprochen werden (192.168.2.115), NICHT 127.0.0.1 — 127.0.0.1 waere im +# Proxy-Container er selbst. Der publicsite-Container veroeffentlicht Port 8081 +# auf dem Host (PUBLICSITE_PORT in deploy/truenas/.env). +# +# SICHERHEIT: Dieser Vhost zeigt NUR auf den publicsite-Container (statische +# oeffentliche Seite). Der Manager (API + Frontend) bleibt LAN-only und ist von +# aussen NICHT erreichbar (keine Authentifizierung). +# +# ersetzen (z. B. rennmaus.rismer.de). DNS-Eintrag auf die oeffentliche +# IP + Router-Weiterleitung 80/443 -> 11080/11443 muss wie bei den anderen +# rismer.de-Subdomains vorhanden sein. + +# HTTP: ACME-Challenge zulassen + auf HTTPS umleiten (Muster der anderen Vhosts) server { listen 80; + listen [::]:80; server_name ; + location /.well-known/acme-challenge/ { + root /usr/share/nginx/letsencrypt; + } location / { - proxy_pass http://127.0.0.1:8081; + return 301 https://$host$request_uri; + } +} + +# HTTPS: Reverse-Proxy auf den publicsite-Container (Host-Port 8081) +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name ; + + # Let's Encrypt (gleicher Cert-Store wie die uebrigen rismer.de-Vhosts). + # Zertifikat anfordern, z. B.: + # docker exec ix-webserver-webserver-1 certbot --nginx -d + # oder webroot: certbot certonly --webroot -w /usr/share/nginx/letsencrypt -d + ssl_certificate /etc/letsencrypt/live//fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live//privkey.pem; + + # HSTS gehoert auf die TLS-terminierende Ebene (hier), nicht in den HTTP-only + # publicsite-Container. Erst aktivieren, wenn HTTPS sicher steht (max-age dann ggf. erhoehen). + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + # Die uebrigen Security-Header (CSP, X-Content-Type-Options, X-Frame-Options, + # Referrer-Policy, Permissions-Policy) liefert der publicsite-nginx bereits mit und + # werden vom Proxy durchgereicht (siehe nginx/publicsite.conf). + + location / { + proxy_pass http://192.168.2.115:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - - # Kein Buffering fuer kleine statische HTML-Seiten - proxy_buffering off; + proxy_buffering off; # kleine statische HTML-Seiten } } - -# Fuer HTTPS (empfohlen, z.B. per Let's Encrypt via certbot): -# server { -# listen 443 ssl; -# server_name ; -# ssl_certificate /etc/letsencrypt/live//fullchain.pem; -# ssl_certificate_key /etc/letsencrypt/live//privkey.pem; -# location / { -# proxy_pass http://127.0.0.1:8081; -# proxy_set_header Host $host; -# proxy_set_header X-Real-IP $remote_addr; -# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -# proxy_set_header X-Forwarded-Proto $scheme; -# } -# } diff --git a/docs/active-triage-summary.md b/docs/active-triage-summary.md new file mode 100644 index 0000000..c49f1bc --- /dev/null +++ b/docs/active-triage-summary.md @@ -0,0 +1,615 @@ +# Active Tickets Triage Summary +*Generated automatically on 2026-06-28. Total active: 20* + +This file aggregates all active feedback tickets with their full database context and source provenance to allow quick resolution. + +--- + +## 1. [NeedsInfo] Ticket `21c8ff24` (ID: `21c8ff24-cc03-4508-b6c6-849ab36ba5b2`) +- **Breeder's Message**: "Warum ist bei Krümel kein Todesdatum?" +- **Category**: `General` | **Target Name**: `F-Wurf 2010` + +### Target Litter Context: +- **Litter Name**: `F-Wurf 2010` +- **Date**: `2010-09-21` | **Total Born**: `4` +- **Father ID**: `9b40b9e5-a9d3-5d63-ae34-8105297a8b18` | **Mother ID**: `e4898ccc-1896-5258-b1a9-4c89d30cf428` +- **Notes**: `Blacky + Kruke v. 21.09.2010; WS=4` +- **ExternalRef**: `None` + +### AI Question Asked: +> Hallo! Ich habe die handschriftliche Wurfchronik noch einmal ganz genau geprüft. Auf Seite 13 (F-Wurf 2010) ist Krümmel als abgegeben an 'Jana Beikert' (02.01.11) und dann 'Clan of little longnoses' (06.03.11) vermerkt, jedoch ohne Todesdatum (im Gegensatz zu seinen Wurfgeschwistern Fiona, Keks und Flori). Auf Seite 5 ist er als Auszug am 18.02.2015 an 'Lisa Christ' eingetragen, ebenfalls ohne Todesdatum (während für seine Partnerin Ina das Todesdatum ca. Feb 2017 vermerkt ist). Gibt es vielleicht noch eine andere Stelle in deinen Unterlagen, oder weißt du noch, wann er verstorben ist? + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 2. [NeedsInfo] Ticket `3a4fd3c8` (ID: `3a4fd3c8-ab45-4bcf-b7bb-118f44f2529a`) +- **Breeder's Message**: "RennmausPro-Import: Name „Donny" kommt mehrfach vor (2 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Donny` + +### AI Question Asked: +> Hier die Farben der beiden Partner von Donny: + +• Aimee – Polarfuchs-Schecke (geboren 28.04.2009) +• Bluebelle – Agouti-Schecke (geboren 09.11.2007) + +Beide sind in deiner alten Software nur als Vorfahren bzw. Fremdtiere hinterlegt, keines stammt aus deiner eigenen Zucht. + +Magst du mir jetzt noch sagen, ob die beiden Donny-Einträge – einmal „Schwarz-Schecke“, einmal „Schwarz“, beide geboren am 11.02.2008, Eltern Chango und Paula – dasselbe Tier sind oder zwei verschiedene? Dann kann ich die Einträge richtig zusammenführen oder getrennt lassen. + +### Internal Agent Context: +```json +rpro3 Donny: 2 Varianten (A=Schwarz-Schecke u1240 2x, B=Schwarz u990 1x), beide DOB 2008-02-11, Eltern Chango+Paula, Herkunft Clan of Happy Mice. Gencode beider identisch (aa CC DD Ee Gg Pp Spsp). Geliefert: Partner+Nachzucht beider Varianten; Herkunft aller verwandten Tiere (kein Schenkungs-Feld in RPRO3); jetzt Farben der Partner: Aimee=Polarfuchs-Schecke (DOB 2009-04-28), Bluebelle=Agouti-Schecke (DOB 2007-11-09). Alle externe Ahnen. Erneut um Merge-/Getrennt-Entscheidung A vs B gebeten. Offen: A=B? +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 3. [NeedsInfo] Ticket `422f5c3f` (ID: `422f5c3f-b6f2-40e8-9b0e-a396cbe4a5f1`) +- **Breeder's Message**: "RennmausPro-Import: Name „Lukas" kommt mehrfach vor (2 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Lukas` + +### AI Question Asked: +> Hier die beiden Tessy-Linien mit Nachzucht und Partner – jeweils Farbe, Gencode und Geburtstag. + +— Tessy aus der Lukas-Linie A (Lukas Anthrazit) — +• Tessy: Colourpoint-Agouti-Hell, geb. 24.05.2006 · Gencode Aa c[chm]c[h] D- E- Gg Pp spsp +• Partner Pünktchen: Anthrazit, geb. 12.05.2006 · Gencode aa Cc[chm] DD E- gg P- spsp +• Kind Maxx: Marder-Schecke, geb. 29.09.2007 · Gencode aa c[chm]c[chm] DD Ee Gg PP Spsp + +— Tessy aus der Lukas-Linie B (Lukas Marder-Hell-Schecke) — +• Tessy: Marder-Hell-Schecke, geb. 24.05.2006 · Gencode aa c[chm]c[h] DD E- G- PP Spsp +• Partner Pünktchen: Marder-Schecke, geb. 12.05.2006 · Gencode aa c[chm]c[chm] DD EE Gg PP Spsp +• Kind Baileys: Marder-Hell-Schecke, geb. 19.06.2007 · Gencode aa c[chm]c[h] DD E- G- PP Spsp + +Beide Tessy sind am selben Tag geboren und haben einen Partner namens Pünktchen, aber Farben, Gencodes und sogar die Kinder (Maxx auf der einen, Baileys auf der anderen Seite) sind durchweg verschieden – genau wie schon bei Lukas selbst. Das sieht nach zwei kompletten, getrennten Familien aus. + +Sag mir damit bitte abschließend: Sind die beiden Lukas (und damit auch die beiden Tessy) für dich dasselbe Tier – dann lege ich sie zusammen, sag mir nur, welche Farbe und welche Herkunft stehen bleiben sollen – oder sind es zwei verschiedene Tiere mit zufällig gleichem Namen? + +### Internal Agent Context: +```json +rpro3 Lukas (2 Varianten, beide externer Ahn). A=u557 (DOB 2005-11-14, Anthrazit, Herkunft unbekannt), B=u1441 (DOB 2005-11-14, Marder-Hell-Schecke, Herkunft Laura Gerber). +Runde 4 (2026-06-27): Zuechterin bat um Gencode/Nachzucht/Partner BEIDER Tessy (Lukas-Kinder). Aus _rpro3.db (rpro3_lookup.py) geliefert, gruppiert nach Lukas-Linie: +Linie A: Tessy Colourpoint-Agouti-Hell *2006-05-24 [Aa c[chm]c[h] D- E- Gg Pp spsp]; Partner Puenktchen(Variante C) Anthrazit *2006-05-12 [aa Cc[chm] DD E- gg P- spsp]; Kind Maxx Marder-Schecke *2007-09-29 [aa c[chm]c[chm] DD Ee Gg PP Spsp]. +Linie B: Tessy Marder-Hell-Schecke *2006-05-24 [aa c[chm]c[h] DD E- G- PP Spsp]; Partner Puenktchen(Variante D) Marder-Schecke *2006-05-12 [aa c[chm]c[chm] DD EE Gg PP Spsp]; Kind Baileys Marder-Hell-Schecke *2007-06-19 [aa c[chm]c[h] DD E- G- PP Spsp]. +Hinweis: zwei parallele, durchweg verschiedene Familien (verschiedene Farben/Gencodes/Kinder Maxx vs Baileys) trotz gleicher Namen+DOB -> starkes Indiz fuer zwei getrennte Tiere. Puenktchen hat in RPRO3 5 Varianten (A/B Rotaugen-Weiss/unbek, C Anthrazit, D Marder-Schecke, E Anthrazit-Starkschecke); nur C+D gehoeren zu den Tessy-Linien. +Warte weiter auf finale Merge-/Getrennt-Entscheidung der Zuechterin (+ bei Merge welche Farbe/Herkunft bleiben soll). +Geplante Umsetzung je nach Antwort: rpro3-decisions.json Eintrag fuer Lukas-rids -> 'different' [u557, u1441] bei 'verschieden' ODER 'same' mit Farb/Herkunft-fields bei 'dasselbe'. Keine Live-Materialisierung noetig solange kein App-Kind als Anker (alle reine externe Ahnen). +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 4. [NeedsInfo] Ticket `035ee655` (ID: `035ee655-d354-4e90-949a-16921c77ab64`) +- **Breeder's Message**: "RennmausPro-Import: Name „Dunny" kommt mehrfach vor (2 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Dunny` + +### AI Question Asked: +> Dunny taucht aktuell in keinem Stammbaum deiner App auf: Weder Dunny selbst noch sein Kind Dana (noch die Tiere drumherum) sind als Tiere in der App vorhanden — sie stehen bislang nur in den RennmausPro-Ahnendaten. Sobald der große RennmausPro-Ahnen-Import läuft, kommt Dunny mit dazu. + +Damit ich Dunny richtig anlege: Sind die beiden Dunny-Einträge (beide geboren am 18.06.2005) dasselbe Tier oder zwei verschiedene? Und falls dasselbe — welche Farbe stimmt: Zobel-Hell oder Marder-Hell-Schecke? + +### Internal Agent Context: +```json +rpro3 Dunny (2 Var, beide *2005-06-18): A=u556 Zobel-Hell (Eltern unbek.), B=u1445 Marder-Hell-Schecke (Eltern Suntie+Sissy). R3: Frage 'in welchem Stammbaum' beantwortet -> Dunny/Dana/Umfeld NICHT in Live-App (0 Treffer, nur unverwandte Vandana/Danako). Warte weiter auf same/different + Farbe (Zobel-Hell vs Marder-Hell-Schecke). Bei 'same' spaeter rpro3-decisions.json same [[u556,u1445]] + color. +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 5. [NeedsInfo] Ticket `4b38b3d6` (ID: `4b38b3d6-3597-4f3e-9aa6-ae32babc0934`) +- **Breeder's Message**: "RennmausPro-Import: Name „Nunny" kommt mehrfach vor (2 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Nunny` + +### AI Question Asked: +> Hier die Gencodes zu Nunny und ihren Eltern: + +Nunny selbst (beide geboren am 06.09.2005): +• Variante A — Marder-Schecke: aa c[chm]c[chm] DD E- Gg PP Spsp · Eltern unbekannt +• Variante B — Zobel-Schecke: aa c[chm]c[chm] DD E- gg PP Spsp · Eltern Hannes & Hanny + +Die Eltern (nur bei Variante B hinterlegt): +• Vater Hannes — Agouti-Schecke: Aa Cc[chm] DD Ee Gg PP Spsp +• Mutter Hanny — Zobel: aa c[chm]c[chm] DD EE gg PP spsp + +Sag mir bitte: Sind die beiden Nunny-Einträge dasselbe Tier oder zwei verschiedene? Und falls dasselbe — welche Farbe stimmt: Marder-Schecke oder Zobel-Schecke? + +### Internal Agent Context: +```json +rpro3 Nunny (2 Var, beide *2005-09-06, Partner Dunny, Kind Dana): A=u555 Marder-Schecke (aa c[chm]c[chm] DD E- Gg PP Spsp, Eltern unbek.), B=u1442 Zobel-Schecke (aa c[chm]c[chm] DD E- gg PP Spsp, Eltern Hannes=Agouti-Schecke Aa Cc[chm] DD Ee Gg PP Spsp / Hanny=Zobel aa c[chm]c[chm] DD EE gg PP spsp). R3: Gencodes Nunny+Eltern geliefert. Warte auf same/different + Farbe (Marder-Schecke vs Zobel-Schecke). Bei 'same' spaeter rpro3-decisions.json same [[u555,u1442]] + color. +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 6. [NeedsInfo] Ticket `2530bf41` (ID: `2530bf41-bf4d-42a3-a291-bc8f8100e051`) +- **Breeder's Message**: "RennmausPro-Import: Name „Tessy" kommt mehrfach vor (2 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Tessy` + +### AI Question Asked: +> Gerne! Hier die Farben rund um beide „Tessy"-Varianten: + +**A** (geboren 24.05.2006, Marder-Hell-Schecke): +• Vater Lukas: Marder-Hell-Schecke +• Mutter Dana: Marder-Schecke +• Partner Pünktchen: Marder-Schecke +• Nachzucht: Baileys (geb. 19.06.2007), Marder-Hell-Schecke + +**B** (geboren 24.05.2006, Colourpoint-Agouti-Hell): +• Vater Lukas: Anthrazit +• Mutter Dana: Colourpoint-Silberagouti-Schecke +• Partner Pünktchen: Anthrazit +• Nachzucht: Maxx (geb. 29.09.2007), Marder-Schecke + +Beide haben dasselbe Geburtsdatum und dieselben Elternnamen (Lukas und Dana), aber die Farben passen nicht zusammen: A gehört in eine ganz marderfarbene Familie, B in eine deutlich hellere/colourpoint-farbene. Auch die Nachzucht und der Partner unterscheiden sich farblich klar. + +Sind A und B für dich dasselbe Tier oder zwei verschiedene? Antworte gern einfach mit „dasselbe" oder „verschieden". + +### Internal Agent Context: +```json +{"source": "rpro3-ambiguity", "name": "Tessy", "variants": [{"label": "A", "rids": ["u1439", "u1455"], "count": 2, "dob": ["2006-05-24"], "farbe": ["Marder-Hell-Schecke"], "origin": ["Baru of Lienehma"], "is_own": false, "matched_gerbil_id": null, "vater": "Lukas (Marder-Hell-Schecke)", "mutter": "Dana (Marder-Schecke)", "partner": "Pünktchen (Marder-Schecke)", "nachzucht": "Baileys 2007-06-19 Marder-Hell-Schecke"}, {"label": "B", "rids": ["u553"], "count": 1, "dob": ["2006-05-24"], "farbe": ["Colourpoint-Agouti-Hell"], "origin": [], "is_own": false, "matched_gerbil_id": null, "vater": "Lukas (Anthrazit)", "mutter": "Dana (Colourpoint-Silberagouti-Schecke)", "partner": "Pünktchen (Anthrazit)", "nachzucht": "Maxx 2007-09-29 Marder-Schecke"}], "status": "Farben von Eltern/Partner/Nachzucht je Variante geliefert (Runde 2). Warte auf gleich/verschieden-Entscheidung. Hinweis: gleiche DOB+Elternnamen, aber Farben klar abweichend -> spricht fuer verschieden."} +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 7. [NeedsInfo] Ticket `945143e4` (ID: `945143e4-d219-4bf0-8a8f-91d04b1ad94e`) +- **Breeder's Message**: "RennmausPro-Import: Name „Bubi" kommt mehrfach vor (2 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Bubi` + +### AI Question Asked: +> Nein, es gibt nur eine einzige Tasmania in RennmausPro. Hier ihre Angaben: + +Tasmania - Farbe Anthrazit, Geburtsdatum nicht hinterlegt, Herkunft Privatzucht Mettmann +- Eltern: Vater Bubi, Mutter Meiky +- Nachzucht: Samira (geboren 07.08.2008, Anthrazit-Schecke), mit Partner Garfield + +Sie taucht also nur einmal auf - es sind nicht mehrere Tasmania hinterlegt. Tasmania ist damit das Kind von dem Bubi mit der Zobel-Schecken-Faerbung (Variante A). + +Damit zurueck zur eigentlichen Frage: Die beiden „Bubi“-Eintraege haben dasselbe Geburtsdatum (18.04.2007), aber unterschiedliche Farben (Zobel-Schecke gegen Marder-Schecke), und nur einer (A) hat Nachzucht (Dora und Tasmania). Sind die beiden Bubi dasselbe Tier oder zwei verschiedene? + +### Internal Agent Context: +```json +{"source": "rpro3-ambiguity", "name": "Bubi", "variants": [{"label": "A", "rids": ["u115"], "count": 1, "dob": ["2007-04-18"], "farbe": ["Zobel-Schecke"], "origin": ["Clan of Tamyna"], "is_own": false, "matched_gerbil_id": null, "gencode": "aa c[chm]c[chm] DD Ee gg P- Spsp", "parents": {"vater": "unbekannt", "mutter": "unbekannt"}, "offspring": [{"name": "Dora", "dob": "2008-04-12", "farbe": "Marder", "gencode": "aa c[chm]c[chm] DD E- Gg P- spsp", "co_parent": "Tina"}, {"name": "Tasmania", "dob": null, "farbe": "Anthrazit", "gencode": "aa Cc[chm] DD E- gg P- spsp", "co_parent": "Meiky"}]}, {"label": "B", "rids": ["u430"], "count": 1, "dob": ["2007-04-18"], "farbe": ["Marder-Schecke"], "origin": [], "is_own": false, "matched_gerbil_id": null, "gencode": "aa c[chm]c[chm] DD E- Gg P- Spsp", "parents": {"vater": "unbekannt", "mutter": "unbekannt"}, "offspring": []}], "status_note": "Runde 3. Zuechterin fragte 'Sind mehrere Tasmania hinterlegt'. Geprueft via rpro3_lookup: nur EINE Tasmania in RPRO3 (Anthrazit, kein DOB, Eltern Vater Bubi/Mutter Meiky, Herkunft Privatzucht Mettmann, Nachzucht Samira geb 2008-08-07 Anthrazit-Schecke mit Partner Garfield). Tasmania ist Kind von Bubi-A (Zobel-Schecke). KORREKTUR zu frueher: Meiky ist NICHT Partner sondern MUTTER von Tasmania (also Co-Elternteil von Bubi-A). An Zuechterin geliefert + Kernfrage A==B wiederholt. Warte auf gleich/verschieden-Entscheidung."} +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 8. [NeedsInfo] Ticket `bedb4017` (ID: `bedb4017-0499-4a50-b2a4-32544601636c`) +- **Breeder's Message**: "RennmausPro-Import: Name „Speedy" kommt mehrfach vor (3 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Speedy` + +### AI Question Asked: +> Danke! Ich führe die drei „Speedy" als verschiedene Tiere getrennt. Zu deinem Speedy (Kohlfuchs, geboren am 12.02.2019, Vater der Speedy×Rin-Jungen): Bei mir steht er gerade fälschlich als „abgegeben an Sabrina Lüftner" — das war aber der Vertrag für sein Junges Fizz, nicht für ihn selbst. Ich trage ihn richtig als dein eigenes (verstorbenes) Zuchttier ein. Weißt du noch ungefähr, wann er gestorben ist? Mit einem (auch ungefähren) Datum kann ich ihn korrekt als verstorben statt als abgegeben anzeigen. + +### Internal Agent Context: +```json +{"ticket":"bedb4017 Speedy rpro3-ambiguity","breeder_answer":"Alle verschieden + Folgefrage: ist B als ehemaliges (verstorbenes) Zuchttier hinterlegt?","decision":"Alle 3 verschieden -> KEIN rpro3 same/different noetig; A/B/C trennen automatisch ueber DOB+Farbe. A=u706/u2883/u6775 Agouti *2009-07-01 Privat (externer Ahn, kein App-Record). B=u6974 Kohlfuchs *2019-02-12 von den Kleinen Chaoten. C=u6991 Schwarz-Ansatzschecke *2010-05-01 (externer Ahn).","variant_B_app_record":"798998c4-cc06-5c75-b762-3e3b68fde442 'Speedy von den Kleinen Chaoten', male, *2019-02-12, geno 'aa Cc[-] D- ee[f] Gg Pp spsp', externalRef stammbaum-speedykleinenchaoten-12022019, fromWurfchronik=false, voller Stammbaum-Record.","current_state_PROBLEM":"status=GivenAway, receiverContactId=5df5b149 (Sabrina Lueftner), goHomeDate=2019-09-01, isResident=false, originBreeder=None, dateOfDeath=None. Abgabe FALSCH attribuiert: receiver stammt aus Vertrag 'Fizz (Speedy.Rin)-Sabrina Lueftner' = Vertrag fuer JUNGES Fizz; parent-Notation (Speedy.Rin) wurde faelschlich als Speedy-Abgabe gematcht (siehe provenance). originContactId korrekt = 'Zucht der kleinen Chaoten' b7e9a81e.","breeder_says":"Speedy B ist ihr ehemaliges verstorbenes Zuchttier. Nachzucht 'von den Kleinen Chaoten' (Fizz/Josy/Lotta/Mausi aus Speedy x Rin; rpro3 Nachzucht 'Rins Tochter' Partner Rin) -> erfuellt isResident-Regel.","plan":"conflict-decisions.json resolution name='Speedy von den Kleinen Chaoten' dob='12.02.2019' (Match-Key name+dob via _apply_conflict_decisions): (1) excludeContractMatch=true -> Fizz-Vertrag nicht mehr als Speedy-Abgabe binden (Praezedenz Danako 85fc96fe); (2) isResident=true Override (Nachzucht von den Kleinen Chaoten); (3) deceased + dateOfDeath= -> Status Deceased (GerbilStatusService: DateOfDeath->Deceased gewinnt). originBreeder ggf. 'Zucht der kleinen Chaoten'.","BLOCKER":"exaktes/ungefaehres Sterbedatum fehlt -> ohne Datum bleibt Status GivenAway statt Deceased. Genau das erfragt diese Rueckfrage.","affectedFiles":["tools/import/conflict-decisions.json"],"overlapKey":"import-data-conflict-decisions","precedent":"Benjiro b97dd9ad gleiches Muster (dort schon korrekt; hier NICHT -> echte Korrektur). excludeContractMatch=Danako 85fc96fe; deceased+dateOfDeath=ec9267b9/Akane 36a3fcde."} +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 9. [NeedsInfo] Ticket `cd4b24b0` (ID: `cd4b24b0-7b48-4fcf-880d-6f86f12755d9`) +- **Breeder's Message**: "RennmausPro-Import: Name „Sina" kommt mehrfach vor (3 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Sina` + +### AI Question Asked: +> Notiert: C ist ein anderes Tier. Hier nun alle Gencodes zu A und B — jeweils das Tier selbst, die Eltern, der Partner und die Nachzucht: + +=== Variante A (Marder-Hell, geb. 28.12.2005, Herkunft unbekannt) === +• Sina selbst: aa c[chm]c[h] DD Ee G- P- spsp +• Eltern: unbekannt +• Partner Sam: aa Cc[chm] DD Ee Gg Pp Spsp +• Nachzucht: + – Nany (geb. unbekannt, Marder-Schecke): aa c[chm]c[chm] DD Ee G- P- Spsp + – Cassie (geb. 15.01.2011, Colourpoint-Agouti): Aa c[chm]c[chm] D- EE GG Pp spsp + +=== Variante B (Marder-Schecke, geb. 28.12.2005, Herkunft Clan of Letter Live) === +• Sina selbst: aa c[chm]c[chm] DD EE Gg PP Spsp +• Eltern: + – Vater Duka (Zobel): aa c[chm]c[chm] DD EE gg PP spsp + – Mutter Helsy (Marder-Hell-Schecke): aa c[chm]c[h] DD EE GG PP Spsp +• Partner Lupo (Marder-Hell-Schecke): aa c[chm]c[h] DD EE GG PP Spsp +• Nachzucht: + – Nany (geb. 03.02.2006, Marder-Schecke): aa c[chm]c[chm] DD EE GG PP Spsp + +Ein Hinweis, der dir helfen kann: Beide Varianten haben ein Junges namens „Nany“ — aber bei A ist Nany ohne Geburtsdatum und mit Partner Sam eingetragen, bei B mit Geburtsdatum 03.02.2006 und Partner Lupo. Es könnte also gut sein, dass A und B trotz gleichem Namen wirklich zwei verschiedene Sina-Tiere mit unterschiedlichen Partnern und Nachkommen sind. + +Sag mir bitte mit diesen Daten: Sind A und B dasselbe Tier (dann lege ich sie zusammen) oder doch zwei verschiedene? Und falls zusammenlegen: Welcher Name und welche Farbe sollen am Ende stehen? + +### Internal Agent Context: +```json +rpro3 Sina: 3 Varianten (Ticket-Buchstaben). Ticket A=Marder-Hell geb.2005-12-28 Herkunft unbekannt = Lookup-Block B (Gencode aa c[chm]c[h] DD Ee G- P- spsp; Eltern unbekannt; Partner Sam; Nachzucht Nany, Cassie). Ticket B=Marder-Schecke geb.2005-12-28 Clan of Letter Live = Lookup-Block C (Gencode aa c[chm]c[chm] DD EE Gg PP Spsp; Eltern Duka/Helsy; Partner Lupo; Nachzucht Nany 2006-02-03). Ticket C=Agouti-Schecke = Lookup-Block A -> von Zuechterin als ANDERES Tier bestaetigt (getrennt lassen). Runde davor: A/B-Merge-Entscheidung offen; Zuechterin wollte Gencodes von Tier+Eltern+Partner+Nachzucht zu A und B. JETZT geliefert: alle Gencodes (Sina A/B, Eltern Duka/Helsy, Partner Sam/Lupo, Nachzucht Nany/Cassie) + Hinweis dass die je 'Nany' unterschiedliche DOB/Partner haben (Indiz, dass A!=B). WARTE auf: Merge-/Getrennt-Entscheidung zu A vs. B + gewuenschter Name/Farbe bei Merge. +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 10. [NeedsInfo] Ticket `33af41eb` (ID: `33af41eb-ffac-47bc-8467-561dba513c06`) +- **Breeder's Message**: "RennmausPro-Import: Name „Anton" kommt mehrfach vor (5 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Anton` + +### AI Question Asked: +> Ja, die Zusammenführung für Anton als 'WFNZ Agouti' (Geburtstag 31.07.2007) sowie für Romina und Bärchen ist damit abgeschlossen! + +Hier sind die Daten aus RennmausPro für BlackEye, nach denen du gefragt hast: + +• **Farbe**: Silberschimmel +• **Gencode**: Aa Cc[-] DD e[f]e[f] gg Pp spsp (Kombination aus deinen verschiedenen Einträgen) +• **Eltern**: Anton und Pünktchen +• **Herkunft**: Clan of Desert / Unbekannt +• **Kinder**: Pegasus (geboren am 27.02.2008 mit Djen) und Yukon (geboren am 16.03.2007 mit Desert) + +In RennmausPro taucht BlackEye insgesamt 28-mal als Vorfahr in verschiedenen Stammbäumen auf, hat aber überall diese übereinstimmenden Daten. + +Sollen wir alle diese 28 Einträge zu einer einzigen BlackEye zusammenlegen? + +### Internal Agent Context: +```json +{"source":"rpro3-ambiguity","name":"Anton","stage":"Züchterin entschied: A&B=same, C&D&E=same; bat zusätzlich Nachzucht zu mergen + Inkonsistenzen zu melden. Rückfrage gestellt wg. DOB/Farb-Inkonsistenzen in C/D/E und in den Kindern Romina/Bärchen sowie Blackeye-Großcluster.","breeder_decision":"A&B dasselbe Tier; C&D&E dasselbe Tier; Nachzucht ggf. mergen; Inkonsistenzen (außer unbekannt) rückfragen","rpro3_db_rids":{"Anton_unknownDOB_AundB":["u1064","u1605","u1948","u2246","u2387","u2599","u3041","u3199","u3505"],"Anton_BenJeannie_CDE":{"C_31.07.2007_Agouti":["u2899","u6791","u6921"],"D_30.07.2007_Agouti":["u3352"],"E_21.03.2007_WFNZ-Agouti":["u1407"]}},"note_rid_u3199":"u3199 ist ein weiterer Anton mit DOB unbekannt/HERK 205, NICHT im ursprünglichen agentContext gelistet, gehört aber profilgleich in den A&B-Cluster.","merge_data_to_keep":{"AundB":"Daten von B (Anthrazit-Schecke, Gencode 'aa C- D- Ee[f] gg P- Spsp'); A ist komplett unbekannt","CDE":"Gencode bei allen identisch 'AA CC DD EE GG PP spsp'; DOB+Farbe offen (Rückfrage 1)"},"offspring":{"Blackeye":"28 fremd_tb-Records, mehrere Genotypen/Herkünfte (Silberschimmel, Clan of Desert), eigene Nachzucht (Pegasus, Yukon) -> eigener Klärungsschritt, NICHT blind mergen","Romina_rids":["u1405(01.05.2011 Agouti)","u2897","u3348","u6789","u6917(01.03.2011)"],"Baerchen_rids":["u1408(01.03.2011)","u2900(06.04.2012)","u3355(01.03.2011)","u6792(06.04.2012)","u6924(01.03.2011 WFNZ)"]},"matched_gerbil_id":null,"live_app":"Anton + Nachzucht (Blackeye/Romina/Bärchen) sind reine rpro3-Ahnen, noch NICHT in der App (rpro3-Ahnen-Import = ausstehende Folgeaufgabe). Entscheidungen gehören in rpro3-decisions.json und materialisieren beim nächsten RennmausPro-Ahnen-Import.","plan_nach_antwort":"rpro3-decisions.json: (1) same [[u1064,...,u3505]] für A&B, Feld-Override Farbe Anthrazit-Schecke + genotype 'aa C- D- Ee[f] gg P- Spsp'; (2) same [[u2899,u6791,u6921,u3352,u1407]] für C&D&E, Feld-Override DOB+Farbe = Antwort der Züchterin (Notiz für abweichendes Datum). Romina/Bärchen: je nach Antwort same-Cluster + DOB/Farb-Override. Blackeye separat. Schlüssel IMMER rid, nie Variantenbuchstabe."} +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 11. [NeedsInfo] Ticket `12440e00` (ID: `12440e00-829e-4209-b3d9-a3d28e7879e7`) +- **Breeder's Message**: "RennmausPro-Import: Name „Pünktchen" kommt mehrfach vor (5 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Pünktchen` + +### AI Question Asked: +> Ich habe Pünktchen A und B (die Eltern von Blackeye) zusammengeführt und C und D wie gewünscht zusammengelegt. Zu den Differenzen bei C und D (geb. 12.05.2006, Eltern Geyo x Nany): Ihre Eltern Geyo und Nany haben in RennmausPro keine eingetragenen Geburtsdaten oder Gencodes. Pünktchen selbst hat mit Partner Tessy zwei Kinder: Maxx (*12.05.2006, Marder-Schecke, u560) und Baileys (*12.05.2006, Marder-Hell-Schecke, u1460). Sollen wir diese Kinder auch zusammenführen, oder welche Daten möchtest du hier anpassen? + +### Internal Agent Context: +```json +{"source": "rpro3-ambiguity", "name": "Pünktchen", "status": "awaiting-merge-decision", "delivered": "Pro Variante A-E: DOB, Farbe, Gencode, Herkunft, Eltern, Nachzucht+Partner geliefert (Antwort auf Bitte um Eltern/Nachzucht/Partner/Farben/Gencodes).", "hints_given": ["A & B teilen Kind 'Blackeye' (Silberschimmel) mit Partner Anton -> moeglicherweise dasselbe Tier", "C & D teilen DOB 2006-05-12 und Eltern Geyo x Nany -> moeglicherweise dasselbe Tier oder Geschwister"], "variants": [{"label": "A", "rids": ["u1065", "u1124", "u1606", "u1667"], "count": 8, "dob": null, "farbe": "Rotaugen-Weiß", "gencode": "-- c[chm]c[chm] DD -- -- pp spsp", "origin": "The little warrior with Claws", "offspring": "BlackEye (Silberschimmel) / Partner Anton"}, {"label": "B", "rids": ["u2245", "u2386", "u2598", "u3040"], "count": 5, "dob": null, "farbe": "unbekannt", "gencode": "-- -- -- -- -- -- spsp", "origin": null, "offspring": "Blackeye (Silberschimmel) / Partner Anton"}, {"label": "C", "rids": ["u560"], "count": 1, "dob": "2006-05-12", "farbe": "Anthrazit", "gencode": "aa Cc[chm] DD E- gg P- spsp", "origin": null, "parents": "Geyo x Nany", "offspring": "Maxx (Marder-Schecke) / Partner Tessy"}, {"label": "D", "rids": ["u1460"], "count": 1, "dob": "2006-05-12", "farbe": "Marder-Schecke", "gencode": "aa c[chm]c[chm] DD EE Gg PP Spsp", "origin": "Baru of Lienehma", "parents": "Geyo x Nany", "offspring": "Baileys (Marder-Hell-Schecke) / Partner Tessy"}, {"label": "E", "rids": ["u3742"], "count": 1, "dob": "2010-10-12", "farbe": "Anthrazit-Starkschecke DP", "gencode": "aa Cc[chm] DD EE gg PP Spsp", "origin": "Clan of Braveheart", "parents": "Jumanji x Lola", "offspring": "Hell Boy (Agouti-Starkschecke) / Partner Picasso"}]} +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 12. [NeedsInfo] Ticket `5931f460` (ID: `5931f460-b39d-4ab4-913b-c29f18df0f13`) +- **Breeder's Message**: "RennmausPro-Import: Name „Paula" kommt mehrfach vor (5 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Paula` + +### Target Gerbil Context: +- **Name**: `Paula` +- **Gender**: `female` | **Status**: `Deceased` +- **DOB**: `2016-04-07` | **DOD**: `2018-09-10` +- **Genotype**: `None` +- **Notes**: `None` +- **Import Source**: `Wurfchronik` +- **ExternalRef**: `Wurfchronik Teil 2_page_0021.md-15cd3c3c-64fb-4d97-bbf9-aba561972a0a` +- **Provenance History**: + - In „Wurfchronik“ gefunden. + - Geburtsdatum (07.04.2016) aus „Wurfchronik“. + - Geschlecht (weiblich) aus „Wurfchronik“. + - Farbschlag aus „Wurfchronik“. + - Sterbedatum (10.09.2018) aus „Wurfchronik“. + - Angaben aus der Wurfchronik übernommen. + +### AI Question Asked: +> Hier sind die Details aus RennmausPro für die beiden Paula-Einträge: + +• **Paula A (u1241)**: Farbe Anthrazit-Schecke, geboren am 25.08.2007. Eltern sind Princess of Kinghouse (Silberagouti-Schecke) und Young Boy (Marder-Schecke). Sie hat ein Kind namens Donny (*10.02.2008). +• **Paula B (u989)**: Farbe Anthrazit, geboren am 25.08.2007. Eltern sind Princess (Silberagouti-Schecke) und Young Boy1 (Marder-Schecke). Ihr Partner war Chango (Kohlfuchs-Schecke) und ihr Kind ebenfalls Donny (*10.02.2008). + +Sind diese beiden Paula dasselbe Tier (dann führen wir sie zusammen) oder zwei verschiedene? + +### Internal Agent Context: +```json +{"source": "rpro3-ambiguity", "name": "Paula", "variants": [{"label": "A", "rids": ["u1241", "u6865"], "count": 2, "dob": ["2007-08-25"], "farbe": ["Anthrazit-Schecke"], "origin": [], "is_own": false, "matched_gerbil_id": "b41c3686-67a3-5e01-9201-347622ade9d7"}, {"label": "B", "rids": ["u989"], "count": 1, "dob": ["2007-08-25"], "farbe": ["Anthrazit"], "origin": ["Clan of Happy Mice"], "is_own": false, "matched_gerbil_id": "b41c3686-67a3-5e01-9201-347622ade9d7"}, {"label": "C", "rids": ["u1481"], "count": 1, "dob": ["2004-08-01"], "farbe": ["Schwarz-Schecke"], "origin": [], "is_own": false, "matched_gerbil_id": "b41c3686-67a3-5e01-9201-347622ade9d7"}, {"label": "D", "rids": ["u608"], "count": 1, "dob": [], "farbe": ["Agouti"], "origin": ["Züchter aus NL"], "is_own": false, "matched_gerbil_id": "b41c3686-67a3-5e01-9201-347622ade9d7"}, {"label": "E", "rids": ["u352"], "count": 1, "dob": [], "farbe": ["Kohlfuchs, hell"], "origin": [], "is_own": false, "matched_gerbil_id": "b41c3686-67a3-5e01-9201-347622ade9d7"}]} +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 13. [NeedsInfo] Ticket `e74e1fab` (ID: `e74e1fab-515b-4296-9161-7b0d6f4be382`) +- **Breeder's Message**: "RennmausPro-Import: Name „Nancy" kommt mehrfach vor (2 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Nancy` + +### Target Gerbil Context: +- **Name**: `Nancy` +- **Gender**: `female` | **Status**: `Deceased` +- **DOB**: `2014-04-20` | **DOD**: `2016-09-10` +- **Genotype**: `AA CC DD EE GG PP Spsp rere` +- **Notes**: `Behalten (Zucht-Symbol). | Breeder Dam` +- **Import Source**: `Wurfchronik Teil 1_page_0028.md` +- **ExternalRef**: `Wurfchronik Teil 1_page_0028.md-c3d4e5f6-0028-0003-0003-000000000003` +- **Provenance History**: + - In „Wurfchronik Teil 1_page_0028.md“ gefunden. + - Geburtsdatum (20.04.2014) aus „Wurfchronik Teil 1_page_0028.md“. + - Geschlecht (weiblich) aus „Wurfchronik Teil 1_page_0028.md“. + - Genotyp aus „Wurfchronik Teil 1_page_0028.md“. + - Farbschlag aus „Wurfchronik Teil 1_page_0028.md“. + - Sterbedatum (10.09.2016) aus „Wurfchronik Teil 1_page_0028.md“. + - Auch in „Wurfchronik Teil 1_page_0046.md“ gefunden → Datensätze zusammengeführt. + - Auch in „Wurfchronik Teil 1_page_0051.md“ gefunden → Datensätze zusammengeführt. + - Angaben aus der Wurfchronik übernommen. + - Abgabe an „Natascha Marienfeld“ aus Vertrag „Zucht der kleinen Chaoten _Nancy-Natascha M._.docx“ übernommen. + - Abgabedatum 2016-08-27 aus Vertrag „Zucht der kleinen Chaoten _Nancy-Natascha M._.docx“ übernommen. + - Vertrag „Zucht der kleinen Chaoten _Grace aka Suri (Milon. Nancy of little Hobbits)-Gina Hepp_.docx“ nennt anderen Abnehmer „Gina Heep“ — bestehende Zuordnung beibehalten. + +### AI Question Asked: +> Alles klar - notiert: Die beiden Nancys sind verschiedene Tiere und werden getrennt gefuehrt. + +Zu deinen Fragen: + +1) Ist die zweite Nancy (geboren am 20.04.2014, Agouti-Schecke, aus der Linie der Kleinen Chaoten) als ehemaliges Zuchttier aufgefuehrt? +Ja - genau diese Nancy ist in deinem Bestand als Zuchttier gefuehrt. (Die andere Nancy, geboren am 18.05.2014, Agouti, von Annemarie Knief, wird als reiner Vorfahre gefuehrt, nicht als dein Zuchttier.) + +2) Kommt bei der ersten Nancy ein "Cloud" im Stammbaum vor - wenn ja, seine Daten? +Bei "Cloud" gibt es eine Schwierigkeit: In RennmausPro stehen zwei Tiere namens Cloud, beide Rotfuchs-Schecke und beide als Vorfahren (keine eigenen Zuchttiere). Damit ich dir die richtigen Daten gebe, brauche ich von dir, welcher gemeint ist. Hier beide zum Vergleich: + +- Cloud (1): geboren 09.06.2012, Rotfuchs-Schecke, Herkunft unbekannt. Eltern unbekannt. Nachwuchs: Anubis (geboren 06.01.2013, Algierfuchs-Punktschecke), zusammen mit Arya. + +- Cloud (2): geboren 31.08.2012, Rotfuchs-Schecke, Herkunft "Clan of New Dawn". Eltern unbekannt. Nachwuchs: Sweet Heart (geboren 14.02.2013, Platin-Hell), zusammen mit Arya. + +Welcher der beiden ist der Cloud, den du meinst? Dann ergaenze ich dir gern noch fehlende Details. + +### Internal Agent Context: +```json +{"source": "rpro3-ambiguity", "name": "Nancy", "round": 2, "decision": "Zuechterin: 'alle verschieden'. Nancy A (rid 279, 2014-05-18, Agouti, Annemarie Knief1, eigenes/Vorfahre) und Nancy B (rid u6443, 2014-04-20, Agouti-Schecke, Kleinen Chaoten) sind VERSCHIEDENE Tiere -> getrennt fuehren.", "variants": [{"label": "A", "rids": ["279"], "dob": "2014-05-18", "farbe": "Agouti", "origin": "Annemarie Knief1", "eltern": "Fegur x Lumina", "nachzucht": "Cyrano (2016-03-12) mit Milon", "live_match": "Nancy of little hobbits / d923abcf-0364-5a1a-ba6a-53b2586e58fa / isResident=False (reiner Vorfahre)"}, {"label": "B", "rids": ["u6443"], "dob": "2014-04-20", "farbe": "Agouti-Schecke", "origin": "Kleinen Chaoten", "eltern": "Taro x Kazumi", "nachzucht": "Charly (2015-06-05) mit Zorro", "live_match": "Nancy / 2a5e6d88-9b8d-5371-b0d5-b6cdc3bfc787 / isResident=True"}], "answered": {"B_im_bestand": "JA - B (2014-04-20) = isResident=True, als Zuchttier gefuehrt. A (2014-05-18) = isResident=False, reiner Vorfahre."}, "open": {"cloud_query": "Zuechterin fragt nach 'Cloud' im Stammbaum von Nancy A. RPRO3 hat ZWEI Cloud (beide externe Ahnen, Rotfuchs-Schecke): Cloud1 dob 2012-06-09, Herkunft unbek, Gencode aa CC DD ee[f] GG pp Spsp, Kind Anubis (mit Arya); Cloud2 dob 2012-08-31, Herkunft 'Clan of New Dawn', Gencode aa CC D- ee[f] G- pp Spsp, Kind Sweet Heart (mit Arya). Beide vorgelegt; warte auf Auswahl welcher gemeint ist, dann fehlende Details ergaenzen."}} +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 14. [NeedsInfo] Ticket `625a86ab` (ID: `625a86ab-51d8-4612-8292-5dbd93be2a5c`) +- **Breeder's Message**: "RennmausPro-Import: Name „Max" kommt mehrfach vor (2 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Max` + +### AI Question Asked: +> Ich habe die beiden Max-Einträge getrennt belassen. In deiner RennmausPro-Datenbank ist Max A (*01.02.2013, Marder) noch ohne Sterbedatum und Herkunft eingetragen (er hat den Status 'abgegeben'). Da er dein Zuchttier war: Wann ist er gestorben, was war die Todesursache und weißt du noch, woher du ihn gekauft hattest? + +### Internal Agent Context: +```json +{"source": "rpro3-ambiguity", "name": "Max", "variants": [{"label": "A", "rids": ["133"], "count": 1, "dob": ["2013-02-01"], "farbe": ["Marder"], "origin": [], "is_own": true, "matched_gerbil_id": null}, {"label": "B", "rids": ["u7533"], "count": 1, "dob": [], "farbe": ["Schwarz-Schecke"], "origin": ["Clan of Tamyna"], "is_own": false, "matched_gerbil_id": null}]} + +RESOLVED via rpro3-decisions.json: {"same": null, "different": [["133"], ["u7533"]], "fields": null} +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 15. [NeedsInfo] Ticket `8d2f8f12` (ID: `8d2f8f12-2767-41af-86d9-07ae67530d61`) +- **Breeder's Message**: "RennmausPro-Import: Name „Drake" kommt mehrfach vor (2 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Drake` + +### Target Gerbil Context: +- **Name**: `Drake von den kleinen Chaoten` +- **Gender**: `male` | **Status**: `Deceased` +- **DOB**: `2012-06-06` | **DOD**: `2015-09-22` +- **Genotype**: `Aa CC DD ee uw[d]uw[d] PP spsp` +- **Notes**: `Farbschlag: Polarfuchs. Behalten (b). Tod durch Alterschwäche.` +- **Import Source**: `Stammbaum von Jin.xlsx, Stammbaum von South Dakota.xlsx` +- **ExternalRef**: `stammbaum-drakekleinenchaoten-06062012` +- **Provenance History**: + - In „Stammbaum von Jin.xlsx“ gefunden. + - Geburtsdatum (06.06.2012) aus „Wurfchronik Teil 1_page_0019.md“. + - Geschlecht (männlich) aus „Wurfchronik Teil 1_page_0019.md“. + - Genotyp aus „Stammbaum von Jin.xlsx“. + - Farbschlag aus „Stammbaum von Jin.xlsx“. + - Sterbedatum (22.09.2015) aus „Stammbaum von Jin.xlsx“. + - Auch in „Wurfchronik Teil 1_page_0019.md“ gefunden → Datensätze zusammengeführt. + - ⚠ Sterbedatum 20.02.2016 aus „Wurfchronik Teil 1_page_0019.md“ verworfen — abweichend; 22.09.2015 aus „Stammbaum von Jin.xlsx“ verwendet (Mehrheit). + - Konflikt bei Sterbedatum per Mehrheitsentscheidung gelöst. + - Angaben aus der Wurfchronik übernommen. + +### AI Question Asked: +> Alles klar, danke! Ich habe notiert: Die beiden Drake-Einträge sind verschiedene Tiere und werden getrennt geführt. + +Zu deiner Frage: Ja, das erste Tier (A) — dein eigener Drake, geboren am 06.06.2012, eigene Zucht — ist bei dir im Bestand eingetragen. Da er aus 2012 stammt, führe ich ihn als dein früheres Zuchttier bzw. Vorfahre. Soll ich bei ihm zusätzlich vermerken, dass er bereits verstorben ist? Wenn ja, sag mir gern das Sterbedatum (oder einfach „nur verstorben, Datum unbekannt"). + +### Internal Agent Context: +```json +{"source": "rpro3-ambiguity", "name": "Drake", "decision": "all_distinct", "variants": [{"label": "A", "rids": ["52"], "dob": "2012-06-06", "farbe": "Polarfuchs", "origin": "eigene Zucht", "is_own": true, "matched_gerbil_id": "00f7dee7-d590-58d2-81b2-0b305049265a", "distinct": true, "in_bestand": true, "isResident": true, "note": "eigenes Tier, im Bestand, geb 2012 -> faktisch ehemaliges Zuchttier/Vorfahre"}, {"label": "B", "rids": ["u158"], "dob": "2005-12-14", "farbe": "Schwarz-Schecke", "origin": "Clan of Longfieldcity", "is_own": false, "distinct": true, "note": "externer Ahn"}], "status": "Bestätigt: alle verschieden. Bestätigt: A ist im Bestand (isResident=true). Offen: ob A als verstorben markiert werden soll + ggf. Sterbedatum."} +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 16. [NeedsInfo] Ticket `90a757b4` (ID: `90a757b4-5599-4cd9-9903-4995305f3051`) +- **Breeder's Message**: "RennmausPro-Import: Name „Mia" kommt mehrfach vor (3 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Mia` + +### Target Gerbil Context: +- **Name**: `Mia von den Kleinen Chaoten` +- **Gender**: `female` | **Status**: `Deceased` +- **DOB**: `2013-03-04` | **DOD**: `2017-04-15` +- **Genotype**: `aa c[chm]c[h] DD EE UwUw Pp spsp` +- **Notes**: `Farbschlag: Siam. Behalten. Died + AS 15.4.17.` +- **Import Source**: `Stammbaum von Jeremy.xlsx, Stammbaum von Kalea.xlsx, Stammbaum von South Dakota.xlsx, Stammbaum von Vance.xlsx` +- **ExternalRef**: `stammbaum-miakleinenchaoten-04032013` +- **Provenance History**: + - In „Stammbaum von Jeremy.xlsx“ gefunden. + - Geburtsdatum (04.03.2013) aus „Wurfchronik Teil 1_page_0022.md“. + - Geschlecht (weiblich) aus „Wurfchronik Teil 1_page_0022.md“. + - Genotyp aus „Stammbaum von Jeremy.xlsx“. + - Farbschlag aus „Wurfchronik Teil 1_page_0022.md“. + - Sterbedatum (15.04.2017) aus „Wurfchronik Teil 1_page_0022.md“. + - Auch in „Wurfchronik Teil 1_page_0022.md“ gefunden → Datensätze zusammengeführt. + - Auch in „Wurfchronik Teil 1_page_0043.md“ gefunden → Datensätze zusammengeführt. + - Eltern über Position im Stammbaum erkannt (Quelle: „Stammbaum von Jeremy.xlsx“). + - ⚠ Vater-Kandidat „Ina von den kleinen Chaoten (*14.04.2013)“ verworfen — falsches Geschlecht für die Vaterrolle; „Flippi von den kleinen Chaoten (*12.01.2011)“ verwendet. + - ⚠ Mutter-Kandidat „Flippi von den kleinen Chaoten (*12.01.2011)“ verworfen — bereits als anderer Elternteil gewählt; „Koo gen. Kyra of Golden Lights (*29.10.2012)“ verwendet. + - Angaben aus der Wurfchronik übernommen. + +### AI Question Asked: +> Ich habe Mia A (eigene Zucht, Siam) und die schwarze Mia B+C getrennt geführt. In RennmausPro ist Mia A (*04.03.2013) noch als 'aktiv' eingetragen und hat kein Sterbedatum. Da sie verstorben ist: Wann und woran ist sie gestorben? + +### Internal Agent Context: +```json +{"source": "rpro3-ambiguity", "name": "Mia", "round": 2, "decisions": {"B==C": "same animal (schwarze Mia, Fam.Wendt/Privat) -> merge B(u3600)+C(u3744)", "A": "Zuechterin fragte ob A im Bestand -> beantwortet: ja, isResident=true (Mia von den Kleinen Chaoten, #107, dob 2013-03-04, Marder-Hell, eigene Zucht). A vermutlich separates Tier von B/C; finale Bestaetigung A!=BC angefragt."}, "pending": "Bestaetigung dass A ein anderes Tier ist als B+C (dann 2 distinkte Mias: A eigen-resident, BC extern).", "variants": [{"label": "A", "rids": ["107"], "dob": ["2013-03-04"], "farbe": ["Marder-Hell"], "origin": ["eigene Zucht"], "is_own": true, "matched_gerbil_id": "0166b845-6567-5b35-bc72-19f5d8e11112", "isResident": true}, {"label": "B", "rids": ["u3600"], "farbe": ["Schwarz"], "origin": ["Fam.Wendt"], "is_own": false}, {"label": "C", "rids": ["u3744"], "farbe": ["Schwarz"], "origin": ["Privat"], "is_own": false}]} +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 17. [NeedsInfo] Ticket `be6e1446` (ID: `be6e1446-4eef-46a3-8b86-a2fc9e41a699`) +- **Breeder's Message**: "RennmausPro-Import: Name „Ben" kommt mehrfach vor (3 Varianten) — dasselbe Tier oder verschiedene?" +- **Category**: `General` | **Target Name**: `Ben` + +### Target Gerbil Context: +- **Name**: `Ben` +- **Gender**: `female` | **Status**: `Breeding` +- **DOB**: `2023-01-21` | **DOD**: `None` +- **Genotype**: `None` +- **Notes**: `None` +- **Import Source**: `Wurfchronik-Detail.docx` +- **ExternalRef**: `docx-145-ben-2023-01-21` +- **Provenance History**: + - In „Wurfchronik-Detail.docx“ gefunden. + - Geburtsdatum (21.01.2023) aus „Wurfchronik-Detail.docx“. + - Geschlecht (weiblich) aus „Wurfchronik-Detail.docx“. + - Farbschlag aus „Wurfchronik-Detail.docx“. + - Angaben aus der Wurfchronik übernommen. + +### AI Question Asked: +> Die drei Ben-Einträge bleiben getrennt. Ben B (Ben of Ben & Lulu) trägt nun die Farbe 'WFNZ Agouti'. In RennmausPro hat Ben A (*04.03.2013) noch den Status 'aktiv' und kein Sterbedatum. Da er dein ehemaliges Zuchttier ist: Wann ist er gestorben und woran? + +### Internal Agent Context: +```json +{"source": "rpro3-ambiguity", "name": "Ben", "round": 2, "decision": "all_different", "variants": [{"label": "A", "rids": ["105"], "dob": "2013-03-04", "farbe": "Colourpoint-Agouti-Hell", "origin": "eigene Zucht", "is_own": true, "parents": {"vater": "Flippi", "mutter": "Kyra"}, "offspring": ["Lighting", "Amelia", "Derek", "Alex", "Eliza"], "co_parent": "Sammy 11", "note": "Zuechterin bestaetigt alle verschieden; A ist ihr eigenes Zuchttier -> beim RennmausPro-Ahnen-Import als (ehemaliges) Zuchttier fuehren, nicht nur Vorfahre."}, {"label": "B", "rids": ["u3354", "u6923"], "dob": "2005-02-01", "farbe": "Agouti", "origin": "Clan of Ben und Lulu", "is_own": false}, {"label": "C", "rids": ["u1409"], "dob": "2005-01-01", "farbe": "Agouti", "origin": "Privat aus Holland", "is_own": false}], "status": "Zuechterin sagt: alle verschieden (getrennt fuehren). A als eigenes Zuchttier bestaetigt. Offen: evtl. Sterbedatum fuer A angefragt.", "matched_gerbil_id_note": "matched_gerbil_id 121afa38... ist fuer alle 3 Varianten identisch und zeigt in der Live-App auf ein anderes Tier (dob 2023-01-21) -> nicht als A-Residenz verwenden. A-Residenz ergibt sich aus RPRO3 (eigene Zucht + eigene Wuerfe)."} +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 18. [NeedsInfo] Ticket `955e39e6` (ID: `955e39e6-299c-4f46-b6b7-0556aa9c2673`) +- **Breeder's Message**: "Wieso wird das Tier als Zuchttier angezeigt? Du solltest die Jungtiere aus einer Verpaarung nicht automatisch als Bestandstiere (Zucht- oder Liebhabertier) aufnehmen. Ausnahme es gibt einen Stammbaum, wo steht, dass ich dies getan habe. Ansonsten solltest du diese Tiere bei den Würfen lassen, bis ich selbst sie nicht als Bestandstiere ausgewählt habe. Denn es kann auch vorkommen, dass ich Tiere aus den Würfen abgebe oder zur Abgabe stehen oder zurückbekomme. Da sollte es also eine Option geben, wo das auswählen kann. Bitte mach das für alle Tiere, die in den Würfen vorkommen, aber nicht in einem Stammbaum." +- **Category**: `General` | **Target Name**: `-` + +### Target Gerbil Context: +- **Name**: `-` +- **Gender**: `female` | **Status**: `Breeding` +- **DOB**: `2022-03-05` | **DOD**: `None` +- **Genotype**: `aa Cc[chm] D- ee[f] Gg Pp spsp` +- **Notes**: `None` +- **Import Source**: `Wurfchronik-Detail.docx` +- **ExternalRef**: `docx-74--2022-03-05` +- **Provenance History**: + - In „Wurfchronik-Detail.docx“ gefunden. + - Geburtsdatum (05.03.2022) aus „Wurfchronik-Detail.docx“. + - Geschlecht (weiblich) aus „Wurfchronik-Detail.docx“. + - Genotyp aus „Wurfchronik-Detail.docx“. + - Farbschlag aus „Wurfchronik-Detail.docx“. + - Angaben aus der Wurfchronik übernommen. + +### AI Question Asked: +> Kurze Rückfrage, bevor ich das umstelle: Diese Regel würde aktuell rund 102 Tiere betreffen, die nur aus der Wurfchronik stammen (94 davon haben einen Namen, 8 sind ohne Namen). Viele der benannten könnten ja durchaus deine echten Zuchttiere sein. Wie möchtest du es: (a) nur die namenlosen Jungtiere künftig NICHT als Zuchttier führen, oder (b) wirklich alle 102, oder (c) als „kein Zuchttier“ nur die markieren, die du schon abgegeben hast? Sag mir einfach a, b oder c. + +### Internal Agent Context: +```json +TRIAGE 2026-06-23 Lauf2: isResident-Override-Mechanismus implementiert (merge_and_resolve post-dedup, matcht externalRef oder name+dob). Allgemeine Jungtier-Regel würde 102 resident Breeding-Tiere aus Wurfchronik/docx ohne Stammbaum betreffen (94 benannt, 8 namenlos) -> zu breit für Blind-Umsetzung, Scope-Rückfrage a/b/c gestellt. Bei (a): externalRef-isResident-Overrides fuer die 8 namenlosen; bei (b): generelle Default-Aenderung in merge_and_resolve; bei (c): an ReceiverContactId/GivenAway koppeln. +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 19. [NeedsInfo] Ticket `e9790985` (ID: `e9790985-306d-4f32-94f1-a02a9469e3ad`) +- **Breeder's Message**: "Arya Stark wurde an die Zucht Clan of Black Forest abgegeben und sollte nicht mehr bei meiner Zucht angezeigt werden" +- **Category**: `General` | **Target Name**: `Arya Stark von den Kleinen Chaoten` + +### Target Gerbil Context: +- **Name**: `Arya Stark von den Kleinen Chaoten` +- **Gender**: `female` | **Status**: `Deceased` +- **DOB**: `2020-06-30` | **DOD**: `2024-12-31` +- **Genotype**: `aa c[chm]c[chm] DD Ee Gg PP spsp` +- **Notes**: `None` +- **Import Source**: `Stammbaum von Alberto Kids.xlsx, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity.xlsx, Stammbaum von Picus Son.xlsx` +- **ExternalRef**: `stammbaum-aryastarkkleinenchaoten-30062020` +- **Provenance History**: + - In „Stammbaum von Alberto Kids.xlsx“ gefunden. + - Geburtsdatum (30.06.2020) aus „Stammbaum von Alberto Kids.xlsx“. + - Geschlecht (weiblich) aus „Stammbaum von Alberto Kids.xlsx“. + - Genotyp aus „Stammbaum von Alberto Kids.xlsx“. + - Farbschlag aus „Stammbaum von Alberto Kids.xlsx“. + - Sterbedatum (31.12.2024) aus „Stammbaum von Alberto Kids.xlsx“. + - Eltern über manuelle Entscheidung erkannt (Quelle: „Stammbaum von Alberto Kids.xlsx“). + +### AI Question Asked: +> Abgabedatum an Clan of Black Forest und ob Arya dort verstorben ist (DOD 31.12.2024 stammt aus dem Stammbaum). + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- + +## 20. [NeedsInfo] Ticket `d99563fa` (ID: `d99563fa-a5cf-43da-aaa6-78bd4b677cb2`) +- **Breeder's Message**: "Falsches Foto" +- **Category**: `Foto` | **Target Name**: `Odelia von den Kleinen Chaoten` + +### Target Gerbil Context: +- **Name**: `Odelia von den Kleinen Chaoten` +- **Gender**: `female` | **Status**: `Deceased` +- **DOB**: `2015-06-21` | **DOD**: `2018-11-24` +- **Genotype**: `AA Cc[chm] D- Ee uw[d]uw[d] P- Spsp` +- **Notes**: `+ 13.05.18 zu schwach vom Entwurmen geworden. DD-Tumor seit April 18.` +- **Import Source**: `Stammbaum von Vance.xlsx, Stammbaum von Zac (Vance.Dorie).xlsx` +- **ExternalRef**: `stammbaum-odeliakleinenchaoten-21062015` +- **Provenance History**: + - In „Stammbaum von Vance.xlsx“ gefunden. + - Geburtsdatum (21.06.2015) aus „Stammbaum von Vance.xlsx“. + - Geschlecht (weiblich) aus „Wurfchronik Teil 1_page_0049.md“. + - Genotyp aus „Stammbaum von Vance.xlsx“. + - Farbschlag aus „Wurfchronik Teil 1_page_0049.md“. + - Sterbedatum (24.11.2018) aus „Stammbaum von Vance.xlsx“. + - Auch in „Stammbaum von Fire Kids.xlsx“ gefunden → Datensätze zusammengeführt. + - Auch in „Stammbaum von Stella Kids.xlsx“ gefunden → Datensätze zusammengeführt. + - Auch in „Wurfchronik Teil 1_page_0049.md“ gefunden → Datensätze zusammengeführt. + - ⚠ Sterbedatum 13.05.2018 aus „Wurfchronik Teil 1_page_0049.md“ verworfen — abweichend; 24.11.2018 aus „Stammbaum von Vance.xlsx“ verwendet (Mehrheit). + - ⚠ Genotyp AA CC DD EE gg PP Spsp rere aus „Wurfchronik Teil 1_page_0049.md“ verworfen — abweichend; AA Cc[chm] D- Ee uw[d]uw[d] P- Spsp aus „Stammbaum von Vance.xlsx“ verwendet (Mehrheit). + - Eltern über manuelle Entscheidung erkannt (Quelle: „Stammbaum von Vance.xlsx“). + - Konflikt bei Sterbedatum per Mehrheitsentscheidung gelöst. + - Konflikt bei Genotyp per Mehrheitsentscheidung gelöst. + - Angaben aus der Wurfchronik übernommen. + +### AI Question Asked: +> Auf Odelias Seite sollten unter dem Reiter "Fotos" drei Bilder liegen. Schau bitte dort einmal nach: Siehst du dort gar keine Bilder (z. B. nur graue/leere Kästchen), oder siehst du drei Bilder? Und falls du Bilder siehst: Welches davon (das 1., 2. oder 3.) zeigt wirklich Odelia? Dann kann ich die falschen entfernen und das richtige als Profilfoto setzen. + +### Internal Agent Context: +```json +Ticket: 'Falsches Foto' bei Odelia (gerbilId 3609e642-...). Befund: GET /gerbils/{id}/photos liefert 3 Fotos (sortOrder 0/1/2), alle 3 Dateien laden serverseitig sauber (HTTP 200, echte JPEG-Bytes: 16585/8305/16104 B). profilePhotoUrl = /photos/files/33ae45c2...jpeg (= sortOrder 0). Frontend rendert via API_BASE_URL (client.ts) -- gleiche Basis wie die Datenabrufe, die bei ihr offenbar funktionieren, daher kein offensichtlicher Base-URL-Bug. Ihre Antwort 'Ich sehe keine fotos' widerspricht der urspruenglichen Meldung 'Falsches Foto' und beantwortet nicht, welches Foto korrekt ist. Hypothesen: (a) sie schaut nicht im Fotos-Tab; (b) tatsaechliches Anzeige-/Ladeproblem auf dem Handy (Android Chrome, Zugriff ueber 192.168.2.124:5173). Warte auf Klarstellung: sieht sie 0 oder 3 Bilder, und welches zeigt Odelia. Wenn 'keine Bilder trotz vorhandener Dateien' -> Frontend-/Proxy-Bug auf Handy weiterverfolgen; wenn sie 3 sieht -> falsche entfernen + Profilfoto setzen. +``` + +### Triage Resolution Plan: +- [ ] **Action**: Investigate original message and check database records for appropriate override. + +--- diff --git a/docs/ticket-triage.md b/docs/ticket-triage.md index 62ba0ad..b966410 100644 --- a/docs/ticket-triage.md +++ b/docs/ticket-triage.md @@ -1,130 +1,406 @@ -# Ticket-Triage — Lauf (56 Tickets) +# Ticket-Triage — Matrix -**Ergebnis:** {'needs-implementation': 24, 'rueckfrage': 8, 'resolved-alreadyfixed': 24} +Stand: automatischer Triage-Lauf. 74 von 77 Tickets ausgewertet, 3 ohne Ergebnis (nachzutriagieren). -## Umzusetzen (needs-implementation) — nach Cluster +- Rückfrage gestellt (wartet auf Züchterin): **25** +- An Umsetzungs-Cluster übergeben: **49** +- fixLayer-Verteilung: {'import-data': 24, 'import-logic': 23, 'frontend': 7, 'needs-info': 19, 'backend': 1} +## Cluster (nach overlapKey) -### Cluster: non-resident (3) +### rpro3-uebernahme (5 Ticket(s) · import-data, import-logic, needs-info) +Dateien: GerbilManagerWebAPI/Endpoints/AcquisitionEndpoints.cs, GerbilManagerWebAPI/Import/Rpro3/Rpro3Dedup.cs, GerbilManagerWebAPI/Import/Rpro3/Rpro3ImportService.cs, GerbilManagerWebAPI/Import/Rpro3/rpro3-decisions.json, GerbilManagerWebAPI/Models/AcquisitionRecord.cs, tools/import/_rpro3.db, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py, tools/import/rpro3_lookup.py +- [Cluster] **Jana: Daten aus RennmausPro übernehmen (Herkunft Clan of little Longnoses)** (Jana, S/high) — Resolution in tools/import/conflict-decisions.json (per externalRef ...adb0649c... oder name-only "Jana") mit originBreeder="Clan of little longnoses", correctDob="07.12.2009", gender="female", genotype="aa c[chm]c[h] DD ee[f] Gg Pp spsp", farbschlag="Colourpoint-Kohlfuchsschimme + `id=4e7e86bc-c7fc-498b-be5a-1cf019a5768e` +- [Cluster] **RennmausPro-Daten + Fotos für Blacky × Kuke Nachkommen übernehmen** (Domi, L/medium) — rpro3-Übernahme im Cluster umsetzen: rids sicher zuordnen (Blacky=1, Kuke=2, Alvin=3, Eddy=34, Flori=143, Feivel per Namen noch suchen), rpro3-decisions.json um Merge/Farbe/Genotyp/DOB/Herkunft ergänzen, Kruke/Kuke-Namensmerge klären, prüfen welche DB-Tiere den 4 rpro3-Kindern en + `id=ddacd093-b3f4-419f-911d-39ef9e746ae3` +- [Cluster] **RennmausPro-Gesundheitsdaten und Gewichte für Blacky (und alle Tiere) übernehmen** (Blacky von PZ Seligenstadt, L/medium) — Im rpro3-Import (Rpro3ImportService.cs, BuildHealthAndWeights / GerbilIdOfRid) die Weights/Health/Fotos auf das per Identität gematchte, bereits existierende Stammbaum-Gerbil abbilden statt auf ein neues rpro3-Gerbil (Reconciliation rid↔vorhandene Gerbil-Id, analog Ahnen-Merge f3 + `id=2bc311df-2f1e-400c-8fa0-2468cc9c9d9b` +- [Cluster] **Blacky von PZ Seligenstadt: Erwerbs- und Todesdaten aus RennerPro übernehmen** (Blacky von PZ Seligenstadt, S/high) — (1) Erwerb: AcquisitionRecord für GerbilId 9b40b9e5… anlegen (Date 2009-09-01, Price 9.50, Note „aus RennmausPro III“) via POST /acquisitions – FK-frei und re-ingest-stabil, per Design aus rpro3 herktier_tb. (2) Todesdatum: 2011-07-13 → 2011-06-13 korrigieren (causeOfDeath option + `id=267474c8-bd1d-47e7-8c1f-4250ebfbf2cb` +- [RÜCKFRAGE] **Christinas Eltern/Stammbaum aus RennmausPro verknüpfen** (Christina, M/medium) — Nach Bestätigung der Züchterin: Ist es „Cristina" (fremd_tb id=6398, Eltern MID=125/PID=166), Eltern über den RennmausPro-Ahnen-Import bzw. per conflict-decisions.json (name+dob) re-ingest-stabil setzen; andernfalls die von ihr genannten Eltern-Namen als Verknüpfung eintragen. + `id=a78eaa79-db55-4da1-99b0-ed954f9b07c7` -- **09bcac78-d125-4f3d-9070-809980d2c44e** [import-logic] — Bentley/Alexandria faelschlich als Zuchttiere – Residenz-Regel ueber Nachzucht-Herkunft - - Empfehlung: import-logic in tools/import/merge_and_resolve.py: Residenz-Erstmarkierung umbauen. Statt ein Tier resident zu setzen, weil es SELBST den Clan-Namen traegt (Z.1568-1571 is_clan_zucht auf a.zucht), ein Tier nur dann resident markieren, wenn es Nachzucht hat, deren Herkunft/zucht is_clan_zucht erfuellt (Clan kleine Chaoten, nicht 'extern'). Eltern-Propagation (Z.1573-1586) mit is_external_cattery-Ausschluss beibehalten. Dadurch fallen reine Vorfahren-Boxen (Bentley, Alexandria, Bugatti) auf isResident=false, echte Zuchttiere mit Kleine-Chaoten-Nachzucht bleiben true. Wurfchronik-Importpfad (Z.2052ff, IsResident default True) separat pruefen. Danach test_merge_resolve.py gruen halten (#17/#20 Hagrid weiter false), merge_and_resolve + POST /import/ingest-resolved. Gilt fuer ALLE Tiere ('Mach das bitte fuer alle Tiere'). -- **45cc501b-be83-4b2c-9aa3-e8fdc229b367** [import-logic] — Alexandria — externer Vorfahre fälschlich als eigenes Zuchttier angezeigt - - Empfehlung: (1) import-logic: in tools/import/merge_and_resolve.py ~Z.1532-1536 die Residency-Propagation so einschränken, dass reine Vorfahren/externe Ahnen NICHT isResident=true (und nicht originBreeder="Clan kleine Chaoten") erben; danach Re-Ingest via POST /import/ingest-resolved. (2) frontend: in gerbil-manager-web/src/pages/GerbilDetailPage.tsx Bestands-spezifische Abschnitte (Würfe ~Z.81-82/432-455, Charakter/Eigenschaften) für isResident=false ausblenden, sodass für reine Vorfahren nur die Stammbaum-relevanten Basisdaten erscheinen. Mit Cluster non-resident-display bündeln (gemeinsamer Fix mit Hagrid). -- **955e39e6-299c-4f46-b6b7-0556aa9c2673** [import-logic] — Wurf-Jungtiere ohne Stammbaum nicht automatisch als Bestandstier (Zuchttier) anzeigen +### jungtier-reihenfolge (4 Ticket(s) · frontend, import-logic, needs-info) +Dateien: EF-Migration, GerbilManagerWebAPI/Dtos/ApiDtos.cs, GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs, GerbilManagerWebAPI/Migrations, GerbilManagerWebAPI/Models/Gerbil.cs, GerbilManagerWebAPI/Models/Gerbil.cs (neues Order-Feld + Migration), gerbil-manager-web/src/api/types.ts, gerbil-manager-web/src/pages/WurfDetailPage.tsx, tools/import/extract.py, tools/import/merge_and_resolve.py +- [Cluster] **Jungtier-Reihenfolge im Wurf ist alphabetisch statt gewünscht (E-Wurf 2010: Eliza, Eddy)** (E-Wurf 2010, M/medium) — Generalisierbarer Fix: persistiertes, re-ingest-stabiles Order-Feld pro Jungtier (FK-frei analog GerbilOverride, da Import-Tiere) + manuelles Umsortieren in der Wurf-Akte; WurfDetailPage nach diesem Feld statt nach name sortieren. Alternativ Kind-Reihenfolge im Importer (merge_an + `id=573aad81-157e-47fe-8693-c1a4b685222b` +- [RÜCKFRAGE] **Jungtiere im Wurf falsch sortiert (alphabetisch statt Wurfchronik-Reihenfolge)** (C-Wurf 2010, L/medium) — Reihenfolge bei der Zuechterin bestaetigen (Chronik-Listenreihenfolge). Danach Import um ein Kinder-Ordinal je Gerbil erweitern, Backend-Feld plus Migration ergaenzen, WurfDetailPage orderBy von name auf das Ordinal umstellen. + `id=399dbeb2-6909-43fe-b00f-7b6a2b9fc7b2` +- [Cluster] **O-Wurf: Jungtiere alphabetisch statt in Geburtsreihenfolge** (O-Wurf, L/high) — Ein persistiertes int-Reihenfolgefeld (litterOrder/birthOrder) einfuehren: im Import (extract.py/merge_and_resolve.py) aus der Wurfchronik-Zeilenreihenfolge deterministisch je Kind setzen (re-ingest-stabil), am Gerbil-Model + Migration + Summary-DTO ausgeben, und in WurfDetailPag + `id=8ff0ebe6-159a-49ce-80ad-23c100c21056` +- [Cluster] **Jungtier-Reihenfolge im F-Wurf 2010 alphabetisch statt in Wurfchronik-Reihenfolge** (F-Wurf 2010, L/medium) — Wurfchronik-Kind-Reihenfolge im Import (extract.py/merge_and_resolve.py) als sortierbaren Index je Jungtier erfassen und persistieren (neues nullable Feld z. B. BirthOrder/LitterSequence auf Gerbil + EF-Migration), beim Merge den Chronik-Index auch für mitgemergte Stammbaum-Tiere + `id=fa73d0db-53b7-4cc5-ac2a-6cd72dafab4e` -### Cluster: wrong-parents (1) +### extern-badge (3 Ticket(s) · import-data) +Dateien: gerbil-manager-web/src/pages/GerbilDetailPage.tsx, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Chris: falsches "Extern"-Badge entfernen (isResident=true setzen)** (Chris, S/high) — In tools/import/conflict-decisions.json eine resolution {"name": "Chris", "dob": "08.05.2010", "isResident": true} ergänzen (Override gewinnt im Sweep, analog Echo/Elia), dann merge_and_resolve.py neu laufen lassen + POST /import/ingest-resolved. Alternativ per UI ein GerbilOverr + `id=73d99dc4-d95c-49a1-b826-ceb359d8f6cf` +- [Cluster] **Extern-Badge bei Kruke entfernen (isResident auf true)** (Kruke, S/high) — In tools/import/conflict-decisions.json den Kruke-addAnimals-Eintrag von isResident:false auf isResident:true ändern (expliziter Override gewinnt im Residenz-Sweep von merge_and_resolve.py immer), Pipeline neu laufen lassen und POST /import/ingest-resolved. Dann verschwindet das + `id=34220307-23bc-420c-80f8-d273615caffd` +- [Cluster] **Krukes Nachzuchten fehlen — fälschlich als nicht-resident (extern) eingestuft** (Kruke, S/high) — In tools/import/conflict-decisions.json den Kruke-Eintrag isResident von false auf true setzen (bzw. den false-Override entfernen — der Sweep macht sie als Elternteil eigener Würfe ohnehin resident) und neu importieren. Domänenkorrekt, da ihre Nachzucht den Clan-Namen 'von den kl + `id=b7016e5f-9985-4f17-a3f6-2048289958aa` -- **88389f8e-8b8b-44c3-940b-93aa9e571c29** [import-data] — Akane: Roni ist Vater (nicht Mutter), Mutter = Fumi von den Kleinen Chaoten - - Empfehlung: In tools/import/conflict-decisions.json: (a) gender-Override fuer Roni (*27.01.2022) auf male (wie Mozart-Eintrag), (b) 'Fumi von den Kleinen Chaoten' (weiblich) als neuen Mutter-Datensatz anlegen (analog #22 Louis/#31 Danny/#27 Hanau), (c) parent-Override fuer Akane/Wurf Z21: father='Roni', mother='Fumi von den Kleinen Chaoten' (father/mother-Mechanik wie #16/#23/#36). merge_and_resolve.py muss gender-Override beruecksichtigen (assign_parent_roles/pick_parent_ref ~Z.640/679) und benannte fehlende Eltern anlegen koennen. Zusaetzlich pruefen: Roni ist Mutter aller drei Wuerfe T21 (3d510e7c) und 22-Wurf (947c6852) -> dort ebenfalls Vater-Rolle setzen; deren Mutter ist aber unbekannt (ggf. spaeter rueckfragen). Danach extract + merge_and_resolve + POST /import/ingest-resolved. +### rpro3-eltern-ergaenzen (2 Ticket(s) · import-data) +Dateien: Stammbaum-Viewer (Danny-Akte), tools/import/_rpro3.db, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Dannys Eltern (Daniel × Daniela) aus RennmausPro ergänzen** (Danny von Maintaler PZ, S/high) — Eltern per Hiro-Präzedenz (Commit bbd3058) in tools/import/conflict-decisions.json ergänzen: resolutions-Eintrag auf Danny (dob 09.10.2009) mit father="Daniel"/mother="Daniela" plus addAnimals-Stubs Daniel (male, Marder-Hell, aa c[chm]c[h] DD EE GG PP spsp, Herkunft Maintaler Pri + `id=32d57d3d-8bc3-406b-9844-965f7071ded0` +- [Cluster] **Blacky umbenennen + fehlende Eltern (Snoopy und Bella) aus RennmausPro ergänzen** (Blacky von PZ Seligenstadt, S/high) — In tools/import/conflict-decisions.json (Muster Hiro/Commit bbd3058): resolutions-Eintrag für Blacky (Match via externalRef stammbaum-blackypzseligenstadt-23072009) mit renameTo="Blacky", father="Snoopy", mother="Bella"; dazu addAnimals-Stubs {Snoopy, male} und {Bella, female} al + `id=cf6e2c99-746a-46fb-93e2-8ec81ae55663` -### Cluster: parents (3) +### resident-flag (1 Ticket(s) · import-data) +Dateien: gerbil-manager-web/src/pages/GerbilDetailPage.tsx (extern-Badge), tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Philo als Zuchttier/Bestandstier markieren (isResident)** (Philo, S/high) — Expliziten isResident=true-Override für Philo in tools/import/conflict-decisions.json aufnehmen (Resolution per name='Philo' + dob=2011-06-11). Ein expliziter isResident-Override gewinnt immer (vgl. Echo/Elia), ist re-ingest-stabil. Danach merge_and_resolve.py neu laufen + POST / + `id=58bb53f2-280c-4676-9b79-f58497be995c` -- **36a3fcde-d14a-46e1-936f-2263863f8207** [import-data] — Akane: Lebenslauf (Zuchtaustausch Black Forest, Tod bei Ulrike Neu) + Wurf Merle/Fanella/Pete fehlt - - Empfehlung: Manuelle Daten-Korrektur/Override für Akane (e087fb7d-7d13-5733-856e-e8cf4277fc7c): status=Deceased, isResident=false, dateOfDeath/causeOfDeath setzen (verstorben bei Ulrike Neu, Kontakt 33130df1-e164-54ae-be74-a9b7d2ced11b), receiverContactId=Ulrike Neu; Abgabe-Historie: 20.04.23 als Zuchttier (0EUR, Austausch) an Clan of Black Forest (a13f62c9-d506-50a6-99f8-65e85e955f96), Rückkehr 01.04.24, dann 02.04.24 an Ulrike Neu. Gewicht 79,1g (01.04.24) für Akane anlegen. Neuen Wurf bei Clan of Black Forest mit Mutter=Akane anlegen: Töchter Merle (62,1g) und Fanella (CP-Agouti Punktschnecke, 62,7g) sowie Sohn Pete (Farbe wie Merle); Fanella ging mit Akane an Ulrike Neu. Da Quelle nur Freitext (Z21-Wurf-Notiz) ist, als manuellen Override/Import-Patch umsetzen, nicht aus bestehender Importquelle ableitbar. -- **0ba551a3-3be7-4042-8ab7-669a270602f1** [import-logic] — Sunny von PZ Karl — falscher Vater (Hiro statt Bill von Privat) - - Empfehlung: In tools/import/conflict-decisions.json eine resolution ergänzen: {"name":"Sunny von PZ Karl","dob":"10.04.2014","father":"Bill von Privat","mother":"Melly von Privat","decision":"Eltern aus Renner-Pro-3/Yurikas-Stammbaum","source":"Züchterin 2026-06-22 Ticket 0ba551a3"}. apply_conflict_decisions (tools/import/extract.py:1149-1167) setzt daraus parentRefs mit method=decision/high und überschreibt den falschen chart-position-Vater Hiro. Danach Pipeline neu: python tools/import/extract.py → merge_and_resolve.py → POST http://localhost:5179/import/ingest-resolved. Virtueller Wurf 8f1bb1d2 wird nur von Sunny genutzt, Änderung ist isoliert. Beim Schließen Changelog: "Bei Sunny von PZ Karl war der Vater falsch (stand Hiro of Golden Lights). Korrekt sind jetzt Vater Bill von Privat und Mutter Melly von Privat. Bitte den Stammbaum neu laden." -- **f77aa4a6-ecb5-4e29-960e-bdd22d327e19** [import-data] — Danielle: Louis+Roswitha sind Großeltern, echte Mutter ist Ella (Geschwisterverpaarung) - - Empfehlung: conflict-decisions.json-Eintrag für name='Danielle von den Kleinen Chaoten', dob='04.03.2020', mother='Ella' — disambiguiert per Ellas dob 10.06.2019 (Tochter von Louis+Roswitha, ebc87c8c), NICHT die zweite Ella *13.04.2023. Vater nicht setzen (unbekannter Bruder). Da zwei gleichnamige Ella existieren, muss der Eltern-Override in merge_and_resolve.py die richtige per Geburtsdatum/ID wählen (ggf. motherDob/motherId-Feld ergänzen). Danach merge_and_resolve.py + POST /import/ingest-resolved. Roswitha rückt damit auf Großeltern-Ebene. Schwester-Ticket #21 (4692fd5c) betrifft denselben Wurf — gebündelt mit demselben Override schließen. +### danny-dublette-maintal (1 Ticket(s) · import-data) +Dateien: tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Zwei Dannys (Maintaler PZ / PZ Maintal) sind dasselbe Tier — zusammenführen** (Danny von Maintaler PZ, S/high) — Merge-/Alias-Entscheidung in tools/import/conflict-decisions.json ergänzen, damit beide Namensvarianten zu einem Tier deduppen (Ziel-Id 55ce13fc beibehalten). Genotyp-Konflikt beim Merge klären (55ce13fc: "...EE Gg Pp Spsp DP" vs 4756dac8: "...EE Uwuw[d] Pp Spsp DP"). Danach extr + `id=6f521e7c-753c-4947-b8e3-7fe81dba8b12` -### Cluster: genetics-schecke-spsp (1) +### todesursache-nachtrag (1 Ticket(s) · import-logic) +Dateien: tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Todesursache für Danny von Maintaler PZ nachtragen** (Danny von Maintaler PZ, S/high) — Importer-Weg (re-ingest-stabil): Override-Block in merge_and_resolve.py (~Z. 3204-3309) um ein Feld deathCause/causeOfDeath erweitern (analog notes/originBreeder), das g["CauseOfDeath"] setzt und in has_ovr aufgenommen wird; dann Decision-Eintrag in conflict-decisions.json per ex + `id=42782b23-784f-4125-8002-c372de244fdf` -- **e09d6f22-2658-435e-8ef3-7453e366ee65** [import-logic] — Namenloses Tier (*13.08.2025): faelschlich als Schecke berechnet, soll spsp sein - - Empfehlung: Sp-Locus dieses Tieres auf spsp korrigieren (Soll-Genotyp "aa Cc[chm] D- ee[f] Gg Pp spsp", Anzeige ohne "Schecke"). Mechanik: merge_and_resolve.py:471-485 schreibt bei is_schecke den Genotyp auf Spsp um; entweder clean_color_name()-Schecke-Erkennung fuer diese Zeile korrigieren ODER den Quell-Genotyp-Sp-Locus nicht durch das Farblabel ueberschreiben. Wichtig: Standard-Override conflict-decisions.json matcht per name+dob (extract.py:1088-1133), aber das Tier ist NAMENLOS (externalRef stammbaum-unbekannt-13082025-3) -> name+dob nicht eindeutig; daher Override-Schluessel um externalRef erweitern oder die Heuristik fixen. Danach extract -> merge_and_resolve -> POST /import/ingest-resolved. +### nachzucht-link-route (1 Ticket(s) · frontend) +Dateien: gerbil-manager-web/src/pages/GerbilDetailPage.tsx +- [Cluster] **Nachzucht-Links auf der Tier-Akte tot (falsche Route /tiere/)** (Danny von Maintaler PZ, S/high) — In gerbil-manager-web/src/pages/GerbilDetailPage.tsx Zeile 687 den Link-Pfad von `/tiere/${child.id}` auf `/rennmaeuse/${child.id}` ändern (Einzeiler). Danach npx tsc --noEmit und die Nachzucht-e2e-Specs prüfen; keine Datenänderung, kein Re-Ingest nötig. + `id=20a6cc0e-3798-4f3c-8452-efb32ce6dc84` -### Cluster: genetics-farbschlag (3) +### chris-genotyp (1 Ticket(s) · import-data) +Dateien: tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Chris: Gencode korrigieren (Punktschecke)** (Chris, S/high) — Resolution in tools/import/conflict-decisions.json für Chris ergänzen (Match name "Chris" + dob 08.05.2010) mit "genotype":"aa Cc[-] DD Ee gg P- Spsp" (optional "farbschlag":"Punktschecke"), dann merge_and_resolve.py + POST /import/ingest-resolved. R-Locus bleibt wie in bestehend + `id=d8debbb8-5e82-4e5f-8313-209c6caae0d4` -- **5826e8e2-bc83-4572-a09d-9b51ac6e59e9** [genetics-engine] — Genetik: ef/e (Fuchsschimmel) wird als Rotaugenschimmel statt Goldfuchsschimmel errechnet - - Empfehlung: Katalog-Matching in catalog.ts E-Locus-zygotie-bewusst machen: ef/ef -> Schimmel-Eintraege (Rotaugenschimmel/Silberschimmel/Orangeschimmel), ef/e -> Fuchsschimmel-Eintraege (Goldfuchsschimmel etc.). Konkret ein Fuchsschimmel-Token (oder eFamily-Nutzung) einfuehren, sodass Schimmel-Basiseintraege bei ef/e nicht mehr greifen. Tests in genetics.test.ts erweitern: efef pp -> Rotaugenschimmel bleibt; ef/e (ee[f]) + A_ + pp -> Goldfuchsschimmel. Backend-Farbschlag-Mirror und tools/import/genotype.py mitziehen. Separat (import-data): gespeichertes colorVarietyId='Gold' ist ebenfalls falsch importiert. -- **1aac054f-3798-45af-868d-8f905bc90ca1** [import-data] — Namenloses Weibchen (*13.08.2025): gespeicherter Farbschlag "Gold" falsch — korrekt ist berechneter "Kohlfuchsschimmel Schecke" - - Empfehlung: Gespeicherten Farbschlag des Tieres 8bc0445e von 'Gold' auf den berechneten Wert 'Kohlfuchsschimmel' korrigieren (Schecke ist Modifier). Quelle des Fehlers: Import las 'Farbschlag' aus 'Stammbaum von Martin.xlsx'. Mechanik: conflict-decisions.json hat ein farbschlag-Override (Match per name+dob), aber das Tier ist namenlos — Override-Key fuer namenlose Tiere pruefen (ggf. ueber externalRef 'stammbaum-unbekannt-13082025-2'). Sauberere Alternative: colorVarietyId bei vorhandenem Genotyp aus genotypeToFarbschlag denormalisiert ableiten. Betroffen: tools/import/merge_and_resolve.py, tools/import/conflict-decisions.json, Referenz gerbil-manager-web/src/genetics/catalog.ts. -- **998087e2-1265-41a3-a7d5-13ab11576325** [import-logic] — Farbschlag falsch (Agouti statt Dilute Agouti) bei namenlosem dd-Weibchen - - Empfehlung: resolve_color_and_genotype (tools/import/merge_and_resolve.py:455-486) leitet colorVarietyId namensbasiert aus der Quell-Farbschlag-Zelle ab (clean_color_name 400-453, Mapping "a"->"agouti"), nicht aus dem Genotyp - deshalb wird das dd (Dilute) ignoriert. Empfehlung: Wenn ein Genotyp vorhanden ist, colorVarietyId aus dem genotyp-berechneten Farbschlag ableiten (Port der TS-Logik aus gerbil-manager-web/src/genetics/catalog.ts genotypeToFarbschlag) und bei Konflikt Genotyp gewinnen lassen + Provenance-Hinweis. Mindestens fuer dieses Tier colorVarietyId auf 00000000-0000-0000-0000-000000000029 (Dilute Agouti) setzen. Danach merge_and_resolve.py neu laufen + POST /import/ingest-resolved. Gleiche Klasse wie Cluster genetics-farbschlag (#3, #24). +### wurf-foto (1 Ticket(s) · needs-info) +Dateien: GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs, GerbilManagerWebAPI/Models/Litter.cs, gerbil-manager-web/src/pages/WurfDetailPage.tsx, tools/import/extract.py +- [RÜCKFRAGE] **Foto des N-Wurfs aus der Wurfchronik anzeigen — nicht vorhanden/kein Feature** (N-Wurf, L/high) — Zunächst per Rückfrage klären, ob die Züchterin irgendwo ein Wurf-Foto gespeichert hat (Ordner/Datei) oder die Jungtier-Einzelfotos meint. Falls Wurf-Fotos gewünscht: Feature bauen (Litter-Foto-Feld + Migration + Upload/Import + UI-Anzeige auf WurfDetailPage). Kein Importer-Daten + `id=97a3b4d9-4859-4b0a-8e20-2c602ec2c167` -### Cluster: color-scheme (3) +### rpro3-ahnen-uebernahme (1 Ticket(s) · import-data) +Dateien: tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Stammbaum von Malina of minor diabolus aus RennerPro ergänzen** (Malina of minor diabolus, M/high) — Eltern re-ingest-stabil im Importer materialisieren: conflict-decisions.json addAnimals für Big Donat (externer Ahn, DOB 2008-05-09, Clan of Amazing (CH), isResident=false) und Skarlett B (DOB 2009-03-03, Sunny's Sun, isResident=false) + addLitters {mother:Skarlett mit motherDob + `id=ed0eef40-3ccd-4c36-899c-0cf274bab8a9` -- **3f5942a2-60c4-4353-809a-77ee6071dd03** [import-logic] — Goldfuchs (ee) falsch als "Gold" (EE) klassifiziert - - Empfehlung: In tools/import/merge_and_resolve.py, resolve_color_and_genotype() (Z.464-468): Substring-Fallback nicht beim ersten Treffer abbrechen. Stattdessen exakte Treffer priorisieren und unter Substring-Kandidaten den längsten/spezifischsten wählen (z.B. Kandidaten sammeln, nach Länge absteigend sortieren). So wird "Goldfuchs" statt "Gold" gematcht. Danach extract.py + merge_and_resolve.py neu laufen lassen und POST /import/ingest-resolved. Erwartet korrigiert: Jill (235c6a7fedde), 2e7efb127308, bf6f4507 (98f77953d2d1), Banjo of Fiomi (b0e0081ce25d), Christine/Chrissi (c3995ebd0d15) — alle Fuchs-Genotyp ee, aktuell Farbschlag Gold/0012, sollen Goldfuchs/0014 (bzw. passende Fuchs-Variante) werden. -- **06217eb3-7521-4533-ab3d-4bb425f08aa5** [import-logic] — Dilute Anthrazit (ohne Namen, w) — gespeicherter Farbschlag "Anthrazit" statt Dilute Anthrazit - - Empfehlung: Punktfix: colorVarietyId des Tiers aa7978ed von 00000000-0000-0000-0000-000000000009 (Anthrazit) auf 00000000-0000-0000-0000-000000000032 (Dilute Anthrazit) setzen. Systemisch: resolve_color_and_genotype in tools/import/merge_and_resolve.py so erweitern, dass bei vorhandenem Genotyp der genotyp-abgeleitete Farbschlag Vorrang/Plausibilitaetspruefung gegenueber dem reinen Quell-Textlabel hat (insb. dd=Dilute vs. nicht-dilute Label), dann Import neu generieren + POST /import/ingest-resolved. Engine (catalog.ts/phenotype.ts) ist korrekt und nicht zu aendern. -- **3deab547-ece1-4de5-8989-f90c79580b7e** [genetics-engine] — Farbschlag: Dilute CP-Blaufuchs wird als "Unbekannt"/"blau" angezeigt - - Empfehlung: In gerbil-manager-web/src/genetics/catalog.ts den Eintrag 'Dilute Blaufuchs' (Zeile 116) von tokens.P:'p' auf tokens.P:'P' aendern (Dilution dd ist unabhaengig vom Augenpigment-Locus P; das Basis-Blaufuchs Z.75 ist bereits P). Danach resolved die Engine den gespeicherten Genotyp aa c[chm]c[chm] dd ee gg P- korrekt zu 'Dilute CP-Blaufuchs (Schecke)' statt 'Unbekannter Farbschlag'. Zusaetzlich pruefen, ob weitere Dilute-*-Eintraege faelschlich P:'p' tragen (z.B. 'Dilute Topas' Z.115) und ggf. mit ihren Nicht-Dilute-Basisfarben abgleichen. Verifiziert per vitest-Patch. +### totgeburt-tg-tracking (1 Ticket(s) · import-logic) +Dateien: GerbilManagerWebAPI/Dtos/ApiDtos.cs, GerbilManagerWebAPI/Models/Litter.cs, gerbil-manager-web/src/api/types.ts, gerbil-manager-web/src/pages/WurfDetailPage.tsx, gerbil-manager-web/src/pages/WurfFormPage.tsx, gerbil-manager-web/src/strings/de.ts, tools/import/merge_and_resolve.py +- [Cluster] **C-Wurf 2010: Totgeburt (TG) wird nicht angezeigt — Wurfchronik-Mortalität geht im Merge verloren** (C-Wurf 2010, M/high) — Full-Stack, re-ingest-stabil: (1) neues Litter-Feld Stillborn (int?, "Totgeburten") + EF-Migration + DTO + api/types.ts; (2) in merge_and_resolve.py stillborn→Stillborn und diedLater→DeathsWithin8Weeks aus dem Extrakt übernehmen (Fallback via survivedToGoHome); (3) WurfDetailPage + `id=f4935e41-3ec9-4a9b-895f-6a817e63d8b3` -### Cluster: genetics-farbschlag-schimmel-klammer (1) +### abnehmer-zeile-akte (1 Ticket(s) · frontend) +Dateien: gerbil-manager-web/src/pages/GerbilDetailPage.tsx +- [Cluster] **Abnehmer-Zeile fehlt bei verstorbenen Tieren auf der Tier-Akte** (Momo, S/high) — In GerbilDetailPage.tsx Zeile 510 den Deceased-Guard entfernen und die Abnehmer-Zeile statusunabhängig zeigen, wenn ein receiverContactId vorliegt (z. B. `{g.receiverContactId && ()}`). Reine UI-Änderung, Daten sind korrekt; WurfDetailPage zeig + `id=f628ae14-0edb-4120-a84f-0da1fe054b0a` -- **e22764aa-9801-4e02-83c7-5b763652973a** [import-logic] — Blaufuchsschimmel statt Blaufuchs (Klammer-Schimmel = "moeglich, unbekannt") - - Empfehlung: In tools/import/merge_and_resolve.py clean_color_name (Z.412-415) die parenthetische Variante "(schimmel)" NICHT mehr zu definitivem "schimmel" anhaengen, sondern strippen und als "Schimmel moeglich/unbestimmt" fuehren -> Farbschlag auf Blaufuchs (Variety ...020) aufloesen statt Blaufuchsschimmel (...037); den Moeglich-Schimmel-Hinweis separat (Provenance/Flag) ablegen, damit die App "Blaufuchs (Schimmel moeglich, klaert sich mit ~2 J)" zeigen kann. Konsistent mit Genotyp ee[-] (Fuchs, Schimmel-Modifier unbekannt). Betrifft nur dieses 1 Tier (einziges mit (schimmel)-Klammer im Quelldatensatz). Danach extract/merge_and_resolve neu + POST /import/ingest-resolved. Abgrenzung: NICHT Teil von #42 (Engine ee[-]->ee) oder #37-41 (Punnett-Wildcard) - hier Import-Farbnamen-Aufloesung. +### rennmauspro-eltern-ergaenzen (1 Ticket(s) · import-data) +Dateien: Gerbil Charlie (7d301ab5-6043-5d6f-914b-7550f5b0154e), Gerbil Dante (93457716-65d2-5432-a52a-553ae873ca6d), Wurf C-Wurf 2010 (f1fece40-f31a-5757-b89c-402fa47abbd4), tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Charlie: Mutter Evy fehlt (RennmausPro-Ahnin nicht importiert)** (Charlie von den Kleinen Chaoten, S/high) — Wie Commit bbd3058 (Hiros Eltern): in conflict-decisions.json Evy als nicht-residenten Ahnen-Stub via addAnimals {name:"Evy", gender:female, dob:"20.09.2008"} anlegen und je Kind (Charlie + Dante, dob 08.05.2010) eine resolution mit mother:"Evy", motherDob:"20.09.2008" setzen. Da + `id=a419a933-83e1-4383-bf29-6af8c504ae6b` -### Cluster: genotype-engine (4) +### wurfchronik-ocr-mutter-fehlname (1 Ticket(s) · import-logic) +Dateien: Wurfchronik_Bilder/Wurfchronik Teil 1_page_0017.md, gerbil-manager-web WurfDetailPage (Scan-Beleg), tools/import/conflict-decisions.json, tools/import/extract.py, tools/import/merge_and_resolve.py +- [Cluster] **Z-Wurf: falsche Mutter "Macy" ist OCR-Fehllesung von "Mary"** (Z-Wurf, M/high) — Z-Wurf-motherId und die 4 Jungtiere (Zeus/Pina/Flummy/Lea) auf 6ec4527c (Mary v. PZ Seligenstadt) umbiegen und das synthetische "Macy" (516752f8) aufloesen/mergen. Re-ingest-stabil umsetzen: entweder Namenskorrektur in der Wurfchronik-Extraktion (page_0017 "Macy"->"Mary", damit D + `id=f42b6215-0896-4352-ae24-572829e6b42e` -- **cc9ea3fe-db44-46e3-85b6-ab9f34c4433f** [genetics-engine] — Mamta Mini: E-Locus Ee[-] statt Ee (Vererbung vom ee-Vater Geely) - - Empfehlung: Eltern-basierte Allel-Inferenz/Validierung in der Genetik-Engine ergaenzen: pro Locus pruefen, ob ein Elternteil homozygot ist und damit ein Kind-Allel erzwingt; unbekannte Allele ([-]/?) entsprechend aufloesen. Konkret Mamta Mini (da911047) E-Locus 'Ee[-]' -> 'Ee', weil Vater Geely (b71c0fda) homozygot 'ee' ist und nur 'e' vererben kann. Implementieren in gerbil-manager-web/src/genetics (genotype.ts/breed.ts/punnett.ts/warnings.ts) als resolveFromParents/validateAgainstParents mit Hinweis-Chip in der Tier-Akte; optional zusaetzlich im Import (tools/import/genotype.py, merge_and_resolve.py) den gespeicherten Genotyp vervollstaendigen. Vorsicht: Mamta Minis Wurf 23-Wurf (b4b6ffcf) hat derzeit keine verlinkten Eltern in der DB - Inferenz braucht die korrekte Eltern-Verknuepfung. Identisch zu #38 (1a508c04), gebuendelt umsetzen. -- **efa2b232-71f9-4fac-aba6-9100a3d6037e** [genetics-engine] — Farbschlag-Berechnung (Genetik-Engine): aa cchm dd ee gg → Dilute CP-Blaufuchs - - Empfehlung: Genetik-Engine/-Katalog (gerbil-manager-web/src/genetics/catalog.ts): (1) Im aa-Colourpoint-Zweig von colourpointName bzw. in BASE_COLORS eine aa-dd-ee-gg-Fuchs-Basis fuer schwarzaeugige Tiere (P) ergaenzen, sodass aa cchm dd ee gg → "Dilute CP-Blaufuchs" statt "Dilute CP-Anthrazit" erzeugt wird. (2) Unbekanntes E-Allel (ee[-]) so behandeln, dass die Fuchs-Familie nicht durch Default-auf-dominantes-E verdeckt wird (phenotype.ts/loci.ts). Zielwert "Dilute CP-Blaufuchs" stammt aus der Antwort der Zuechterin im Schwester-Ticket 3deab547 — keine neue Rueckfrage noetig. -- **1a508c04-018b-4b93-bf7f-f820fbe9433e** [genetics-engine] — Mamta Mini: E-Locus Ee[-] muss per Vererbung zu Ee aufgeloest werden - - Empfehlung: Eltern-basierte Allel-Inferenz/Validierung in der Genetics-Engine ergaenzen (z.B. resolveFromParents/validateAgainstParents): pro Locus pruefen, ob ein Elternteil homozygot ist und damit ein Kind-Allel erzwingt; unbekannte Allele ([-]/-) entsprechend aufloesen (Ee[-] -> Ee, da Geely homozygot ee). In der Tier-Akte das aufgeloeste Allel + Hinweis-Chip anzeigen. Eltern-Genotypen ueber den Stammbaum/Pedigree beziehen (Mamta Minis gespeicherte litter b4b6ffcf hat keine father/motherId; Geely+Gaida kommen aus der chart-position-Heuristik). Optional schon im Import (tools/import/genotype.py, merge_and_resolve.py) anwenden. Betroffen: gerbil-manager-web/src/genetics/genotype.ts (Ee[-]-Notation, Z.190-275), breed.ts, punnett.ts, warnings.ts. SEPARAT halten von den Wildcard-Expansions-Tickets #37/#39/#41 (punnett.ts parentAlleleWeights, erwartete Wurf-Farbschlaege) - hier geht es um Rueck-Inferenz eines GESPEICHERTEN Genotyps. -- **3e643ef1-6998-45c2-a3e1-45dff666a7c5** [genetics-engine] — Genetik-Engine: Wildcard-Allel (D-/E-) erzeugt unmoegliche Dilute-/Unbekannt-Farbschlaege im Wurf - - Empfehlung: In punnett.ts parentAlleleWeights: ein unbekanntes Allel '?' (aus '-') neben einem bekannten Allel als KOPIE des bekannten dominanten Allels behandeln (A-=AA, D-=DD, E-=EE), statt es ueber unknownPartnerOptions auf gleich-/rezessivere Allele zu streuen. Nur wenn beide Allele unbekannt sind -> Locus voll unbestimmt (dominantestes Allel, konsistent mit phenotype.ts/catalog.ts resolvedPair). Damit fallen Dilute (dd), efef und 'Unbekannter Farbschlag' aus dieser Verpaarung weg. Tests in gerbil-manager-web/src/genetics/__tests__ anpassen. Betroffen: punnett.ts (parentAlleleWeights, unknownPartnerOptions), breed.ts. Loest Sibling-Tickets 3c46d0b4 (#41), 1e7b66e6 (#40), c8ce27e2 (#37) mit. +### zuchttier-badge (1 Ticket(s) · frontend) +Dateien: gerbil-manager-web/src/index.css, gerbil-manager-web/src/pages/GerbilDetailPage.tsx, gerbil-manager-web/src/pages/GerbilsPage.tsx, gerbil-manager-web/src/strings/de.ts +- [Cluster] **Positives „Zuchttier"-Icon/Badge vor dem Verstorben-Status** (Flori von den Kleinen Chaoten, S/medium) — Neues positives „Zuchttier"-Badge/Icon im .ak-heroover-Block der GerbilDetailPage.tsx VOR der ak-statusbadge (Z.267-269) einfügen, analog zum Extern-Badge (Z.270-274). String in de.ts (bei externalBadge, Z.59-60), CSS-Variante in index.css. Kriterium: Tier hatte mind. einen eigen + `id=ec669e7a-1e9a-46aa-9498-bd6efab8a4d7` -### Cluster: gender-override (1) +### domi-todesdatum (1 Ticket(s) · needs-info) +Dateien: tools/import/conflict-decisions.json +- [RÜCKFRAGE] **Domi: Todesdatum-Zweifel der Züchterin** (Domi, S/high) — Datum ist quellenkonform und traceable, daher keine Korrektur ohne Bestätigung. Falls die Züchterin ein anderes Datum nennt: variables Import-Tier (isResident=false, fromWurfchronik) über conflict-decisions.json als name+dob-Resolution mit dateOfDeath-Override korrigieren (analog + `id=929273e9-5463-4934-92ff-60cf25c71030` -- **94892100-3849-4ed8-a2e9-cfcc1a2df982** [import-data] — Cherry Berry's Quqquluuruu — Geschlecht (Weibchen-Icon fehlt, Fragezeichen) - - Empfehlung: In tools/import/conflict-decisions.json eine gender-Override-Resolution ergaenzen (Praezedenzfall: Mozart of Lennylengo, Z.270-275): {"name":"Cherry Berry's Quqquluuruu","dob":"","decision":"gender = female (Box-Farbe weiss nicht als Geschlecht erkannt; Vorfahre ohne Geburtsdatum)","gender":"female","source":"Zuechterin 2026-06-22 — Ticket Weibchen-Icon fehlt"}. dob MUSS leer ("") sein, da das Tier kein Geburtsdatum hat und der Override-Key (canon_name + norm_dob) sonst nicht matcht. Verbraucht in extract.py _apply_conflict_decisions Z.1100-1148. Danach extract.py -> merge_and_resolve.py neu laufen lassen, App neu starten, POST /import/ingest-resolved, anschliessend Ticket schliessen. +### dante-wurf (1 Ticket(s) · needs-info) +Dateien: gerbil 93457716-65d2-5432-a52a-553ae873ca6d, gerbil-manager-web/src/pages/GerbilDetailPage.tsx, litter eb8047f2-c234-512b-b719-fe6ac6610d9c, tools/import/conflict-decisions.json +- [RÜCKFRAGE] **Dante: gemeldeter fehlender Wurf ist bereits vorhanden (Q-Wurf)** (Dante, S/medium) — Antwort abwarten: (a) meint sie den Q-Wurf und sieht ihn nicht → Anzeige/Refresh prüfen und Ticket schließen; (b) zusätzlicher, wirklich fehlender Wurf → per addLitters in conflict-decisions.json ergänzen + neu importieren. Separat prüfen (eigenes Thema): Vertrags-Mismatch — Prov + `id=9af81ae2-464c-480b-8199-6f73fcda8619` -### Cluster: rpro3-pedigree (1) +### todesursache-am-suffix (1 Ticket(s) · import-logic) +Dateien: tools/import/merge_and_resolve.py (parse_death_info), tools/import/test_merge_resolve.py +- [Cluster] **Todesursache bei Eliza: überflüssiges "am" abschneiden** (Eliza, S/high) — Im cod-Cleanup-Block von parse_death_info (tools/import/merge_and_resolve.py ~Z.729-731) trailing Datums-Bindewörter generisch abschneiden, z. B. cod = re.sub(r'\s+(am\|vom\|den\|im)$', '', cod, flags=re.I).strip(' ,;.-'). Danach neu extrahieren + POST /import/ingest-resolved. no + `id=47e66759-1790-4da9-baba-6357c96011d3` -- **f3ad5ec9-7ba5-4e9f-a503-f0afab42d27c** [import-logic] — Hiro of Golden Lights — Stammbaum fehlt (Eltern nur in RennmausPro) - - Empfehlung: Import-Pipeline um eine RennmausPro-Ahnen-Quelle erweitern: neuer Extractor liest _rpro3.db (stamm_tb=eigene Tiere via Name+DOB-Match, baum_tb=Eltern-Links id->_MID/_PID, fremd_tb=externe Ahnen mit u-Präfix); JDN-Datumskonvertierung + smart_decode aus tools/import/compare_rpro3.py wiederverwenden. In merge_and_resolve.py externe Ahnen als nicht-residente Gerbils (isResident=false, deterministische generate_guid auf stabilem rpro-Key) anlegen, Eltern über Litter (fatherId/motherId) verknüpfen, parent_age_plausible anwenden, Provenance-Eintrag 'aus RennmausPro III ergänzt'. Danach resolved_import.json neu generieren + POST /import/ingest-resolved. Betrifft potenziell viele Tiere -> als Cluster behandeln. Betroffene Dateien: tools/import/extract.py (bzw. neuer extract_rpro3.py), tools/import/merge_and_resolve.py. +### flori-vaterschaft (1 Ticket(s) · needs-info) +- [RÜCKFRAGE] **Flori als Vater von 4 Würfen verlinkt – Züchterin erwartet nur JD** (Flori von den Kleinen Chaoten, M/medium) — Nach Bestätigung der Züchterin (welche der 4 Würfe wirklich von Flori sind) in tools/import/conflict-decisions.json die fälschlich Flori zugeordneten Litter-Vater-Links austragen/korrigieren (Litter-Parent-Override bzw. resolution), dann merge_and_resolve.py neu laufen lassen und + `id=b7fbdb94-202b-488b-ab5f-6a2b868e65e3` -### Cluster: origin-label (1) +### falscher-vater-dublette-verstorben (1 Ticket(s) · import-logic) +Dateien: gerbil:04b14348-Feivel-tot, gerbil:be5bca76-Feivel-vdKC, litter:47d3b15a-P2-Wurf, litter:692c382b-Z2-Wurf, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Feivels Würfe fehlen – Vater dem falschen Namensvetter zugeordnet** (Feivel von den Kleinen Chaoten, M/high) — Im Importer (merge_and_resolve.py) die Vater/Mutter-Namensauflösung der Wurfchronik-Würfe um eine Sterbedatum-Plausibilität erweitern (Kandidat muss zum Wurfdatum leben) -> der 2012 verstorbene Feivel fällt weg, der lebende Feivel von den Kleinen Chaoten gewinnt. Fallback: geziel + `id=b441c5ea-ec07-4f87-aa2e-09399ab2a027` -- **b1629f97-3e06-4ef1-9605-95d7492eb352** [import-logic] — Hagrid Rubeus of Black Forest — Herkunft/Zuchtname sollte "Clan of Black Forest" sein - - Empfehlung: In tools/import/merge_and_resolve.py Zeile 204 den Wert von "blackforestgv" von "Black Forest" auf "Clan of Black Forest" ändern. Danach `python tools/import/merge_and_resolve.py` neu generieren und `POST http://localhost:5179/import/ingest-resolved` ausführen. Effekt: der separate Kontakt "Black Forest" (id 2f08f4ef-bfce-5323-aaa6-f1eedfb4c0f7) verschmilzt mit dem bereits existierenden Kontakt "Clan of Black Forest" (id a13f62c9-d506-50a6-99f8-65e85e955f96); die 31 Tiere mit Suffix "of Black Forest" zeigen dann originBreeder="Clan of Black Forest" und verlinken auf den einen Kontakt. Prüfen, dass keine Tiere zurückbleiben, die noch auf den alten "Black Forest"-Kontakt zeigen. Hinweis: Ticket-gerbilId 73759a6a (Hagrid Rubeus) existiert nach Re-Import nicht mehr; Verifikation erfolgte über Namens- und Kontakt-Abgleich. +### charlett-mwurf-owurf (1 Ticket(s) · import-data) +Dateien: tools/import/conflict-decisions.json, tools/import/extract.py, tools/import/merge_and_resolve.py +- [Cluster] **M-Wurf/O-Wurf: Mutter Charlett fehlt, M-Wurf unvollständig** (M-Wurf, M/medium) — Charlett per conflict-decisions.json addAnimals mit rpro3-Daten anlegen (female, dob 2010-05-14, Kohlfuchs-hell-Schecke, genotype aa Cc[chm] DD ee G- PP Spsp, Herkunft Clan of little longnoses, Eltern Sébastian×Angéline), als Mutter von M-Wurf (2011-03-03) und O-Wurf (2011-04-12) + `id=7d032fc5-f6b8-4c72-891d-f87fc4935ac9` -### Cluster: duplicate-merge (1) +### extern-nachzucht-versteckt (1 Ticket(s) · frontend) +Dateien: gerbil-manager-web/src/pages/GerbilDetailPage.tsx +- [Cluster] **Eddys Nachzucht (Sohn Oskar) fehlt auf der Akte externer Ahnen** (Eddy von den Kleinen Chaoten, S/high) — Frontend-Fix in gerbil-manager-web/src/pages/GerbilDetailPage.tsx: Abschnitt "Nachzucht in der Zucht" (breedingOffspring, ~Zeile 673) auch für nicht-residente Tiere rendern, damit externe Ahnen ihre residenten Nachkommen zeigen. Oskar erscheint dann automatisch (steht bereits in + `id=e5900a79-c21c-435d-af3e-9dc9518c5b40` -- **f618dcc3-31c5-4d41-9517-8e1cf422562b** [import-logic] — Namenloser Bock *15.02.2024 (Kind von Inochi gen. Picu) doppelt aufgeführt - - Empfehlung: Namenlose Tiere werden in merge_and_resolve.py (Z.2518-2532) als Placeholder bewusst NIE gemerged, und name-basierte conflict-decisions greifen ohne Namen nicht. Daher einen externalRef-basierten Merge-Override einführen (neues Feld z.B. 'mergeExternalRefs' in conflict-decisions.json), der den Placeholder-Ausschluss für explizit gepaarte Records überbrückt: (a) Kind 13944d01 in 5b5af108 mergen (primary = Picus-Son-Record); (b) Mutter 1ddfceaf in 6864eaef mergen (oder DOB-Remap 13.11.2022->13.11.2023 ebenfalls externalRef-basiert); (c) Folge: die zwei virtuellen Würfe 48bb7715 und 6430018f kollabieren zu einem (gleicher Vater+Mutter+Datum). Betroffen: tools/import/merge_and_resolve.py (Placeholder/Sub-Group Z.2504-2590), tools/import/extract.py (Override-Lader Z.1051/1088), tools/import/conflict-decisions.json. Danach extract.py + merge_and_resolve.py neu laufen lassen + POST /import/ingest-resolved. Schließ-fixNote-Vorschlag (Laiensprache): "Der namenlose Bock (geboren am 15.02.2024, Kind von Inochi gen. Picu) war versehentlich doppelt eingetragen — jetzt gibt es nur noch einen Eintrag und einen Wurf. Bitte die Seite neu laden." +### zuchtpartner-zeile (1 Ticket(s) · frontend) +Dateien: gerbil-manager-web/e2e/mock-api.ts, gerbil-manager-web/src/pages/GerbilDetailPage.tsx, gerbil-manager-web/src/strings/de.ts +- [Cluster] **Zuchtpartner-Zeile in der Tier-Akte anzeigen** (Alvin von den kleinen Chaoten, S/high) — Frontend-only: In GerbilDetailPage.tsx aus den bereits geladenen parentLitters eine Distinct-Liste der jeweils anderen Elternteile ableiten (litter.motherId wenn Tier=Vater, sonst fatherId) und als eigene Kv-Zeile "Zuchtpartner" mit Links auf die Partner-Akten rendern; nur bei is + `id=ce309e79-4e4a-4219-a112-8706dc80de42` -### Cluster: litter-expected-colors (1) +### nachzucht-verlinkung (1 Ticket(s) · import-logic) +Dateien: GerbilManagerWebAPI/Import/Rpro3/rpro3-decisions.json, gerbil-manager-web/src/pages/GerbilDetailPage.tsx, tools/import/merge_and_resolve.py +- [Cluster] **Nachzucht-Verlinkung bei Blacky und Flori fehlerhaft** (Danny von Maintaler PZ, M/medium) — Blacky-Record 22fe841c in 9b40b9e5 zusammenfuehren (Import-Dedup, deterministische IDs, re-ingest-stabil), analog zum "zwei Danny"-Fall; dann konsolidieren sich Wuerfe/Nachzucht. Fuer Flori Feivels Zuchteinsatz/Residenz anhand der handschriftlichen Wurfchronik pruefen (laeuft ueb + `id=8e402d22-f5cb-4993-af05-cfab8c62ccb2` -- **c8ce27e2-8ebe-4662-a816-7899be9c6a82** [genetics-engine] — Wurf-Detail: erwartete Farbschläge falsch (unbekannte Allele erzeugen Phantom-Farben) - - Empfehlung: Erwartete-Farbschlag-Liste in der Genetik-Engine bereinigen: (1) winzige (<1%) spekulative Wildcard-Artefakte, die nur aus unbekannten Eltern-Allelen (D-, P-) entstehen, nicht prominent als 'erwartet' anzeigen, sondern aggregieren oder unter 'unsicher' zusammenfassen; (2) 'Unbekannter Farbschlag' und technische Pseudo-Farben wie 'C-Separator' aus der erwarteten-Liste herausfiltern (Catalog-Gap #39); (3) optional Hinweis 'Eltern tragen unbekannte Allele (D-/P-), erwartete Farben daher unsicher'. Kern: parentAlleleWeights/unknownPartnerOptions in punnett.ts laesst das unbekannte Partner-Allel an D-/P- rezessiv werden (rank(d)>=rank(D)); da beide Eltern D-/P- tragen, entstehen dd/pp -> Dilute/Blau/Platin-Phantome. Betroffen: gerbil-manager-web/src/genetics/punnett.ts, breed.ts (byFarbschlag-Aggregation/Filter), catalog.ts (Unbekannter-Farbschlag-Gap), pages/WurfDetailPage.tsx + components/BreedingResultView.tsx. +### toter-elternteil-namensdublette (1 Ticket(s) · import-logic) +Dateien: Gerbil 29cbd5e6 (Eliza *2010), Litter 2afbbfda-80d3-5e8e-88b5-852b9eaa942a (J5-Wurf), tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py (_resolve_name, parent_age_plausible), tools/import/test_merge_resolve.py +- [RÜCKFRAGE] **Toter Elternteil als Mutter: falscher Wurf auf Elizas Akte (Namensdublette)** (Eliza, M/high) — In merge_and_resolve.py::_resolve_name / parent_age_plausible einen Sterbedatum-Filter ergänzen: Kandidat ausschließen, wenn DateOfDeath vor dem Wurfdatum liegt (Gestations-Marge ~25 Tage). Regressionstest in test_merge_resolve.py. Nach Antwort der Züchterin die korrekte lebende + `id=b22896bc-755c-48a2-aa01-e3788453feb2` -## Rückfragen (NeedsInfo, warten auf Züchterin) +### geschlecht-override (1 Ticket(s) · import-logic) +Dateien: tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Domi als weiblich statt männlich importiert** (Domi, S/high) — Zwei Teile: (1) conflict-decisions.json-Eintrag für Domi {name:"Domi", dob:"2010-06-25", gender:"m"}; (2) in merge_and_resolve.py (~Z.3291) den Guard lockern, sodass ein expliziter decision-gender auch ein bereits gesetztes Geschlecht überschreibt (aktuell nur bei unknown/None). + `id=4edc51d5-1792-4f2a-b75f-c9e57363f70d` -- b8f8b6fb-5d68-4c15-bd58-daee382ea9af — Falscher Name "1.) Butter2.) Trixi" — Vergesellschaftungs-Partner statt Tiername -- 8fe0e93e-45f2-41e7-b27d-aa3cd0bb15e3 — Sunny von PZ Karl — falscher Vater (chart-position geraten) -- d99563fa-a5cf-43da-aaa6-78bd4b677cb2 — Falsches Foto bei Odelia von den Kleinen Chaoten -- 38c26381-ad17-4255-b480-b2ebee3f2133 — Odelia – „Falsche Eltern!!" (Cash × Elena) -- 4692fd5c-5929-435b-b7b4-75ccb7ba3e08 — Danielle – falsche Mutter, Vater fehlt (Stammbaum) -- ba63325a-4c23-4c5b-b52a-c0dc091bc59b — Gaida: falsche Eltern - echte Eltern (Kinder von Nisha & Zenon) fehlen in den Daten -- 7bbc045c-a23b-4d1d-905f-18b087fe90aa — Catelyn Stark — Eltern (Vater fehlt, Mutter vermutlich falsch) -- 1e7b66e6-e46b-49ff-acab-579e9088f859 — Erwartete Farbschläge/Gencodes am Wurf Mamta Mini + Gold +### a-wurf-jungtierzahl (1 Ticket(s) · needs-info) +Dateien: GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [RÜCKFRAGE] **A-Wurf: gemeldete Geburtenzahl (4/2 Todgeburten) widerspricht 3 eingetragenen Jungtieren** (A-Wurf, S/medium) — Antwort der Züchterin abwarten: entweder totalBorn=5 (3 Pups + 2 Todgeburten) oder ein Pup gehört nicht in den Wurf (dann Merge 0001/0013 korrigieren). Danach totalBorn/deathsWithin8Weeks im Importer setzen (merge_and_resolve.py / conflict-decisions.json bzw. addLitters), kein Li + `id=a93938a6-f2d1-4fb6-ba0f-c96b72671236` -## Bereits behoben (geschlossen mit Changelog) +### kruke-dob (1 Ticket(s) · import-data) +Dateien: tools/import/conflict-decisions.json +- [Cluster] **Kruke: Geburtsdatum 18.09.2009 ergänzen** (Kruke, S/high) — In tools/import/conflict-decisions.json beim Kruke-Eintrag (ca. Zeile 949) "dob": "18.09.2009" ergänzen (Format DD.MM.YYYY wie beim Wynn-Eintrag), dann merge_and_resolve.py laufen lassen und POST /import/ingest-resolved. Kein Code- oder Schema-Change nötig; plausibel als Mutter e + `id=b72acf1e-4999-44ae-a2dc-58408864e242` -- f92a9650-eb2b-4298-87aa-0a230ee0aeff — Stich von Privatzucht Gießen — Mutter bekannt -- e9790985-306d-4f32-94f1-a02a9469e3ad — Arya Stark — Abgabe an Clan of Black Forest / Bestandsanzeige -- 50b17699-7113-4348-9d46-95abe2d2b239 — Bill von Privat — Eltern unbekannt (Stammbaum) -- bee684ed-ced6-408b-825c-4248f3bf17cf — Melly von Privat — Eltern unbekannt -- aa530d1b-1811-469d-990a-d834a9cb65b7 — Beatrice von den kleinen Chaoten — "Eltern fehlen" im Stammbaum -- 671f8b5d-e5ce-4b47-aafa-db96fa180e37 — Silver: Vater Taro + Mutter Beatrice-Stammbaum (missing-parents) -- e09bf10e-1a72-494a-92c0-6824129eebf0 — Tony v.d. Kleinen Chaoten — Vater Sammy -- b46d5807-fe23-4922-b09e-dffaf411177c — Cooky vom Zooladen (OBI): Eltern unbekannt (Zooladen-Gruendertier) -- 32de49fe-db33-4b14-bb1e-55ba19b2ed1c — Arya Stark — falsche Mutter (Eltern Sansa Stark + Vance, Geschwisterverpaarung) -- dfaae8fb-6f16-45d9-bf30-3b3480325188 — Hagrid Rubeus of Black Forest — externer Ahne zeigte fälschlich Würfe -- d7189414-5028-4e20-838d-8e6fcf09f8a0 — Hagrid Rubeus of Black Forest — Eltern (Snickers + Milka) -- b3908b69-eddc-4cb1-984c-37fe18e19f17 — Hagrid Rubeus of Black Forest — Charakter/Eigenschaften bei externem Vorfahren ausblenden -- 4493fd8a-1136-4c1c-a6a6-9a7de45a5a8c — Yuki von den Kleinen Chaoten — falscher Vater, Eltern Camaro & Izumi -- ce404b2d-77a9-44bf-80fc-9750b6312fce — Belica gen. Emi - Mutter Hana -- 684f5b7d-8d20-4159-80b2-16b3387b78da — Zadar from Zeko i ptica, Croatia – Eltern unbekannt (Stammbaum) -- 1a51dc7a-05bf-4eac-bfbc-0b6b53e619fc — Theodore von den Kleinen Chaoten — Vater BlackFire -- 864f0c55-5a84-4a99-8314-dfa3496dfee0 — Jamie: Vater = Danny von Privatzucht Maintal -- 473dc345-a610-42b7-b897-121716b36cf4 — Vance: "unbekannter Farbschlag" statt Kohlfuchs -- 46c635ba-53da-4196-a486-106a2adce1e4 — Vance: fehlende Mutter (sollte Enya sein) -- 5151ab20-147c-4e44-b57e-7a28fe962d85 — Vance: Genotyp zeigte "uw" statt G-Locus -- 81a3126b-0414-49a0-ae91-1ba775b2766f — Zac gen. Action — fehlende Mutter (Dorie of Black Forest) -- 2cd5c28f-a353-4477-ac15-9ee78f8c73a9 — Gold v.d. Kleinen Chaoten — Muttertier (Chelsea) -- 3c46d0b4-3cfc-4e7c-ad93-36f45b288410 — E-Locus: Schimmel (efef) faellt bei Mamta Mini x Gold -- b034ddd2-9df7-4d36-8c79-957f30cc19f7 — Algierfuchs / E-Locus e- Validierung (ee[-]) \ No newline at end of file +### scheckungsart-erfassung (1 Ticket(s) · import-logic) +Dateien: gerbil-manager-web/src/pages/GerbilDetailPage.tsx, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Alvins Scheckungsart (Punktschecke) wird nicht angezeigt – Feld leer** (Alvin von den kleinen Chaoten, S/high) — spottingType="Punktschecke" für Alvin (b04dc268…, DOB 2010-02-18) setzen. Kanonisch/re-ingest-stabil: conflict-decisions.json-Resolution per name+dob um Feld spottingType erweitern UND merge_and_resolve.py-Resolutions-Anwender (~Zeile 3235-3279) um einen spottingType-Zweig analog + `id=9d86a7c2-c4dc-4ad2-aedd-4abaf13ca26b` + +### herkunft-clan-kleine-chaoten (1 Ticket(s) · import-logic) +Dateien: GerbilManagerWebAPI/Import/Rpro3/Rpro3ImportService.cs, POST /import/ingest-resolved, tools/import/merge_and_resolve.py +- [Cluster] **Herkunft "Clan kleine Chaoten" global auf "Zucht der Kleinen Chaoten" vereinheitlichen** (Bodo, M/high) — Kanonisches Herkunfts-Label "Zucht der Kleinen Chaoten" definieren und beide Importer darauf normalisieren: (1) RPRO3-Importer an der OriginBreeder-Zuweisung (Rpro3ImportService.cs ~Z.444) "Clan kleine Chaoten"/Varianten mappen; (2) Python-Importer (merge_and_resolve.py) Literal + `id=aaee068b-16a8-46a7-b973-a06577cc5ab1` + +### wurfchronik-dublette-merge (1 Ticket(s) · import-logic) +Dateien: gerbil JackJack ff6735c6-ddc9-5277-a976-1867e5242818, gerbil JackSack b6d8b3ef-3077-53cf-bc52-93fd82e3982b, litter A-Wurf 29f4bae9-ca7d-56c4-b8bc-8283d5284b87, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [RÜCKFRAGE] **JackJack und JackSack im A-Wurf sind dasselbe Tier (Dublette)** (A-Wurf, M/high) — Beide Datensätze zu einem Tier zusammenführen und den reichen Datensatz (JackSack, page_0013, id b6d8b3ef: Farbe/Genotyp/Sterbedatum/Abnehmer) behalten, den Stub (JackJack, page_0001, id ff6735c6) unterdrücken. Da der Decision-Layer bisher nur isResident/notes/receiver kann (kein + `id=0cf6b838-3ee1-4ec0-ac9d-43901e6b5320` + +### wurfchronik-eltern-fehlen (1 Ticket(s) · import-logic) +Dateien: tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Wurf S1 fehlt bei Flori (Eltern Flori x Delia of cb-animals nicht verknüpft)** (Flori von den Kleinen Chaoten, M/medium) — Wurf S1 im Importer mit Vater Flori (90a37fe0) und Mutter Delia of cb-animals (81204d7e) verknüpfen; beide existieren und sind altersplausibel. Bevorzugt per conflict-decisions.json. Achtung Dubletten-Risiko: addLitters erzeugt eine neue Litter (decision-litter-wurf-s1) und würde + `id=55ff9203-58c3-4470-b960-5ca035cb4666` + +### dastan-fehlmerge (1 Ticket(s) · import-logic) +Dateien: 7 Wuerfe 2015 (T6/Y4/E6/H6/Dastan+Elly/Snoops+Dastan/Z5), Genotyp + isResident + Farbschlag, Gerbil fba979a3 (Dastan), Litter 1eb3e1fb (D-Wurf 2010, Blacky+Kuke), tools/import/conflict-decisions.json, tools/import/extract.py, tools/import/merge_and_resolve.py +- [Cluster] **Dastan: Import-Fehl-Merge trennt Pet- und Zucht-Dastan nicht** (Dastan, L/high) — Force-Split im Dedup (merge_and_resolve.py + conflict-decisions.json): 2015er-Zucht-Dastan-Quellzeilen (Teil1 p45/46/50, Teil2 p6) nicht in den 2010er Pet-Dastan mergen. Pet-Dastan fba979a3 korrigieren: Genotyp aa C- DD E- G- P- Spsp (Kragenschecke), isResident=false, "Breeding F + `id=0ebc250f-17a7-45d8-8b21-91e563fbfd9c` + +### rpro3-erwerb-uebernahme (1 Ticket(s) · needs-info) +Dateien: GerbilManagerWebAPI/Endpoints/AcquisitionEndpoints.cs, GerbilManagerWebAPI/Import/Rpro3/Rpro3ImportService.cs, GerbilManagerWebAPI/Models/AcquisitionRecord.cs, tools/import/_rpro3.db +- [RÜCKFRAGE] **Erwerb-Daten aus RennmausPro für Bodo, Coco, JackJack übernehmen** (Bodo, S/medium) — Erwerbs-Felder (Datum, von wem bzw. „eigene Zucht", ggf. Preis/Notiz) für die drei Tiere von der Züchterin erfragen (Rückfrage gestellt) und via POST /acquisitions als ingest-stabile AcquisitionRecords eintragen. Alternativ vollständigen RennmausPro-Export beschaffen und den rpro + `id=efe47d3f-6da8-4928-a05d-875d7a410a44` + +### rpro3-foto-import (1 Ticket(s) · needs-info) +Dateien: /photos-Endpoint, GerbilManagerWebAPI/Import/Rpro3, GerbilPhotosTab.tsx, tools/import/_rpro3.db (photo_tb) +- [RÜCKFRAGE] **Blacky: Fotos aus RennerPro fehlen (nur Dateinamen, keine Bilddateien)** (Blacky von PZ Seligenstadt, L/high) — Speicherort der echten RennerPro-Bilddateien klaeren (Rueckfrage gestellt), dann rpro3-Foto-Import bauen: photo_tb-Dateinamen je stamm-id/rid auf die Bilddateien mappen und ueber den /photos-Endpoint an die gematchten Gerbils haengen (ingest-stabil, deterministisch). Gilt generel + `id=00493a79-b4e1-4856-b0c4-9e08508ca484` + +### tod-abgabe-erfassung (1 Ticket(s) · backend) +Dateien: GerbilDetailPage.tsx (Felder Tod/Abgabe/Abnehmer), GerbilManagerWebAPI Endpoints/GerbilEndpoints.cs (Edit → GerbilOverride), Kontakt-Anlage (Abnehmer Caroline Emmert) +- [Cluster] **Gaida: Tod (09.10.2025, Lungenentzündung) und Abgabe an Caroline Emmert erfassen** (Gaida von den Kleinen Chaoten, S/high) — Neuen Abnehmer-Kontakt "Caroline Emmert" anlegen (existiert nicht, 20 Kontakte geprüft). Auf Gaidas Akte via UI-Edit setzen: dateOfDeath=2025-10-09, causeOfDeath="Lungenentzündung mit Fieber", receiverContactId=neuer Kontakt, status→Verstorben/Deceased, goHomeDate leer lassen. De + `id=c3f374ec-83a2-4999-bbeb-ef3bdbbd9bfb` + +### eltern-namensdopplung (1 Ticket(s) · needs-info) +Dateien: gerbil-manager-web/src/pages/GerbilDetailPage.tsx, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [RÜCKFRAGE] **Alvins „Nachkommen mit Zuchteinsatz“ zeigt falsche Tiere (Namensdopplung)** (Alvin von den kleinen Chaoten, M/medium) — Eltern-Links der 2014-2016er Würfe von Sunny(31533666)/Zora(527a353c)/Lexy(bd97a229)/Zorro(f50bcf7f) auf die korrekten jüngeren gleichnamigen Tiere umhängen (DOB-Plausibilität im Resolver bzw. conflict-decisions.json) und neu importieren; für Lee(8eb68783) & Sam(5b86e826) nach An + `id=d1069edb-1001-4e24-b436-147948b85c8a` + +### gerbil-name-suffix (1 Ticket(s) · import-logic) +Dateien: gerbil c7dbc0b0-8a35-57a1-8f64-2b5351a273d6 (Uriana), tools/import/merge_and_resolve.py +- [Cluster] **Mutter-Name auf Xhemiles Akte ohne Zuchtsuffix ("Uriana" statt "Uriana von den Kleinen Chaoten")** (Xhemile gen. Chanel von den Kleinen Chaoten, S/high) — In tools/import/merge_and_resolve.py in get_normalized_gerbil_name (gerbil_norm_map, Zeile 389ff) Eintrag "uriana": "Uriana von den Kleinen Chaoten" ergänzen (Norm-Key eindeutig, nur dieser Proband). Danach neu generieren (extract.py + merge_and_resolve.py) und POST /import/inges + `id=c55a677c-be2e-4f34-820e-a695ef9260b2` + +### xtra-bounty (1 Ticket(s) · import-data) +Dateien: tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Tiername falsch: „Extra Bounty" muss „Xtra Bounty" heißen** (Extra Bounty, S/high) — Im bereits vorhandenen conflict-decisions.json-Block für externalRef „Wurfchronik Teil 2_page_0006.md-0c9c7be0-de7f-48fa-b8be-c6e87cbcd8fa" ein Feld "renameTo": "Xtra Bounty" ergänzen (Match-Key externalRef bleibt stabil, renameTo greift in merge_and_resolve.py). Danach merge_and + `id=878f4b6e-e5c2-4c35-8b91-e08127914fe4` + +### kuke (1 Ticket(s) · import-data) +Dateien: tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py, tools/import/test_merge_resolve.py +- [Cluster] **Kuke (fälschlich "Kruke"): Name korrigieren + RennmausPro-Daten ergänzen** (Kruke, M/high) — Im Importer beheben: addAnimals-Stub weiterhin auf "Kruke" gekeyt lassen (deterministische Id + 6 Wurf-Elternlinks hängen an dieser Schreibweise), Anzeigenamen via resolutions {name:Kruke, renameTo:Kuke} korrigieren. Reichfelder ergänzen: dob 18.09.2009, zucht "M.Knoss" (OriginBr + `id=1fffdd0e-b9c3-4bcf-a7c9-705fdb111acb` + +### wurfchronik-teil2-phantom (1 Ticket(s) · needs-info) +Dateien: GerbilManagerWebAPI/Import/IngestResolvedService.cs (Stale-Removal), tools/import (Wurfchronik-Teil-2-Import), tools/import/conflict-decisions.json +- [RÜCKFRAGE] **Unbekanntes Tier „Nymphs" aus Wurfchronik Teil 2 (Phantom-Zuchtrüde)** (Nymphs, M/medium) — Antwort der Züchterin abwarten. Bei echtem Tier: korrekte Felder/Namen via Override bzw. Chronik-Parsing ergänzen. Bei Phantom/OCR-Fehler: Datensatz unterdrücken/entfernen. Prüfen, ob Nymphs vom aktuellen tools/import-Lauf überhaupt noch erzeugt wird (Teil-2-Import ohne committet + `id=ca8a1c48-1407-44ed-908f-986cbc0e5e0b` + +### willow (1 Ticket(s) · import-data) +Dateien: Live-DB Gerbil 3f9c5a26-af3a-55dc-9c4b-4ebdea4ce36f, Wurfchronik Teil 2 Excel Quelle (Seite 6), tools/import (Wurfchronik Teil 2 Import), tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py (Dedup-Regel) +- [Cluster] **Willow: falsches Geschlecht (m→w) + fehlende Eltern/Geburtsdatum aus Wurfchronik Teil 2** (Willow, M/medium) — Als variables Wurfchronik-Tier im Importer korrigieren (re-ingest-stabil): Geschlecht auf weiblich setzen; DOB + Eltern aus der Wurfchronik-Teil-2-Excel (Seite 6) bzw. per Dublettenabgleich mit „Willow of Black Forest" (DOB 16.01.2017, Black-Forest-Stammbaum) ermitteln und über c + `id=9935323f-883e-4686-b3ff-de8d10de4722` + +### hero-abgabe-widerspruch (1 Ticket(s) · needs-info) +Dateien: Gerbil Hero 7f0d43cc-0cf6-5afc-98bd-63ab6f031c04, Litter D3-Wurf 421851c3-dedd-5620-80ec-a06e27c9cec9, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [RÜCKFRAGE] **Hero (Alvins Sohn): Abgabe-/Sterbenotiz widerspricht D3-Wurf-Vaterschaft** (Alvin von den kleinen Chaoten, S/medium) — Antwort abwarten. Fall A (blieb bei ihr, D3 korrekt): in conflict-decisions.json name+dob-Resolution fuer Hero (*31.10.2010) die falsche Abgabe-/Sterbenotiz per notes-Override korrigieren, isResident bleibt true. Fall B (2011 abgegeben, D3 gehoert anderem Hero): D3-Vaterlink loes + `id=72d18800-342b-4935-9aad-311a1607b3da` + +### genna-herkunft (1 Ticket(s) · import-data) +Dateien: Herkunft/originBreeder, Stammbaum (Eltern Epoxy x Reika), tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Genna: falsche Herkunft (soll "Clan of Golden Lights" sein)** (Genna, M/high) — In tools/import/conflict-decisions.json eine name(+dob)-Resolution fuer Genna anlegen: originBreeder="Clan of Golden Lights" (merge_and_resolve.py wendet den Override ~Z.3236 an). Optional anreichern: DOB 2014-06-20, Farbe Zobel, Genotyp aa c[chm]c[chm] DD Ee gg Pp spsp. Stammbau + `id=2fbbc0a2-a80c-4d90-a250-567fdd915abe` + +### extern-zuchttier-wurfleiste (1 Ticket(s) · needs-info) +Dateien: gerbil-manager-web/src/pages/GerbilDetailPage.tsx, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [RÜCKFRAGE] **Extern zugekauftes Zuchttier (Blacky von PZ Seligenstadt) zeigt Wurf-Leiste** (Blacky von PZ Seligenstadt, M/medium) — Antwort abwarten. (a) "Nur Vorfahre, nie selbst gezüchtet" -> Blacky per isResident=false-Override in conflict-decisions.json extern markieren (analog Akane/Naémi/Bentley), Wurf-Leiste verschwindet automatisch; dabei die fälschlich "von den Kleinen Chaoten" benannten Nachkommen ( + `id=7dedec07-99b8-4b0c-9f51-bf14a583ec67` + +### xhemile-falsche-mutter (1 Ticket(s) · import-data) +Dateien: Wurf 5b6c873a-a508-5c4d-af5b-66a37c071391, tools/import/conflict-decisions.json, tools/import/extract.py (chart-position Eltern-Resolver), tools/import/merge_and_resolve.py +- [Cluster] **Xhemile gen. Chanel: falsche Mutter (Uriana statt Oyama), Vater korrekt** (Xhemile gen. Chanel von den Kleinen Chaoten, S/high) — Re-ingest-stabiler Fix im Importer (variables Tier, kein Live-DB-Patch): conflict-decisions.json-Resolution fuer Xhemile (name+dob 2021-05-16) mit Mutter-Override auf Oyama von den Kleinen Chaoten (89c61419-4681-5c79-954f-7010a3feefa6, *2018-12-26); Vater Ramzi (6a6d47ec-6ace-5e1 + `id=25890c71-dfde-4a1c-b77e-b04a6b5a9ad3` + +### verpaarung-phantom-tiere (1 Ticket(s) · import-logic) +Dateien: resolved_import.json, tools/import/extract.py, tools/import/test_extract.py +- [Cluster] **"Verpaarung von …" als Tiername importiert (Phantom-Tiere)** (Verpaarung von Chevrolet Camaro of Topolino, M/high) — In tools/import/extract.py Namenszellen, die mit "Verpaarung von" (case-insensitive) beginnen, nicht als Eigennamen/Tier akzeptieren, sondern verwerfen (mit verworfen-Provenance). Danach extract.py, merge_and_resolve.py, POST /import/ingest-resolved; die stale Phantom-Tiere werde + `id=cc85c890-042f-4393-bbcf-f919716a3b12` + +### elliot (1 Ticket(s) · import-data) +Dateien: Contact (neu: Luise Neubert-Kutschke), Gerbil.gender, Gerbil.goHomeDate, Gerbil.receiverContactId, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Elliot (*2023): Geschlecht auf männlich korrigieren + Abgabe an Luise Neubert-Kutschke nachtragen** (Elliot, M/high) — Fix im Importer (re-ingest-stabil): conflict-decisions.json-Resolution auf name=Elliot + dob=25.02.2023 (NICHT nur Name — zweiter Elliot *11.09.2015 existiert; alternativ externalRef docx-161-elliot-2023-02-25) mit gender=male; addContact für Luise Neubert-Kutschke (Am Steinberg + `id=6af0ad73-5987-4310-a757-a47282a4f1f9` + +### shelly-name-merge (1 Ticket(s) · import-data) +Dateien: tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py, tools/import/output/animals.json +- [RÜCKFRAGE] **Shelly: Name unvollständig – Wurfchronik-Rufname nicht mit Stammbaum-Vollnamen gemergt** (Shelly, S/high) — In tools/import/conflict-decisions.json einen Namens-/Dedup-Alias hinterlegen, der den Wurfchronik-„shelly" mit dem Stammbaum-Vollnamen-Record (samgenshellyvdbuntenfellnasen / sammygenshellyvdbuntenfellnasen) merged bzw. den korrekten Vollnamen setzt (merge_and_resolve.py Dedup), + `id=7deec818-ad29-4053-86fb-5e67ac180e70` + +### tai-verstorben (1 Ticket(s) · needs-info) +- [RÜCKFRAGE] **Tai als verstorben markieren – Todesdatum fehlt in RennerPro-Kopie** (Tai, S/high) — Todesdatum (und ggf. Ursache) bei der Züchterin erfragen, dann status=Deceased + dateOfDeath (+ causeOfDeath) auf das Tier setzen. Dabei Ingest-Persistenz beachten: Status wird laut CLAUDE.md nicht per GerbilOverride eingefroren – entweder Freeze-Scope prüfen oder eine name+dob-R + `id=b222b459-57c5-40ea-88b0-0fa696869c6b` + +### elena-dublette (1 Ticket(s) · import-data) +Dateien: gerbil a0aff85b (Elena of KK Chaos), gerbil bc2400c9 (Elena), litter f26293c2, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [Cluster] **Elena ohne Stammbaum ist Dublette von „Elena of KK Chaos"** (Elena, M/high) — Manuelle Merge-/Dubletten-Entscheidung in tools/import/conflict-decisions.json anlegen, die die Wurfchronik-Elena (bc2400c9) mit „Elena of KK Chaos" (a0aff85b) zusammenführt → ein Tier mit vollem Stammbaum (Eltern-Wurf f26293c2 Ibo × Skarlett), DOB 2013-12-05, Genotyp, Status Dec + `id=6cdb22b4-f8c0-4d0b-913c-d16392031585` + +### stammbaum-ohne-eltern (1 Ticket(s) · needs-info) +Dateien: gerbil-manager-web/src/pages/GerbilDetailPage.tsx, gerbil-manager-web/src/pages/StammbaumPage.tsx, tools/import/conflict-decisions.json +- [RÜCKFRAGE] **Aaren: Stammbaum wirkt leer (keine Eltern hinterlegt)** (Aaren, S/high) — Rückfrage an die Züchterin, ob Aarens Eltern bekannt sind. Falls ja: über conflict-decisions.json (addAnimals/parents, deterministische GUID) ergänzen + Re-Ingest, dann füllt sich der Stammbaum. Falls Eltern unbekannt (zugekauft): leerer Stammbaum ist korrekt; ggf. UX-Hinweis "ke + `id=7a9831f6-4d68-41e7-add2-bc06393e6ed6` + +### drake-jr-eltern (1 Ticket(s) · needs-info) +Dateien: gerbil-manager-web/src/pedigree/build.ts, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [RÜCKFRAGE] **Drake Jr: Stammbaum leer, weil keine Eltern hinterlegt** (Drake Jr, S/medium) — Zur Frage der Züchterin: Ja, Drake Jr ist Vater von Q3-Wurf (2014-06-21) mit Jacky v.d. Kleinen Chaoten, 12 Jungtiere. Nach Antwort der Züchterin die Eltern von Drake Jr im Importer verknüpfen (conflict-decisions.json name+dob-Resolution bzw. addLitters mit Drake Jr als Kind, mer + `id=31811914-257b-4c83-bb42-85cc2d140277` + +### vorfahre-nachzucht-zeile (1 Ticket(s) · frontend) +Dateien: gerbil-manager-web/src/pages/GerbilDetailPage.tsx, gerbil-manager-web/src/strings/de.ts +- [Cluster] **Vorfahren: Wurf-Karten durch Nachzucht-Zeile ersetzen** (Percy of little runners, S/high) — In GerbilDetailPage.tsx fuer isResident===false zusaetzlich zum/statt des nonResidentNote eine kompakte Zeile rendern, die die Nachzucht(en) benennt (Kinder-Namen/Links aus dem offspring-Query, der unabhaengig von der Residenz feuert, oder die Wurf-Namen). Neuen de.ts-String (z.B + `id=fbb732a3-f20b-4f12-8408-748be6cf8625` + +### berni-montana-merge (1 Ticket(s) · import-logic) +Dateien: Gerbil 763bf91f (Montana), Gerbil 999baccb (Berni), tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [RÜCKFRAGE] **Berni ist vermutlich Rufname von Montana (Dublette)** (Berni, M/high) — Nach Bestätigung der Züchterin: Alias/Merge-Decision in conflict-decisions.json anlegen (Berni → Montana v.d. Kleinen Chaoten 763bf91f…), die 3 Berni-Würfe (M10/Polar/M12) auf Montana umhängen und den datumsgleichen 2017-07-20-Wurf deduplizieren, dann neu importieren. + `id=0921dba5-d3a7-47d7-ae95-0c6e535815d1` + +### x16-wurf-eltern-fehlen (1 Ticket(s) · import-data) +Dateien: Stammbaum von Goldfuchs Sp (Pikachu) Kids.xlsx, Wurfchronik der Kleine Chaoten Teil1.xlsx (Wurf X16), tools/import/conflict-decisions.json, tools/import/extract.py, tools/import/merge_and_resolve.py +- [RÜCKFRAGE] **Zhuāngzǐ: Eltern/Wurf fehlen — Wurf X16 (Nisha × Zenon von Elea) eindeutig bestimmt** (Zhuāngzǐ von den Kleinen Chaoten, M/medium) — Data-driven, re-ingest-stabil in conflict-decisions.json: name+dob-Resolution (bzw. addLitters für X16) für Zhuāngzǐ UND die gleich betroffene Schwester Zaibunissa, die Wurf X16 mit Mutter Nisha of Black Forest (06dc519d-f92e-5d09-95b4-61d7bbdc228a) und Vater Zenon von Elea (cd2e + `id=785851d0-7b37-45a7-adcc-e78db9c3c4b9` + +### chiko (1 Ticket(s) · needs-info) +Dateien: Gerbil 7d3b7c47-6320-5181-b612-56676e4516d5 (Chiko), tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [RÜCKFRAGE] **Chiko: leerer Stammbaum, reiner Wurfchronik-Partner ohne weitere Daten** (Chiko, S/medium) — Ruckfrage an Zuchterin nach Herkunft/Farbe/DOB von Chiko gestellt. Nach Antwort: falls Daten vorhanden -> Anreicherung via conflict-decisions.json (name+dob-Resolution bzw. addAnimals) + Re-Import; falls nur Partnertier ohne Daten -> als korrekt bestatigen (ggf. GerbilOverride/ve + `id=bc859109-32df-4506-9f7d-6f6415c00f02` + +### kruemmel-todesdatum (1 Ticket(s) · needs-info) +Dateien: Gerbil 38c6b3c9-18ca-530f-8657-02d62e7c0e59 (Krümmel), Litter 10ddf880 (F-Wurf 2010), tools/import/_rpro3.db (geprüft, kein Treffer) +- [RÜCKFRAGE] **Krümmel (F-Wurf 2010): kein Todesdatum in keiner Quelle** (F-Wurf 2010, S/high) — Auf Todesdatum der Züchterin warten. Nennt sie eines, per UI/GerbilOverride direkt am Tier 38c6b3c9 nachtragen (ingest-stabil) – KEIN Importer-Fix nötig, da die Daten in keiner Quelldatei existieren. Nennt sie keins, Feld bleibt korrekt leer und Ticket kann geschlossen werden. + `id=21c8ff24-cc03-4508-b6c6-849ab36ba5b2` + +### percy-eltern (1 Ticket(s) · import-logic) +Dateien: tools/import/conflict-decisions.json, tools/import/extract.py, tools/import/merge_and_resolve.py +- [RÜCKFRAGE] **Percy of little runners: Mutter nicht verknüpft (Stammbaum-Fehlparse + Selbstreferenz)** (Percy of little runners, M/high) — 1) extract.py chart-position: Kandidaten, die auf das Tier selbst zeigen (gleiche id/Name+DOB), als Elternteil ausschließen (Logik-Bug, unabhängig vom Einzelfall). 2) Sobald die Züchterin die echte Mutter nennt: Eltern-Link (Mutter, ggf. Vater Rasputin of little runners) in confl + `id=e87a6da5-0ff1-46b6-b4ec-5d05a1605f5f` + +### dusty-stammbaum-dublette (1 Ticket(s) · needs-info) +Dateien: DB-Gerbil 41d7577e-bcaf-5967-a6ed-fe0b52171f77, DB-Gerbil 43e784a4-61bd-5b52-af74-c007c9cd6996, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py +- [RÜCKFRAGE] **Dusty: Stammbaum fehlt – vermutete Dublette (m. Zuchtbock vs. w. Jungtier)** (Dusty, M/medium) — Bei Bestätigung durch die Züchterin: beide Dusty per Force-Merge in conflict-decisions.json zusammenführen (merge_and_resolve.py), Geschlecht auf männlich korrigieren und Eltern Hero × Mira / Geburtslitter D3-Wurf übernehmen; danach neu importieren + re-ingest → Stammbaum erschei + `id=034b4988-5447-4c54-ad28-9a6708133517` + +### rpro3-stammbaum-verknuepfung (1 Ticket(s) · import-logic) +Dateien: GerbilManagerWebAPI/Import/Rpro3/Rpro3Dedup.cs, GerbilManagerWebAPI/Import/Rpro3/Rpro3ImportService.cs, GerbilManagerWebAPI/Import/Rpro3/rpro3-decisions.json, Stammbaum-Viewer (StammbaumPage.tsx), tools/import/_rpro3.db +- [Cluster] **Katinka: RennmausPro-Daten nicht mit Excel-Stammbaum verknüpft (Farbe/Genotyp/Nachzucht fehlen)** (Katinka of little runner's, L/medium) — rpro3-Importer so erweitern, dass externe Ahnen per Name (+Eltern/DOB) an bestehende Stammbaum-Records gematcht und Felder (Farbe, genotype, DOB, weitere Ahnen, Nachzucht) übernommen werden, statt Duplikate anzulegen — re-ingest-stabil mit deterministischen GUIDs. Für Katinka kon + `id=740360e5-151d-4dca-a596-f71bc128771b` + +### foto-fehlattribution (1 Ticket(s) · import-logic) +Dateien: Foto-Import-Heuristik, tools/import/extract.py (_attach_photos) +- [Cluster] **Percy: Profilfoto zeigt uni-schwarz statt schwarz-schecke** (Percy of little runners, M/medium) — Cluster: Foto-Zuordnung fuer Percy in tools/import/extract.py (_attach_photos, ~Z.388) gegen die Quell-xlsx pruefen; korrektes schwarz-schecke-Foto zuordnen bzw. das fehlattribuierte uni-schwarz-Foto entfernen; danach re-ingest (re-ingest-stabil). Kein Genotyp-/Farbschlag-Fix noe + `id=e04ceda2-1701-4820-b21f-376dc6d0c5d1` + +### rpro3-ahnen-import (1 Ticket(s) · import-data) +Dateien: GerbilManagerWebAPI/Import/Rpro3/rpro3-decisions.json, gerbil 509f4aae-1189-585c-b410-555e75d5d907, litter 4d3afe0a-1d78-5c55-bd99-4f216bd84afc, tools/import/_rpro3.db, tools/import/conflict-decisions.json, tools/import/merge_and_resolve.py, tools/import/rpro3_lookup.py +- [Cluster] **Stammbaum von Fast Boy unvollständig – Eltern aus RennmausPro ergänzen** (Fast Boy of Golden Lights, M/medium) — Fast Boys echte Eltern Charly × Ziwa aus _rpro3.db data-driven materialisieren/verknüpfen (statt Danjo-of-Golden-Lights × Xtra-Bounty), Großeltern Ziwa=Danjo × Isiri ergänzen; korrekte Charly-rid über RPRO3-Eltern-Relation wählen; falsche Platzhalter-Eltern + Litter 4d3afe0a erse + `id=0e139f1f-8aa7-4705-a430-0985cf9aa625` + +## Offene Rückfragen (NeedsInfo) + +- **Foto des N-Wurfs aus der Wurfchronik anzeigen — nicht vorhanden/kein Feature** (N-Wurf): Aktuell zeigt die App Fotos nur bei einzelnen Tieren (aus den Stammbaum-Dateien). Bei Würfen gibt es noch keine Fotos – und in der Wurfchronik-Datei selbst sind gar keine Bilder gespeichert. Hast du von diesem N-Wurf (Wurfdatum 28.03.2011) irgendwo ein Foto abgelegt, das du hier sehen möchtest? Wenn ja, sag mir bitte, in welchem Ordner oder welcher Datei es liegt – dann kann ich es einbauen. + `id=97a3b4d9-4859-4b0a-8e20-2c602ec2c167` +- **Jungtiere im Wurf falsch sortiert (alphabetisch statt Wurfchronik-Reihenfolge)** (C-Wurf 2010): Aktuell werden die Jungtiere eines Wurfs alphabetisch nach ihrem Namen sortiert. In welcher Reihenfolge sollen sie stattdessen erscheinen - genau so, wie sie in der Wurfchronik von oben nach unten aufgelistet sind? + `id=399dbeb2-6909-43fe-b00f-7b6a2b9fc7b2` +- **Domi: Todesdatum-Zweifel der Züchterin** (Domi): Bei Domi steht als Sterbedatum der 15.07.2012 (Mittelohrentzündung) — sie wäre damit knapp 2 Jahre alt geworden (geboren 25.06.2010). Dieses Datum stammt so aus deiner Wurfchronik. Passt es, oder erinnerst du dich an ein anderes Datum? Dann korrigiere ich es gern. + `id=929273e9-5463-4934-92ff-60cf25c71030` +- **Dante: gemeldeter fehlender Wurf ist bereits vorhanden (Q-Wurf)** (Dante): Bei Dante ist unter „Würfe“ bereits ein Wurf eingetragen: der Q-Wurf vom 13.06.2011, Mutter Malina, mit den vier Jungtieren Amy, Beatrice, Quentin (Eddy) und Quvin. Meinst du genau diesen Wurf – siehst du ihn auf Dantes Seite vielleicht gerade nicht? Oder geht es um einen anderen, zusätzlichen Wurf, den ich noch ergänzen soll? + `id=9af81ae2-464c-480b-8199-6f73fcda8619` +- **Flori als Vater von 4 Würfen verlinkt – Züchterin erwartet nur JD** (Flori von den Kleinen Chaoten): In der App ist Flori zurzeit als Vater von vier Würfen eingetragen: R-Wurf (08.10.2011, Mutter Yumi), W-Wurf (25.12.2011, Mutter Jana – hier steht Feivel), A1-Wurf (15.02.2012, Mutter Jana) und Wurf J1 (11.06.2012, Mutter Delia – hier steht JD). Alle vier kommen aus deiner Wurfchronik. Du meinst, Flori war nur bei einem Wurf der Vater (bei JD). Magst du in deiner handgeschriebenen Wurfchronik nachschauen und mir sagen, welche dieser vier Würfe wirklich von Flori stammen? Die falschen Vater-Angaben trage ich dann aus, dann verschwindet z. B. Feivel aus Floris Nachzuchten. tools/import/conflict-decisions.json,tools/import/merge_and_resolve.py,GerbilDetailPage.tsx (Nachzuchten-Ansicht) + `id=b7fbdb94-202b-488b-ab5f-6a2b868e65e3` +- **Toter Elternteil als Mutter: falscher Wurf auf Elizas Akte (Namensdublette)** (Eliza): Es gibt in den Daten mehrere Rennmäuse mit dem Namen Eliza. Der Wurf mit Vanille, Jake und Jerry (geboren am 28.02.2015, Vater Jack Jr.) ist versehentlich bei der ältesten Eliza gelandet (geboren am 20.08.2010) – diese Eliza war Anfang 2015 aber schon nicht mehr am Leben, deshalb zeigt ihre Akte fälschlich einen Wurf. Zwei andere Eliza-Mäuse kommen als echte Mutter in Frage: eine geboren am 31.08.2013 und eine geboren am 13.02.2014. Welche der beiden hat diesen Wurf bekommen? + `id=b22896bc-755c-48a2-aa01-e3788453feb2` +- **A-Wurf: gemeldete Geburtenzahl (4/2 Todgeburten) widerspricht 3 eingetragenen Jungtieren** (A-Wurf): Im A-Wurf (18.02.2010) sind aktuell drei männliche Jungtiere eingetragen: Alvin, JackSack und JackJack. Du schreibst, es wurden 4 Junge geboren, davon 2 Todgeburten. Damit ich die Zahlen richtig eintrage: Kamen die 2 tot geborenen Jungtiere zusätzlich zu diesen drei dazu (dann wären es insgesamt 5 Geburten)? Oder gehört eines der drei genannten Tiere nicht in diesen Wurf, sodass es genau 4 Geburten waren? + `id=a93938a6-f2d1-4fb6-ba0f-c96b72671236` +- **JackJack und JackSack im A-Wurf sind dasselbe Tier (Dublette)** (A-Wurf): Kurze Rückfrage zu diesem Tier aus dem A-Wurf: In der Wurfchronik steht es mit zwei leicht unterschiedlichen Namen – einmal „JackJack" (auf der Wurf-Übersicht) und einmal „JackSack" (auf der Einzelseite mit Farbe, Genetik und Abgabe an Larissa Draeger). Beide meinen dasselbe Tier, geboren am 18.02.2010. Wie heißt die Rennmaus richtig? Sobald du mir das sagst, führe ich die beiden Einträge zu einem einzigen zusammen. + `id=0cf6b838-3ee1-4ec0-ac9d-43901e6b5320` +- **Erwerb-Daten aus RennmausPro für Bodo, Coco, JackJack übernehmen** (Bodo): Ich trage dir die Erwerbs-Daten (woher das Tier kam, das Datum und – falls vorhanden – Preis oder eine Notiz) gerne so in die App ein, wie sie bei „Erwerb" in RennmausPro stehen. In der RennmausPro-Sicherung, die mir vorliegt, finde ich Bodo, Coco und JackJack aber nicht. Magst du mir für die drei jeweils kurz aufschreiben, was bei „Erwerb" steht (Datum, von wem bzw. „eigene Zucht", und ggf. Preis oder Notiz)? Dann pflege ich es ein. + `id=efe47d3f-6da8-4928-a05d-875d7a410a44` +- **Blacky: Fotos aus RennerPro fehlen (nur Dateinamen, keine Bilddateien)** (Blacky von PZ Seligenstadt): Die RennerPro-Daten, die wir vorliegen haben, nennen zwar die Dateinamen von Blackys Fotos (blacky.jpg, B1.JPG, bla.JPG), enthalten die Bilder selbst aber nicht. Wo liegen denn die echten Foto-Dateien aus deinem RennerPro-Programm - zum Beispiel in einem Bilder-Ordner auf dem Rechner oder auf dem TrueNAS? Sobald wir an diesen Ordner kommen, koennen wir Blackys Fotos (und die der anderen Tiere) uebernehmen. + `id=00493a79-b4e1-4856-b0c4-9e08508ca484` +- **Alvins „Nachkommen mit Zuchteinsatz“ zeigt falsche Tiere (Namensdopplung)** (Alvin von den kleinen Chaoten): Bei Alvin zeigt die App unter „Nachkommen mit Zuchteinsatz“ gerade Hero, Flippi und Gimli — aber zusätzlich auch Sunny, Zora, Lexy und Zorro, während Lee und Sam fehlen. Damit ich es richtig zuordnen kann, eine kurze Rückfrage: Hatten Lee und Sam bei dir eigene Babys (jeweils einen eigenen Wurf)? Und stimmt es, dass Sunny, Zora, Lexy und Zorro NICHT in deiner Zucht waren (deren angezeigte Würfe gehören vermutlich zu gleichnamigen, später geborenen Mäusen)? + `id=d1069edb-1001-4e24-b436-147948b85c8a` +- **Unbekanntes Tier „Nymphs" aus Wurfchronik Teil 2 (Phantom-Zuchtrüde)** (Nymphs): Das Tier „Nymphs" habe ich aus deiner eingescannten Wurfchronik Teil 2 (Seite 26) übernommen. Dort taucht es nur als Zuchtrüde auf (im Original „CP-Nymphs") — ganz ohne Geburtsdatum, Farbe, Eltern oder zugeordneten Wurf. Kannst du dich an einen Rennmaus-Rüden namens „Nymphs" erinnern, oder ist das ein Lese-/Übertragungsfehler, den ich entfernen soll? + `id=ca8a1c48-1407-44ed-908f-986cbc0e5e0b` +- **Hero (Alvins Sohn): Abgabe-/Sterbenotiz widerspricht D3-Wurf-Vaterschaft** (Alvin von den kleinen Chaoten): Bei Hero (Sohn von Alvin, geboren am 31.10.2010) steht in den Notizen, dass er am 1.4.2011 an Brigitta Struve abgegeben wurde und dort am 4.4.2014 verstarb. Gleichzeitig ist er als Vater des D3-Wurfs mit Mira vom 22.11.2013 eingetragen - also mitten in der Zeit, in der er laut Notiz schon bei Brigitta war. Das passt zeitlich nicht zusammen. Was stimmt: Blieb Hero bei dir und hatte mit Mira seinen einen Wurf (dann ist die Abgabe an Brigitta ein Versehen)? Oder wurde er 2011 abgegeben - und der D3-Wurf gehoert eigentlich zu einem anderen Rennmaeuserich? + `id=72d18800-342b-4935-9aad-311a1607b3da` +- **Extern zugekauftes Zuchttier (Blacky von PZ Seligenstadt) zeigt Wurf-Leiste** (Blacky von PZ Seligenstadt): Kurze Frage zu Blacky von PZ Seligenstadt: Hast du früher selbst mit ihm gezüchtet, sind also bei dir eigene Würfe mit ihm gefallen? Oder ist er nur ein Vorfahre aus einer fremden Zucht, den du selbst nie zur Zucht eingesetzt hast? Danach richte ich, ob und welche Würfe bei ihm angezeigt werden. + `id=7dedec07-99b8-4b0c-9f51-bf14a583ec67` +- **Shelly: Name unvollständig – Wurfchronik-Rufname nicht mit Stammbaum-Vollnamen gemergt** (Shelly): In Ihrem Stammbaum taucht dieses Tier mit einem laengeren Namen auf: einmal als „Sammy gen. Shelly v.d. bunten Fellnasen" und einmal als „Sam gen. Shelly v.d. bunten Fellnasen". In der Wurfchronik (Vater vom R5-Wurf) steht aber nur „Shelly". Wie lautet sein vollstaendiger, richtiger Name genau – und heisst er „Sam" oder „Sammy"? + `id=7deec818-ad29-4053-86fb-5e67ac180e70` +- **Tai als verstorben markieren – Todesdatum fehlt in RennerPro-Kopie** (Tai): Danke für den Hinweis! In unserer RennerPro-Kopie ist Tai (Tai gen. Karlchen, geboren am 01.10.2011) leider noch nicht als verstorben eingetragen – dort steht kein Todesdatum. Kannst du mir bitte sagen, wann Tai ungefähr gestorben ist (Monat und Jahr reichen), und falls bekannt woran? Dann trage ich das direkt ein. GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs,GerbilManagerWebAPI/Import/IngestResolvedService.cs,tools/import/conflict-decisions.json,tools/import/_rpro3.db (Backup 27.06., ohne Todesdatum für Tai) + `id=b222b459-57c5-40ea-88b0-0fa696869c6b` +- **Aaren: Stammbaum wirkt leer (keine Eltern hinterlegt)** (Aaren): Zu Aaren (geb. 20.04.2024): In der App ist er als zugekauftes Zuchttier aus einem Kaufvertrag eingetragen - seine Eltern (Vater/Mutter) sind bei uns bisher nicht hinterlegt. Deshalb bleibt seine Ahnentafel leer, es gibt oberhalb von ihm nichts zum Anzeigen. Weißt du, wie Aarens Mutter und Vater heißen? Dann können wir sie ergänzen und der Stammbaum füllt sich. Falls er mit unbekannten Eltern gekauft wurde, ist der leere Stammbaum korrekt so. + `id=7a9831f6-4d68-41e7-add2-bc06393e6ed6` +- **Drake Jr: Stammbaum leer, weil keine Eltern hinterlegt** (Drake Jr): Ja, Drake Jr hat einen Wurf gezeugt: den 'Q3-Wurf' am 21.06.2014 mit Jacky v.d. Kleinen Chaoten (12 Jungtiere, darunter Quebec, Pünktchen und Kai-Uwe). Der Stammbaum bleibt aber leer, weil bei Drake Jr keine Eltern hinterlegt sind - in der Wurfchronik (Seite 9) war er nur als Elternteil genannt, ohne eigene Abstammung. Weißt du, wer die Eltern von Drake Jr waren? Zum Beispiel: Ist er ein Nachkomme von 'Drake von den kleinen Chaoten' (geb. 06.06.2012)? Dann können wir seinen Stammbaum ergänzen. Drake Jr hat keine verknüpften Eltern (litterId=null); als reiner Elternteil-Eintrag aus der Wurfchronik importiert, fehlt seine eigene Abstammung, daher zeigt der Ahnenbaum nur Platzhalter. + `id=31811914-257b-4c83-bb42-85cc2d140277` +- **Christinas Eltern/Stammbaum aus RennmausPro verknüpfen** (Christina): Ich habe die RennmausPro-Daten durchsucht, finde dort aber keinen Eintrag genau unter dem Namen „Christina". Am ehesten passt eine „Cristina" (ohne „h", weiblich, geboren am 16.09.2014, mit fremder Herkunft). Ist das dieselbe Maus wie deine Christina? Falls ja, sag mir bitte kurz Bescheid – dann verknüpfe ich ihre Eltern/den Stammbaum. Falls nein, nenn mir einfach die Namen von Christinas Mutter und Vater, dann trage ich sie ein. ["tools/import/_rpro3.db", "tools/import/conflict-decisions.json", "tools/import/merge_and_resolve.py", "GerbilManagerWebAPI/Import/Rpro3"] + `id=a78eaa79-db55-4da1-99b0-ed954f9b07c7` +- **Berni ist vermutlich Rufname von Montana (Dublette)** (Berni): Bei diesem Tier „Berni" steht in deiner Wurfchronik der Wurf „M10 – Berni (Montana) + Frodo" – also Berni mit Montana in Klammern. Ist „Berni" einfach der Rufname deiner „Montana v.d. Kleinen Chaoten" (geboren 14.06.2015), sodass beide dasselbe Tier sind? Dann führen wir die beiden Einträge zusammen. + `id=0921dba5-d3a7-47d7-ae95-0c6e535815d1` +- **Zhuāngzǐ: Eltern/Wurf fehlen — Wurf X16 (Nisha × Zenon von Elea) eindeutig bestimmt** (Zhuāngzǐ von den Kleinen Chaoten): Ich habe in der Wurfchronik den Wurf vom 21.02.2020 gefunden: Zhuāngzǐs Eltern sind demnach Nisha (of Black Forest) als Mutter und Zenon von Elea als Vater — im Chronik-Eintrag stehen „Zhuāng Zhōu" (= Zhuāngzǐ) und Zaibunissa ausdrücklich als die behaltenen Jungtiere dieses Wurfs. „Gandari" und „Gaidas" finde ich allerdings nirgends in der App. Kannst du mir kurz sagen, wer Gandari und Gaidas sind und wie sie mit Zhuāngzǐ zusammenhängen (z. B. seine Wurfgeschwister aus genau diesem Wurf, die noch fehlen)? + `id=785851d0-7b37-45a7-adcc-e78db9c3c4b9` +- **Chiko: leerer Stammbaum, reiner Wurfchronik-Partner ohne weitere Daten** (Chiko): Chiko taucht bei dir nur ein einziges Mal auf: als Vater von Sherrys Wurf am 26.04.2018 (aus der Wurfchronik Teil 2, Seite 35). Zu Chiko selbst gibt es sonst keine Angaben - kein Geburtsdatum, keine Farbe und keine eigenen Eltern, deshalb bleibt sein Stammbaum leer. Weisst du noch, woher Chiko kam (z. B. zugekauft und von wem) und welche Farbe er hatte? Dann koennen wir seine Akte ergaenzen. Falls er einfach nur Sherrys Partner ohne weitere Daten war, sag kurz Bescheid - dann lassen wir es so. + `id=bc859109-32df-4506-9f7d-6f6415c00f02` +- **Krümmel (F-Wurf 2010): kein Todesdatum in keiner Quelle** (F-Wurf 2010): Hallo! Ich habe jetzt auch die RennmausPro-Dateien durchgeschaut, wie du vorgeschlagen hast. Dort gibt es zwar zwei Tiere namens „Krümel"/„Krümmel" – die sind aber viel älter (geboren 2003 bzw. 2005) und haben ganz andere Nachkommen, das ist also nicht unser Krümmel aus dem F-Wurf 2010. Für unseren Krümmel (geboren am 21.09.2010) ist damit weder in der handschriftlichen Wurfchronik noch in den RennmausPro-Dateien ein Todesdatum eingetragen. Weißt du vielleicht noch ungefähr, wann er gestorben ist (Jahr oder Alter reicht schon)? Dann tragen wir es gerne nach – ansonsten lassen wir das Feld einfach leer. + `id=21c8ff24-cc03-4508-b6c6-849ab36ba5b2` +- **Percy of little runners: Mutter nicht verknüpft (Stammbaum-Fehlparse + Selbstreferenz)** (Percy of little runners): Bei Percy (of little runners, geboren am 16.12.2017) konnte die automatische Stammbaum-Erkennung die Mutter leider nicht sicher zuordnen: sie hat sich im Diagramm „verheddert" und versehentlich Percy selbst in die Mutter-Rolle gesetzt. Als Vater erkennen wir Rasputin of little runners. Kannst du mir bitte sagen, wie Percys Mutter mit vollem Namen heißt? Dann trage ich sie fest ein. + `id=e87a6da5-0ff1-46b6-b4ec-5d05a1605f5f` +- **Dusty: Stammbaum fehlt – vermutete Dublette (m. Zuchtbock vs. w. Jungtier)** (Dusty): Bei dir gibt es zwei Mäuse mit dem Namen „Dusty": einmal den Zuchtbock Dusty, der 2015 Vater der Würfe mit Bleach, Sanza und Harumi war (genau bei ihm fehlt der Stammbaum), und einmal eine Dusty, die am 22.11.2013 im D3-Wurf aus Hero × Mira geboren wurde. Ist das dieselbe Maus? Falls ja, können wir Dustys Eltern (Hero und Mira) verknüpfen und sein Stammbaum wird angezeigt – die genannte Dusty ist nämlich als Weibchen erfasst, der Zuchtbock aber als Männchen, deshalb sind sie bisher getrennt. + `id=034b4988-5447-4c54-ad28-9a6708133517` + +## Ohne Triage-Ergebnis (nachziehen) + +- Domi `id=7c6d53ac-c353-4a6f-874f-96d363e732d0` +- A-Wurf `id=a555dd0a-f693-487c-8c7f-db8e29b363f5` +- Blacky von PZ Seligenstadt `id=690946e6-714f-4816-b6b9-b76a5da6057a` \ No newline at end of file diff --git a/gerbil-manager-web/e2e/mock-api.ts b/gerbil-manager-web/e2e/mock-api.ts index de14a45..952df62 100644 --- a/gerbil-manager-web/e2e/mock-api.ts +++ b/gerbil-manager-web/e2e/mock-api.ts @@ -414,6 +414,44 @@ export async function installMockApi(page: Page): Promise { return json(route, 200, result) } + // GEPRÜFTE TIERE: „vollständig korrekt"-Markierung / Schutz (/verified-gerbils). + if (path === '/verified-gerbils' && method === 'GET') { + return json(route, 200, [...db.verified].reverse()) + } + const verMatch = path.match(/^\/verified-gerbils\/([^/]+)$/) + if (verMatch) { + const gid = decodeURIComponent(verMatch[1]) + const vIdx = db.verified.findIndex((v) => v.gerbilId === gid) + if (method === 'GET') { + return vIdx >= 0 ? json(route, 200, db.verified[vIdx]) : json(route, 404, { title: 'Not Found' }) + } + if (method === 'POST') { + const g = db.gerbils.find((x) => x.id === gid) + if (!g) return json(route, 404, { title: 'Not Found' }) + const body = (request.postDataJSON() ?? {}) as { note?: string | null } + const row = { + gerbilId: gid, + entityName: g.name ?? null, + isVerified: true, + status: 'unchanged', + verifiedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + note: body.note ?? null, + protectedFields: ['name', 'gender', 'dateOfBirth', 'genotype'], + importDiff: [], + } + if (vIdx >= 0) db.verified[vIdx] = row + else db.verified.push(row) + return json(route, 200, row) + } + if (method === 'DELETE') { + if (vIdx < 0) return json(route, 404, { title: 'Not Found' }) + db.verified.splice(vIdx, 1) + return json(route, 204) + } + return json(route, 405) + } + // FEEDBACK: "Fehler melden" — POST persistiert, GET listet (neueste zuerst). if (path === '/feedback') { if (method === 'POST') { diff --git a/gerbil-manager-web/e2e/mock-data.ts b/gerbil-manager-web/e2e/mock-data.ts index 19c8f9c..67dbbc5 100644 --- a/gerbil-manager-web/e2e/mock-data.ts +++ b/gerbil-manager-web/e2e/mock-data.ts @@ -80,6 +80,8 @@ export interface MockDb { namesConfigured: boolean // FEEDBACK: "Fehler melden" — gesammelte Berichte (POST /feedback) feedback: Record[] + // GEPRÜFTE TIERE: „vollständig korrekt"-Markierungen / Schutz (/verified-gerbils) + verified: Record[] // RPRO3: RennmausPro-III-Import — feste Auswertung + Import-Ergebnis für die UI-Specs. rpro3: { analyze: Record @@ -222,10 +224,10 @@ export function seedDb(): MockDb { ] const litters: Litter[] = [ - { id: 'w-zwillinge', name: 'Wurf Z', date: '2020-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'opa-w', motherId: 'oma-u' }, - { id: 'w-inzucht', name: 'Wurf I', date: '2021-06-01', totalBorn: 3, expectedGoHomeDate: null, notes: null, fatherId: 'zwilling-bock', motherId: 'zwilling-maus' }, + { id: 'w-zwillinge', name: 'Wurf Z', date: '2020-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'opa-w', motherId: 'oma-u', showInChronicle: true }, + { id: 'w-inzucht', name: 'Wurf I', date: '2021-06-01', totalBorn: 3, expectedGoHomeDate: null, notes: null, fatherId: 'zwilling-bock', motherId: 'zwilling-maus', showInChronicle: true }, { - id: 'w-kruemel', name: 'Wurf K', date: '2025-03-12', totalBorn: 5, expectedGoHomeDate: '2025-04-16', notes: null, fatherId: 'fridolin', motherId: 'luna', deathsWithin8Weeks: 1, + id: 'w-kruemel', name: 'Wurf K', date: '2025-03-12', totalBorn: 5, expectedGoHomeDate: '2025-04-16', notes: null, fatherId: 'fridolin', motherId: 'luna', deathsWithin8Weeks: 1, stillborn: 1, showInChronicle: true, // NACHVERFOLGUNG: Datenherkunft des Wurfs (vom Import erzeugter JSON-String). provenance: JSON.stringify({ sourceFiles: ['Wurfchronik Teil 1_page_0009.md', 'Wurfchronik Teil 1_page_0028.md'], @@ -239,13 +241,16 @@ export function seedDb(): MockDb { ], }), }, - { id: 'w-fridolin', name: 'Wurf F', date: '2023-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'balu', motherId: 'maja' }, - { id: 'w-luna', name: 'Wurf L', date: '2023-08-15', totalBorn: 6, expectedGoHomeDate: null, notes: null, fatherId: 'karlsson', motherId: 'smilla' }, - { id: 'w-balu', name: 'Wurf B', date: '2021-04-20', totalBorn: 3, expectedGoHomeDate: null, notes: null, fatherId: 'anton', motherId: 'greta' }, + { id: 'w-fridolin', name: 'Wurf F', date: '2023-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'balu', motherId: 'maja', showInChronicle: true }, + { id: 'w-luna', name: 'Wurf L', date: '2023-08-15', totalBorn: 6, expectedGoHomeDate: null, notes: null, fatherId: 'karlsson', motherId: 'smilla', showInChronicle: true }, + { id: 'w-balu', name: 'Wurf B', date: '2021-04-20', totalBorn: 3, expectedGoHomeDate: null, notes: null, fatherId: 'anton', motherId: 'greta', showInChronicle: true }, // Anton ist auch Karlssons Vater -> gemeinsamer Vorfahre (Inzucht-Demo) - { id: 'w-karlsson', name: 'Wurf C', date: '2022-02-02', totalBorn: 5, expectedGoHomeDate: null, notes: null, fatherId: 'anton', motherId: 'frieda' }, - { id: 'w-anton', name: 'Wurf A', date: '2019-07-07', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'emil', motherId: 'hilde' }, - { id: 'w-emil', name: 'Wurf E', date: '2017-03-03', totalBorn: 2, expectedGoHomeDate: null, notes: null, fatherId: 'max', motherId: null }, + { id: 'w-karlsson', name: 'Wurf C', date: '2022-02-02', totalBorn: 5, expectedGoHomeDate: null, notes: null, fatherId: 'anton', motherId: 'frieda', showInChronicle: true }, + { id: 'w-anton', name: 'Wurf A', date: '2019-07-07', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'emil', motherId: 'hilde', showInChronicle: true }, + { id: 'w-emil', name: 'Wurf E', date: '2017-03-03', totalBorn: 2, expectedGoHomeDate: null, notes: null, fatherId: 'max', motherId: null, showInChronicle: true }, + // WURFCHRONIK-FILTER: dieser Wurf ist NICHT in der Wurfchronik (showInChronicle: false) + // -> taucht in der Wurf-Liste nicht auf, aber auf Fridolins Tier-Detailseite (parentLitters) weiterhin. + { id: 'w-versteckt', name: 'Wurf V', date: '2024-02-02', totalBorn: 2, expectedGoHomeDate: null, notes: null, fatherId: 'fridolin', motherId: 'luna', showInChronicle: false }, ] const enclosures: Enclosure[] = [ @@ -426,6 +431,8 @@ export function seedDb(): MockDb { ], saleAdConfigured: true, namesConfigured: true, + // GEPRÜFTE TIERE: startet leer; die Verify-Specs markieren über POST. + verified: [], // FEEDBACK-TICKETS: ein offenes + ein gelöstes Ticket (GET liefert neueste zuerst → // db.feedback wird im Mock umgekehrt; das offene steht hier zuletzt = erscheint oben). feedback: [ diff --git a/gerbil-manager-web/e2e/verified.spec.ts b/gerbil-manager-web/e2e/verified.spec.ts new file mode 100644 index 0000000..9066363 --- /dev/null +++ b/gerbil-manager-web/e2e/verified.spec.ts @@ -0,0 +1,36 @@ +/** + * GEPRÜFTE TIERE: „vollständig korrekt"-Markierung auf der Rennmausakte (Badge + Toggle) + * und die Übersichtsseite unter Hilfe. Mock-gebunden → skipUnlessMock. + */ +import { de, expect, gotoSection, acceptNextDialog, skipUnlessMock, test } from './fixtures' + +const vt = de.verified + +test.describe('Geprüfte Tiere', () => { + test('Tier als vollständig korrekt markieren und Markierung entfernen', async ({ page }) => { + skipUnlessMock() + await gotoSection(page, de.nav.gerbils) + + // Erste Rennmaus aus der Liste öffnen. + await page.locator('.gerbil-card').first().click() + + // Prüfstatus-Karte ist sichtbar und erklärt sich selbst. + await expect(page.getByRole('heading', { name: vt.sectionTitle })).toBeVisible() + + // Markieren → Bestätigungs-Badge erscheint. + await page.getByRole('button', { name: vt.markAction }).click() + await expect(page.getByText(vt.verifiedBadge).first()).toBeVisible() + + // Markierung entfernen (Klartext-Bestätigung akzeptieren) → Badge verschwindet. + acceptNextDialog(page) + await page.getByRole('button', { name: vt.unmarkAction }).click() + await expect(page.getByText(vt.verifiedBadge)).toHaveCount(0) + }) + + test('Übersichtsseite unter Hilfe erreichbar', async ({ page }) => { + skipUnlessMock() + await page.goto('/hilfe/verifizierte-tiere') + await expect(page.getByRole('heading', { name: vt.pageTitle })).toBeVisible() + await expect(page.getByText(vt.pageIntro1)).toBeVisible() + }) +}) diff --git a/gerbil-manager-web/src/App.tsx b/gerbil-manager-web/src/App.tsx index bd4c7dc..83df9fc 100644 --- a/gerbil-manager-web/src/App.tsx +++ b/gerbil-manager-web/src/App.tsx @@ -21,6 +21,7 @@ import StatistikPage from './pages/StatistikPage' import HilfePage from './pages/HilfePage' import TicketsPage from './pages/TicketsPage' import RennmausProImportPage from './pages/RennmausProImportPage' +import VerifizierteTierePage from './pages/VerifizierteTierePage' import FarbkatalogPage from './pages/FarbkatalogPage' import VertraegeListPage from './pages/VertraegeListPage' import VertragWizardPage from './pages/VertragWizardPage' @@ -83,6 +84,8 @@ export default function App() { } /> {/* RPRO3: RennmausPro-III-Backup-Import (Rubrik unter „Hilfe") */} } /> + {/* GEPRÜFTE TIERE: „vollständig korrekt"-Übersicht + Import-Abweichungen */} + } /> } /> {/* FEAT-13: Abgabeverträge + Einstellungen (Zuchtprofil) */} diff --git a/gerbil-manager-web/src/api/types.ts b/gerbil-manager-web/src/api/types.ts index 1d22dba..f49d255 100644 --- a/gerbil-manager-web/src/api/types.ts +++ b/gerbil-manager-web/src/api/types.ts @@ -68,6 +68,11 @@ export interface Gerbil { * null = manuell angelegtes Tier / keine Importdaten. */ provenance?: string | null + /** + * Reihenfolge des Jungtiers innerhalb seines Wurfs (aus der Wurfchronik, nach + * Geburtsgewicht). null = unbekannt → alphabetischer Fallback. + */ + birthOrder?: number | null } /** @@ -191,6 +196,15 @@ export interface Litter { motherId: string | null /** LITTER-MORTALITY: pups that died within the first 8 weeks. */ deathsWithin8Weeks?: number | null + /** Totgeburten (tot geboren) — aus der xlsx-Wurfchronik importiert. */ + stillborn?: number | null + /** + * WURFCHRONIK-FILTER: true = in der Wurfchronik (Wurf-Liste) sichtbar, + * false = ausgeblendet (z. B. fremde/pedigree-only Würfe). Server-seitig + * Gridify-filterbar (`showInChronicle==true`). Auf der Tier-Detailseite werden + * versteckte Würfe trotzdem gezeigt. Absent = behandeln wie true (Backend-Default). + */ + showInChronicle?: boolean /** * NACHVERFOLGUNG: Datenherkunft des Import-Eintrags als JSON-String (vom * Python-Merge erzeugt; siehe {@link EntityProvenance}). null = manuell angelegt. @@ -208,10 +222,44 @@ export interface CreateLitter { fatherId?: string | null motherId?: string | null deathsWithin8Weeks?: number | null + stillborn?: number | null + /** WURFCHRONIK-FILTER: false = aus der Wurfchronik ausblenden (Default true). */ + showInChronicle?: boolean } export type UpdateLitter = Partial +/** + * GEPRÜFTE TIERE: Status einer „vollständig korrekt"-Markierung / eines Schutzes + * relativ zum Rohimport. "unchanged" = Import stimmt mit dem geprüften Stand überein; + * "drifted" = Import weicht ab (siehe importDiff); "missing" = das Tier existiert im + * Import nicht mehr. + */ +export type VerifiedStatus = 'unchanged' | 'drifted' | 'missing' + +/** Ein Feld-Unterschied zwischen geprüftem Stand (golden) und Rohimport. */ +export interface SnapshotDiff { + path: string + goldenValue: string | null + importValue: string | null +} + +/** Override-/Prüfstatus eines Tiers (Backend: GET /verified-gerbils[/{id}]). */ +export interface VerifiedGerbil { + gerbilId: string + entityName: string | null + /** true = „vollständig korrekt" (zertifiziert + voller Freeze); false = „geschützt" (Bearbeitung, feldweise). */ + isVerified: boolean + status: VerifiedStatus + verifiedAt: string | null + updatedAt: string + note: string | null + /** Eigene-Feld-Schlüssel, die gegen den Import eingefroren sind. */ + protectedFields: string[] + /** Wo der Rohimport vom geprüften Stand abweicht (nur verifizierte Tiere). */ + importDiff: SnapshotDiff[] +} + /** Paged list envelope returned by Gridify list endpoints. */ export interface Paged { items: T[] diff --git a/gerbil-manager-web/src/api/verified.ts b/gerbil-manager-web/src/api/verified.ts new file mode 100644 index 0000000..437943b --- /dev/null +++ b/gerbil-manager-web/src/api/verified.ts @@ -0,0 +1,37 @@ +/** + * GEPRÜFTE TIERE: API-Client für die „vollständig korrekt"-Markierung + den Schutz + * manueller Änderungen (/verified-gerbils). Eine markierte Maus wird vom automatischen + * Import nicht mehr verändert (Freeze). + */ +import { api, ApiError } from './client' +import type { VerifiedGerbil } from './types' + +const RESOURCE = '/verified-gerbils' + +/** Alle geprüften/geschützten Tiere mit Drift-Status. */ +export function listVerified(): Promise { + return api.get(RESOURCE) +} + +/** + * Prüfstatus eines einzelnen Tiers. Gibt null zurück, wenn das Tier weder markiert + * noch geschützt ist (Backend liefert dann 404) — dann gibt es einfach keinen Status. + */ +export async function getVerifiedStatus(gerbilId: string): Promise { + try { + return await api.get(`${RESOURCE}/${gerbilId}`) + } catch (e) { + if (e instanceof ApiError && e.status === 404) return null + throw e + } +} + +/** Als „vollständig korrekt" markieren (Freeze aller eigenen Felder). Optionaler Hinweis. */ +export function verifyGerbil(gerbilId: string, note?: string | null): Promise { + return api.post(`${RESOURCE}/${gerbilId}`, { note: note ?? null }) +} + +/** Markierung/Schutz entfernen (nicht-destruktiv; nächster Import macht wieder import-getrieben). */ +export function unverifyGerbil(gerbilId: string): Promise { + return api.delete(`${RESOURCE}/${gerbilId}`) +} diff --git a/gerbil-manager-web/src/format/gerbilName.ts b/gerbil-manager-web/src/format/gerbilName.ts index 6e35f49..2020b73 100644 --- a/gerbil-manager-web/src/format/gerbilName.ts +++ b/gerbil-manager-web/src/format/gerbilName.ts @@ -10,7 +10,10 @@ type NameParts = Pick /** Own-bred = no external origin recorded (no origin contact, no freetext breeder). */ export function isOwnBred(g: NameParts): boolean { - return !g.originContactId && !(g.originBreeder && g.originBreeder.trim()) + if (g.originContactId) return false + const b = g.originBreeder?.trim().toLowerCase() + if (!b) return true + return b.includes('chaoten') } /** diff --git a/gerbil-manager-web/src/genetics/catalog.ts b/gerbil-manager-web/src/genetics/catalog.ts index e1d74be..710e302 100644 --- a/gerbil-manager-web/src/genetics/catalog.ts +++ b/gerbil-manager-web/src/genetics/catalog.ts @@ -25,6 +25,7 @@ import { makeGenotype, resolveAllelePair, toDisplayString, + fromDisplayString, wildType, type AllelePair, type Genotype, @@ -335,6 +336,20 @@ export function genotypeToFarbschlag(g: Genotype): string { return farbschlagFor(g).name } +export function formatVarietyName(baseName: string, genotypeStr: string | null | undefined): string { + if (!baseName) return '' + if (!genotypeStr?.trim()) return baseName + try { + const g = fromDisplayString(genotypeStr) + const modifiers: string[] = [] + if (locusToken(g, 'Sp') === 'Sp') modifiers.push('Schecke') + if (locusToken(g, 'Re') === 'Re') modifiers.push('Rex') + return [baseName, ...modifiers].join(' ') + } catch { + return baseName + } +} + /** * A representative full genotype for a catalog entry: each specified locus is * homozygous for its token allele; unspecified loci take the wild-type allele. diff --git a/gerbil-manager-web/src/genetics/index.ts b/gerbil-manager-web/src/genetics/index.ts index b5a2eef..8e99ff8 100644 --- a/gerbil-manager-web/src/genetics/index.ts +++ b/gerbil-manager-web/src/genetics/index.ts @@ -41,6 +41,7 @@ export type { PhenotypeTokens } from './phenotype' export { farbschlagFor, genotypeToFarbschlag, + formatVarietyName, representativeGenotype, farbschlagImageUrl, findVariety, diff --git a/gerbil-manager-web/src/pages/BeckenDetailPage.tsx b/gerbil-manager-web/src/pages/BeckenDetailPage.tsx index 67f46d3..91f6034 100644 --- a/gerbil-manager-web/src/pages/BeckenDetailPage.tsx +++ b/gerbil-manager-web/src/pages/BeckenDetailPage.tsx @@ -15,6 +15,7 @@ import { useApi, useMutation } from '../hooks/useApi' import { formatDate } from '../format/labels' import { useToast } from '../components/toast' import EnclosurePhotosSection from '../components/EnclosurePhotosSection' +import { formatVarietyName } from '../genetics' /** true, wenn die nächste fällige Reinigung am/vor heute liegt. */ function isCleaningDue(nextCleaningDate: string | null): boolean { @@ -201,7 +202,7 @@ export default function BeckenDetailPage() { {g.name} - {g.colorVarietyId ? (colorNameById.get(g.colorVarietyId) ?? '—') : '—'} + {g.colorVarietyId ? formatVarietyName(colorNameById.get(g.colorVarietyId) ?? '—', g.genotype) : '—'} diff --git a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx index 3bbfc51..093a649 100644 --- a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx +++ b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx @@ -1,7 +1,8 @@ import { useMemo, useState, type ReactNode } from 'react' import { Link, useParams } from 'react-router-dom' import { de } from '../strings/de' -import { getGerbil, updateGerbil } from '../api/gerbils' +import { getGerbil, updateGerbil, listGerbils } from '../api/gerbils' +import { getVerifiedStatus, verifyGerbil, unverifyGerbil } from '../api/verified' import { listLitters as listLittersPaged } from '../api/litters' import { listColorVarieties, listContacts, listEnclosures, listLitters } from '../api/lookups' import { useApi, useMutation } from '../hooks/useApi' @@ -10,6 +11,7 @@ import { ALL_TRAITS, TRAIT_CATEGORIES } from '../format/traits' import { fromDisplayString, genotypeToFarbschlag, + formatVarietyName, displayGenotypeSafe, toDisplayString, hasUnknown, @@ -73,6 +75,10 @@ export default function GerbilDetailPage() { const gerbil = useApi(() => getGerbil(id), [id]) const forSale = useMutation(() => updateGerbil(id, { status: 'ForSale' })) + // GEPRÜFTE TIERE: „vollständig korrekt"-Markierung / Schutz-Status dieses Tiers. + const verifiedStatus = useApi(() => getVerifiedStatus(id), [id]) + const verify = useMutation(() => verifyGerbil(id)) + const unverify = useMutation(() => unverifyGerbil(id)) // FEAT-14: Charakterbogen (persisted on the Gerbil). const [charTraits, setCharTraits] = useState([]) const [charNote, setCharNote] = useState('') @@ -92,6 +98,51 @@ export default function GerbilDetailPage() { [id], ) + const breedingParentIds = useMemo(() => { + const ids = new Set() + for (const l of litters.data ?? []) { + if (l.fatherId) ids.add(l.fatherId) + if (l.motherId) ids.add(l.motherId) + } + return ids + }, [litters.data]) + + const offspring = useApi( + () => { + const litterIds = (parentLitters.data?.items ?? []).map((l) => l.id) + if (litterIds.length === 0) { + return Promise.resolve({ items: [], totalCount: 0, page: 1, pageSize: 100 }) + } + const filter = litterIds.map((lid) => `litterId=${lid}`).join('|') + return listGerbils({ filter, pageSize: 200 }) + }, + [parentLitters.data], + ) + + // ZUCHTPARTNER: die jeweils anderen Elterntiere der eigenen Würfe (distinct). + const partnerIds = useMemo(() => { + const ids = new Set() + for (const l of parentLitters.data?.items ?? []) { + const pid = l.fatherId === id ? l.motherId : l.fatherId + if (pid && pid !== id) ids.add(pid) + } + return [...ids] + }, [parentLitters.data, id]) + const partners = useApi( + () => + partnerIds.length === 0 + ? Promise.resolve({ items: [], totalCount: 0, page: 1, pageSize: 100 }) + : listGerbils({ filter: partnerIds.map((pid) => `id=${pid}`).join('|'), pageSize: 100 }), + [partnerIds], + ) + + const breedingOffspring = useMemo(() => { + return (offspring.data?.items ?? []).filter( + (child) => child.status === 'Breeding' || breedingParentIds.has(child.id), + ) + }, [offspring.data, breedingParentIds]) + + const colorName = useMemo( () => new Map((colorVarieties.data ?? []).map((c) => [c.id, c.name])), [colorVarieties.data], @@ -179,7 +230,8 @@ export default function GerbilDetailPage() { })() const lookup = (map: Map, key: string | null) => (key ? (map.get(key) ?? '—') : '—') - const storedColorName = g.colorVarietyId ? (colorName.get(g.colorVarietyId) ?? null) : null + const baseColorName = g.colorVarietyId ? (colorName.get(g.colorVarietyId) ?? null) : null + const storedColorName = baseColorName ? formatVarietyName(baseColorName, g.genotype) : null // Geschwisterverpaarung: Vater und Mutter dieses Tiers sind Vollgeschwister — // erkennbar an gemeinsamer litterId ODER gleichem Geburtsdatum (starkes Indiz: @@ -229,6 +281,15 @@ export default function GerbilDetailPage() {

{gerbilName(g) || de.pages.gerbils.nameless}

+ {g.isResident !== false && (parentLitters.data?.items.length ?? 0) > 0 && ( + + {t.detail.zuchttierBadge} + + )} {statusLabel(g.status)} @@ -237,6 +298,33 @@ export default function GerbilDetailPage() { {de.pages.gerbils.externalBadge} )} + {verifiedStatus.data?.isVerified && ( + + {de.verified.verifiedBadge} + + )} + {verifiedStatus.data && !verifiedStatus.data.isVerified && ( + + {de.verified.protectedBadge} + + )} + {verifiedStatus.data?.status === 'drifted' && ( + + {de.verified.driftBadge} + + )}
@@ -311,6 +399,82 @@ export default function GerbilDetailPage() { + {/* GEPRÜFTE TIERE: Prüfstatus & Schutz — laienverständlich erklärt. */} + {(() => { + const vt = de.verified + const vs = verifiedStatus.data + const isVerified = vs?.isVerified === true + const isProtected = vs != null && vs.isVerified === false + const drifted = vs?.status === 'drifted' + const fieldLabel = (path: string) => vt.fieldLabels[path] ?? path + const runVerify = async () => { + const r = await verify.run() + if (r.ok) { toast.success(vt.markSuccess); verifiedStatus.reload(); gerbil.reload() } + else toast.error(r.error) + } + const runUnverify = async (confirmMsg: string) => { + if (!window.confirm(confirmMsg)) return + const r = await unverify.run() + if (r.ok) { toast.success(vt.unmarkSuccess); verifiedStatus.reload(); gerbil.reload() } + else toast.error(r.error) + } + return ( +
+

{vt.sectionTitle}

+

{isProtected ? vt.explainProtected : vt.explainIntro}

+
+ {!isVerified && ( + + )} + {isVerified && ( + + )} + {isProtected && ( + + )} +
+ {isVerified && !drifted &&

{vt.statusUnchanged}

} + {isVerified && drifted && vs!.importDiff.length > 0 && ( + <> +

+ {vt.driftHeading} +

+

{vt.driftHint}

+
+ {vs!.importDiff.map((d) => ( + + {d.goldenValue || '—'} + → {vt.colImport}: {d.importValue || '—'} + + ))} +
+ + )} +
+ ) + })()} +

{t.detail.masterData}

@@ -349,13 +513,18 @@ export default function GerbilDetailPage() { {showEnclosure && ( {lookup(enclosureName, g.enclosureId)} )} - - {g.litterId && litterName.has(g.litterId) ? ( - {litterName.get(g.litterId)} - ) : ( - lookup(litterName, g.litterId) - )} - + {/* Ticket 7dedec07: Die „Wurf"-Zeile nur zeigen, wenn das Tier einen eigenen + Geburtswurf in dieser Zucht hat. Zugekaufte/externe Zuchttiere (z. B. Blacky, + litterId=null) sind nicht hier geboren → keine Wurf-Zeile. */} + {g.litterId && ( + + {litterName.has(g.litterId) ? ( + {litterName.get(g.litterId)} + ) : ( + lookup(litterName, g.litterId) + )} + + )} {ownLitter && ( <> {parentLink(fatherId, father.data)} @@ -369,9 +538,19 @@ export default function GerbilDetailPage() { g.originBreeder || '—' )} - {g.status !== 'Deceased' && ( + {(g.receiverContactId || g.status !== 'Deceased') && ( {lookup(contactName, g.receiverContactId)} )} + {g.isResident !== false && (partners.data?.items.length ?? 0) > 0 && ( + + {(partners.data?.items ?? []).map((p, i) => ( + + {i > 0 && ', '} + {p.name || de.pages.gerbils.nameless} + + ))} + + )} {g.notes || '—'}
@@ -419,6 +598,19 @@ export default function GerbilDetailPage() { {g.isResident === false && (

{t.detail.nonResidentNote}

+ {(offspring.data?.items.length ?? 0) > 0 && ( +

+ {t.detail.nonResidentOffspringLabel}{' '} + {(offspring.data?.items ?? []).map((child, i) => ( + + {i > 0 && ', '} + + {child.name || de.pages.gerbils.nameless} + + + ))} +

+ )}
)} @@ -519,6 +711,11 @@ export default function GerbilDetailPage() { {litter.totalBorn} {de.pages.litters.countLabel} )} + {litter.showInChronicle === false && ( + + {t.detail.parentLittersNotInChronicle} + + )} ))} @@ -527,6 +724,42 @@ export default function GerbilDetailPage() { )} + {g.isResident !== false && (parentLitters.data?.items.length ?? 0) > 0 && ( +
+

{t.detail.offspringInBreedingTitle}

+ {offspring.loading &&

{de.common.loading}

} + {!offspring.loading && (breedingOffspring.length === 0) && ( +

{t.detail.offspringInBreedingEmpty}

+ )} + {breedingOffspring.length > 0 && ( +
    + {breedingOffspring.map((child) => { + const childLitter = (parentLitters.data?.items ?? []).find(l => l.id === child.litterId); + const suffix = child.originBreeder ? ` (${child.originBreeder})` : ''; + return ( +
  • + + {child.name || de.pages.gerbils.nameless} + + {statusLabel(child.status)} + + {childLitter && ( + + {childLitter.name} + + )} + + {child.gender === 'male' ? 'Bock' : child.gender === 'female' ? 'Weibchen' : 'Unbekannt'}{suffix} + + +
  • + ); + })} +
+ )} +
+ )} +

{t.detail.moreData}

diff --git a/gerbil-manager-web/src/pages/GerbilsPage.tsx b/gerbil-manager-web/src/pages/GerbilsPage.tsx index dddc14830dc5f1a177ddf120659a49ffdb14fa1d..84f32e92198dabf184fc212ce5cd7fde5ab826ee 100644 GIT binary patch delta 43 ycmdm3exZDWil|&#W==_Jk%mUPrh=`VLb`&Yt*wH3X