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

@@ -414,6 +414,44 @@ export async function installMockApi(page: Page): Promise<MockDb> {
return json(route, 200, result)
}
// GEPRÜFTE TIERE: „vollständig korrekt"-Markierung / Schutz (/verified-gerbils).
if (path === '/verified-gerbils' && method === 'GET') {
return json(route, 200, [...db.verified].reverse())
}
const verMatch = path.match(/^\/verified-gerbils\/([^/]+)$/)
if (verMatch) {
const gid = decodeURIComponent(verMatch[1])
const vIdx = db.verified.findIndex((v) => v.gerbilId === gid)
if (method === 'GET') {
return vIdx >= 0 ? json(route, 200, db.verified[vIdx]) : json(route, 404, { title: 'Not Found' })
}
if (method === 'POST') {
const g = db.gerbils.find((x) => x.id === gid)
if (!g) return json(route, 404, { title: 'Not Found' })
const body = (request.postDataJSON() ?? {}) as { note?: string | null }
const row = {
gerbilId: gid,
entityName: g.name ?? null,
isVerified: true,
status: 'unchanged',
verifiedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
note: body.note ?? null,
protectedFields: ['name', 'gender', 'dateOfBirth', 'genotype'],
importDiff: [],
}
if (vIdx >= 0) db.verified[vIdx] = row
else db.verified.push(row)
return json(route, 200, row)
}
if (method === 'DELETE') {
if (vIdx < 0) return json(route, 404, { title: 'Not Found' })
db.verified.splice(vIdx, 1)
return json(route, 204)
}
return json(route, 405)
}
// FEEDBACK: "Fehler melden" — POST persistiert, GET listet (neueste zuerst).
if (path === '/feedback') {
if (method === 'POST') {

View File

@@ -80,6 +80,8 @@ export interface MockDb {
namesConfigured: boolean
// FEEDBACK: "Fehler melden" — gesammelte Berichte (POST /feedback)
feedback: Record<string, unknown>[]
// GEPRÜFTE TIERE: „vollständig korrekt"-Markierungen / Schutz (/verified-gerbils)
verified: Record<string, unknown>[]
// RPRO3: RennmausPro-III-Import — feste Auswertung + Import-Ergebnis für die UI-Specs.
rpro3: {
analyze: Record<string, unknown>
@@ -222,10 +224,10 @@ export function seedDb(): MockDb {
]
const litters: Litter[] = [
{ id: 'w-zwillinge', name: 'Wurf Z', date: '2020-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'opa-w', motherId: 'oma-u' },
{ id: 'w-inzucht', name: 'Wurf I', date: '2021-06-01', totalBorn: 3, expectedGoHomeDate: null, notes: null, fatherId: 'zwilling-bock', motherId: 'zwilling-maus' },
{ id: 'w-zwillinge', name: 'Wurf Z', date: '2020-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'opa-w', motherId: 'oma-u', showInChronicle: true },
{ id: 'w-inzucht', name: 'Wurf I', date: '2021-06-01', totalBorn: 3, expectedGoHomeDate: null, notes: null, fatherId: 'zwilling-bock', motherId: 'zwilling-maus', showInChronicle: true },
{
id: 'w-kruemel', name: 'Wurf K', date: '2025-03-12', totalBorn: 5, expectedGoHomeDate: '2025-04-16', notes: null, fatherId: 'fridolin', motherId: 'luna', deathsWithin8Weeks: 1,
id: 'w-kruemel', name: 'Wurf K', date: '2025-03-12', totalBorn: 5, expectedGoHomeDate: '2025-04-16', notes: null, fatherId: 'fridolin', motherId: 'luna', deathsWithin8Weeks: 1, stillborn: 1, showInChronicle: true,
// NACHVERFOLGUNG: Datenherkunft des Wurfs (vom Import erzeugter JSON-String).
provenance: JSON.stringify({
sourceFiles: ['Wurfchronik Teil 1_page_0009.md', 'Wurfchronik Teil 1_page_0028.md'],
@@ -239,13 +241,16 @@ export function seedDb(): MockDb {
],
}),
},
{ id: 'w-fridolin', name: 'Wurf F', date: '2023-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'balu', motherId: 'maja' },
{ id: 'w-luna', name: 'Wurf L', date: '2023-08-15', totalBorn: 6, expectedGoHomeDate: null, notes: null, fatherId: 'karlsson', motherId: 'smilla' },
{ id: 'w-balu', name: 'Wurf B', date: '2021-04-20', totalBorn: 3, expectedGoHomeDate: null, notes: null, fatherId: 'anton', motherId: 'greta' },
{ id: 'w-fridolin', name: 'Wurf F', date: '2023-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'balu', motherId: 'maja', showInChronicle: true },
{ id: 'w-luna', name: 'Wurf L', date: '2023-08-15', totalBorn: 6, expectedGoHomeDate: null, notes: null, fatherId: 'karlsson', motherId: 'smilla', showInChronicle: true },
{ id: 'w-balu', name: 'Wurf B', date: '2021-04-20', totalBorn: 3, expectedGoHomeDate: null, notes: null, fatherId: 'anton', motherId: 'greta', showInChronicle: true },
// Anton ist auch Karlssons Vater -> gemeinsamer Vorfahre (Inzucht-Demo)
{ id: 'w-karlsson', name: 'Wurf C', date: '2022-02-02', totalBorn: 5, expectedGoHomeDate: null, notes: null, fatherId: 'anton', motherId: 'frieda' },
{ id: 'w-anton', name: 'Wurf A', date: '2019-07-07', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'emil', motherId: 'hilde' },
{ id: 'w-emil', name: 'Wurf E', date: '2017-03-03', totalBorn: 2, expectedGoHomeDate: null, notes: null, fatherId: 'max', motherId: null },
{ id: 'w-karlsson', name: 'Wurf C', date: '2022-02-02', totalBorn: 5, expectedGoHomeDate: null, notes: null, fatherId: 'anton', motherId: 'frieda', showInChronicle: true },
{ id: 'w-anton', name: 'Wurf A', date: '2019-07-07', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'emil', motherId: 'hilde', showInChronicle: true },
{ id: 'w-emil', name: 'Wurf E', date: '2017-03-03', totalBorn: 2, expectedGoHomeDate: null, notes: null, fatherId: 'max', motherId: null, showInChronicle: true },
// WURFCHRONIK-FILTER: dieser Wurf ist NICHT in der Wurfchronik (showInChronicle: false)
// -> taucht in der Wurf-Liste nicht auf, aber auf Fridolins Tier-Detailseite (parentLitters) weiterhin.
{ id: 'w-versteckt', name: 'Wurf V', date: '2024-02-02', totalBorn: 2, expectedGoHomeDate: null, notes: null, fatherId: 'fridolin', motherId: 'luna', showInChronicle: false },
]
const enclosures: Enclosure[] = [
@@ -426,6 +431,8 @@ export function seedDb(): MockDb {
],
saleAdConfigured: true,
namesConfigured: true,
// GEPRÜFTE TIERE: startet leer; die Verify-Specs markieren über POST.
verified: [],
// FEEDBACK-TICKETS: ein offenes + ein gelöstes Ticket (GET liefert neueste zuerst →
// db.feedback wird im Mock umgekehrt; das offene steht hier zuletzt = erscheint oben).
feedback: [

View File

@@ -0,0 +1,36 @@
/**
* GEPRÜFTE TIERE: „vollständig korrekt"-Markierung auf der Rennmausakte (Badge + Toggle)
* und die Übersichtsseite unter Hilfe. Mock-gebunden → skipUnlessMock.
*/
import { de, expect, gotoSection, acceptNextDialog, skipUnlessMock, test } from './fixtures'
const vt = de.verified
test.describe('Geprüfte Tiere', () => {
test('Tier als vollständig korrekt markieren und Markierung entfernen', async ({ page }) => {
skipUnlessMock()
await gotoSection(page, de.nav.gerbils)
// Erste Rennmaus aus der Liste öffnen.
await page.locator('.gerbil-card').first().click()
// Prüfstatus-Karte ist sichtbar und erklärt sich selbst.
await expect(page.getByRole('heading', { name: vt.sectionTitle })).toBeVisible()
// Markieren → Bestätigungs-Badge erscheint.
await page.getByRole('button', { name: vt.markAction }).click()
await expect(page.getByText(vt.verifiedBadge).first()).toBeVisible()
// Markierung entfernen (Klartext-Bestätigung akzeptieren) → Badge verschwindet.
acceptNextDialog(page)
await page.getByRole('button', { name: vt.unmarkAction }).click()
await expect(page.getByText(vt.verifiedBadge)).toHaveCount(0)
})
test('Übersichtsseite unter Hilfe erreichbar', async ({ page }) => {
skipUnlessMock()
await page.goto('/hilfe/verifizierte-tiere')
await expect(page.getByRole('heading', { name: vt.pageTitle })).toBeVisible()
await expect(page.getByText(vt.pageIntro1)).toBeVisible()
})
})