feat(triage): Ticket-Fixes (Daten + Code) + prod-fähige Triage
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 1m36s
CI / Frontend Tests (Node/Vite) (push) Successful in 9m35s
CI / Docker Build & Push (push) Successful in 1m28s
CI / Deploy auf TrueNAS (Custom App) (push) Failing after 3s

Daten-Fixes (conflict-decisions.json, re-ingest-stabil) für ~30 Tickets:
Merges (Jamie/Hiro/Mino/Jana/Blacky/Sakura/Malou/Socke→Marty), Eltern-Korrekturen
(Jacky/Idefix/Ichika/Roni/Ethan), Kruke→Kuke (+ Todesdatum), Targa-Wurf R14 + Druna,
Stacy/Merle/Domi/Eliza; Joghurt-Phantomwurf entfernt.

Code-Fixes:
- Gaida & alle Verstorbenen: Status wird aus Todesdatum/Abgabe abgeleitet
  (Program.cs Startup-Sweep heilt Altfälle; IngestResolved re-derived nach Freeze).
- CoCo: Scheckungsart wird bei jeder Schecke angezeigt (Platzhalter wenn leer).
- M-Wurf/Gale: über-gemergte Fremdtiere via neuem litterChildren-Override entfernt.
- renameTo eltern-verknüpfungssicher (Quell-Name im Index); dateOfDeath als Override.

Prod-fähige Triage (API):
- GET /feedback/{id} + GET /feedback?status= (kein 2-MB-Dump).
- POST /import/ingest-resolved/upload (multipart) → Ingest gegen Prod ohne SSH.

Tests: 280 Backend, 149 Frontend, alle Python, betroffene Playwright grün.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 00:41:43 +02:00
parent 0a4bcabd98
commit 2e7911074f
17 changed files with 749 additions and 468 deletions

View File

@@ -3209,7 +3209,7 @@ def main():
_ovr_by_namedob = {}
for d in _decisions:
has_ovr = (("isResident" in d) or d.get("notes") or d.get("receiver")
or d.get("renameTo") or d.get("deceased")
or d.get("renameTo") or d.get("deceased") or d.get("dateOfDeath")
or d.get("correctDob") or d.get("genotype") or d.get("farbschlag")
or d.get("gender") or d.get("father") or d.get("mother")
or d.get("originBreeder"))
@@ -3274,6 +3274,10 @@ def main():
# erst HIER wird der Name überschrieben.
rename_to = (d.get("renameTo") or "").strip()
if rename_to and g.get("Name") != rename_to:
# Quell-Namen merken, damit spätere Eltern-Verknüpfungen, die noch den
# alten Namen referenzieren, weiter auflösen (siehe Namens-Index unten,
# Ticket e0a0c304 Kruke→Kuke: sonst verlieren Kukes Würfe die Mutter).
g["_pre_rename_name"] = g.get("Name")
g["Name"] = rename_to
applied = True
# deceased: markiert ein bestehendes Tier als verstorben, falls noch
@@ -3281,6 +3285,14 @@ def main():
if d.get("deceased") and g.get("Status") != "Deceased":
g["Status"] = "Deceased"
applied = True
# dateOfDeath: setzt das Todesdatum (falls noch keins) + Status Deceased.
# rpro3 hat keine Sterbespalte, daher gibt die Züchterin/das Ticket das Datum vor.
if d.get("dateOfDeath") and not g.get("DateOfDeath"):
_dod = parse_date(d["dateOfDeath"])
if _dod:
g["DateOfDeath"] = _dod
g["Status"] = "Deceased"
applied = True
if d.get("correctDob") and not g.get("DateOfBirth"):
dob_iso = parse_date(d["correctDob"])
g["DateOfBirth"] = dob_iso
@@ -3414,6 +3426,13 @@ def main():
# …and by the connector-folding canon key so abbreviation variants match
# (e.g. „BlackFire v.d. Kleinen Chaoten“ vs „… von den …“ — ticket #30).
keys.add(canon_name_key(g["Name"]))
# Umbenannte Tiere (renameTo) auch unter ihrem Quell-Namen indexieren, damit
# Eltern-Verknüpfungen, die noch den alten Namen referenzieren, weiter auflösen.
pre = g.get("_pre_rename_name")
if pre:
keys.add(normalize_name(pre))
keys.add(normalize_name(get_call_name(pre)))
keys.add(canon_name_key(pre))
for k in keys:
if k:
gerbil_by_norm_name.setdefault(k, []).append(g)
@@ -4140,6 +4159,42 @@ def main():
if _lo_applied:
print(f"litterOrder: {_lo_applied} Jungtier-Reihenfolgen gesetzt")
# ── litterChildren: autoritative Kinder-Whitelist eines Wurfs (Mensch-Entscheidung) ──
# Für Fälle, in denen der Wurf-Merge fremde Tiere (z. B. Abnehmer-Sammelseiten der
# Wurfchronik) in einen datierten Wurf gezogen hat (Tickets 4b9f49fb M-Wurf, 8d259edf Gale).
# Match per date + `matchChild` (ein aktuell zugeordnetes Kind, disambiguiert gleich-datierte
# Würfe). Alle Kinder, deren Ruf-Name NICHT in `keep` steht, werden aus dem Wurf gelöst
# (LitterId=None) — sie verschwinden aus dem Wurf, bleiben aber als Tier erhalten. Optional
# `renameTo` setzt den kanonischen Wurf-Namen (z. B. 'Ungeklärt' -> 'M-Wurf').
try:
with open(CONFLICT_DECISIONS_PATH, encoding="utf-8") as _lcfh:
_litter_children = (json.load(_lcfh).get("litterChildren") or [])
except Exception:
_litter_children = []
_lc_removed = 0
for lc in _litter_children:
lc_date = parse_date(lc["date"]) if lc.get("date") else None
match_child = normalize_name(get_call_name(lc.get("matchChild") or ""))
keep = {normalize_name(get_call_name(n)) for n in (lc.get("keep") or [])}
if not keep:
continue
for l in resolved_litters:
if lc_date and l.get("Date") != lc_date:
continue
kids = [g for g in resolved_gerbils if g.get("LitterId") == l["Id"]]
names = {normalize_name(get_call_name(g.get("Name") or "")) for g in kids}
if match_child and match_child not in names:
continue # falscher, nur zufällig gleich-datierter Wurf
for g in kids:
if normalize_name(get_call_name(g.get("Name") or "")) not in keep:
g["LitterId"] = None
_lc_removed += 1
if lc.get("renameTo"):
l["Name"] = lc["renameTo"]
break
if _lc_removed:
print(f"litterChildren: {_lc_removed} fälschlich zugeordnete Jungtiere aus Würfen gelöst")
# ── isResident-Sweep (Ticket 381f7e51, freigegeben) ───────────────────────
# Entscheidung der Züchterin: „Nimm wirklich ALLE Tiere aus dem Bestand heraus,
# MIT AUSNAHME der, die bereits ein ELTERNTIER sind." Datengetrieben + re-ingest-