feat(deploy): TrueNAS Custom-App + Auto-Deploy, plus aufgelaufene Arbeit
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 1m11s
CI / Frontend Tests (Node/Vite) (push) Failing after 4m59s
CI / Docker Build & Push (push) Has been skipped
CI / Deploy auf TrueNAS (Custom App) (push) Has been skipped

Deployment:
- custom-app.compose.yaml: self-contained Compose fuer TrueNAS "Custom App"
  (absolute Host-Bind-Pfade, postgres:18, pull_policy always, Port 8090)
- scripts/truenas-deploy.sh: Host-Skript create/redeploy via midclt (App
  bleibt unter Apps sichtbar) inkl. Image-Pull + Health-Check
- ci.yml Deploy-Job: laeuft auf ubuntu-latest-Runner, kopiert Deploy-Dateien
  per SSH auf den NAS-Host und triggert truenas-deploy.sh (statt runs-on goldeye)
- compose.yaml/.env.example: postgres:18 (Locale-Match zur Quell-DB), Port 8090
- .gitignore: .agents/, tools/rag/, deploy/truenas/.env (Secrets/Scratch)

Aufgelaufene Feature-Arbeit (verified/Freeze, Migrationen, Import-Triage):
- GerbilOverride/VerifiedGerbil-Endpoints + GerbilSnapshotService + Tests
- EF-Migrationen (ShowInChronicle, Stillborn, BirthOrder, ManualFlag, DSGVO)
- Frontend VerifizierteTierePage + verified-API + e2e-Spec
- diverse Import-/Triage-Skripte und -Tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 09:19:11 +02:00
parent 84365bba7f
commit 45b8533f18
93 changed files with 20477 additions and 432 deletions

View File

@@ -30,5 +30,10 @@ namespace GerbilManagerWebAPI.Models
/// notes). Written by the Python merge_and_resolve step and surfaced read-only
/// ("Datenherkunft"). Null = manually-added contact / no import data.</summary>
public string? Provenance { get; set; }
/// <summary>true = manually created in the UI; false = produced by an import path. The ingest
/// re-import wipe deletes ONLY IsManual=false contacts, so a manually-added contact (e.g. the
/// Herkunft/Abnehmer of a manual animal) is never wiped. All import paths leave this false.</summary>
public bool IsManual { get; set; }
}
}

View File

@@ -90,6 +90,16 @@ namespace GerbilManagerWebAPI.Models
/// imported animals. See hive/agents/god/OWNERSHIP-residency.md. Gridify-filterable.</summary>
public bool IsResident { get; set; } = true;
public bool IsCastrated { get; set; }
/// <summary>true = manually created in the UI (POST /gerbils); false = produced by an import
/// path. The ingest re-import wipe deletes ONLY IsManual=false rows, so manually-added animals
/// (and their sub-records) are never wiped or overwritten. All import paths leave this false.</summary>
public bool IsManual { get; set; }
/// <summary>Reihenfolge der Jungtiere innerhalb ihres Wurfs (aus der Wurfchronik, sortiert nach
/// Geburtsgewicht). null = unbekannt → alphabetische Sortierung als Fallback. Wird im Import aus
/// conflict-decisions.json (`litterOrder`) gesetzt; von der Wurf- und Akten-Anzeige genutzt.</summary>
public int? BirthOrder { get; set; }
}
/// <summary>Shared normalisation for the separator-insensitive name search.</summary>

View File

