Compare commits

..

14 Commits

Author SHA1 Message Date
5361a292f1 NAMEGEN-2-BE: 5 neue Themen-Kategorien (disney/pokemon/encities/hrcities/usstates)
UsageMap bildet alle Codes auf deutsche Beschreibungen ab; thematische Kategorien
landen unter "Themen-Kategorien" im Prompt (kein Etymologie-Zwang, origin=Kategoriename).
22 neue Tests, 179/179 gruen.
2026-06-07 01:32:56 +02:00
36795bd974 Merge feature/seed-rew (SEED-REW): Backend ColorVariety 'Pink Eyed White (PEW)' → 'REW' rename-in-place (FK-safe)
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 59s
CI / Frontend Tests (Node/Vite) (push) Successful in 9m33s
CI / Docker Build & Push (push) Failing after 9s
2026-06-07 01:19:26 +02:00
ae60b47ad3 D7: Quelldatei(en) je offenem Konflikt-Tier angereichert (Dakota/Max + alle 6 offenen)
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 1m1s
CI / Docker Build & Push (push) Has been cancelled
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
2026-06-07 01:15:04 +02:00
adb9e93cdb SEED-REW: rename ColorVariety 'Pink Eyed White (PEW)' to 'REW'
Single UpdateData migration for ID 00000000-...-0001 (sortOrder 0).
Name-only rename, genotype/ID/FK unchanged — no drift risk.
Matches colorVarietySeed.backend.json after GEN-4c (main 3aa9ac9).

165/165 tests, ef has-pending=No.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-07 01:12:55 +02:00
3aa9ac9811 Merge feature/gen-4c (GEN-4c): PEW→REW konsolidiert (Julian REW-1) + REW-2 A-unabhängig verifiziert + aa-Test
Some checks failed
CI / Docker Build & Push (push) Has been cancelled
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
CI / Backend Tests (.NET) (push) Successful in 58s
2026-06-07 01:10:18 +02:00
b72e0a9d9a GEN-4c: PEW=REW konsolidiert (Julian) + REW-2-Verifikation
- catalog.ts: 'Pink Eyed White (PEW)' -> 'REW' (frozen rename; chch+pp
  engine-computes 'REW' per Julian; entry now round-trips correctly)
- genetics.test.ts: REW_SHADOWED-Ausnahme entfernt (kein Bedarf mehr);
  CATALOG[0]-Assertion auf 'REW'; aa cchmcchm+pp-Test als REW-2-Nachweis
  (Julian: A-Locus egal fuer REW-Bedingung)
- chipColors.ts: 'REW'-Chip ergaenzt (gleiche Farbe wie PEW; additive;
  PEW-Eintrag bleibt fuer Rueckwaertskompatibilitaet waehrend DB-Migration)
- Seed-Artefakte regeneriert (70 Reihen, REW statt PEW an Position 0)
- FLAGGED to god: 'Altweiss (REW)' hat volles C (C:'C') -> faellt NICHT
  unter REW-Bedingung; warte auf Julian-Entscheid vor Aenderung
2026-06-07 01:08:23 +02:00
12d85d9796 WEB-Hosting-Pivot: Cloudflare → Self-Host auf TrueNAS (Strato/DynDNS + nginx-Reverse-Proxy); A4 umgestellt
Some checks failed
CI / Docker Build & Push (push) Has been cancelled
CI / Backend Tests (.NET) (push) Successful in 58s
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
2026-06-07 01:07:30 +02:00
76bdde4649 B2 Firewall erledigt — Handy-WLAN-Zugriff (Ports 5173/5179) offen
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 59s
CI / Docker Build & Push (push) Has been cancelled
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
2026-06-07 01:04:00 +02:00
025bca1840 REW-1/REW-2 beantwortet: PEW=REW (zusammenführen, GEN-4c) + REW A-unabhängig (schon implementiert)
Some checks failed
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
CI / Docker Build & Push (push) Has been cancelled
CI / Backend Tests (.NET) (push) Has been cancelled
2026-06-07 01:03:05 +02:00
58dc811574 E1/E2 Charakterbogen als ERLEDIGT markiert (CHARAKTERBOGEN-2: 4 Kategorien, +10 Traits, Warnsignale) — war stale
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 1m0s
CI / Docker Build & Push (push) Has been cancelled
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
2026-06-07 00:59:37 +02:00
367304940f D7: Osamu/Percy/Iwana Konfliktentscheidungen (Züchterin) + Eragon=Elieus C=CC bestätigt → conflict-decisions.json + HUMANQUESTION (7/13 erledigt)
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 1m0s
CI / Docker Build & Push (push) Has been cancelled
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
2026-06-07 00:58:04 +02:00
7e1acdb5e4 A5 beantwortet: TrueNAS Goldeye 25.10 + eigener Postgres + Backups unter /mnt/JailStorage/DockerVolumes — OPS-2 entsperrt
Some checks failed
CI / Docker Build & Push (push) Has been cancelled
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
CI / Backend Tests (.NET) (push) Successful in 1m2s
2026-06-07 00:54:26 +02:00
5ee6a4edf6 Merge feature/stammbaum-expand (STAMMBAUM-EXPAND): prominenter +-Vorfahren-Button (40px) + Bottom-Mini-Legende (Name→Akte)
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 58s
CI / Docker Build & Push (push) Has been cancelled
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
2026-06-07 00:47:05 +02:00
a42966ad8e STAMMBAUM-EXPAND: +-Button prominenter + Mini-Legende (Stammbaum-UX)
Punkt 1 - Expand-Button sichtbar/zuverlaessig:
- 40px solid-fill (war 32px outline), box-shadow, flex-centered;
  deutlich sichtbar auf 190px Karte, touch-tauglich (>=40px Tap-Target).
  Hover/focus-visible: opacity + outline.
