Files
kochwas/docs/OPERATIONS.md

213 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Kochwas — Deployment & Operations
## Deployment-Topologie
```
Browser
↓ HTTPS (kochwas.siegeln.net)
Cloudflare DNS (A-Record auf Pi-IP oder Tunnel)
Raspberry Pi 5 (arm64, Debian/Ubuntu)
Traefik v3 (Docker, Container "traefik" im Netz traefik_proxy)
↓ reverse proxy
Kochwas-Container (Node 22 Alpine, Port 3000, internal bridge)
↔ SearXNG-Container (Sidecar im gleichen Stack, Port 8080 intern)
```
- **Traefik** terminiert TLS mit Wildcard-Cert `*.siegeln.net` von Let's Encrypt (DNS-01 Challenge über Cloudflare-API).
- **SearXNG** läuft als Sidecar im kochwas-Compose. Kochwas spricht ihn über `http://searxng:8080` intern an.
- **Gitea Registry** `gitea.siegeln.net/claude/kochwas` hostet das arm64-Image.
- **Daten** liegen im Volume `/opt/docker/kochwas/data/` (SQLite + images/).
## Build & Publish (Gitea Actions)
Workflow in `.gitea/workflows/docker.yml`:
1. Trigger: push auf `main`
2. Checkout, Setup QEMU + Buildx
3. Login an `gitea.siegeln.net` mit Secret `REGISTRY_TOKEN` (PAT mit `write:package` + `read:package` Scope)
4. `docker/build-push-action` baut **nativ arm64** (nicht via emuliertem amd64!), mit `cache-from/to: type=registry,ref=...:buildcache`
5. Push als `:latest` und `:${commit}`
Wenn die Pipeline rot ist, häufig:
- `unauthorized`: Token fehlt oder ohne Package-Scope. PAT unter Gitea → Settings → Applications → Generate Token.
- Build-Cache i/o-Timeout: Registry-Cache benutzen, nicht GHA-Artifact-Cache.
## Deploy auf den Pi
```bash
ssh admin@pi5
cd /opt/docker/kochwas
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
docker compose logs -f kochwas
```
Was der Pi braucht (einmalig):
- `/opt/docker/kochwas/docker-compose.prod.yml` — gespiegelt aus dem Repo
- `/opt/docker/kochwas/.env` mit `KOCHWAS_TAG=latest` (optional) und `SEARXNG_SECRET=...`
- `/opt/docker/kochwas/searxng/settings.yml` — aus dem Repo, mit `limiter: false` und `public_instance: false`
- `/opt/docker/kochwas/data/` existiert (für SQLite + images)
- Netzwerk `traefik_proxy` existiert, damit Traefik den Container sieht
## Traefik-Integration
Labels am kochwas-Container (siehe `docker-compose.prod.yml`):
```yaml
- traefik.enable=true
- traefik.docker.network=traefik_proxy
- traefik.http.routers.kochwas.rule=Host(`kochwas.siegeln.net`)
- traefik.http.routers.kochwas.entrypoints=websecure
- traefik.http.routers.kochwas.tls.certresolver=cloudflareResolver
- traefik.http.routers.kochwas.tls.domains[0].main=siegeln.net
- traefik.http.routers.kochwas.tls.domains[0].sans=*.siegeln.net
- traefik.http.services.kochwas.loadbalancer.server.port=3000
```
Die `tls.domains`-Zeilen sorgen dafür, dass der Router das Wildcard-Cert nutzt statt einen neuen per-Host-Cert zu holen. **Nie per-Host für neue Subdomains** — Let's Encrypt Rate-Limit (5 failed Authorizations pro Identifier pro Stunde, 50 Certs pro Registered Domain pro Woche).
### Wenn Cert fehlt / TLS-Fehler
1. `echo | openssl s_client -servername kochwas.siegeln.net -connect kochwas.siegeln.net:443 2>/dev/null | openssl x509 -noout -issuer -subject` — ist der Issuer „TRAEFIK DEFAULT CERT"? Dann hat Traefik kein Cert.
2. `sudo jq '.cloudflareResolver.Certificates | map(.domain.main)' /opt/docker/traefik/letsencrypt/acme.json` — ist `siegeln.net` (mit SAN `*.siegeln.net`) dabei?
3. `docker logs traefik 2>&1 | grep -iE 'lego|acme|cloudflare|kochwas' | tail -60` — Fehler?
- `Invalid access token` → Cloudflare-API-Token abgelaufen, neu erstellen mit `Zone → DNS → Edit` Scope, `CF_DNS_API_TOKEN` im Traefik-Compose setzen, `docker compose up -d traefik`
- `429 rateLimited` → Warten (zeitangabe im Error) oder auf Wildcard umstellen
## Troubleshooting
### Container läuft, Traefik filtert ihn raus
Symptom: Traefik-Logs sagen `Filtering unhealthy or starting container`.
Ursache: Healthcheck schlägt fehl. Der Check ruft `wget 127.0.0.1:3000/api/health` (muss IPv4 sein!).
```bash
docker inspect kochwas-kochwas-1 --format '{{json .State.Health}}' | jq
docker exec kochwas-kochwas-1 wget -qO- 127.0.0.1:3000/api/health
```
### SearXNG gibt 403 zurück
Log: `Internet-Suche zurzeit nicht möglich: HTTP 403`
Ursache: Bot-Detection. Fix war schon einmal nötig — `src/lib/server/http.ts` setzt via `extraHeaders` `X-Forwarded-For: 127.0.0.1` und `X-Real-IP: 127.0.0.1`. Wenn trotzdem 403: `searxng/settings.yml` prüfen:
```yaml
use_default_settings: true
server:
limiter: false
public_instance: false
secret_key: ${SEARXNG_SECRET:-dev-secret-change-in-prod}
search:
formats: [html, json]
```
Der Server-Container muss diese Datei per Volume Mount sehen. Nach Änderung: `docker compose restart searxng`.
### Thumbnail-Cache leeren
```bash
docker exec kochwas-kochwas-1 sqlite3 /data/kochwas.db 'DELETE FROM thumbnail_cache;'
```
Oder gezielt eine URL:
```bash
docker exec kochwas-kochwas-1 sqlite3 /data/kochwas.db \
"DELETE FROM thumbnail_cache WHERE url = 'https://www.chefkoch.de/rezepte/...';"
```
### Datenbank-Backup manuell
```bash
ssh admin@pi5 'docker exec kochwas-kochwas-1 sqlite3 /data/kochwas.db ".backup /data/backup.db"'
scp admin@pi5:/opt/docker/kochwas/data/backup.db ./kochwas-$(date +%F).db
```
Die App hat ein eingebautes Backup unter `/admin` (ZIP-Export mit DB + Bildern). Restore via `/admin` ebenfalls.
## Umgebungsvariablen
| Name | Default | Bedeutung |
|---|---|---|
| `SEARXNG_URL` | `http://localhost:8888` | SearXNG-Endpoint, im Compose auf `http://searxng:8080` |
| `KOCHWAS_THUMB_TTL_DAYS` | `30` | TTL für Thumbnail-Cache in der SQLite |
| `DATABASE_PATH` | `data/kochwas.db` | Pfad zur SQLite, relativ oder absolut |
| `IMAGE_DIR` | `data/images` | Pfad für lokale Bild-Dateien |
| `PORT` | `3000` | Node-HTTP-Port (adapter-node) |
Siehe `.env.example` im Repo.
## Häufige Commits als Referenz
- **Healthcheck-Fix** → `Dockerfile` (localhost → 127.0.0.1, tightened interval)
- **SearXNG-Bot-Bypass** → `src/lib/server/http.ts` (extraHeaders)
- **Traefik-Wildcard** → `docker-compose.prod.yml` (tls.domains Labels)
- **Thumbnail-Cache in SQLite** → `003_thumbnail_cache.sql` + `searxng.ts`
Git-Log ist die Wahrheit; diese Datei ist eine Orientierung.
## PWA / Offline-Modus
Kochwas ist eine installierbare PWA. Erkennbar an:
- `static/manifest.webmanifest` (Manifest + Icons: SVG + 192×192 + 512×512, alle maskable)
- `src/service-worker.ts` (Cache + Sync)
Caches im Browser (siehe DevTools → Application → Cache Storage):
- `kochwas-shell-<version>` — App-Shell (JS/CSS/Static-Icons), cache-first
- `kochwas-data-v1` — Rezept-HTMLs + API-JSON (network-first, 3 s Timeout → Cache-Fallback)
- `kochwas-images-v1` — Bilder (cache-first)
- `kochwas-meta` — Cache-Manifest (Liste der gecachten Rezept-IDs unter `/__cache-manifest__`)
Sync-Verhalten:
- **Initial-Sync** (nach erstem Install): SW lädt alle Rezepte + Bilder im Hintergrund. Fortschritt im `SyncIndicator`-Pill unten rechts.
- **Update-Sync** (bei jedem App-Start online): Diff gegen Cache-Manifest, nur Delta nachladen, gelöschte IDs räumen.
- **Storage-Quota-Check**: < 100 MB frei → abbrechen mit Fehler-Toast.
Bei SW-Problemen Debug-Pfad:
1. Admin → „App"-Tab → „Offline-Cache leeren" (destructive, zweistufig bestätigt)
2. Alternative: DevTools → Application → Service Workers → Unregister, dann Seite neu laden.
E2E-Tests (Playwright): `npm run test:e2e`. Setzt `npm run build` voraus (Playwright startet automatisch `npm run preview`).
Icons einmalig rendern: `npm run render:icons` (schreibt nach `static/icon-*.png`, committen).
## Dev-System / Remote-E2E
`https://kochwas-dev.siegeln.net/` ist ein separates Deployment (eigener Container, eigene DB unter `/opt/docker/kochwas-dev/data/`). Zweck: E2E-Tests gegen eine prod-nahe Umgebung ohne Angst vor DB-Schäden. Die Remote-Suite (`tests/e2e/remote/`, Config `playwright.remote.config.ts`) darf dort frei CRUDen — User stellt die DB bei Bedarf per Backup wieder her.
```bash
npm run test:e2e:remote # gegen kochwas-dev
E2E_REMOTE_URL=https://... npm run test:e2e:remote # andere URL
```
Wichtige Config-Eigenschaften:
- `workers: 1` — DB-Race-Sicherheit bei CRUD-Tests.
- `serviceWorkers: 'block'` — verhindert Chromium-Crashes durch akkumulierten SW-State über 40+ Contexts.
- Fixtures unter `tests/e2e/remote/fixtures/`: `profile.ts` (Profile-Auswahl via localStorage vor Seitenladen), `api-cleanup.ts` (idempotente DELETE-Helfer für afterEach).
**Niemals gegen `kochwas.siegeln.net` (ohne `-dev`)** die destruktiven Tests laufen lassen — das ist Prod.
## Gemini / Foto-Rezept-Magie
Die Funktion „Foto → Rezept" ruft Google Gemini 2.5 Flash mit Vision auf. Im Header erscheint dann ein Kamera-Icon, das auf `/new/from-photo` führt.
**Env-Vars** (in `docker-compose.prod.yml`):
| Variable | Default | Zweck |
|---|---|---|
| `GEMINI_API_KEY` | _(leer)_ | Ohne Key ist das Feature graceful deaktiviert — Camera-Icon erscheint nicht. |
| `GEMINI_MODEL` | `gemini-2.5-flash` | Modell-Wechsel ohne Rebuild, z. B. auf `gemini-2.5-pro` bei harter Handschrift. |
| `GEMINI_TIMEOUT_MS` | `20000` | Timeout für den Vision-Call. |
**Wichtig:** Env-Änderungen greifen erst nach `docker compose up -d --force-recreate`, nicht nach `restart`.
**Privacy:** Das hochgeladene Foto geht einmal an Google Gemini und wird serverseitig nicht gespeichert. Google trainiert im Paid-Tier nicht auf API-Daten. Der Server loggt nur Status-Code, Dauer und Bildgröße — nie Prompt oder Response-Inhalt.
**Rate-Limit:** 10 Requests/Minute pro IP (in-memory, resettet beim Prozess-Restart).
**Key aus Gitea Secrets:** `GEMINI_API_KEY` als Secret in der CI-Umgebung hinterlegen; der Deploy-Schritt injiziert ihn in die `.env` des Pi-Stacks. Ablauf-Monitoring über die Google-Cloud-Konsole (≥1× pro Quartal checken).
**Build-Dep `sharp`:** Der Foto-Preprocess nutzt `sharp` (libvips). Im `Dockerfile`-Builder-Stage ist `vips-dev` enthalten, damit der npm-install auf arm64 sauber durchläuft — insbesondere für HEIC-Input von iOS-Geräten (libheif kommt via vips-dev).