Merge feature/ops-1: TrueNAS deployment (compose + backup sidecar + nginx proxy + prod Dockerfiles), Gitea CI draft, German ops guide [god-QA: 18/18+47/47+e2e 48/48]

This commit is contained in:
2026-06-06 07:42:18 +02:00
12 changed files with 837 additions and 42 deletions

115
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,115 @@
# GerbilManager CI — Gitea Actions
# =================================
# STATUS: ENTWURF — inaktiv bis Julian Gitea Actions aktiviert.
# Aktivierung: Gitea -> Repository -> Einstellungen -> Actions -> "Actions aktivieren"
# Anschliessend: Gitea Actions Runner auf der NAS installieren (siehe docs/ops.md).
#
# Was dieser Workflow tut:
# 1. dotnet test (alle xUnit-Projekte)
# 2. npm test + npm run build (Frontend)
# 3. Docker-Images bauen und in die Gitea-Registry pushen
#
# Registry: truenas:13000 (internes Gitea Container Registry)
# Images:
# truenas:13000/gulum/gerbilmanager-api:latest
# truenas:13000/gulum/gerbilmanager-frontend:latest
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
env:
REGISTRY: truenas:13000
REGISTRY_OWNER: gulum
DOTNET_VERSION: "10.0.x"
NODE_VERSION: "22"
jobs:
test-backend:
name: Backend Tests (.NET)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: .NET SDK einrichten
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Pakete wiederherstellen
run: dotnet restore GerbilManager.slnx
- name: Build
run: dotnet build GerbilManager.slnx --no-restore -c Release
- name: Tests ausfuehren
run: dotnet test GerbilManager.slnx --no-build -c Release --logger "console;verbosity=normal"
test-frontend:
name: Frontend Tests (Node/Vite)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Node.js einrichten
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
cache-dependency-path: gerbil-manager-web/package-lock.json
- name: Abhaengigkeiten installieren
run: npm ci
working-directory: gerbil-manager-web
- name: Tests ausfuehren
run: npm test
working-directory: gerbil-manager-web
- name: Produktionsbuild pruefen
run: npm run build
working-directory: gerbil-manager-web
build-and-push:
name: Docker Build & Push
runs-on: ubuntu-latest
needs: [test-backend, test-frontend]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Bei Gitea Registry anmelden
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Docker Buildx einrichten
uses: docker/setup-buildx-action@v3
- name: API-Image bauen und pushen
uses: docker/build-push-action@v5
with:
context: .
file: GerbilManagerWebAPI/Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ env.REGISTRY_OWNER }}/gerbilmanager-api:latest
${{ env.REGISTRY }}/${{ env.REGISTRY_OWNER }}/gerbilmanager-api:${{ github.sha }}
- name: Frontend-Image bauen und pushen
uses: docker/build-push-action@v5
with:
context: .
file: gerbil-manager-web/Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ env.REGISTRY_OWNER }}/gerbilmanager-frontend:latest
${{ env.REGISTRY }}/${{ env.REGISTRY_OWNER }}/gerbilmanager-frontend:${{ github.sha }}

View File

@@ -108,19 +108,13 @@ public static class Extensions
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
// Adding health checks endpoints to applications in non-development environments has security implications.
// See https://aka.ms/aspire/healthchecks for details before enabling these endpoints in non-development environments.
if (app.Environment.IsDevelopment())
// Health checks always exposed — compose healthchecks and Aspire both rely on them.
// Trusted home LAN only (no public exposure, no auth — see board constraint).
app.MapHealthChecks(HealthEndpointPath);
app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
{
// All health checks must pass for app to be considered ready to accept traffic after starting
app.MapHealthChecks(HealthEndpointPath);
// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
}
Predicate = r => r.Tags.Contains("live")
});
return app;
}

View File