- Edge-case (Litter mit null-Eltern): +-Button bleibt sichtbar --
  unbekannt-Karten sind korrektes Feedback fuer quarantaenierte Eltern.

Punkt 2 - Bottom-Hint Mini-Legende:
- stammbaum-hint -> stammbaum-hints ul (flex-wrap, 3 Items):
  Umwurzeln / Name=Akte / +=Nachladen. Gut lesbar auf 390px.
  de.ts: hintName + hintExpand (neue Keys; tapHint bleibt fuer card title).

e2e: +2 Specs x 2 Viewports = 4 neue Tests (Legende + Expand-Klick).
Emil (Gen 4, litterId w-emil) liefert Testanker im Mock-Stammbaum.

Gate: vitest 122/122 e2e 166+68skipped build+tsc+eslint clean.
2026-06-07 00:38:46 +02:00
17 changed files with 1735 additions and 76 deletions

View File

@@ -8,8 +8,8 @@ using Microsoft.Extensions.Options;
namespace GerbilManager.Tests
{
/// <summary>
/// FEAT-NAMEGEN: NameSuggestionService — prompt assembly, JSON parse (incl. Markdown
/// fence strip), 503-not-configured path, upstream-error path.
/// FEAT-NAMEGEN / NAMEGEN-2-BE: NameSuggestionService — prompt assembly, usage-code mapping,
/// JSON parse (incl. Markdown fence strip), 503-not-configured path, upstream-error path.
/// </summary>
public class NameSuggestionTests
{
@@ -26,6 +26,14 @@ namespace GerbilManager.Tests
Assert.Contains("origin", prompt);
}
[Fact]
public void SystemPrompt_erklärt_thematische_Kategorien()
{
var prompt = NameSuggestionService.BuildSystemPrompt();
Assert.Contains("thematischen Kategorien", prompt);
Assert.Contains("Erfinde KEINE Etymologie", prompt);
}
[Fact]
public void UserPrompt_enthält_Anzahl_und_Anfangsbuchstaben()
{
@@ -33,7 +41,10 @@ namespace GerbilManager.Tests
Assert.Contains("6", prompt);
Assert.Contains("\"A\"", prompt);
Assert.Contains("weibliche", prompt);
Assert.Contains("norn,mythg", prompt);
// codes are mapped to German descriptions
Assert.Contains("Nordische/Altnordische Etymologie", prompt);
Assert.Contains("Griechische Mythologie", prompt);
Assert.Contains("Kulturkreisen", prompt);
}
[Fact]
@@ -54,6 +65,67 @@ namespace GerbilManager.Tests
Assert.DoesNotContain("männliche", prompt);
}
// ── NAMEGEN-2-BE: neue Themen-Kategorien ─────────────────────────────
[Theory]
[InlineData("disney", "Disney-Charaktere")]
[InlineData("pokemon", "Pokémon-Namen")]
[InlineData("encities", "Namen englischer Städte")]
[InlineData("hrcities", "Namen kroatischer Städte")]
[InlineData("usstates", "Namen von US-Bundesstaaten")]
public void UsageMap_enthält_alle_fünf_neuen_Codes(string code, string expectedDescription)
{
Assert.True(NameSuggestionService.UsageMap.TryGetValue(code, out var entry));
Assert.Equal(expectedDescription, entry.Description);
Assert.True(entry.Thematic);
}
[Theory]
[InlineData("disney", "Disney-Charaktere")]
[InlineData("pokemon", "Pokémon-Namen")]
[InlineData("encities", "Namen englischer Städte")]
[InlineData("hrcities", "Namen kroatischer Städte")]
[InlineData("usstates", "Namen von US-Bundesstaaten")]
public void UserPrompt_enthält_Themen_Kategorie_Beschreibung(string code, string expectedDescription)
{
var prompt = NameSuggestionService.BuildUserPrompt(null, null, code, 5);
Assert.Contains(expectedDescription, prompt);
Assert.Contains("Themen-Kategorien", prompt);
Assert.DoesNotContain("Kulturkreisen", prompt);
}
[Fact]
public void UserPrompt_trennt_etym_und_thematische_Kategorien()
{
var prompt = NameSuggestionService.BuildUserPrompt("D", null, "norn,disney", 4);
Assert.Contains("Nordische/Altnordische Etymologie", prompt);
Assert.Contains("Kulturkreisen", prompt);
Assert.Contains("Disney-Charaktere", prompt);
Assert.Contains("Themen-Kategorien", prompt);
}
[Fact]
public void UserPrompt_thematisch_enthält_Geschlecht_Hinweis()
{
var prompt = NameSuggestionService.BuildUserPrompt(null, "female", "encities", 3);
Assert.Contains("Geschlecht-Filter kann ignoriert werden", prompt);
}
[Fact]
public async Task SuggestAsync_parst_thematische_Kategorie_Antwort()
{
var payload = """[{"name":"Dumbo","meaning":"Elefant mit großen Ohren aus dem Disney-Film Dumbo (1941)","origin":"Disney"}]""";
var handler = new StubHandler(_ => Canned(payload));
var service = CreateService("https://api.example.com/v1", "k", "m", handler);
var result = await service.SuggestAsync("D", null, "disney", 1);
Assert.Equal(NameSuggestionStatus.Ok, result.Status);
Assert.NotNull(result.Suggestions);
Assert.Equal("Dumbo", result.Suggestions![0].Name);
Assert.Equal("Disney", result.Suggestions[0].Origin);
}
// ── JSON parsing ──────────────────────────────────────────────────────
[Fact]

