Files
GerbilManager/CLAUDE.md
Gulum 748ce4733f
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 1m12s
CI / Docker Build & Push (push) Has been cancelled
CI / Deploy auf TrueNAS (Custom App) (push) Has been cancelled
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
docs: Dauerwissen aus dem Ticket-Lauf + Ergebnis/Folgeaufgaben
CLAUDE.md: C-Locus-Zygotie (Platin/Saphir/Platin-Hell, Paar-Token "x/y"),
c[hm]-Alias, die neuen Resolution-Schlüssel goHomeDate/spottingType und die
Präzedenz des genotype-Overrides, "pro Tier nur EINE Resolution" (inkl. der
Falle, dass Wurfchronik-Jungtiere zur Override-Zeit kein DOB haben), der
renameTo-Notiz-Sweep mit seinen Guards, der parse_date-Jahresguard und die
sternlosen Geburtsdaten in Stammbaum-Charts.

docs/ticket-triage.md: Ergebnis des Laufs (16/16 Resolved, 27/27 Prod-Checks)
und die acht bewusst offen gelassenen Folgeaufgaben.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:16:00 +02:00

30 KiB
Raw Blame History

CLAUDE.md — GerbilManager

Leitfaden für die Arbeit an diesem Repo. Bitte vor Änderungen lesen.

Was ist das?