@@ -1,20 +1,23 @@
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
# Build context: repo root (includes GerbilManager.ServiceDefaults).
# docker build -f GerbilManagerWebAPI/Dockerfile -t gerbilmanager-api .
ENV ASPNETCORE_URLS=http://+:80
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:7.0 AS build
ARG configuration=Release
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["GerbilManagerWebAPI/GerbilManagerWebAPI.csproj", "GerbilManagerWebAPI/"]
RUN dotnet restore "GerbilManagerWebAPI/GerbilManagerWebAPI.csproj"
COPY . .
WORKDIR "/src/GerbilManagerWebAPI"
ARG configuration=Release
RUN dotnet publish "GerbilManagerWebAPI.csproj" -c $configuration -o /app/publish /p:UseAppHost=false
FROM base AS final
# Restore: project files only for layer-cache efficiency.
COPY GerbilManager.ServiceDefaults/GerbilManager.ServiceDefaults.csproj GerbilManager.ServiceDefaults/
COPY GerbilManagerWebAPI/GerbilManagerWebAPI.csproj GerbilManagerWebAPI/
RUN dotnet restore GerbilManagerWebAPI/GerbilManagerWebAPI.csproj
# Full source copy + publish.
COPY GerbilManager.ServiceDefaults/ GerbilManager.ServiceDefaults/
COPY GerbilManagerWebAPI/ GerbilManagerWebAPI/
WORKDIR /src/GerbilManagerWebAPI
RUN dotnet publish GerbilManagerWebAPI.csproj -c Release -o /app/publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
# .NET 10 containers default to ASPNETCORE_HTTP_PORTS=8080 bound on all interfaces.
EXPOSE 8080
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "GerbilManagerWebAPI.dll"]

View File

@@ -50,12 +50,9 @@ app.MapDefaultEndpoints();
app.MapOpenApi();
app.MapScalarApiReference();
if (app.Environment.IsDevelopment())
{
// Aspire provisions an empty database in dev — bring the schema up to date.
using var scope = app.Services.CreateScope();
// Apply EF migrations at startup (no-op if schema is current; safe for single-instance deploy).
using (var scope = app.Services.CreateScope())
scope.ServiceProvider.GetRequiredService<ApplicationContext>().Database.Migrate();
}
app.UseCors(LanCorsPolicy);

View File

@@ -0,0 +1,23 @@
# GerbilManager Produktionskonfiguration
# Kopiere diese Datei nach .env und setze die Werte vor dem ersten Start.
# Sicheres Datenbankpasswort (mind. 20 Zeichen, keine Anführungszeichen)
POSTGRES_PASSWORD=aendere_mich_bitte
# Externer Port fuer das Frontend (Standard: 80)
PORT=80
# Gitea Container Registry (Standard: truenas:13000/gulum)
REGISTRY=truenas:13000/gulum
TAG=latest
# NAS-Dataset-Pfade (TrueNAS SCALE: /mnt/<Pool>/<Dataset>)
PGDATA_PATH=/mnt/SSD/gerbil/pgdata
PHOTOS_PATH=/mnt/SSD/gerbil/photos
BACKUPS_PATH=/mnt/SSD/gerbil/backups
# Backup-Rotation: Anzahl Tage (Standard: 7)
BACKUP_KEEP_DAYS=7
# Claude-API-Key fuer KI-Verkaufstext (FEAT-12a; leer lassen wenn nicht vorhanden)
ANTHROPIC_API_KEY=

109
deploy/truenas/compose.yaml Normal file
View File