View File

@@ -291,7 +291,7 @@ public class ApplicationContext : DbContext
(string Name, string Genotype, int SortOrder)[] catalog =
{
// --- C-locus white / partial albino (IDs 1-3) ---
("Pink Eyed White (PEW)", "AA chch DD EE GG pp spsp rere", 0),
("REW", "AA chch DD EE GG pp spsp rere", 0),
("Hermelin", "aa chch DD EE GG PP spsp rere", 1),
("Himalaya", "AA chch DD EE GG PP spsp rere", 2),
// --- Zobel / colourpoint dark (ID 4) ---

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GerbilManagerWebAPI.Migrations
{
/// <inheritdoc />
public partial class RenameREW : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "ColorVarieties",
keyColumn: "Id",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
column: "Name",
value: "REW");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "ColorVarieties",
keyColumn: "Id",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
column: "Name",
value: "Pink Eyed White (PEW)");
}
}
}

View File

@@ -216,7 +216,7 @@ namespace GerbilManagerWebAPI.Migrations
{
Id = new Guid("00000000-0000-0000-0000-000000000001"),
CanonicalGenotype = "AA chch DD EE GG pp spsp rere",
Name = "Pink Eyed White (PEW)",
Name = "REW",
SortOrder = 0
},
new

View File

@@ -20,6 +20,23 @@ namespace GerbilManagerWebAPI.Names
PropertyNameCaseInsensitive = true,
};
// Known usage codes → (German description, isThematic).
// Thematic categories provide real source names (not etymology); unknown codes fall through as etym.
internal static readonly Dictionary<string, (string Description, bool Thematic)> UsageMap =
new(StringComparer.OrdinalIgnoreCase)
{
["norn"] = ("Nordische/Altnordische Etymologie", false),
["japa"] = ("Japanische Etymologie", false),
["mythg"] = ("Griechische Mythologie", false),
["ger"] = ("Germanische/Deutsche Etymologie", false),
["arb"] = ("Arabische Etymologie", false),
["disney"] = ("Disney-Charaktere", true),
["pokemon"] = ("Pokémon-Namen", true),
["encities"] = ("Namen englischer Städte", true),
["hrcities"] = ("Namen kroatischer Städte", true),
["usstates"] = ("Namen von US-Bundesstaaten", true),
};
public async Task<NameSuggestionResult> SuggestAsync(
string? letter, string? gender, string? usages, int count,
CancellationToken ct = default)
@@ -44,7 +61,11 @@ namespace GerbilManagerWebAPI.Names
"Du bist ein Helfer für Rennmaus-Züchter. " +
"Antworte IMMER mit einem reinen JSON-Array — KEINE Markdown-Code-Blöcke, " +
"KEINE Erklärungen, KEIN Text außerhalb des Arrays. " +
"Jedes Element hat genau die Felder: name, meaning, origin (alle Strings, alle auf Deutsch).";
"Jedes Element hat genau die Felder: name, meaning, origin (alle Strings, alle auf Deutsch). " +
"Bei thematischen Kategorien (Disney, Pokémon, Städte, Bundesstaaten): " +
"origin = Kategoriename (z.B. \"Disney\", \"Pokémon\", \"Englische Stadt\", \"Kroatische Stadt\", \"US-Bundesstaat\"), " +
"meaning = kurzer Kontext aus der Quelle (z.B. Film/Figur-Beschreibung, Lage der Stadt, Bundesstaat-Bezug). " +
"Erfinde KEINE Etymologie für thematische Kategorien.";
internal static string BuildUserPrompt(string? letter, string? gender, string? usages, int count)
{
@@ -55,10 +76,44 @@ namespace GerbilManagerWebAPI.Names
if (!string.IsNullOrWhiteSpace(gender) &&
!gender.Equals("any", StringComparison.OrdinalIgnoreCase))
sb.Append($", passend für {(gender.Equals("female", StringComparison.OrdinalIgnoreCase) ? "weibliche" : "männliche")} Tiere");
bool hasThematic = false;
if (!string.IsNullOrWhiteSpace(usages))
sb.Append($", aus den Kulturkreisen: {usages}");
sb.Append(". Jeder Name muss eine echte etymologische Bedeutung und Herkunft haben ");
sb.Append("(keine erfundenen oder zufälligen Namen). ");
{
var codes = usages.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var etymDescriptions = new List<string>();
var thematicDescriptions = new List<string>();
foreach (var code in codes)
{
if (UsageMap.TryGetValue(code, out var entry))
{
if (entry.Thematic) thematicDescriptions.Add(entry.Description);
else etymDescriptions.Add(entry.Description);
}
else
{
etymDescriptions.Add(code);
}
}
if (etymDescriptions.Count > 0)
sb.Append($", aus den Kulturkreisen: {string.Join(", ", etymDescriptions)}");
if (thematicDescriptions.Count > 0)
sb.Append($", aus den Themen-Kategorien: {string.Join(", ", thematicDescriptions)}");
hasThematic = thematicDescriptions.Count > 0;
}
sb.Append(". ");
if (hasThematic)
{
sb.Append("Für Kulturkreis-Namen: echte etymologische Bedeutung und Herkunft. ");
sb.Append("Für Themen-Kategorien: echte Namen aus der Quelle, origin = Kategoriename, ");
sb.Append("Geschlecht-Filter kann ignoriert werden wenn nicht sinnvoll anwendbar. ");
}
else
{
sb.Append("Jeder Name muss eine echte etymologische Bedeutung und Herkunft haben ");
sb.Append("(keine erfundenen oder zufälligen Namen). ");
}
sb.Append($"Antworte mit genau {count} Elementen als reines JSON-Array: ");
sb.Append("[{\"name\":\"...\",\"meaning\":\"...\",\"origin\":\"...\"}]");
return sb.ToString();

View File

@@ -19,63 +19,61 @@ Feinschliff** — siehe unten._
| # | Was gebraucht wird | Schaltet frei |
|---|---|---|
| **A3** | **Gmail App-Passwort.** Google-Konto → 2-Faktor aktivieren → „App-Passwörter" → eines für „GerbilManager" → 16-stelligen Code an Michael. | E-Mail-Posteingang (Anfragen abrufen + KI-Antwortentwürfe). Backend ist fertig, wartet nur auf den Zugang. |
| **A4** | **Domain-Name** (registriert) + **Cloudflare-Konto & API-Token** (Berechtigung „Cloudflare Pages → Edit"). | Öffentliche Webseite veröffentlichen (Jimdo-Ersatz). Seite ist gebaut. |
| **A5** | **TrueNAS-Restfragen:** (a) SCALE-Version? · (c) eigener Postgres-Container (empfohlen) oder bestehender NAS-Postgres? · (d) Dataset-Pfad für Daten/Backups, Port 80 frei? | Produktiv-Betrieb auf dem NAS (compose ist fertig vorbereitet). |
| **A4** (umgestellt 2026-06-07) | **Self-Host statt Cloudflare:** Strato-Domain → DynDNS → deine IP (auto-aktualisiert) + **nginx-Reverse-Proxy auf TrueNAS** leitet auf die statische Seite. Cloudflare NICHT mehr nötig. **Offen:** welcher Hostname/Subdomain für die öffentliche Seite? Bau = WEB-2 (Dwight, self-host-Variante). | Öffentliche Webseite (Jimdo-Ersatz), self-hosted auf dem NAS. |
| ~~**A5**~~ | **TrueNAS beantwortet (2026-06-07):** (a) SCALE **25.10.2.1 Goldeye** → Custom App (Docker compose) ✓ · (c) **eigener Postgres-Container** (legen wir an, compose macht das) ✓ · (d) Daten/Backups unter **`/mnt/JailStorage/DockerVolumes/`** (neuer Ordner) ✓. **Rest-Detail:** Port **80** frei? (sonst `PORT` in .env ändern). → OPS-2 (Dwight) finalisiert compose/.env/ops.md auf diese Werte. | Produktiv-Betrieb auf dem NAS. |
| **A5b2** | **2 Gitea-Repo-Secrets** anlegen (Repo `Gulum/GerbilManager`**Einstellungen → Actions → Secrets → Secret hinzufügen**): `REGISTRY_USER` = `gulum` · `REGISTRY_TOKEN` = Gitea-Zugriffstoken mit Scope **`write:package`** (erzeugen unter **Benutzer-Einstellungen → Anwendungen → Zugriffstoken verwalten**, Token wird nur einmal angezeigt → in das Secret kopieren). Danach Action erneut laufen lassen. Registry ist jetzt **`git.rismer.de`** (externes HTTPS) → keine `insecure-registry`-Daemon-Konfig nötig. | CI pusht fertige Docker-Images in die Registry. (Tests laufen grün; aktuell rot ist NUR der Login-Schritt: `secrets.REGISTRY_USER`/`REGISTRY_TOKEN` sind leer → „Username and password required".) |
## B. Kleine Aktion (jederzeit)
| # | Aktion | Warum |
|---|---|---|
| **B2** | **Firewall-Regel** (PowerShell **als Administrator**): `New-NetFirewallRule -DisplayName "GerbilManager dev" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 5173,5179 -Profile Any` | Damit das Handy deiner Frau im WLAN auf die App kommt (http://192.168.2.124:5173). |
| ~~**B2**~~ | **Firewall-Regel erledigt** (2026-06-07) — Ports 5173/5179 inbound offen. Handy im WLAN kommt auf http://192.168.2.124:5173 (sobald App läuft). | — |
## C. Genetik-Feinschliff (optional, blockiert nichts)
| # | Frage | Betrifft |
|---|---|---|
| **REW-1** | **PEW = REW?** „Pink Eyed White" (PEW) und „Rotaugenweiß" (REW) sind dasselbe — soll der alte PEW-Eintrag mit REW **zusammengeführt** werden? | Doppel-Eintrag im Farbkatalog. |
| **REW-2** | **REW bei nicht-agouti?** Wird ein `aa`-Tier (z. B. Marder) mit Colourpoint + `pp` **auch** als REW gewertet, oder soll REW nur für agouti-basierte Tiere gelten? | REW-Erkennung (wird gerade A-unabhängig gebaut = aa zählt mit). |
| ~~**REW-1**~~ | **JA, PEW = REW (dasselbe)** PEW-Eintrag mit REW zusammenführen (GEN-4c, Kevin). | Doppel-Eintrag im Farbkatalog. |
| ~~**REW-2**~~ ✅ | **A-unabhängig:** egal ob `AA`/`Aa` (groß) oder `aa` (klein) — wenn die REW-Bedingung erfüllt ist, ist es **immer REW**. Schon so implementiert (GEN-4, A/D/E/G-unabhängig). | REW-Erkennung. |
| **C7** | *(optional)* Was hat deiner Frau bei **Renner Pro** gefehlt? Lieblings-Auswertungen? | mögliche neue Funktionen |
## E. Charakterbogen-Eigenschaften (optional)
## E. Charakterbogen-Eigenschaften — ✅ ERLEDIGT (CHARAKTERBOGEN-2)
Aktuell eingebaute Häkchen-Eigenschaften (für die KI-Verkaufstexte) — **soll etwas ergänzt/gestrichen werden?**
**E1 ✅** — Deine Frau hat die Eigenschaften geliefert, Kelly hat sie eingebaut: **4 Kategorien** (Sozialverhalten · Eignung & Umgang · Hobbys & Eigenarten · Wesen & Temperament), die 15 alten Häkchen unverändert + **10 neue** (dominant, rangniedrig, sozialkompetent, für erfahrene Halter, Beobachtungstier, familiengeeignet, Schredder-Meister, Nestbauer/Architekt, territorial …).
**E2 ✅** — Ehrliche **Warnsignale** (z. B. *schwer vergesellschaftbar*, *territorial*) sind als eigene, optisch abgesetzte Gruppe drin — fließen in die KI-Verkaufstexte ein.
> zutraulich · handzahm · neugierig · aufgeschlossen · ruhig/ausgeglichen ·
> lebhaft/aktiv · verschmust · eigenständig · anfängergeeignet · futterfreudig ·
> buddelt gern · klettert gern · läuft gern im Laufrad · gut verträglich · schreckhaft
- **E1** — Eigenschaften hinzufügen/entfernen? (z. B. *dominant, rangniedrig, für erfahrene Halter, einzelgängerisch*?)
- **E2** — Auch ehrliche **negative** Eigenschaften aufnehmen (z. B. *beißt manchmal*) für ehrliche Inserate?
Weitere Eigenschaften lassen sich jederzeit **additiv** ergänzen (1-Zeilen-Änderung) — falls deiner Frau noch etwas einfällt, einfach sagen.
---
## D7 · Neue Konflikt-Tiere aus den 41 Stammbäumen (bitte entscheiden)
Durch die vielen neuen Stammbaum-Dateien sind **13 neue Konflikt-Tiere** aufgetaucht (**3 erledigt:** Kazumi/Filou/Sokrates ✅ — **10 offen**) (gleicher Name+Datum, widersprüchliche Angaben in mehreren Diagrammen). Sie warten in Quarantäne — **nichts ist verloren**, sie laden automatisch nach, sobald du je Tier kurz sagst was stimmt. (Uw=G + „Vorhandensein gewinnt" sind schon angewendet; das hier ist der echte Rest.)
Durch die vielen neuen Stammbaum-Dateien sind **13 neue Konflikt-Tiere** aufgetaucht (**7 erledigt:** Kazumi/Filou/Sokrates/Osamu/Percy/Iwana/Eragon ✅ — **6 offen**) (gleicher Name+Datum, widersprüchliche Angaben in mehreren Diagrammen). Sie warten in Quarantäne — **nichts ist verloren**, sie laden automatisch nach, sobald du je Tier kurz sagst was stimmt. (Uw=G + „Vorhandensein gewinnt" sind schon angewendet; das hier ist der echte Rest.)
_Quelldatei = die Stammbaum-Datei(en) in `C:\Users\gulum\dev\Sttammbäume\`, in denen das Tier vorkommt. **Mehrere Dateien = Ursache des Konflikts** (widersprüchliche Angaben in verschiedenen Diagrammen)._
**A) Nur Sterbedatum offen** (Gencode einig — bei Osamu/Filou/Sunny zusätzlich „taub" beibehalten):
| Tier | Sterbedatum — welches? |
|---|---|
| Isa of Golden Lights (*24.12.2014) | 21.07.2018 ↔ 21.10.2018 |
| Jack II v.d. K.C. (*14.02.2016) | 06.10.2019 ↔ 20.10.2019 |
| Osamu v.d. K.C. (*10.12.2015) | 01.10.2020 ↔ 18.12.2020 |
| ~~Filou v.d. K.C. (*24.11.2014)~~ ✅ | **31.08.2019** (erledigt) |
| Sunny von PZ Karl (*10.04.2014) | 30.04.2019 ↔ 05.05.2019 |
| Tier | Sterbedatum — welches? | Quelldatei(en) |
|---|---|---|
| Isa of Golden Lights (*24.12.2014) | 21.07.2018 ↔ 21.10.2018 | Stammbaum von **Ella**.xlsx · von **Kalea**.xlsx |
| Jack II v.d. K.C. (*14.02.2016) | 06.10.2019 ↔ 20.10.2019 | Stammbaum von **Kalea**.xlsx · von **Rainny**.xlsx · von **Ren**.xlsx |
| ~~Osamu v.d. K.C. (*10.12.2015)~~ ✅ | **18.12.2020** (erledigt) | — |
| ~~Filou v.d. K.C. (*24.11.2014)~~ ✅ | **31.08.2019** (erledigt) | — |
| Sunny von PZ Karl (*10.04.2014) | 30.04.2019 ↔ 05.05.2019 | Stammbaum von **Vance**.xlsx · von **Yurikas und Pintos Sohn**.xlsx |
**B) Gencode-Konflikt** (+ ggf. Sterbedatum):
| Tier | Konflikt — was stimmt? |
|---|---|
| Milon v.d. K.C. (*27.11.2014) | A-Locus: **Aa****aa** |
| Percy of little runners (*16.12.2017) | P-Locus: **PP** **Pp** |
| Iwana of little runners (*02.10.2018) | P-Locus: **PP** **Pp** |
| ~~Sokrates v.d. K.C. (*14.12.2015)~~ ✅ | **D-** + Sterbedatum **20.05.2019** (erledigt) |
| Eragon (Elieus, *18.05.2016) | C-Locus: **CC** (vollfarbig) **c[chm]c[chm]** (Colourpoint) |
| Dakota of sweet little mouse (*30.01.2015) | A: **Aa**↔**aa** · P: **pp**↔**PP** · Sp: **Spsp**↔**spsp** |
| ~~Kazumi v.d. K.C. (*23.04.2013)~~ ✅ | **Aa Cc[chm] DD ee[f] GG PP Spsp** (erledigt) |
| Max von Privat (*01.02.2013) | D: **D-**↔**DD** · P: **P-**↔**PP** · Sterbedatum (4 Varianten: 04.02.2016 / 04.03.2016 / 2014 / 30.12.2015) |
| Tier | Konflikt — was stimmt? | Quelldatei(en) |
|---|---|---|
| Milon v.d. K.C. (*27.11.2014) | A-Locus: **Aa****aa** | Stammbaum von **South Dakota**.xlsx · von **Tennessee**.xlsx |
| ~~Percy of little runners (*16.12.2017)~~ | P-Locus **Pp** (erledigt) | — |
| ~~Iwana of little runners (*02.10.2018)~~ | P-Locus **Pp** (erledigt) | — |
| ~~Sokrates v.d. K.C. (*14.12.2015)~~ ✅ | **D-** + Sterbedatum **20.05.2019** (erledigt) | — |
| ~~Eragon (Elieus, *18.05.2016)~~ ✅ | C **CC** (vollfarbig) + Name **„Elieus gen. Eragon"**. Korrekter Datensatz lädt bereits (CC); Colourpoint-Variante „Kleiner Warnowrenner …" bleibt als Dublette in Quarantäne — kein weiterer Schritt nötig. | — |
| Dakota of sweet little mouse (*30.01.2015) | A: **Aa**↔**aa** · P: **pp**↔**PP** · Sp: **Spsp**↔**spsp** | Stammbaum von **Jiminy of Black Forest**.xlsx · von **Yurikas und Pintos Sohn**.xlsx |
| ~~Kazumi v.d. K.C. (*23.04.2013)~~ ✅ | **Aa Cc[chm] DD ee[f] GG PP Spsp** (erledigt) | — |
| Max von Privat (*01.02.2013) | D: **D-**↔**DD** · P: **P-**↔**PP** · Sterbedatum (4 Varianten: 04.02.2016 / 04.03.2016 / 2014 / 30.12.2015) | Stammbaum von **Danako**.xlsx · von **Kalea**.xlsx · von **Vance**.xlsx · von **Wildfire und Vestras Kids**.xlsx · von **Yurikas und Pintos Sohn**.xlsx |
*(Alle Gencode-Varianten + Quelldateien: `tools/import/output/review-report.md`.)*
*(Alle Gencode-Varianten + Quelldateien-Details: `tools/import/output/review-report.md`.)*
---
@@ -85,9 +83,9 @@ Durch die vielen neuen Stammbaum-Dateien sind **13 neue Konflikt-Tiere** aufgeta
|---|---|---|
| KI-Verkaufstexte · Charakterbogen-KI · Namensgenerator | ✅ **LIVE & kostenlos** (Gemini Free-Tier) | — |
| E-Mail-Posteingang (Anfragen) | gebaut | **A3** (Gmail-App-Passwort) |
| Öffentliche Webseite + KI-Chat-Editor | gebaut | **A4** (Domain + Cloudflare) |
| Öffentliche Webseite + KI-Chat-Editor | gebaut; Hosting **self-hosted** (Strato/DynDNS + TrueNAS nginx) | Hostname-Wahl + WEB-2-Bau (Dwight) |
| NAS-Produktiv-Deployment + Docker-Push | vorbereitet, CI grün | **A5** + **A5b2** |
| Handy-Zugriff im WLAN | App läuft | **B2** (Firewall) |
| Handy-Zugriff im WLAN | **B2 Firewall erledigt** — testbar sobald App läuft | — |
| Alle importierten Konflikt-Tiere | ✅ **alle 32 entschieden** | finaler Re-Import (startbereit, läuft demnächst) |
---

View File

@@ -79,3 +79,34 @@ test('Namenloser Ahne zeigt Platzhalter in der Stammbaum-Karte (UI-POLISH-2)', a
// Karte selbst zeigt '(ohne Namen)' statt leer
await expect(page.locator('.pedigree-card__nametext')).toHaveText(de.pages.gerbils.nameless)
})
test('Mini-Legende zeigt alle drei Interaktionshinweise (STAMMBAUM-EXPAND)', async ({ page }) => {
skipUnlessMock()
await page.goto('/rennmaeuse/kruemel/stammbaum')
await expect(page.locator('.pedigree-card').first()).toBeVisible()
const hints = page.locator('.stammbaum-hints')
await expect(hints).toBeVisible()
await expect(hints).toContainText(t.tapHint)
await expect(hints).toContainText(t.hintName)
await expect(hints).toContainText(t.hintExpand)
})
test('-Knopf ist sichtbar und lädt weitere Vorfahren nach (STAMMBAUM-EXPAND)', async ({ page }) => {
skipUnlessMock()
await page.goto('/rennmaeuse/kruemel/stammbaum')
await expect(page.locator('.pedigree-card').first()).toBeVisible()
// Einpassen, damit die 4. Generation (Emil mit ) im Viewport liegt.
const fit = page.getByRole('button', { name: t.zoomFit })
if (await fit.isVisible()) await fit.click()
await page.waitForTimeout(600)
// -Button muss an der Tiefengrenze erscheinen (Emil hat litterId w-emil).
const expandBtn = page.getByRole('button', { name: t.expand }).first()
await expect(expandBtn).toBeVisible()
// Klick auf : Emil wird aufgeklappt → Max (sein Vater) taucht als Link auf.
await expandBtn.click({ force: true })
await expect(page.getByRole('link', { name: 'Max' })).toBeVisible({ timeout: 8000 })
})

View File

@@ -185,15 +185,11 @@ describe('Farbschlag catalog', () => {
})
it('frozen contract names round-trip to themselves (DB-key guard)', () => {
// The first 18 are the frozen ColorVariety keys — their representative
// genotype MUST resolve back to their own name, never a later variety.
// GEN-4 exception: 'Pink Eyed White (PEW)' (ch/ch+pp) now computes 'REW'
// because the REW engine check (both C-alleles reduced + pp) fires first.
// PEW stays in the catalog as a user-pickable import name; its computed
// farbschlag is intentionally 'REW' per Julian's extended rule.
const REW_SHADOWED = new Set(['Pink Eyed White (PEW)'])
// The first 17 frozen ColorVariety keys — their representative genotype
// MUST resolve back to their own name, never a later variety.
// GEN-4c: 'REW' (formerly 'Pink Eyed White (PEW)') round-trips correctly:
// chch+pp → REW engine check → 'REW' = entry.name.
for (const entry of BASE_COLORS.slice(0, FROZEN_COUNT)) {
if (REW_SHADOWED.has(entry.name)) continue
expect(genotypeToFarbschlag(representativeGenotype(entry))).toBe(entry.name)
}
})
@@ -224,7 +220,7 @@ describe('Farbschlag catalog', () => {
it('CATALOG seed view mirrors the ColorVariety table shape', () => {
expect(CATALOG).toHaveLength(CATALOG_SIZE)
expect(CATALOG[0]).toMatchObject({ name: 'Pink Eyed White (PEW)', sortOrder: 0 })
expect(CATALOG[0]).toMatchObject({ name: 'REW', sortOrder: 0 })
// Every row has a non-empty canonical genotype display string and unique name.
// GEN-3h: bracket notation (e[f], c[chm], c[h]) allowed in tokens.
expect(new Set(CATALOG.map((c) => c.name)).size).toBe(CATALOG.length)
@@ -396,10 +392,10 @@ describe('GEN-3c: no Unbekannt when the E locus is known (family fallback)', ()
describe('GEN-3d: dominance tiebreak for unknown loci', () => {
it('unknown-C reads as full-colour, NOT a c^h/c^chm white', () => {
// 'aa C- DD EE GG PP' -> Schwarz (dominant C reading), never PEW/Hermelin/Himalaya.
// 'aa C- DD EE GG PP' -> Schwarz (dominant C reading), never REW/Hermelin/Himalaya.
const name = genotypeToFarbschlag(fromDisplayString('aa C- DD EE GG PP spsp rere'))
expect(name).toBe('Schwarz')
expect(['Pink Eyed White (PEW)', 'Hermelin', 'Himalaya']).not.toContain(name)
expect(['REW', 'Hermelin', 'Himalaya']).not.toContain(name)
})
it('unknown second marker allele defaults UNMARKED, not Schecke (marker-aware)', () => {
@@ -549,15 +545,16 @@ describe('GEN-4: Dilute prefix, REW, no-bare-Fuchs', () => {
})
it('REW: both C alleles reduced (no full C) + pp = REW — all three cases (Julian confirmed)', () => {
// hom cchm/cchm + pp
expect(name('AA cchmcchm DD EE GG pp spsp rere')).toBe('REW') // CP-Gold
// hom cchm/cchm + pp — A/D/E/G-independent (REW-2: Julian: "egal ob AA oder aa")
expect(name('AA cchmcchm DD EE GG pp spsp rere')).toBe('REW') // CP-Gold (A-)
expect(name('aa cchmcchm DD EE GG pp spsp rere')).toBe('REW') // aa: REW-2 verification
expect(name('AA cchmcchm DD ee GG pp spsp rere')).toBe('REW') // CP-Goldfuchs
expect(name('AA cchmcchm DD EE gg pp spsp rere')).toBe('REW') // CP-Elfenbein
expect(name('AA cchmcchm DD ee gg pp spsp rere')).toBe('REW') // CP-Apricot
expect(name('AA cchmcchm dd EE GG pp spsp rere')).toBe('REW') // CP-dd Gold
// het cchm/ch + pp (Julian: also REW)
expect(name('AA cchmch DD EE GG pp spsp rere')).toBe('REW')
// ch/ch + pp (Julian: also REW — subsumes PEW)
// ch/ch + pp (Julian: also REW; PEW renamed to REW in catalog per GEN-4c)
expect(name('AA chch DD EE GG pp spsp rere')).toBe('REW')
expect(name('aa chch DD EE GG pp spsp rere')).toBe('REW')
// Counterproof: full C present → NOT REW (residual pigment)

View File

@@ -51,7 +51,7 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
// GEN-3a: 'Schwarzschimmel' REMOVED (breeder C5: no such variety; efef base is
// Orangeschimmel — see the GEN-2 block below). This was an authorized exception
// to the frozen-name rule; the ColorVariety seed drops it too.
{ name: 'Pink Eyed White (PEW)', english: 'Pink Eyed White', tokens: { C: 'ch', P: 'p' }, image: 'rotaugen-weiss-pew-d-sep-e-sep.jpg' },
{ name: 'REW', english: 'Pink Eyed White', tokens: { C: 'ch', P: 'p' }, image: 'rotaugen-weiss-pew-d-sep-e-sep.jpg' },
{ name: 'Hermelin', english: 'Dark Tailed White', tokens: { A: 'a', C: 'ch', D: 'D', P: 'P' }, image: 'hermelin.jpeg' },
{ name: 'Himalaya', english: 'Himalayan', tokens: { A: 'A', C: 'ch', D: 'D', P: 'P' }, image: 'himalaya.jpg' },
{ name: 'Zobel', english: 'Sable', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'zobel.jpeg' },
@@ -268,7 +268,7 @@ export function farbschlagFor(g: Genotype): FarbschlagMatch {
// AND pink-eyed (pp) = REW (Rotaugenweiß), A/D/E/G-independent.
// cchm/cchm + pp → REW (CP varieties with pink-eye)
// cchm/ch + pp → REW (het colourpoint + pink-eye)
// ch/ch + pp → REW (this also subsumes the frozen 'Pink Eyed White (PEW)' entry)
// ch/ch + pp → REW (Julian confirmed PEW=REW; the 'REW' catalog entry matches here)
// Counterproof: at least one full 'C' + pp → NOT REW (residual pigment remains).
const [c0, c1] = resolvedPair(g, 'C')
const [p0, p1] = resolvedPair(g, 'P')

View File

@@ -1,6 +1,6 @@
[
{
"name": "Pink Eyed White (PEW)",
"name": "REW",
"english": "Pink Eyed White",
"canonicalGenotype": "AA chch DD EE GG pp spsp rere",
"sortOrder": 0,

View File

@@ -1,6 +1,6 @@
[
{
"name": "Pink Eyed White (PEW)",
"name": "REW",
"english": "Pink Eyed White",
"canonicalGenotype": "AA c[h]c[h] DD EE GG pp spsp rere",
"sortOrder": 0,

View File

@@ -335,7 +335,11 @@ export default function StammbaumPage() {
/>
)}
</div>
<p className="stammbaum-hint">{t.tapHint}</p>
<ul className="stammbaum-hints">
<li>{t.tapHint}</li>
<li>{t.hintName}</li>
<li>{t.hintExpand}</li>
</ul>
</section>
{/* Druckansicht: am Bildschirm unsichtbar, ersetzt beim Drucken alles. */}

View File

@@ -34,10 +34,15 @@
color: var(--color-text);
}
.stammbaum-hint {
font-size: 0.8rem;
color: var(--color-text-muted);
.stammbaum-hints {
list-style: none;
padding: 0;
margin: 0.4rem 0 0;
display: flex;
flex-wrap: wrap;
gap: 0.1rem 1rem;
font-size: 0.78rem;
color: var(--color-text-muted);
}
/* ── Zeichenfläche ────────────────────────────────────────────── */
@@ -175,20 +180,27 @@
.pedigree-card__expand {
flex: none;
margin-left: auto;
width: 32px;
height: 32px;
width: 40px;
height: 40px;
padding: 0;
border-radius: 50%;
border: 1px solid var(--color-accent);
background: var(--color-accent-soft);
color: var(--color-accent);
font-size: 1.05rem;
line-height: 1;
cursor: pointer;
}
.pedigree-card__expand:hover {
border: none;
background: var(--color-accent);
color: #fff;
font-size: 1.3rem;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 5px rgb(0 0 0 / 28%);
}
.pedigree-card__expand:hover,
.pedigree-card__expand:focus-visible {
opacity: 0.82;
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* ── Druckansicht (Ahnentafel als CSS-Grid, Hunde-Zertifikat-Optik) ──

View File

@@ -13,6 +13,9 @@ export interface ChipColor {
const CHIP_COLORS: Record<string, ChipColor> = {
// Colourpoint / C-Serie
// GEN-4c: PEW renamed to REW in catalog (Julian: PEW=REW). Both kept here for
// backward-compat during DB migration (old animals may still carry the old name).
'REW': { bg: '#f7f3ec', fg: '#8a7d6b' },
'Pink Eyed White (PEW)': { bg: '#f7f3ec', fg: '#8a7d6b' },
Hermelin: { bg: '#f3ede2', fg: '#8a7d6b' },
Himalaya: { bg: '#efe6d8', fg: '#8a7d6b' },

View File

@@ -355,6 +355,9 @@ export const de = {
/** Auf Karten am Rand: weitere Vorfahren nachladen. */
expand: 'Vorfahren laden',
tapHint: 'Tippe auf ein Tier, um dessen Stammbaum anzuzeigen.',
/** Mini-Legende unter dem Baum (STAMMBAUM-EXPAND). */
hintName: 'Namenslink: Tierakte öffnen',
hintExpand: ': weitere Vorfahren nachladen',
zoomIn: 'Vergrößern',
zoomOut: 'Verkleinern',
zoomFit: 'Ansicht einpassen',

View File

@@ -141,6 +141,27 @@
"genotype": "aa Cc[-] D- ee Gg Pp spsp",
"dateOfDeath": "20.05.2019",
"source": "Julian/Züchterin 2026-06-07 — HUMANQUESTION D7"
},
{
"name": "Percy of little runners",
"dob": "16.12.2017",
"decision": "P-Locus = Pp (die Quellen unterschieden sich nur bei P: PP // Pp)",
"genotype": "aa Cc[chm] DD Ee Gg Pp Spsp",
"source": "Julian/Züchterin 2026-06-07 — HUMANQUESTION D7"
},
{
"name": "Iwana of little runners",
"dob": "02.10.2018",
"decision": "P-Locus = Pp (die Quellen unterschieden sich nur bei P: PP // Pp)",
"genotype": "Aa CC DD Ee Gg Pp spsp",
"source": "Julian/Züchterin 2026-06-07 — HUMANQUESTION D7"
},
{
"name": "Osamu von den Kleinen Chaoten",
"dob": "10.12.2015",
"decision": "Sterbedatum = 18.12.2020 (die 01.10.2020-Variante war falsch); Gencode war einig, taub-Flag bleibt via 'Vorhandensein gewinnt'",
"dateOfDeath": "18.12.2020",
"source": "Julian/Züchterin 2026-06-07 — HUMANQUESTION D7"
}
]
}