From 347b1de555f5eeffe2b3c0e0db3473b6eb200f3e Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:38:00 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20CLAUDE.md=20+=20Architecture=20+=20Oper?= =?UTF-8?q?ations=20f=C3=BCr=20Session-Fortsetzung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drei fokussierte Dokumente, damit eine frische Claude-Session direkt weiterarbeiten kann, ohne den gesamten Session-Kontext zu brauchen: - CLAUDE.md (Root): "read me first" — Gotchas, Workflow, No-Gos, Quickstart, Verweise auf die Tiefen-Docs. - docs/ARCHITECTURE.md: Stack, Verzeichnisbaum, Datenfluss (Import, Web-Suche, Confirm/Alert), Design-Entscheidungen, Test-Strategie. - docs/OPERATIONS.md: Deployment-Topologie (Cloudflare → Pi → Traefik → kochwas), Gitea-CI-Pipeline, Traefik-Labels mit Wildcard-Cert, Troubleshooting (TLS, SearXNG-403, Healthcheck, Thumbnail-Cache leeren, Backup), Env-Vars. Die bestehenden Specs und Plans unter docs/superpowers/ bleiben unangetastet — die sind Planungs-Artefakte, nicht Betriebsdoku. --- CLAUDE.md | 69 ++++++++++++++++++++ docs/ARCHITECTURE.md | 114 +++++++++++++++++++++++++++++++++ docs/OPERATIONS.md | 148 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+) create mode 100644 CLAUDE.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/OPERATIONS.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5736f4d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# Kochwas — Hinweise für Claude (Fortsetzung nach Session-Neustart) + +> **Lies mich zuerst.** Wenn du eine neue Session öffnest und hier weiterarbeitest, steht hier das Wesentliche. Tiefer: `docs/ARCHITECTURE.md` (Code) und `docs/OPERATIONS.md` (Deployment). + +## Was das ist +Selbstgehostete Rezept-PWA für die Familie Siegeln. Erreichbar unter `https://kochwas.siegeln.net`. Deutschsprachiges UI, ohne Login, Profile werden per Klick gewählt. Läuft in Docker auf einem Raspberry Pi 5 (arm64). + +## Wichtigste Gotchas (wiederkehrende Stolpersteine) + +| Thema | Regel | +|---|---| +| **Node-Binding** | `better-sqlite3` ist **synchron** und native — im `Dockerfile` gibt es einen Build-Stage, der das Native-Module explizit für arm64 baut. | +| **Healthcheck** | Muss `127.0.0.1` verwenden, nicht `localhost`. Node bindet nur IPv4; `localhost` wird oft zu `::1` aufgelöst und der Check schlägt fehl. Traefik filtert unhealthy Container raus → kein Routing, kein ACME. | +| **SearXNG Bot-Detection** | Bei Requests aus dem Docker-Netzwerk müssen `X-Forwarded-For: 127.0.0.1` und `X-Real-IP: 127.0.0.1` im Header stehen (`src/lib/server/http.ts` `extraHeaders`). Sonst 403. | +| **Traefik Cloudflare-Token** | Token muss `Edit zone DNS` Berechtigung für `siegeln.net` haben. Expired Tokens → DNS-Challenge failt → Let's-Encrypt-Rate-Limit nach 5 Versuchen in 1 h. | +| **Wildcard-Cert** | Für neue Subdomains auf siegeln.net sollten die Labels das Wildcard nutzen, nicht per-Host-Cert: `tls.domains[0].main=siegeln.net` + `sans=*.siegeln.net`. | +| **Migrations** | Werden via Vite `import.meta.glob('./migrations/*.sql', {eager, query:'?raw'})` gebundelt. Neue Migration einfach als `00N_name.sql` ablegen, kein Copy-in-Dockerfile nötig. | +| **$lib/server in Client** | Svelte-Import aus `$lib/server/*` in einem `.svelte`-Komponenten-Script bricht den Build. Pures JS/TS, das beidseitig funktioniert (z. B. Portionen-Scaler), gehört nach `$lib/`, nicht `$lib/server/`. | +| **Preview-Bilder** | `recipe.image_path` kann **absolute URL** (Preview-Modus) oder **lokaler Filename** sein. `RecipeView.svelte` prüft mit `/^https?:\/\//i`. | + +## Dateien, die man typischerweise anfasst + +- `src/routes/+page.svelte` — Startseite mit Live-Search + Quote +- `src/routes/+layout.svelte` — Header, mobile expand, Dropdown-Search auf Rezeptseiten +- `src/routes/recipes/[id]/+page.svelte` — Rezept-Detail mit allen Actions (Rating, Favorit, Cooked, Wunschliste, Kommentar, Umbenennen, Löschen) +- `src/routes/preview/+page.svelte` — importierte Vorschau vor dem Speichern +- `src/lib/server/search/searxng.ts` — Web-Suche + Thumbnail-Enrichment + SQLite-Cache +- `src/lib/server/recipes/importer.ts` — JSON-LD → Recipe, orchestriert Bild-Download +- `src/lib/server/db/migrations/*.sql` — Schema; bei Änderung immer **neue** Migration statt bestehende bearbeiten + +## Arbeitsweise (wie wir es machen) + +- **Terse Antworten auf Deutsch**; Code-Kommentare auf Englisch, sparsam. +- **Commits** kleinteilig, deutscher Body, englische Zeile, Subject unter 72 Zeichen. +- **Tests nach jeder Änderung**: `npm test` (vitest) + `npm run check` (svelte-check). Beides muss grün sein, bevor gepusht wird. +- **Push nach jedem Commit**, außer der Nutzer sagt explizit nein. CI baut dann das arm64-Image und published es nach `gitea.siegeln.net/claude/kochwas:latest`. +- **Keine Backwards-Compat-Krücken** für nicht-ausgelieferten Code. Direkt refactoren, alte Signaturen raus. +- **Nie mit `--no-verify`** committen. Wenn ein Hook fehlschlägt, den echten Grund beheben. + +## Quickstart + +```bash +npm install # erstes Mal +npm run dev # lokal auf http://localhost:5173 +npm test # volle Vitest-Suite +npm run check # svelte-check Types +npm run format # Prettier +``` + +Lokaler Docker-Test des Prod-Builds: +```bash +docker compose -f docker-compose.prod.yml up --build +``` + +## Was NICHT tun + +- Keine neuen Top-Level-Docs erzeugen, wenn ein bestehendes Dokument (Specs, Plans, ARCHITECTURE, OPERATIONS) passt. +- Keine Emojis in Code/Commits — außer UI-Icons (🍽️, ⚙️, 🥘 etc.) sind explizit im UX-Design. +- Keine `alert()`/`confirm()` — wir haben `alertAction()` / `confirmAction()` in `src/lib/client/confirm.svelte.ts`. +- Keine hardcoded `localhost` in Healthchecks → `127.0.0.1`. +- Keinen sensiblen Output in Commits (Cloudflare-Tokens, acme.json). + +## Offene Themen / Stand + +Siehe Session-Handoff-Dokumente unter `docs/superpowers/` und dort besonders `session-handoff-2026-04-17.md`. Die Roadmap-Phasen liegen als `docs/superpowers/plans/*.md`. Was als „Later" markiert ist, ist nicht beauftragt. + +## Auto-Memory (lokal, nicht im Repo) + +Persönliche Präferenzen / projektspezifische Entscheidungen landen in deinem Auto-Memory unter `~/.claude/projects/C--Users-Hendrik-Documents-projects-kochwas/memory/`. Der aktuelle Index (`MEMORY.md`) hält fest: Deployment-Target, Registry. Bei Bedarf erweitern — nicht in dieser Datei dokumentieren, da sie versioniert ist. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..e0d80aa --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,114 @@ +# Kochwas — Architektur + +## Stack + +- **SvelteKit 2** + **Svelte 5 Runes** (`$state`, `$derived`, `$effect`, `$props`) +- **TypeScript strict** +- **SQLite** über `better-sqlite3` (synchron, native Binding arm64) +- **FTS5** Virtual Tables mit BM25-Ranking für Volltext-Suche +- **linkedom** für HTML-Parsing (JSON-LD-Extraktion, og:image-Enrichment) +- **zod** für API-Schema-Validierung +- Adapter: `@sveltejs/adapter-node` → Node 22 Alpine im Container + +## Top-Level-Struktur + +``` +src/ +├── app.html, app.d.ts # Shell + Env-Types +├── service-worker.ts # PWA-Shell +├── lib/ +│ ├── client/ # clientseitig: Profil-Store, Confirm-Dialog +│ ├── components/ # Svelte-Komponenten (RecipeView, StarRating, ConfirmDialog, ProfileSwitcher) +│ ├── recipes/ # shared: Portionen-Scaler (Client UND Server) +│ ├── server/ # nur Server-Code (nie in Client-Bundle!) +│ │ ├── db/ # openDb, Migrations, DB-Singleton +│ │ ├── domains/ # Whitelist-Repo +│ │ ├── http.ts # fetch-Wrapper mit Timeout / maxBytes / extraHeaders +│ │ ├── images/ # Download, SHA256-Dedup, Save +│ │ ├── parsers/ # json-ld-recipe.ts, iso8601-duration.ts +│ │ ├── profiles/ # Profile-Repo +│ │ ├── recipes/ # importer, actions, repository, search-local +│ │ ├── search/ # searxng.ts (Web-Suche + Thumbnail-Cache) +│ │ ├── wishlist/ # Repo +│ │ └── backup/ # ZIP-Export via archiver, Import via yauzl +│ ├── quotes.ts # 49 Flachwitze für die Homepage +│ └── types.ts # shared types +└── routes/ + ├── +layout.svelte # Header, Confirm-Dialog-Mount, Header-Search-Dropdown + ├── +page.svelte # Home: Hero + Live-Search + Zuletzt-hinzugefügt + ├── recipes/[id]/ # Rezept-Detail + ├── preview/ # Vorschau vor dem Speichern + ├── search/ # /search (lokal), /search/web (Internet) + ├── wishlist/ + ├── admin/ # Whitelist, Profile, Backup/Restore + ├── images/[filename] # Statische Auslieferung lokaler Bilder + └── api/ # REST-Endpoints +``` + +## Datenfluss + +### Import (User klickt auf Web-Treffer) + +1. User klickt auf Web-Hit → `/preview?url=...` +2. `/api/recipes/preview` → `importer.ts` lädt HTML, `parseHTML` von linkedom, `json-ld-recipe.ts` extrahiert `Recipe`-Objekt mit **externer** Bild-URL +3. Preview-Seite rendert das `Recipe` via `RecipeView.svelte` (erkennt externe URL und lädt direkt vom Original-CDN) +4. User klickt „Speichern" → `/api/recipes/import` → Importer lädt Bild (`images/downloader.ts`), SHA256-Hash-Dedup, speichert lokal, INSERT in `recipe` + `recipe_ingredient` + `recipe_step` + `recipe_tag` +5. Redirect zu `/recipes/[id]` + +### Web-Suche + +1. User tippt → 300 ms Debounce → `/api/recipes/search?q=...` (lokal FTS5) +2. Wenn 0 Treffer: automatisch `/api/recipes/search/web?q=...` +3. `searxng.ts` → SearXNG-API mit `site:domain OR site:domain2 ...`-Filter aus Whitelist +4. Filtert Non-Recipe-Pfade (Foren, Magazin, Listings) via `NON_RECIPE_PATH_PATTERNS` +5. Pro Treffer: parallel (max 6) `enrichThumbnail`: + - SQLite-Cache hit → return + - Sonst: Seite holen (max 512 KB, 4 s), `extractPageImage`: og:image → link rel=image_src → JSON-LD → erstes Content-img + - Ergebnis (auch null) in `thumbnail_cache` persistieren (30 Tage TTL) + - **Überschreibt** bestehendes SearXNG-Thumbnail, weil das meist LowRes ist + +### Confirm / Alert + +Promise-basiert statt `window.confirm`/`window.alert`: + +```ts +import { confirmAction, alertAction } from '$lib/client/confirm.svelte'; + +if (await confirmAction({ title: 'Löschen?', destructive: true })) { /* ... */ } +await alertAction({ title: 'Fehler', message: 'xyz' }); +``` + +Gemeinsame Komponente `ConfirmDialog.svelte` wird im Root-Layout einmal gemountet. Store (`confirmStore`) hält die Promise-Resolve-Funktion, Komponente rendert nur wenn `pending !== null`. + +## Design-Entscheidungen + +- **Kein Login, nur Profile**: Profile werden beim Start gewählt, in localStorage persistiert. Actions (Rating, Favorit, Cooked, Kommentar) brauchen aktives Profil → sonst Custom-Alert „Bitte Profil wählen". +- **FTS5 als Haupt-Suche**: statt externer Search-Engine-DB. Passt zu SQLite-only-Stack. +- **JSON-LD first**: Alle drei Ziel-Domains (Chefkoch, Emmi, Experimente) liefern `schema.org/Recipe` im JSON-LD. LLM-Fallback war geplant, aktuell nicht nötig. +- **SearXNG als Such-Engine**: Self-hosted, daher keine API-Keys. Das Bot-Detection-Theater wird mit gesetzten `X-Forwarded-For`-Headern aus Docker-IPs umgangen. +- **Thumbnail-Cache in SQLite**: 30 Tage TTL (per `KOCHWAS_THUMB_TTL_DAYS`). Negative Einträge (Seite ohne Bild) werden auch gecacht. +- **Svelte 5 Runes** — kein `$:` mehr, keine alten Stores außer `$app/stores`. Neue Stores via Klasse mit `$state`-Feldern. +- **Service Worker** rein zum Shell-Cachen für Offline-First-PWA, kein intelligentes Cache-Matching (keine externe Rezept-Seiten). + +## Migrations-Workflow + +Bei Schema-Änderung: + +1. Neue Datei `src/lib/server/db/migrations/00N_beschreibung.sql` — nächste freie Nummer +2. SQL sollte nicht-destruktiv sein (nur `CREATE`, `ALTER ADD`); keine `DROP` auf bestehende Daten +3. `migrate.ts` liest via Vite-Glob und führt neue Einträge aus (über `schema_migrations`-Tabelle getrackt) +4. Tests anpassen: `db idempotent` zählt vorher/nachher — bleibt automatisch grün + +## Test-Strategie + +- **Unit**: `tests/unit/` — pure Funktionen (json-ld-recipe, iso8601-duration, quotes-random, smoke) +- **Integration**: `tests/integration/` — mit `openInMemoryForTest()` fresh SQLite pro Test. Externe HTTP via `node:http`-TestServer auf Port 0 gemockt. +- **Keine Svelte-Component-Tests** (bewusst, Aufwand/Nutzen stimmt nicht; UI wird manuell getestet) +- **Vor Commit**: `npm test && npm run check` muss grün sein. + +## Was später kommt (laut Spec, aktuell nicht implementiert) + +- LLM-Fallback für nicht-JSON-LD-Seiten +- Print-View ist nur rudimentär, könnte ein eigenes Print-CSS bekommen +- Tag-Editor im Admin +- Export-Format JSON zusätzlich zu ZIP diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md new file mode 100644 index 0000000..b023705 --- /dev/null +++ b/docs/OPERATIONS.md @@ -0,0 +1,148 @@ +# 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 | +| `IMAGES_PATH` | `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.