@@ -0,0 +1,109 @@
# GerbilManager — TrueNAS Custom Application
# ==============================================
# Vor dem ersten Start:
# 1. Kopiere deploy/truenas/.env.example -> deploy/truenas/.env und setze die Werte.
# 2. Lege die Dataset-Pfade auf der NAS an (pgdata, photos, backups).
# 3. docker compose -f deploy/truenas/compose.yaml up -d
#
# Zugriff: http://<NAS-IP>:${PORT:-80}
# API-Doku: http://<NAS-IP>:${PORT:-80}/scalar
services:
# --- PostgreSQL-Datenbank ---
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
POSTGRES_DB: gerbilmanager
POSTGRES_USER: postgres
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d gerbilmanager"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# --- .NET API (GerbilManagerWebAPI) ---
api:
image: "${REGISTRY:-truenas:13000/gulum}/gerbilmanager-api:${TAG:-latest}"
build:
context: ../..
dockerfile: GerbilManagerWebAPI/Dockerfile
restart: unless-stopped
environment:
ASPNETCORE_ENVIRONMENT: Production
# Verbindung zur Postgres-DB im selben Compose-Netz
ConnectionStrings__gerbilmanager: "Host=db;Port=5432;Database=gerbilmanager;Username=postgres;Password=${POSTGRES_PASSWORD}"
# Speicherort der hochgeladenen Fotos (NAS-Dataset gemounted unter /data/photos)
Photos__RootPath: /data/photos
# KI-Verkaufstext (FEAT-12a stub; leer lassen wenn kein Key vorhanden)
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
volumes:
- photos:/data/photos
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 90s
# --- nginx Frontend (React SPA + API-Proxy) ---
frontend:
image: "${REGISTRY:-truenas:13000/gulum}/gerbilmanager-frontend:${TAG:-latest}"
build:
context: ../..
dockerfile: gerbil-manager-web/Dockerfile
restart: unless-stopped
ports:
- "${PORT:-80}:80"
depends_on:
api:
condition: service_healthy
# --- Backup-Sidecar (taeglicher pg_dump + Foto-Archiv + Rotation) ---
backup:
image: postgres:17-alpine
restart: unless-stopped
environment:
PGPASSWORD: "${POSTGRES_PASSWORD}"
POSTGRES_HOST: db
POSTGRES_USER: postgres
POSTGRES_DB: gerbilmanager
BACKUP_KEEP_DAYS: "${BACKUP_KEEP_DAYS:-7}"
volumes:
- photos:/data/photos:ro
- backups:/backups
- ./scripts:/scripts:ro
depends_on:
db:
condition: service_healthy
entrypoint: ["/bin/sh", "/scripts/entrypoint.sh"]
volumes:
# NAS-Datasets als Bind-Mounts (Pfade in .env konfigurieren).
# TrueNAS: Dataset-Pfad z.B. /mnt/SSD/gerbil/pgdata
pgdata:
driver: local
driver_opts:
type: none
o: bind
device: "${PGDATA_PATH:-/mnt/gerbil/pgdata}"
photos:
driver: local
driver_opts:
type: none
o: bind
device: "${PHOTOS_PATH:-/mnt/gerbil/photos}"
backups:
driver: local
driver_opts:
type: none
o: bind
device: "${BACKUPS_PATH:-/mnt/gerbil/backups}"

View File

@@ -0,0 +1,56 @@
#!/bin/sh
# GerbilManager Backup-Skript (Container-Welt)
# Laeuft als Cron-Job im backup-Sidecar-Container.
# Volumes: /data/photos (read-only), /backups (read-write)
# Umgebungsvariablen: PGPASSWORD, POSTGRES_HOST, POSTGRES_USER, POSTGRES_DB, BACKUP_KEEP_DAYS
set -e
TIMESTAMP=$(date +%Y-%m-%d_%H-%M)
BACKUP_DIR="/backups/$TIMESTAMP"
DB_DUMP="$BACKUP_DIR/gerbilmanager_${TIMESTAMP}.sql"
PHOTO_ARCHIVE="$BACKUP_DIR/photos_${TIMESTAMP}.tar.gz"
KEEP_DAYS="${BACKUP_KEEP_DAYS:-7}"
HOST="${POSTGRES_HOST:-db}"
USER="${POSTGRES_USER:-postgres}"
DB="${POSTGRES_DB:-gerbilmanager}"
log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"; }
log "=== Backup gestartet ($TIMESTAMP) ==="
mkdir -p "$BACKUP_DIR"
# --- Datenbank-Dump ---
log "Erstelle pg_dump fuer $DB@$HOST..."
if ! pg_dump -h "$HOST" -U "$USER" -d "$DB" --clean --if-exists --format=plain \
> "$DB_DUMP" 2>/tmp/pg_err; then
log "FEHLER bei pg_dump: $(cat /tmp/pg_err)"
exit 1
fi
DUMP_SIZE=$(du -k "$DB_DUMP" | cut -f1)
log "Dump erstellt: $DB_DUMP (${DUMP_SIZE} KB)"
# Validierung: mindestens CREATE TABLE muss vorhanden sein
if ! grep -q "CREATE TABLE" "$DB_DUMP" 2>/dev/null; then
log "WARNUNG: Dump sieht ungueltig aus -- bitte $DB_DUMP pruefen"
fi
# --- Fotos archivieren ---
if [ -d /data/photos ] && [ "$(ls -A /data/photos 2>/dev/null)" ]; then
PHOTO_COUNT=$(find /data/photos -type f | wc -l)
log "Archiviere $PHOTO_COUNT Fotos..."
tar czf "$PHOTO_ARCHIVE" -C /data photos/ 2>/tmp/tar_err || {
log "WARNUNG: Foto-Archiv fehlgeschlagen: $(cat /tmp/tar_err)"
}
PHOTO_SIZE=$(du -k "$PHOTO_ARCHIVE" 2>/dev/null | cut -f1)
log "Foto-Archiv erstellt: $PHOTO_ARCHIVE (${PHOTO_SIZE} KB)"
else
log "Keine Fotos gefunden -- Foto-Backup uebersprungen"
fi
# --- Rotation: alte Backups loeschen ---
log "Rotiere Backups (behalte $KEEP_DAYS Tage)..."
find /backups -maxdepth 1 -type d -name '????-??-??_??-??' \
-mtime "+$KEEP_DAYS" -exec rm -rf {} + 2>/dev/null || true
REMAINING=$(find /backups -maxdepth 1 -type d -name '????-??-??_??-??' | wc -l)
log "Backup abgeschlossen. Vorhandene Sicherungen: $REMAINING"
log "=== Ende ==="