@@ -0,0 +1,62 @@
using System.ComponentModel.DataAnnotations;
namespace GerbilManagerWebAPI.Models
{
/// <summary>
/// VERIFIED/PROTECTED OVERRIDE: authoritative, hand-curated per-gerbil field values. Decoupled
/// from the rest of the model on purpose — <see cref="GerbilId"/> is a plain Guid column (NOT an
/// enforced foreign key, no navigation property), so the import re-ingest wipe
/// (<c>IngestResolvedService</c>) can delete and reload gerbils without deleting or breaking
/// override rows. After every ingest the override is re-applied on top of the fresh import (the
/// "freeze"), so the breeder's curated values are never lost. Matched to the (deterministic)
/// gerbil id. Same ingest-surviving pattern as <see cref="Feedback"/>.
///
/// Two flavours, both stored here:
/// - <see cref="IsVerified"/> = true → "vollständig korrekt": the whole own-field freeze set is
/// pinned, and a full Akte snapshot (<see cref="SnapshotJson"/>) is kept for the drift report,
/// export and regression test.
/// - <see cref="IsVerified"/> = false → "geschützt": created by a manual edit; only the changed
/// fields are pinned (per-field) so untouched fields keep receiving import improvements.
/// </summary>
public class GerbilOverride
{
[Key]
public Guid Id { get; set; }
/// <summary>Loose reference (no FK) to the gerbil this override curates.</summary>
public Guid GerbilId { get; set; }
/// <summary>Captured gerbil name — keeps the row human-readable and survives an ingest wipe.</summary>
public string? EntityName { get; set; }
/// <summary>true = "vollständig korrekt" (full freeze + certified); false = "geschützt" (per-field edit).</summary>
public bool IsVerified { get; set; }
/// <summary>Frozen own-field values as a JSON object (field name → value). Verified rows carry
/// the full freeze set; protected rows carry only the manually-changed fields. Re-applied on
/// top of the fresh import after every ingest.</summary>
public required string OverrideJson { get; set; }
/// <summary>Full human-readable Akte snapshot (self + parents + own litters/children) captured
/// at verify time — feeds the drift report, export and regression test. null for protected-only rows.</summary>
public string? SnapshotJson { get; set; }
/// <summary>Raw-import Akte snapshot captured during the LAST ingest, BEFORE the freeze was
/// applied — lets the UI show where the raw import disagrees with the golden state. null until
/// the first ingest after verifying.</summary>
public string? LastImportSnapshotJson { get; set; }
/// <summary>Cached diff (golden ↔ last raw import) as a JSON array, computed at ingest time.
/// null/empty = no drift / not yet computed.</summary>
public string? LastImportDiffJson { get; set; }
/// <summary>Optional free-text note the breeder attached when verifying.</summary>
public string? Note { get; set; }
/// <summary>When the row was marked "vollständig korrekt"; null for protected-only rows.</summary>
public DateTimeOffset? VerifiedAt { get; set; }
/// <summary>When the override was last created/updated.</summary>
public DateTimeOffset UpdatedAt { get; set; }
}
}

View File

@@ -17,6 +17,11 @@ namespace GerbilManagerWebAPI.Models
/// Null = unbekannt (Altdaten). Must be ≤ TotalBorn if both are set.</summary>
public int? DeathsWithin8Weeks { get; set; }
/// <summary>Totgeburten (TG): tot geborene Jungtiere dieses Wurfs. Getrennt von
/// DeathsWithin8Weeks (in den ersten 8 Wochen verstorben). Aus der xlsx-Wurfchronik
/// (litters.json breakdown) importiert. Null = unbekannt/nicht erfasst.</summary>
public int? Stillborn { get; set; }
public Guid? FatherId { get; set; }
public Gerbil? Father { get; set; }
public Guid? MotherId { get; set; }
@@ -45,5 +50,17 @@ namespace GerbilManagerWebAPI.Models
/// Written by the Python merge_and_resolve step and surfaced read-only ("Datenherkunft").
/// Null = manually-added litter / no import data.</summary>
public string? Provenance { get; set; }
/// <summary>Steuert, ob dieser Wurf in der Wurfchronik (Würfe-Liste) erscheint.
/// true = normal sichtbar. false = aus der Wurfchronik ausgeblendet, aber weiterhin
/// auf der Tier-Detailseite des Eltern­tiers sichtbar — z. B. ein extern (bei einer
/// anderen Zucht) entstandener Wurf eines nicht-residenten Tieres, der nur zur
/// Dokumentation der Abstammung gehört. Default true (alle bestehenden Würfe).</summary>
public bool ShowInChronicle { get; set; } = true;
/// <summary>true = manually created in the UI; false = produced by an import path. The ingest
/// re-import wipe deletes ONLY IsManual=false litters, so a manually-added litter (e.g. the
/// birth litter of a manual animal) is never wiped. All import paths leave this false.</summary>
public bool IsManual { get; set; }
}
}