Hiro of Golden Lights erschien fälschlich als Gründertier (keine Eltern im Import).
Aus dem RennmausPro-Backup sicher zugeordnet (gleiches Geburtsdatum 13.02.2012):
Eltern Wynn × Amidala — als nicht-residente Ahnen-Stubs angelegt und verknüpft.
Bewusst nur EINE Generation: tiefere Ahnen würden über addAnimals-Stubs nicht
verkettet (der Eltern-Override greift nur auf echte Stammbaum-/Wurfchronik-Tiere,
nicht auf Stubs) — und mehrere Großeltern-Namen (Harry/Shy/Jack/Speedy) kollidieren
mit bestehenden, anderen Tieren. Voller mehrstufiger RennmausPro-Ahnen-Import =
eigenes Feature (Import-Code-Änderung + robustes Matching), nicht blind machbar.
python test_merge_resolve.py + test_extract.py grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Zwei Ursachen, warum es auf der Tickets-Seite nicht ging (am echten Firefox/Chromium
gegen die Live-App verifiziert):
1) Tab ging verloren: der gewählte Filter-Tab (z. B. „Geschlossen") war reiner
Component-State und sprang nach „Zurück" (Remount) auf „Offen" zurück — also war man
in der falschen, kurzen Liste. Jetzt wird der Tab in der Session gemerkt und
wiederhergestellt (userPicked startet weiterhin false, damit ?focus=-Deep-Links
weiter funktionieren).
2) Scroll-Save-Heuristik zu unzuverlässig: Beim Wegnavigieren schrumpft der Inhalt von
.app-main → der Container klemmt auf eine kleinere Position → dieses scroll-Event
überschrieb die gemerkte Stelle. Erkennung präzisiert: nur verwerfen, wenn Position
UND scrollHeight gleichzeitig SINKEN (= Inhalt geschrumpft/Seitenwechsel). Eine echte
Nutzer-Scrollung verkleinert die scrollHeight nie — damit kein fälschliches Verwerfen
echter Positionen mehr (vorherige „Höhe geändert"-Heuristik schluckte zu viel).
Tests: scroll-restore.spec erweitert (realistisches 2-Schritt-Scrollen + Tickets-Tab
überlebt Zurück), desktop+phone grün; vitest 149 grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deckt die Regression ab, die zuletzt durchrutschte:
- Liste scrollen → Tier öffnen → Browser-Zurück → Position bleibt erhalten.
- Position überlebt App-Hintergrund/Wiederanzeige (visibilitychange).
Läuft im phone-Projekt (390x844, Touch) UND desktop, am echten Scroll-Container .app-main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Echte Ursache (per Firefox/Chromium gegen die Live-App verifiziert): Beim Wegklicken
eines Listeneintrags wird die Seite ausgehängt → der Inhalt von .app-main kollabiert →
der Container springt auf scrollTop 0 → das löste ein scroll-Event aus, das 0 als neue
Position speicherte und die gemerkte Stelle ZERSTÖRTE, kurz bevor wir wegnavigierten.
Fix:
- scroll-Events ignorieren, wenn der Container gerade NICHT scrollbar ist
(scrollHeight ~ clientHeight = Inhalt kollabiert/Seitenwechsel) → kein „unechter" 0-Save.
- Wiederherstellung hält den Schutz über die gesamte Settling-Phase: bei jeder DOM-Änderung
Position erneut setzen, erst nach ~350 ms Ruhe freigeben (gegen Reflow-Races beim Nachladen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Die App scrollt NICHT das Fenster: .app-shell ist height:100dvh/overflow:hidden, der
Inhaltsbereich .app-main hat overflow-y:auto. window.scrollY/scrollTo waren daher
wirkungslos (am Desktop sichtbar). Der Hook arbeitet jetzt auf .app-main (scrollTop/
scrollTo, Scroll-Listener am Container) und beobachtet den Inhalt per MutationObserver,
um nach dem Nachladen exakt zur gemerkten Position zu springen. Fallback auf window,
falls der Container fehlt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Zwei Probleme bei der großen Rennmausliste (und allen Infinite-Scroll-Listen) nach
Zurück-Navigation behoben:
1) Event- statt Timer-basiert: Die Scroll-Wiederherstellung hängt sich jetzt per
ResizeObserver an die Höhenänderung der Seite (Liste ist wieder da) und springt dann
zur gemerkten Position — statt auf einen festen Timer zu warten. Bricht bei eigener
Scroll-Eingabe ab; Sicherheitsnetz nach 15 s.
2) Infinite-Scroll baut Höhe wieder auf: useInfiniteList merkt sich (optionaler
restoreKey) die geladene Seitenzahl je Query und lädt sie nach einer Zurück-
Navigation wieder nach — sonst war die Liste nur 1 Seite hoch und die Position
unerreichbar. Aktiviert für Rennmäuse, Kontakte, Würfe, Becken, Anfragen, Verträge.
tsc/eslint/vitest grün; e2e (Listen + Navigation, 41) grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bei einem HMR-/Fast-Refresh-Update rendert die Seite kurz neu (Liste kollabiert →
Sprung nach oben), ohne Navigation/pagehide. Jetzt vor `vite:beforeUpdate`/
`vite:beforeFullReload` sichern und nach `vite:afterUpdate` wiederherstellen (nur Dev;
import.meta.hot ist in Produktion undefiniert). Restore-Fenster auf 4 s erhöht, damit
auch langsam nachladende Listen die Position noch erreichen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neuer Hook useScrollRestoration (einmal im AppShell gemountet) erhält die
Scrollposition je Route über:
- Zurück/Vorwärts-Navigation (POP restauriert, PUSH startet oben),
- Bildschirm-Sperre / App-Hintergrund (visibilitychange, pagehide/pageshow) —
damit das Handy nach dem Entsperren nicht mehr oben landet.
Robust gegen nachladende Listen (rAF-Nachjustierung bis ~2,5 s, während die Liste
wächst), bricht bei eigener Scroll-Eingabe ab, Deep-Links (?focus=, #anchor) haben
Vorrang. Die bisher nur in der Tickets-Seite eingebaute Logik wurde durch den
globalen Hook ersetzt.
tsc/eslint/vitest grün; e2e (navigation/listen/tickets, 42) grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher wurde die Scrollposition nur einmal beim Mount wiederhergestellt — sperrte das
Handy und kam zurück, war die Seite oben. Jetzt:
- Position wird beim Ausblenden sicher gesichert (visibilitychange→hidden, pagehide),
nicht nur bei Scroll-Events.
- Wiederherstellung beim Wiederanzeigen (visibilitychange→visible, pageshow) UND nach
dem Laden — mit mehreren rAF-Versuchen gegen nachwachsende Inhalte.
- Schutz gegen den Aufwach-Sprung nach oben: während des Wiederherstellens werden
Scroll-Speicherungen blockiert, damit ein vom Browser ausgelöster 0-Sprung die
gemerkte Position nicht überschreibt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Neuer isResident-Override im Import (merge_and_resolve, NACH dem Dedup, damit die
Zusammenführung ihn nicht überschreibt). Matcht über externalRef (präzise, auch für
namenlose Tiere) oder normalize(call-name)+dob. Ein Nicht-Bestandstier verliert den
eigenen Zucht-Breeder.
- conflict-decisions.json: Bentley, Alexandria, Bugatti = nur Vorfahren (isResident=false),
bestätigt von der Züchterin (Tickets 09bcac78 / 45cc501b).
- Regressionstests ergänzt/aktualisiert (Bentley/Alexandria/Bugatti isResident; Danielle
Vater Makoto; Catelyn Eddard×Milena; Gaida Zhuāngzǐ×Zaibunissa). Alle python test_*.py grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Danielle: Vater Makoto ergänzt (Mutter Ella war schon gesetzt) — Geschwisterverpaarung.
- Catelyn: Eltern Eddard Stark of Sunset Glow × Milena.
- Gaida: Eltern über Geburtsdatum gefunden — Zhuāngzǐ × Zaibunissa (beide *21.02.2020,
Geschwisterverpaarung, Kinder von Nisha × Zenon).
Per conflict-decisions.json (re-ingest verifiziert: Eltern live korrekt). Tickets
4692fd5c / 7bbc045c / ba63325a geschlossen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Die Züchterin kann auf der Tickets-Seite Push-Benachrichtigungen aktivieren
(🔔-Schalter). Service Worker ist BEWUSST push-only (kein fetch/Caching), damit das
Laden der App nie beeinträchtigt wird.
- Backend: WebPush-Bibliothek + VAPID; PushNotifier sendet an alle Abos, räumt veraltete
(404/410) auf. Endpoints: GET /push/vapid-public-key, POST /push/subscribe|unsubscribe.
- Ausgelöst über ein notify-Flag auf PUT /feedback: NUR die KI setzt es (z. B. neue
Rückfrage / Ticket gelöst) → keine Selbst-Pushes durch Aktionen der Züchterin. Text wird
aus dem Status abgeleitet, Klick öffnet das Ticket (?focus=).
- Schalter/Logik blenden sich aus, wenn das Gerät kein Push kann oder der Server keine
VAPID-Schlüssel hat (z. B. e2e-Mock).
Migration WebPushSubscriptions. VAPID-Schlüssel in appsettings.json (lokale Single-User-App
im Heimnetz — bewusste, vertretbare Vereinfachung). Tests: 264 Backend grün (+Push-Endpoints),
e2e Tickets Desktop grün, vitest 149.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Anhänge im Melde-Fenster (beim Erstellen) und direkt an bestehenden Tickets:
Vorschaubilder, Öffnen im neuen Tab, Entfernen.
- Bytes liegen in eigener Tabelle (FeedbackAttachment, lose FeedbackId ohne FK →
übersteht den Ingest-Wipe); GET /feedback liefert nur Metadaten (id/Name/Typ/Größe),
die Bytes über /feedback/attachments/{id}. Größenlimit 10 MB.
- Endpoints: POST /feedback/{id}/attachments (base64), GET /feedback/attachments/{id}
(Bytes), DELETE /feedback/attachments/{id}.
Migration FeedbackAttachments. Tests: 262 Backend grün (+Upload/Serve/Delete +Validierung),
e2e Tickets Desktop+Phone grün (+Foto-Upload), vitest 149.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Suche: Volltext über Nachricht/Frage/Antwort/Changelog/Name/Kategorie, plus
Kategorie-Filter (KI-gesetzte Kategorie als Chip + Auswahlmenü), je innerhalb der
aktiven Ansicht.
- 👍/👎 auf gelösten Tickets: 👍 speichert die Rückmeldung, 👎 öffnet das Ticket
wieder (zurück in die Rückfragen) und bittet um die fehlende Info.
- Melde-Fenster: schlägt beim Tippen ähnliche bereits gelöste Tickets vor
(Wort-Überlappung + Bonus bei gleichem Tier/Wurf/Kontakt) — Self-Service.
Backend: Feedback.Category + Feedback.Helpful (+ PUT-Handling), Migration
FeedbackCategoryAndHelpful (beide nullable). Tests: 260 Backend grün, e2e Tickets
Desktop +4 (Suche/Kategorie/👍/👎), vitest 149.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wiederöffnen eines gelösten Tickets:
- landet jetzt in „Rückfragen" (NeedsInfo) statt „Offen".
- War es ein echt gelöstes Ticket OHNE Rückfrage, wird um mehr Infos gebeten und
„Wieder geöffnet am" (ReopenedAt) gestempelt.
- Hatte es bereits eine offene Rückfrage (z. B. manuell gelöste Rückfrage), kehrt es
ohne neuen Hinweis und ohne Zeitstempel in genau diese Frage zurück.
Löschen über die UI = Soft-Delete:
- Tickets wandern in die neue Kategorie „Gelöscht" (Papierkorb) statt entfernt zu werden.
- Von dort wiederherstellbar (POST /feedback/{id}/restore → DeletedAt = null).
- Countdown „Wird in N Tagen endgültig gelöscht"; nach 30 Tagen endgültige Löschung
(lazy beim Abruf in GET /feedback).
Außerdem: Scrollposition bleibt beim Zurück-Navigieren (z. B. nach Klick auf einen
Tier-Link) erhalten, statt an den Seitenanfang zu springen.
EF-Migration FeedbackReopenedAndSoftDelete (ReopenedAt, DeletedAt — beide nullable).
Tests: 260 Backend grün (+2 Reopen-Fälle, CRUD auf Soft-Delete umgestellt), e2e Tickets
Desktop+Phone grün (+Soft-Delete/Restore/Reopen), vitest 149.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sobald ein Ticket eine Antwort hat, ist der ursprüngliche Ticket-Text nicht
mehr bearbeitbar. Editierbar ist dann nur noch die neueste Antwort — und auch
die nur, bis die KI darauf reagiert hat (Status verlässt „Answered"). Ältere
Antworten im Verlauf (thread) bleiben wie bisher schreibgeschützt.
Regressionstest + Answered-Mock-Ticket ergänzt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
genotype.py: Python-Port von genotypeToFarbschlag (0 Abw. über 3402 Genotypen).
resolve_color_and_genotype: bei vorhandenem Genotyp gewinnt der berechnete Farbschlag
(Goldfuchs≠Gold, Dilute Agouti/Anthrazit, Blaufuchs statt -schimmel bei (schimmel),
spsp statt Schecke). Mamta Mini: Ee + Eltern Geely×Gaida am Wurf. Regressionstests je Fall.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Statt fest http://localhost:5179 (auf dem Handy = das Handy selbst) nutzt der
Client im Dev-Modus den Host, von dem die Seite geladen wurde → 192.168.x.y:5179.
e2e/Rechner bleiben bei localhost.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Klarstellung für die Züchterin: übernommen werden Tiere/Stammbäume/Genotypen/
Würfe/Kontakte/Gehege+Reinigung/Erwerb/Reservierung/Rücknahmen/Warteliste/
Ausstellungen; (noch) nicht: Finanzen, Termine, Tierarzt-Stammdaten, Tagebuch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Die KI kann im Text `tier:<8hex>` / `kontakt:` / `wurf:` / `gehege:` schreiben;
Frontend löst per POST /refs/resolve (Präfix-Match, DB-agnostisch) zu Name +
internem Link auf. Reine KI-Schreibhilfe — kein Nutzer-Button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Ticket-Texte (Frage/Antwort/Nachricht/Changelog/Verlauf) rendern eine
Markdown-Teilmenge: **fett**, `code`, Aufzählungen, Links, Tier-/Ticket-IDs
- Offene Rückfrage (NeedsInfo): nicht bearbeitbar — die Züchterin soll antworten
- Beantwortet (Answered): Antwort nachträglich änderbar, bevor die KI übernimmt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher landeten beantwortete Tickets (Answered) in „Rückfragen". Sobald die
Züchterin geantwortet hat, ist sie fertig → das Ticket gehört in „Offen"
(von der KI zu erledigen). „Rückfragen" zeigt jetzt nur noch NeedsInfo
(wartet auf ihre Antwort).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Web-App-Manifest, App-Icons (192/512 + maskable, Rennmaus-Motiv) und
apple-touch-icon/theme-color in index.html → "Zum Startbildschirm hinzufügen".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>