View File

@@ -0,0 +1,16 @@
#!/bin/sh
# Backup-Sidecar Entrypoint
# Installiert den Cron-Job und startet crond im Vordergrund.
set -e
# Cron-Job: taeglich um 03:00 Uhr
CRON_SCHEDULE="${BACKUP_CRON:-0 3 * * *}"
echo "$CRON_SCHEDULE /bin/sh /scripts/backup.sh >> /backups/backup.log 2>&1" > /etc/crontabs/root
echo "[$(date)] Backup-Sidecar gestartet. Naechste Sicherung: $CRON_SCHEDULE"
echo "[$(date)] Backup-Verzeichnis: /backups Fotos: /data/photos"
# Ersten Backup-Lauf direkt beim Start ausfuehren (optional, auskommentieren wenn unerwuenscht)
# /bin/sh /scripts/backup.sh
exec crond -f -l 6

View File

@@ -0,0 +1,77 @@
#!/bin/sh
# GerbilManager Wiederherstellung (Container-Welt)
# Aufruf: docker compose -f deploy/truenas/compose.yaml exec backup /bin/sh /scripts/restore.sh
# Optional: .../restore.sh 2026-06-06_03-00 (bestimmtes Backup)
#
# WARNUNG: Ueberschreibt alle aktuellen Datenbankdaten und Fotos!
set -e
HOST="${POSTGRES_HOST:-db}"
USER="${POSTGRES_USER:-postgres}"
DB="${POSTGRES_DB:-gerbilmanager}"
BACKUP_ROOT=/backups
log() { echo "[$(date +'%H:%M:%S')] $1"; }
# --- Backup-Verzeichnis bestimmen ---
if [ -n "$1" ]; then
BACKUP_DIR="$BACKUP_ROOT/$1"
else
BACKUP_DIR=$(find "$BACKUP_ROOT" -maxdepth 1 -type d -name '????-??-??_??-??' \
| sort -r | head -1)
fi
if [ -z "$BACKUP_DIR" ] || [ ! -d "$BACKUP_DIR" ]; then
log "FEHLER: Backup-Verzeichnis nicht gefunden: ${BACKUP_DIR:-$BACKUP_ROOT}"
log "Verfuegbare Backups:"
find "$BACKUP_ROOT" -maxdepth 1 -type d -name '????-??-??_??-??' | sort -r
exit 1
fi
SQL_FILE=$(find "$BACKUP_DIR" -name '*.sql' | head -1)
PHOTO_ARCHIVE=$(find "$BACKUP_DIR" -name '*.tar.gz' | head -1)
if [ -z "$SQL_FILE" ]; then
log "FEHLER: Kein SQL-Dump in $BACKUP_DIR gefunden."
exit 1
fi
log "Wiederherstellung aus: $BACKUP_DIR"
log " Datenbank : $(basename $SQL_FILE)"
[ -n "$PHOTO_ARCHIVE" ] && log " Fotos : $(basename $PHOTO_ARCHIVE)"
# --- Bestaetigung (interaktiv; mit -f ueberspringen) ---
if [ "$1" != "-f" ] && [ "$2" != "-f" ]; then
printf "WARNUNG: Aktuelle Daten werden unwiderruflich ueberschrieben!\nFortfahren? (ja/nein): "
read -r CONFIRM
if [ "$CONFIRM" != "ja" ]; then
log "Abgebrochen."
exit 0
fi
fi
# --- Datenbankwiederherstellung ---
log "Trenne laufende DB-Verbindungen..."
psql -h "$HOST" -U "$USER" -d postgres -c \
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$DB' AND pid<>pg_backend_pid();" \
> /dev/null 2>&1 || true
log "Spiele SQL-Dump ein..."
if ! psql -h "$HOST" -U "$USER" -d "$DB" < "$SQL_FILE" > /tmp/restore_out 2>&1; then
log "FEHLER bei der DB-Wiederherstellung:"
cat /tmp/restore_out
exit 1
fi
log "Datenbankwiederherstellung erfolgreich."
# --- Fotos wiederherstellen ---
if [ -n "$PHOTO_ARCHIVE" ]; then
log "Stelle Fotos wieder her..."
rm -rf /data/photos/* 2>/dev/null || true
tar xzf "$PHOTO_ARCHIVE" -C /data
PHOTO_COUNT=$(find /data/photos -type f 2>/dev/null | wc -l)
log "Fotos wiederhergestellt: $PHOTO_COUNT Dateien."
fi
log "Wiederherstellung abgeschlossen."
log "Starte API-Container neu: docker compose ... restart api"

381
docs/ops.md Normal file
View File

@@ -0,0 +1,381 @@
# GerbilManager — Betriebsanleitung (TrueNAS)
> Zielgruppe: Julian (Systemadministration) und Ehefrau (tägliche Nutzung).
> Bookmark für die Ehefrau: **http://\<NAS-IP\>/** (z. B. http://truenas/)
---
## Inhaltsverzeichnis
1. [Übersicht & Architektur](#1-übersicht--architektur)
2. [Voraussetzungen](#2-voraussetzungen)
3. [Erstinstallation auf TrueNAS](#3-erstinstallation-auf-truenas)
4. [App starten / stoppen / aktualisieren](#4-app-starten--stoppen--aktualisieren)
5. [Backup & Wiederherstellung](#5-backup--wiederherstellung)
6. [ZFS-Snapshot-Schichtung](#6-zfs-snapshot-schichtung)
7. [CI/CD via Gitea Actions](#7-cicd-via-gitea-actions)
8. [Offene Fragen (bitte beantworten)](#8-offene-fragen)
9. [Fehlerbehebung](#9-fehlerbehebung)
---
## 1. Übersicht & Architektur
```
Browser / Handy
| HTTP :80
v
┌──────────────────┐
│ frontend (nginx) │ statisches React-SPA + Reverse-Proxy
└──────┬───────────┘
│ /api/* → http://api:8080/*
│ /scalar → http://api:8080/scalar
v
┌──────────────────┐
│ api (.NET 10) │ GerbilManagerWebAPI, Minimal API
└──────┬───────────┘
│ ConnectionStrings__gerbilmanager
v
┌──────────────────┐ ┌──────────────────────┐
│ db (Postgres 17)│ │ backup (Sidecar) │
└──────────────────┘ │ pg_dump + tar + cron │
│ └──────────────────────┘
└─ pgdata-Volume (NAS-Dataset)
photos-Volume (NAS-Dataset)
backups-Volume (NAS-Dataset)
```
**Einziger veröffentlichter Port:** `80` (konfigurierbar via `PORT` in `.env`).
Alles andere läuft intern im Docker-Netz.
---
## 2. Voraussetzungen
| Was | Details |
|-----|---------|
| TrueNAS SCALE | Electric Eel 24.10+ (native Docker Custom Apps) |
| Gitea | http://truenas:13000 — Repository `Gulum/GerbilManager` |
| Docker | bereits auf TrueNAS vorhanden (Custom Apps nutzen es) |
| Datasets | Drei ZFS-Datasets anlegen (siehe Schritt 3) |
---
## 3. Erstinstallation auf TrueNAS
### 3.1 ZFS-Datasets anlegen
In TrueNAS → **Datasets****Dataset hinzufügen** (je einmal wiederholen):
| Dataset-Name | Empfohlener Pfad | Verwendung |
|---|---|---|
| `gerbil/pgdata` | `/mnt/SSD/gerbil/pgdata` | Postgres-Datenbankdateien |
| `gerbil/photos` | `/mnt/SSD/gerbil/photos` | Hochgeladene Tierfotos |
| `gerbil/backups` | `/mnt/SSD/gerbil/backups` | Tägliche Backups |
> **Tipp:** Passe die Pool-Bezeichnung (`SSD`) an deinen tatsächlichen Pool an.
### 3.2 Repository klonen
```bash
# SSH in TrueNAS oder lokale Shell
git clone http://truenas:13000/Gulum/GerbilManager.git /opt/gerbilmanager
cd /opt/gerbilmanager
```
### 3.3 Konfiguration anlegen
```bash
cp deploy/truenas/.env.example deploy/truenas/.env
# Jetzt .env bearbeiten:
nano deploy/truenas/.env
```
Mindestens setzen:
- `POSTGRES_PASSWORD` — sicheres Passwort (mind. 20 Zeichen)
- `PGDATA_PATH`, `PHOTOS_PATH`, `BACKUPS_PATH` — tatsächliche Dataset-Pfade
### 3.4 Images bauen und App starten
```bash
cd /opt/gerbilmanager
docker compose -f deploy/truenas/compose.yaml build
docker compose -f deploy/truenas/compose.yaml up -d
```
Erster Start dauert ca. 23 Minuten (Postgres-Init + EF-Migrationen).
### 3.5 Prüfen
```bash
# Alle Container laufen?
docker compose -f deploy/truenas/compose.yaml ps
# API-Healthcheck
curl http://localhost/api/health
# Webapp im Browser
http://<NAS-IP>/
```
---
## 4. App starten / stoppen / aktualisieren
### Starten
```bash
docker compose -f deploy/truenas/compose.yaml up -d
```
### Stoppen
```bash
docker compose -f deploy/truenas/compose.yaml down
```
### Aktualisieren (nach `git push` auf main)
```bash
cd /opt/gerbilmanager
git pull
docker compose -f deploy/truenas/compose.yaml build
docker compose -f deploy/truenas/compose.yaml up -d
```
> EF-Migrationen laufen automatisch beim API-Start — kein manueller Schritt nötig.
### Mit Gitea CI (wenn Actions aktiviert)
Push auf `main` triggert automatisch Build → Test → Image-Push.
Danach auf der NAS:
```bash
docker compose -f deploy/truenas/compose.yaml pull
docker compose -f deploy/truenas/compose.yaml up -d
```
---
## 5. Backup & Wiederherstellung
### Automatisches Backup
Der `backup`-Sidecar-Container läuft dauerhaft und sichert täglich um **03:00 Uhr**:
- Kompletter `pg_dump` der Datenbank als `.sql`
- Komprimiertes Foto-Archiv als `.tar.gz`
- Rotation: Backups älter als `BACKUP_KEEP_DAYS` (Standard: 7) werden gelöscht
Backups liegen unter: `${BACKUPS_PATH}/YYYY-MM-DD_HH-MM/`
```
/mnt/SSD/gerbil/backups/
2026-06-06_03-00/
gerbilmanager_2026-06-06_03-00.sql (Datenbank)
photos_2026-06-06_03-00.tar.gz (Fotos)
backup.log (Protokoll)
```
### Manuelles Backup auslösen
```bash
docker compose -f deploy/truenas/compose.yaml exec backup /bin/sh /scripts/backup.sh
```
### Backup-Log prüfen
```bash
tail -50 /mnt/SSD/gerbil/backups/backup.log
```
### Wiederherstellung — Runbook
> **WARNUNG:** Alle aktuellen Daten werden überschrieben!
**Schritt 1:** App stoppen (optional, aber empfohlen)
```bash
docker compose -f deploy/truenas/compose.yaml stop api frontend
```
**Schritt 2:** Restore ausführen
```bash
# Neuestes Backup wiederherstellen:
docker compose -f deploy/truenas/compose.yaml exec backup \
/bin/sh /scripts/restore.sh
# Bestimmtes Backup wiederherstellen:
docker compose -f deploy/truenas/compose.yaml exec backup \
/bin/sh /scripts/restore.sh 2026-06-05_03-00
```
**Schritt 3:** API neu starten
```bash
docker compose -f deploy/truenas/compose.yaml start api frontend
```
**Schritt 4:** Prüfen
```bash
curl http://localhost/api/color-varieties | grep -c '"id"'
# Erwarteter Wert: 73
```
### Restore-Nachweis (Round-Trip-Test)
Protokoll vom Test auf lokalem Aspire-Postgres (Vorgänger-Instanz, 2026-06-06 07:09):
```
73 ColorVarieties vorhanden
→ DELETE 12 Zeilen → 61 verbleibend
→ pg_restore eingespielt
→ 73 ColorVarieties bestätigt
Exit-Code: 0
```
Die Container-Restore-Skripte nutzen dieselbe `psql < dump.sql` Logik.
**Erster echter Test auf TrueNAS:** nach Erstinstallation bitte ausführen und das Ergebnis notieren.
---
## 6. ZFS-Snapshot-Schichtung
ZFS-Snapshots ergänzen die pg_dump-Backups als zweite Sicherungsebene.
Sie schützen vor versehentlichem Datenverlust auf Dataset-Ebene.
### Empfohlene Snapshot-Konfiguration
In TrueNAS → **Datasets** → Dataset auswählen → **Snapshots****Regelmäßige Snapshots**:
| Dataset | Häufigkeit | Aufbewahrung |
|---------|-----------|--------------|
| `gerbil/photos` | Stündlich | 24 Stunden |
| `gerbil/photos` | Täglich | 30 Tage |
| `gerbil/pgdata` | Stündlich | 24 Stunden |
| `gerbil/pgdata` | Täglich | 30 Tage |
| `gerbil/backups` | Täglich | 90 Tage |
> **Hinweis:** `pgdata` enthält Live-Postgres-Dateien. ZFS-Snapshots davon sind crash-konsistent,
> aber **nicht** application-konsistent — für einen sauberen DB-Restore immer den `pg_dump` verwenden,
> nicht den ZFS-Snapshot von `pgdata`.
### Snapshot manuell erstellen (z. B. vor Update)
```bash
# TrueNAS CLI
zfs snapshot SSD/gerbil/photos@vor-update-$(date +%Y%m%d)
zfs snapshot SSD/gerbil/backups@vor-update-$(date +%Y%m%d)
```
### Aus ZFS-Snapshot wiederherstellen (Fotos)
```bash
# Snapshot auflisten
zfs list -t snapshot SSD/gerbil/photos
# Datei aus Snapshot kopieren
cp /mnt/SSD/gerbil/photos/.zfs/snapshot/<NAME>/datei.jpg /mnt/SSD/gerbil/photos/
```
---
## 7. CI/CD via Gitea Actions
Der Workflow `.gitea/workflows/ci.yml` ist als **Entwurf vorhanden, aber inaktiv**.
### Aktivierung
1. **Gitea Actions aktivieren:**
Gitea → Repository `GerbilManager` → Einstellungen → Actions → "Actions aktivieren"
2. **Gitea Actions Runner installieren** (auf TrueNAS oder einem separaten Gerät):
```bash
# Gitea Runner Container (einfachste Variante für TrueNAS)
docker run -d --name gitea-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /opt/gitea-runner:/data \
-e GITEA_INSTANCE_URL=http://truenas:13000 \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<TOKEN> \
gitea/act_runner:latest
```
Token: Gitea → Admin → Actions → Runner → "Runner hinzufügen"
3. **Registry-Secrets konfigurieren:**
Gitea → Repository → Einstellungen → Secrets:
- `REGISTRY_USER` — dein Gitea-Benutzername
- `REGISTRY_TOKEN` — Gitea Access Token mit `package:write`-Berechtigung
### Workflow nach Aktivierung
```
git push origin main
→ Gitea Actions: dotnet test + npm test + npm run build
→ Bei Erfolg: docker build + push zu truenas:13000/gulum/
→ Auf NAS: docker compose pull + up -d
```
---
## 8. Offene Fragen
Bitte beantworte diese Fragen, damit das Setup fertiggestellt werden kann:
| # | Frage | Auswirkung |
|---|-------|-----------|
| 1 | **TrueNAS SCALE Version?** Electric Eel 24.10 hat native Docker Custom Apps. Ältere Versionen nutzen Kubernetes. | Bestimmt ob `docker compose` direkt läuft |
| 2 | **Gitea Actions verfügbar/aktivierbar?** | CI/CD-Workflow aktiv oder nur manuell deployen |
| 3 | **Eigener Postgres-Container (empfohlen) oder vorhandene NAS-Postgres-App?** | Isolation vs. geteilte Instanz |
| 4 | **Genaue Dataset-Pfade?** Poolname und Pfad-Präfix | `.env`-Konfiguration |
| 5 | **Port-Wahl?** Standard 80 — frei auf der NAS? | `PORT`-Wert in `.env` |
---
## 9. Fehlerbehebung
### App startet nicht
```bash
# Logs aller Container
docker compose -f deploy/truenas/compose.yaml logs
# Logs eines bestimmten Containers
docker compose -f deploy/truenas/compose.yaml logs api
docker compose -f deploy/truenas/compose.yaml logs db
```
### Datenbank nicht erreichbar
```bash
# DB-Container läuft?
docker compose -f deploy/truenas/compose.yaml ps db
# Verbindung testen
docker compose -f deploy/truenas/compose.yaml exec db \
psql -U postgres -d gerbilmanager -c "\dt"
```
### Backup-Fehler
```bash
# Backup-Log prüfen
cat /mnt/SSD/gerbil/backups/backup.log | tail -30
# Backup manuell starten (mit Fehlerausgabe)
docker compose -f deploy/truenas/compose.yaml exec backup \
/bin/sh /scripts/backup.sh
```
### Fotos werden nicht angezeigt
Prüfe ob das `photos`-Volume korrekt gemounted ist:
```bash
docker compose -f deploy/truenas/compose.yaml exec api ls /data/photos
```
### Container-Status zurücksetzen (Neustart)
```bash
docker compose -f deploy/truenas/compose.yaml restart api
```
### Kompletter Neustart (Daten bleiben erhalten)
```bash
docker compose -f deploy/truenas/compose.yaml down
docker compose -f deploy/truenas/compose.yaml up -d
```

View File

@@ -1,25 +1,48 @@
# Build-Stufe: Vite-Produktionsbuild
# Build context: repo root (Dockerfile liest aus gerbil-manager-web/).
# docker build -f gerbil-manager-web/Dockerfile -t gerbilmanager-frontend .
#
# Produktion: kein VITE_API_BASE_URL nötig — client.ts fällt auf '/api' zurück,
# nginx proxyt /api/* transparent zum API-Container (keine Host-Kopplung zur Build-Zeit).
FROM node:22-alpine AS build
WORKDIR /app
COPY gerbil-manager-web/package.json gerbil-manager-web/package-lock.json ./
RUN npm ci
COPY gerbil-manager-web/ .
# Basis-URL der API (zur Build-Zeit eingebettet; Browser erreicht die API über den Host)
ARG VITE_API_BASE_URL=http://localhost:80
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
RUN npm run build
# Laufzeit-Stufe: statische Auslieferung über nginx
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
# SPA-Fallback: alle Pfade auf index.html umleiten (react-router)
# nginx: SPA-Fallback + Reverse-Proxy für /api, /scalar, /openapi zum API-Container.
RUN printf 'server {\n\
listen 3000;\n\
listen 80;\n\
root /usr/share/nginx/html;\n\
index index.html;\n\
\n\
# API-Aufrufe: /api/* -> API-Container /* (Praefix wird entfernt)\n\
location /api/ {\n\
proxy_pass http://api:8080/;\n\
proxy_set_header Host $host;\n\
proxy_set_header X-Real-IP $remote_addr;\n\
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\
}\n\
\n\
# Scalar API-Doku und OpenAPI-Spec direkt vom Backend\n\
location /scalar {\n\
proxy_pass http://api:8080/scalar;\n\
proxy_set_header Host $host;\n\
}\n\
location /openapi/ {\n\
proxy_pass http://api:8080/openapi/;\n\
proxy_set_header Host $host;\n\
}\n\
\n\
# SPA-Fallback: alle anderen Pfade laden index.html (React Router)\n\
location / {\n\
try_files $uri $uri/ /index.html;\n\
}\n\
}\n' > /etc/nginx/conf.d/default.conf
EXPOSE 3000
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -2,11 +2,12 @@ import { de } from '../strings/de'
/**
* Basis-URL der GerbilManagerWebAPI.
* Per VITE_API_BASE_URL konfigurierbar (z. B. in Docker / Aspire);
* Standard ist das lokale Dev-Profil der API (launchSettings.json, Profil "http").
* Dev (Vite dev server / e2e mock): absolute URL, damit Playwright-Route-Interception greift.
* Produktion (Vite build → nginx): relativer Pfad /api; nginx proxyt zum API-Container.
* Aspire (VITE_API_BASE_URL gesetzt): überschreibt immer (LAN-IP + Port für Handy-Zugriff).
*/
export const API_BASE_URL: string =
import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:5179'
import.meta.env.VITE_API_BASE_URL ?? (import.meta.env.PROD ? '/api' : 'http://localhost:5179')
export class ApiError extends Error {
readonly status: number | null