Im Tier-Formular gibt es jetzt den Block „Abstammung" mit Wurf-Auswahl und
Vater-/Mutter-Picker — die Eltern müssen nicht mehr über die Wurf-Seite
gesucht und dort editiert werden. Das Datenmodell bleibt unverändert: Eltern
hängen weiterhin am Geburtswurf.
- Backend: PUT /gerbils/{id}/parents schreibt Litter.FatherId/MotherId des
Geburtswurfs. Ohne Wurf wird ein bestehender mit gleichem Elternpaar +
gleichem Datum verknüpft, sonst ein Träger-Wurf angelegt
("Wurf von X + Y", ShowInChronicle=false, IsManual=true).
Geschlechts-Regel wiederverwendet LitterEndpoints.ValidateParents,
Selbstbezug (Tier als eigener Elternteil) wird abgewiesen.
- UI: Vorbelegung aus dem gewählten Wurf, Warnung mit Anzahl der Geschwister
(Eltern gehören dem Wurf → Änderung gilt für alle), Hinweis wenn ein
Wurf-Eintrag angelegt wird. Texte in de.ts.
- Nebenbei: Speichern nutzt im Edit-Modus die Route-Id (PUT /gerbils/{id}
antwortet 204 ohne Body) und der vorher schon rote Spec-Locator
„Würfe als Elternteil" ist auf den Abschnitt eingegrenzt.
- Tests: GerbilParentsTests (9), e2e tiere.spec (2 neu) + Mock-Route.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
398 lines
27 KiB
Markdown
398 lines
27 KiB
Markdown
# 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)
|
||
|
||
```bash
|
||
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):**
|
||
```bash
|
||
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_PASSWORD` — **nur 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 99–107 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**.
|
||
|
||
```bash
|
||
# 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`):
|
||
```bash
|
||
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**:
|
||
```bash
|
||
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):
|
||
```bash
|
||
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`.
|
||
- **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`).
|
||
- **`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-mcp` → **19 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.
|