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

@@ -44,6 +44,39 @@ namespace GerbilManagerWebAPI.Endpoints
return TypedResults.Ok(result);
});
// Upload-Variante: lädt eine resolved_import.json (+ optional Fotos-ZIP) hoch, legt sie in
// dem Verzeichnis ab, aus dem der Ingest liest, und ingested direkt. Damit lässt sich der
// Import gegen PROD fahren, OHNE die Datei per SSH/docker cp in den Container zu kopieren
// (der lokal generierte Payload geht per HTTP an die Prod-API). Abwärtskompatibel — der
// bestehende /ingest-resolved-Endpoint bleibt unverändert.
group.MapPost("/ingest-resolved/upload", async Task<Results<Ok<string>, BadRequest<string>>> (
IFormFile resolved, IFormFile? photos,
ApplicationContext db, IConfiguration config, IWebHostEnvironment env) =>
{
if (resolved.Length == 0)
return TypedResults.BadRequest("resolved_import.json ist leer.");
var sourceDir = IngestResolvedService.ResolveSourceDir(config, env);
Directory.CreateDirectory(sourceDir);
// resolved_import.json an den erwarteten Ort schreiben.
var destPath = Path.Combine(sourceDir, "resolved_import.json");
await using (var fs = File.Create(destPath))
await resolved.CopyToAsync(fs);
// Optionale Fotos-ZIP in den Source-Dir entpacken (Einträge relativ), damit der
// Foto-Kopierschritt (Path.Combine(sourceDir, relPath)) neue Fotos findet.
if (photos is not null)
{
await using var zs = photos.OpenReadStream();
using var seekable = await ToSeekable(zs);
ExtractZipInto(seekable, sourceDir);
}
var result = await new IngestResolvedService(db, config, env).RunAsync();
return TypedResults.Ok(result);
}).WithTags("Import").DisableAntiforgery();
// ── RPRO3 ──────────────────────────────────────────────────────────
group.MapPost("/rpro3/analyze", async Task<Results<Ok<Rpro3AnalyzeResult>, BadRequest<string>>> (
IFormFile backup, IFormFile? images,
@@ -143,6 +176,25 @@ namespace GerbilManagerWebAPI.Endpoints
return map;
}
// Entpackt ein ZIP relativ in destDir (Einträge behalten ihren relativen Pfad). Schützt vor
// Zip-Slip (Einträge, die aus destDir ausbrechen wollen) und überspringt Verzeichnis-Einträge.
private static void ExtractZipInto(Stream zip, string destDir)
{
var root = Path.GetFullPath(destDir);
using var archive = new ZipArchive(zip, ZipArchiveMode.Read, leaveOpen: true);
foreach (var e in archive.Entries)
{
if (string.IsNullOrEmpty(e.Name)) continue; // Verzeichnis-Eintrag
var target = Path.GetFullPath(Path.Combine(root, e.FullName));
if (!target.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.Ordinal)
&& !string.Equals(target, root, StringComparison.Ordinal))
continue; // Zip-Slip abwehren
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
try { e.ExtractToFile(target, overwrite: true); }
catch { /* defektes Archiv-Entry überspringen */ }
}
}
private static void TryDeleteDir(string dir)
{
try { if (Directory.Exists(dir)) Directory.Delete(dir, recursive: true); }