Files
GerbilManager/GerbilManagerWebAPI/Program.cs
Gulum 2e7911074f
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
feat(triage): Ticket-Fixes (Daten + Code) + prod-fähige Triage
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>
2026-07-21 00:41:43 +02:00

148 lines
6.4 KiB
C#

using System.Text.Json.Serialization;
using GerbilManagerWebAPI.Endpoints;
using GerbilManagerWebAPI.Models;
using GerbilManagerWebAPI.Services;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// LAN reachability: bind on all interfaces (keep the Aspire/launch-assigned port),
// so phones/laptops on the home network can reach the API (trusted LAN, no auth).
var aspnetUrls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS");
if (!string.IsNullOrWhiteSpace(aspnetUrls))
{
var lanUrls = string.Join(';', aspnetUrls
.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(u => u.Replace("localhost", "0.0.0.0").Replace("127.0.0.1", "0.0.0.0")));
builder.WebHost.UseUrls(lanUrls);
}
builder.AddNpgsqlDbContext<ApplicationContext>("gerbilmanager");
// JSON: camelCase property names + enums serialised as their string names (frontend contract).
builder.Services.ConfigureHttpJsonOptions(o =>
{
o.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
o.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
// Permissive CORS for the trusted home LAN (no auth — see board constraint).
const string LanCorsPolicy = "lan";
builder.Services.AddCors(options => options.AddPolicy(LanCorsPolicy, policy =>
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()));
// OpenAPI document for the Scalar API reference (Swashbuckle removed).
builder.Services.AddOpenApi();
// FEAT-12a: provider-agnostic AI config (env vars AI__BaseUrl/AI__ApiKey/AI__Model
// or user-secrets — never committed) + typed client for sale-ad generation.
builder.Services.AddOptions<GerbilManagerWebAPI.SaleAd.AiOptions>()
.BindConfiguration(GerbilManagerWebAPI.SaleAd.AiOptions.SectionName);
builder.Services.AddHttpClient<GerbilManagerWebAPI.SaleAd.SaleAdService>(
http => http.Timeout = TimeSpan.FromSeconds(60));
// INBOX-2: KI-Antwortentwurf (gleiche AI-Sektion, gleicher Wire-Client).
builder.Services.AddHttpClient<GerbilManagerWebAPI.Inbox.DraftReplyService>(
http => http.Timeout = TimeSpan.FromSeconds(60));
// FEAT-NAMEGEN: Name suggestions via Gemini (same AI section, same wire client).
builder.Services.AddHttpClient<GerbilManagerWebAPI.Names.NameSuggestionService>(
http => http.Timeout = TimeSpan.FromSeconds(60));
// WEB-2: public site publish path (env var PublicSite__RootPath; empty = disabled)
builder.Services.AddOptions<GerbilManagerWebAPI.Cms.PublicSiteOptions>()
.BindConfiguration(GerbilManagerWebAPI.Cms.PublicSiteOptions.SectionName);
// INBOX-0: Gmail inbox. App Password encrypted at rest via Data Protection.
// AR-3: persist the key ring so encrypted passwords survive image redeployments.
// In prod the path is mounted to a persistent volume (compose DataProtection__KeyRingPath).
// In dev (Aspire) keys live in the content root — ephemeral, which is fine there.
{
var keyRingPath = builder.Configuration["DataProtection:KeyRingPath"]
?? Path.Combine(builder.Environment.ContentRootPath, ".data-protection-keys");
Directory.CreateDirectory(keyRingPath);
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keyRingPath))
.SetApplicationName("GerbilManager");
}
builder.Services.AddScoped<GerbilManagerWebAPI.Inbox.MailSettingsService>();
builder.Services.AddScoped<GerbilManagerWebAPI.Inbox.IGmailMailReader, GerbilManagerWebAPI.Inbox.GmailMailReader>();
builder.Services.AddScoped<GerbilManagerWebAPI.Inbox.RequestSyncService>();
builder.Services.AddScoped<GerbilManagerWebAPI.Inbox.IGmailMailSender, GerbilManagerWebAPI.Inbox.GmailMailSender>();
builder.Services.AddScoped<GerbilManagerWebAPI.Inbox.SendReplyService>();
builder.Services.AddSingleton<GerbilManagerWebAPI.Push.PushNotifier>();
var app = builder.Build();
app.MapDefaultEndpoints();
// API reference at /scalar (built on the OpenAPI doc at /openapi/v1.json).
app.MapOpenApi();
app.MapScalarApiReference();
// Apply EF migrations at startup (no-op if schema is current; safe for single-instance deploy).
// Unter "Testing" übersprungen: die Endpoint-Tests laufen auf SQLite in-memory
// (EnsureCreated) — die Npgsql-Migrationen sind dort nicht anwendbar.
if (!app.Environment.IsEnvironment("Testing"))
{
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationContext>();
db.Database.Migrate();
// Startup sweep: heal any stored status that disagrees with the derived status — animals
// that silently crossed the 7-year threshold (→ Deceased), or whose death date / Abgabe was
// set on a path that didn't re-derive the status (e.g. a frozen GerbilOverride re-applied after
// ingest — Ticket 37ab228a "Gaida"). Only currently-active animals (Breeding/Pet/ForSale) are
// considered, so nobody gets un-deceased. No-op if all statuses are already current.
var today = DateOnly.FromDateTime(DateTime.UtcNow);
var candidates = await db.Gerbils
.Where(g => g.Status != GerbilStatus.Deceased && g.Status != GerbilStatus.GivenAway)
.ToListAsync();
var healed = 0;
foreach (var g in candidates)
{
var derived = GerbilStatusService.Derive(g.Status, g.DateOfBirth, g.DateOfDeath,
isAbgegeben: g.ReceiverContactId is not null, today);
if (derived != g.Status) { g.Status = derived; healed++; }
}
if (healed > 0)
await db.SaveChangesAsync();
}
app.UseCors(LanCorsPolicy);
// Endpoint groups (Minimal API, no controllers).
app.MapGerbilEndpoints();
app.MapLitterEndpoints();
app.MapContactEndpoints();
app.MapEnclosureEndpoints();
app.MapColorVarietyEndpoints();
app.MapHealthRecordEndpoints();
app.MapWeightRecordEndpoints();
app.MapInbreedingEndpoints();
app.MapPhotoEndpoints();
app.MapSaleAdEndpoints();
app.MapImportEndpoints();
app.MapImportDocxEndpoints();
app.MapContractEndpoints();
app.MapSettingsEndpoints();
app.MapExportEndpoints();
app.MapCmsEndpoints();
app.MapRequestEndpoints();
app.MapNamesEndpoints();
app.MapFeedbackEndpoints();
app.MapVerifiedGerbilEndpoints();
app.MapPushEndpoints();
app.MapAcquisitionEndpoints();
app.MapSaleReservationEndpoints();
app.MapWaitingListEndpoints();
app.MapReturnRecordEndpoints();
app.MapExhibitionEndpoints();
app.MapRefsEndpoints();
app.Run();
// Sichtbarer Programmtyp für WebApplicationFactory<Program> (Endpoint-Tests).
public partial class Program { }