Verwaltungs-App für eine Rennmaus-Zucht („Zucht der kleinen Chaoten"): Tiere, Würfe, Stammbäume/Ahnentafeln, Genetik (Farbschläge/Genotypen), Gehege, Kontakte (Züchter/Abnehmer), Abgabeverträge, Statistik und eine öffentliche CMS-Webseite. Single-User (die Züchterin), Daten größtenteils aus bestehenden Quelldateien importiert.

Architektur / Projekte

Projekt Zweck
GerbilManager.AppHost .NET Aspire Orchestrator — startet PostgreSQL (Container) + WebAPI
GerbilManagerWebAPI Backend: ASP.NET Minimal API, EF Core + Postgres, Endpoints in Endpoints/, Entities in Models/, DTOs in Dtos/
GerbilManager.ServiceDefaults Aspire-Defaults (Telemetry/Health)
GerbilManager.Tests Backend-Tests (xUnit)
gerbil-manager-web Frontend: React + Vite + TypeScript (React Router, react-d3-tree für den Stammbaum)
tools/import Python-Import-Pipeline (zero-dep), wandelt Quelldateien → resolved_import.json

App starten (IMMER über Aspire, nicht nur die WebAPI)

dotnet run --project GerbilManager.AppHost
  • WebAPI: http://localhost:5179 · OpenAPI: /openapi/v1.json
  • Aspire-Dashboard: https://localhost:17004 (Login-Token steht im AppHost-Log)
  • EF-Migrationen werden beim Start automatisch angewandt (db.Database.Migrate() in Program.cs).
  • Postgres läuft als Container (Name wechselt pro Run, z. B. postgres-xxxx); die Daten liegen in einem persistenten Volume → überleben Neustarts. DB-Name: gerbilmanager.

Produktion / Deployment (TrueNAS)

Prod läuft als TrueNAS SCALE Custom-App gerbilmanager auf dem Host truenas (LAN-IP 192.168.2.115, TrueNAS 25.10 „Goldeye", Docker). Erstinstallation 2026-07-19.

  • Zugriff (Bookmark der Züchterin): http://truenas:8090/ · API-Doku: /scalar. (Port 80 ist durch den nginx-Reverse-Proxy der NAS belegt → App auf 8090, LAN-only, keine Auth.)
  • Sichtbar unter Apps, weil via midclt call app.create {custom_app:true, app_name:"gerbilmanager", custom_compose_config_string:<yaml>} angelegt — nicht via docker compose (das taucht in Apps NICHT auf). Container: ix-gerbilmanager-{db,api,frontend,backup}-1.
  • 5 Dienste: db (postgres:18) · api (.NET, EF-Migrationen laufen beim Start) · frontend (nginx: SPA + /api-Proxy → api:8080, Host-Port 8090) · publicsite (nginx, nur intern, Host-Port 8081 — serviert statisches HTML aus dem publicsite-Volume, das die API via POST /api/publish schreibt; ohne Publish leer → 404) · backup (täglich 03:00 pg_dump + Foto-Archiv, Shell-Scheduler).
  • App-Home auf dem Pool: /mnt/JailStorage/DockerVolumes/gerbilmanager/ mit pgdata/ photos/ keys/ backups/ scripts/ publicsite/ und deploy/truenas/{custom-app.compose.yaml,.env,scripts}. Images aus der Gitea-Registry git.rismer.de/gulum/gerbilmanager-{api,frontend}:latest.

Deployen

Automatisch (Regelfall): Push auf main → Gitea Actions (.gitea/workflows/ci.yml): Tests → Images bauen+pushen → Job deploy verbindet sich per SSH auf den NAS-Host und ruft deploy/truenas/scripts/truenas-deploy.sh (→ midclt app.redeploy, zieht :latest neu, pgdata/ photos bleiben erhalten). Braucht die Gitea-Repo-Secrets NAS_SSH_HOST (=192.168.2.115) und NAS_SSH_KEY (privater Deploy-Key; Pubkey liegt in /root/.ssh/authorized_keys).

Manuell (auf dem NAS-Host):

sh /mnt/JailStorage/DockerVolumes/gerbilmanager/deploy/truenas/scripts/truenas-deploy.sh

Legt die App an, wenn sie fehlt (create), sonst redeploy. Rendert custom-app.compose.yaml mit den Werten aus deploy/truenas/.env (enthält POSTGRES_PASSWORDnur auf der NAS, nie im Repo).

Lokale DB → Prod kopieren (einmalig gemacht): pg_dump der Aspire-DB → psql-Restore in den ix-gerbilmanager-db-1-Container (dabei api kurz stoppen); Fotos aus GerbilManagerWebAPI/photo-storage/ per tar | ssh nach …/gerbilmanager/photos/.

Deployment-Stolperfallen

  • /opt ist auf Goldeye read-only → App-Home MUSS auf den Pool (/mnt/JailStorage/...).
  • postgres:18 legt PGDATA in /var/lib/postgresql/18/docker ab und deklariert das Volume als /var/lib/postgresql. Mount auf /var/lib/postgresql/data (alte v≤17-Konvention) lässt v18 nicht starten ("data in unused mount/volume") → db unhealthy → App-Rollback. Immer /var/lib/postgresql mounten. (Debian-Image, nicht alpine → Locale libc/en_US.utf8 = Quell-DB.)
  • Gitea-Runner ist containerisiert (Label ubuntu-latest, kein goldeye, kein Host-/midclt-Zugriff) und läuft maxParallel=1 (Jobs sequenziell) — daher der SSH-Weg im Deploy-Job.
  • Auto-Deploy lief bis 2026-08-19 nie durch (Runs 99107 rot), zwei Ursachen: (1) die Repo-Secrets NAS_SSH_HOST/NAS_SSH_KEY waren nie angelegt → ssh root@ mit leerem Key („Could not resolve hostname"); jetzt gesetzt, Deploy-Key gitea-ci-deploy@gerbilmanager (privat nur im Gitea-Secret, Pubkey in /root/.ssh/authorized_keys). (2) Der Runner hat kein rsync, und der apt-Guard prüfte nur ssh → „rsync: command not found". Dateitransfer läuft daher per tar | ssh (tar gibt es auf beiden Seiten; -xof erhält die Modi, Skripte bleiben ausführbar).
  • CI-Run-Status notfalls aus der Gitea-DB: docker exec -e PGPASSWORD=gitea ix-gitea-postgresdb-1 psql -U gitea -d gitea -c "select name,status from action_run_job where run_id=(select max(id) from action_run);" (status: 1=success, 2=failure, 4=skipped).

Import → DB (3-stufig, WICHTIG)

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.

# 1. Stammbäume + Wurfchronik → animals.json/litters.json (+ Fotos)
python tools/import/extract.py
# 2. Abgabeverträge (DOCX) → contracts.json
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): UPSERT + Freeze
curl -X POST http://localhost:5179/import/ingest-resolved
# 4b. Gegen PROD (ohne SSH): resolved_import.json per HTTP hochladen + ingesten
curl -X POST http://truenas:8090/api/import/ingest-resolved/upload \
     -F "resolved=@tools/import/output/resolved_import.json"   # [-F "photos=@neue_fotos.zip"] nur bei neuen Fotos
  • POST /import/ingest-resolved/upload (multipart resolved + optional photos-ZIP): staged die hochgeladene Datei serverseitig in Import:SourcePath und ruft denselben UPSERT-Ingest — damit läuft der Import gegen Prod ohne SSH/docker cp. Der alte POST /import/ingest-resolved (liest die serverseitige Datei) bleibt unverändert. DB-Copy vom lokalen Stand ist verboten, sobald Prod kuratiert wurde — nur der Upsert-Ingest bewahrt manuelle/verified/feedback-Zeilen.

  • 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.

Quelldateien (lokal/Netzwerk)

  • Stammbäume: C:\Users\gulum\dev\Sttammbäume\*.xlsx
  • Wurfchronik: C:\Users\gulum\dev\Wurfchronik der Kleine Chaoten Teil1.xlsx
  • Abgabeverträge: \\truenas\Datengrab\Rennmäuse\Verträge\*.docx (~1,4k; kann zeitweise offline sein)

Build & Test

Frontend (cd gerbil-manager-web):

npx tsc --noEmit          # Typecheck (muss sauber sein)
npx vitest run            # Unit-Tests
npx eslint <dateien>      # Lint
npx playwright test [spec]# e2e (Projekte: desktop + phone), läuft gegen Mock-API
  • e2e nutzt einen Mock-Modus (e2e/mock-api.ts + e2e/mock-data.ts), kein echtes Backend. Neue API-Routen/Felder dort spiegeln. skipUnlessMock() am Testanfang.

Backend:

dotnet build
dotnet test                                   # GerbilManager.Tests
dotnet ef migrations add <Name> --project GerbilManagerWebAPI   # neue Migration
  • Zum Bauen muss eine laufende WebAPI ggf. gestoppt werden (Datei-Lock auf bin).

Python (cd tools/import, zero-dep, exit 0 = pass):

python test_extract.py  test_extract_docx.py  test_genotype.py  test_merge_resolve.py  test_extract_contracts.py

Konventionen (bitte einhalten)

  • Alle deutschen UI-Texte in gerbil-manager-web/src/strings/de.ts — niemals in Komponenten hartkodieren.
  • EF-Migration für jede Schema-Änderung; wird beim Aspire-Start angewandt.
  • Provenance/Datenherkunft liegt als JSON-Text-Spalte (Provenance) auf Gerbil/Contact/Litter → JSON-Feld-Erweiterungen brauchen keine Migration.
  • Lose nullable Guid-Spalten ohne FK für Tabellen, die den Ingest-Wipe überleben.
  • Commits: nur wenn der Nutzer es verlangt; auf main wird gearbeitet (Remote: http://truenas:13000/Gulum/GerbilManager.git). Commit-Messages auf Deutsch, Format feat(scope): ….
  • Subagenten committen NICHT — Haupt-Loop verifiziert und committet.

Domänenwissen (Rennmaus-Zucht)

  • Lebensspanne ~6 Jahre → ein Elternteil kann max. ~6 Jahre älter als sein Kind sein und muss vorher geboren sein (parent_age_plausible im Import; verwirft unplausible Eltern-Links).
  • Geschwisterverpaarung (Vollgeschwister als Eltern): erkannt, wenn Vater & Mutter dieselbe litterId ODER dasselbe Geburtsdatum teilen (gleiches DOB = starkes 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 zwingend homozygot → e- ist ungültig (wirft), ee[-]ee.
  • C-Locus-Zygotie (GEN-6, Tickets bde4ec70/f89e95ad — Regel der Züchterin): Platin, Saphir und Platin-Hell unterscheiden sich nur in der C-Zygotie: aa CC …pp = Platin, aa Cc[chm] …pp = Saphir, aa Cc[h] …pp = Platin-Hell. Der Katalog-Match kann deshalb pro Locus ein exaktes, ungeordnetes Allelpaar verlangen — Token-Form "x/y" (z. B. C: 'C/cchm'), sonst reduziert locusToken() auf das dominante Allel und der erste Treffer (Platin) verschluckt die anderen. Gilt in beiden Mirrors (catalog.ts + genotype.py) und im Seed (colorVarietySeed.*, ApplicationContext.SeedColorVarieties, dort in Kompaktnotation Ccchm/Cch ohne Klammern).
  • c[hm] ist ein Schreibfehler-Alias für c[chm] (fehlendes c in einigen Stammbaum-Charts): beide Parser normalisieren ihn, sonst ist der Gencode unparsebar bzw. ergibt „…-Hell".
  • Genetik-Engine = korrektheitskritisch: IMMER vollständig mit Tests absichern. Jede Änderung an src/genetics/** (+ Backend-Mirror + genotype.py) braucht pro Use-Case/Ticket einen Regressionstest (src/genetics/__tests__/genetics.test.ts und tools/import/test_genotype.py), damit nie eine Regression entsteht. npx vitest run + python test_genotype.py müssen grün sein, bevor „fertig".
  • Stammbaum-Charts: Generationen = Spaltenbänder (Proband links in Spalte B/2, je Generation +3 Spalten); Eltern-Position: Vater oben, Mutter unten. Fotos liegen je nach Datei links/auf der Namenszelle — die Seite wird heuristisch über die geringste Spalten-Fehlausrichtung bestimmt (extract._attach_photos).

Stolperfallen

  • Aspire-Containername wechselt pro Run, das Daten-Volume bleibt → DB-Inhalt überlebt Neustarts (auch alte Schema-Stände → Migrationskonflikte möglich, s. u.).
  • Migrations-Historie vs. committete Migration-IDs: Wird eine Migration angewandt und danach mit neuem Zeitstempel neu generiert, sieht EF sie als „pending" und der Start crasht („column … already exists"). Schema ist dann korrekt — nur die __EFMigrationsHistory-IDs müssen an die committeten Dateinamen angeglichen werden.
  • PowerShell + psql: PascalCase-Identifier ("__EFMigrationsHistory") über PowerShell→docker→psql zerschießen das Quoting. Stattdessen Bash-Heredoc: docker exec -i -e PGPASSWORD=$PW <container> psql -U postgres -d gerbilmanager <<'SQL' … SQL. Passwort: docker exec <container> printenv POSTGRES_PASSWORD.
  • Direkte DB-Schreibzugriffe (psql) werden vom Auto-Mode-Classifier blockiert — vorher beim Nutzer rückversichern.
  • Bash-Tool ist Git-Bash (POSIX), das andere Tool ist PowerShell — je eigene Syntax.

Wo liegt was (Feature-Landkarte)

  • Stammbaum-Viewer: gerbil-manager-web/src/pages/StammbaumPage.tsx, src/pedigree/* (build.ts = Baum-Aufbau + Geschwister-Erkennung; types.ts). Desktop passt den ganzen Baum ein; Touch: Long-Press öffnet das Kontextmenü (ID kopieren / Fehler melden).
  • Datenherkunft/Nachverfolgung: src/components/ProvenanceDialog.tsx; erzeugt im Import via build_entity_provenance/_build_*_history (chronologische, datei- 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.
  • Eltern an der Tier-Akte bearbeiten (QOL): Im Tier-Formular gibt es den Block „Abstammung" (gerbil-manager-web/src/pages/GerbilFormPage.tsx) mit Wurf-Auswahl + Vater/Mutter-Picker (components/AnimalPicker), damit die Züchterin die Eltern ohne den Umweg über die Wurf-Seite pflegen kann. Datenmodell unverändert: Eltern hängen weiter am Geburtswurf. Backend PUT /gerbils/{id}/parents (GerbilEndpoints, DTOs GerbilParentsInput/ GerbilParentsResult, Tests GerbilManager.Tests/GerbilParentsTests.cs) schreibt genau dorthin: hat das Tier einen Wurf → dessen FatherId/MotherId (gilt zwangsläufig für alle Geschwister, die UI warnt mit siblingCount); hat es keinen → ein Wurf mit gleichem Elternpaar + gleichem Datum wird verknüpft (litterAttached), sonst wird ein Träger-Wurf angelegt (litterCreated, Name „Wurf von X + Y", ShowInChronicle=false, IsManual=true). Geschlechts-Validierung wiederverwendet LitterEndpoints.ValidateParents (400 InvalidParentGender). Grenze: Eltern/Abstammung sind vom Freeze ausgenommen (GerbilSnapshotService.FreezeFields ohne litterId) → an einem importierten Wurf überlebt eine Eltern-Korrektur den Re-Ingest nicht (gleiches Verhalten wie beim Editieren über die Wurf-Seite; dauerhaft nur über den Importer/conflict-decisions.json).
  • 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-<slug>"), 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).
  • Über-gemergte Würfe korrigieren: conflict-decisions.json kennt litterChildren [{date, matchChild, keep[], renameTo?, note}] — autoritative Kinder-Whitelist eines Wurfs (später Schritt in merge_and_resolve.py nach litterOrder). Match per date + matchChild (ein aktuell zugeordnetes Kind, disambiguiert gleich-datierte Würfe); Kinder außerhalb keep werden aus dem Wurf gelöst (LitterId=None, Tier bleibt erhalten), optional renameTo setzt den kanonischen Wurf-Namen. Use-Case: Wurfchronik-Abnehmer-Sammelseiten, die fälschlich in einen datierten Buchstaben-Wurf gemergt wurden (Tickets 4b9f49fb M-Wurf, 8d259edf Gale). add [Name | {name, dob}] ist das Gegenstück (seit 2026-08-18): hängt ein Jungtier an diesen Wurf, wenn die Chart-Positions-Heuristik es in einen falschen virtuellen Stammbaum-Wurf gezogen hat; wird der alte Wurf dadurch kinderlos UND virtuell, entfernt der Importer ihn (sonst bliebe ein Phantom-Wurf in der Wurfchronik stehen). Use-Case Akane (Ticket 88389f8e): die neuen Stammbäume führen die Eltern eine Zeile unter dem Kind-Block (Vater N80/Mutter N81) — _reconstruct_parents greift dann eine Zeile zu hoch und paart die Mutter des Nachbar-Blocks mit dem echten Vater.
  • EMF/WMF sind keine Fotos: Excel legt neben dem Foto teils ein Vektor-Metafile ab; Browser können es nicht darstellen (kaputte Bildkachel in der Akte) → extract._attach_photos überspringt .emf/.wmf (Regressionstest in test_extract.py).
  • Resolution-Schlüssel in conflict-decisions.json (Stand 2026-08-19): neben father/mother(+Dob), isResident, notes, receiver, dateOfDeath, deceased, correctDob, renameTo, mergeExternalRefs, suppressExternalRefs, farbschlag auch goHomeDate (autoritativ, überschreibt auch einen vorhandenen/kaputten Wert und zieht Status GivenAway nach, sofern nicht Deceased) und spottingType (Scheckungsart — der Importer leitet sie aus keiner Quelle ab; IngestResolvedService überträgt sie nur, wenn der Payload einen Wert hat, sonst bliebe eine von Hand erfasste Angabe nicht stehen). genotype gewinnt bei präzisem Match (externalRef ODER name+dob) auch gegen einen bereits gefüllten Wert — bei Wurfchronik-Tieren ist der meist nur der Kanon-Fill des Farbschlags; ein name-only-Match füllt weiterhin nur leere Felder (Namensvetter-Schutz).
  • Pro Tier greift nur EINE Resolution (first match: externalRef → endswith → name+dob → name-only). Nie einen zweiten Eintrag mit derselben externalRef/demselben (name,dob) anlegen, sondern den bestehenden in place erweitern — sonst gewinnt still der letzte und Felder gehen verloren. Wurfchronik-Jungtiere haben zur Override-Zeit noch kein DOB (das kommt später aus dem Wurfdatum) → dort ist externalRef der einzige zuverlässige Schlüssel.
  • renameTo zieht durch Freitext-Notizen (apply_rename_note_sweep, letzter Schritt vor dem Payload-Dump): der alte Name blieb sonst in Wurf-Notizen stehen („Blacky + Kruke v. …"). Wortgenau, mit Guards — Quellname < 4 Zeichen oder > 2 Wörter wird ignoriert, kein Ersetzen, wenn der alte Name im neuen Rufnamen steckt, und Notizen, die die Umbenennung selbst dokumentieren („umbenannt"/„ursprünglich"), bleiben unberührt. Ohne diese Guards trifft der Sweep Abnehmer-Namen und Gewichtsnotizen (im Probelauf 16 von 22 Ersetzungen falsch).
  • parse_date verwirft implausible Jahre (< 1900 oder > aktuelles Jahr + 1) → None; Wurfchronik-Transkriptionen bauen Daten sonst falsch zusammen (JackJack: „1310-05-13").
  • Sternloses Geburtsdatum in Stammbaum-Charts: extract.py normalisiert Zellen wie 22.08.2019/+09.04.2024 (ohne *) zu *…, wenn Block-Kontext vorliegt (Genotyp ≤3 Zeilen darunter, Namenszelle ≤3 darüber). Ohne das erkennt die Blockheuristik das Tier nicht und _reconstruct_parents greift in den Nachbar-Ast (Ticket 65266679 Bijou).
  • renameTo ist eltern-verknüpfungssicher: umbenannte Tiere werden im Eltern-Namensindex zusätzlich unter ihrem Quell-Namen (_pre_rename_name) geführt, sonst verlieren Würfe, die den alten Namen referenzieren, ihren Eltern-Link (Ticket e0a0c304 Kruke→Kuke). dateOfDeath ist als Resolution-Override unterstützt (setzt Todesdatum + Status Deceased; rpro3 hat keine Sterbespalte).

Verifikations-Checkliste vor „fertig"

  1. npx tsc --noEmit, npx vitest run, npx eslint sauber.
  2. Betroffene npx playwright test-Specs grün.
  3. dotnet build + dotnet test grün.
  4. Bei Import-Änderungen: python test_*.py grün + resolved_import.json plausibel (Stichprobe), danach App neu starten + POST /import/ingest-resolved.

Gedächtnis (MemPalace) — IMMER nutzen

Dieses Projekt nutzt MemPalace als KI-Langzeitgedächtnis (lokal, kein API-Key). Die Anbindung ist im Repo persistiert und lädt automatisch:

  • .mcp.json (committet) registriert den MCP-Server mempalace-mcp19 MCP-Tools (mempalace_search, mempalace_kg_query/add/invalidate/timeline, mempalace_add_drawer, mempalace_diary_* …). Aktiv ab Claude-Neustart (MCP-Server einmal bestätigen).
  • .claude/settings.json (committet) verdrahtet die Hooks mempalace hook run --hook {session-start,stop,precompact} --harness claude-code: SessionStart lädt relevanten Kontext automatisch, Stop/PreCompact persistieren.
  • Voraussetzung: mempalace ist installiert und auf PATH (~/.local/bin). Nach git clone ggf. MemPalace installieren; mempalace.yaml (Raum-Config) ist bewusst gitignored (per-project, lokal). Der Palast selbst liegt lokal unter ~/.mempalace (nicht im Repo).

Arbeitsweise pro Session: zu Aufgabenbeginn relevantes Wissen abrufen (mempalace_search / mempalace_kg_query), bevor du im Code suchst; am Ende neue durable Erkenntnisse ablegen.

Subagenten MÜSSEN MemPalace ebenfalls nutzen — das im Subagent-Prompt IMMER explizit anweisen: zu Aufgabenbeginn den Palast abfragen (am einfachsten per CLI, kein Key nötig: mempalace search "<keywords>" bzw. mempalace search "<…>" --wing gerbilmanager; falls MCP-Tools verfügbar auch mempalace_search/mempalace_kg_query) — relevante Domänenregeln, frühere Entscheidungen und Stolperfallen holen, BEVOR im Code gesucht wird; durable Erkenntnisse am Ende ablegen (mempalace add-drawer/MCP). Gilt für JEDEN gespawnten Agenten.

Memory-Policy — WANN / WIE / WO eine Memory anlegen

WANN (anlegen): nur dauerhaft nützliches, nicht-offensichtliches Wissen, das künftige Sessions braucht — Architektur-/Design-Entscheidungen und ihr Warum, Domänenregeln (z. B. 6-Jahre-Lebensspanne, Geschwisterverpaarung), Konventionen, Stolperfallen, bestätigte Nutzer-Präferenzen, wiederkehrende Workflows. NICHT anlegen: transienter Task-Status; was Code/Git/CLAUDE.md ohnehin festhalten; Dinge, die nur für das aktuelle Gespräch zählen.

WIE (anlegen/pflegen):

  • Fakt mit Gültigkeit → mempalace_kg_add. Freitext-Notiz/Erkenntnis → mempalace_add_drawer (vorher mempalace_check_duplicate).
  • Ändert/erledigt sich ein Fakt → mempalace_kg_invalidate (setzt End-Datum, nicht löschen — der Graph ist temporal). So bleibt nichts veraltet stehen.
  • Repo-Dateien werden über mempalace mine <dir> automatisch gefilet; mempalace sync --apply --wing gerbilmanager entfernt Drawers gelöschter/verschobener Dateien.

WO (welcher Speicher):

  • MemPalace-Palast (~/.mempalace, lokal): reichhaltiges, durchsuchbares Recall — der Default für Memories.
  • CLAUDE.md (Repo, committet): die kanonischen Projektfakten/Konventionen, die geteilt und versioniert sein müssen. Wichtige durable Erkenntnisse hier spiegeln, damit sie im Repo (nicht nur lokal) liegen.

Faustregel: durable + projektweit kanonisch → CLAUDE.md und Palast; sonstiges durable Wissen → Palast; veraltetes → invalidieren, nicht stehen lassen.