diff --git a/CLAUDE.md b/CLAUDE.md index 5dd7d6a..45ac6db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,8 @@ Selbstgehostete Rezept-PWA für die Familie Siegeln. Erreichbar unter `https://k | **Preview-Bilder** | `recipe.image_path` kann **absolute URL** (Preview-Modus) oder **lokaler Filename** sein. `RecipeView.svelte` prüft mit `/^https?:\/\//i`. | | **Service Worker nur ab HTTPS** | `npm run dev` liefert HTTP → SW registriert nicht. Für PWA-Tests `npm run build && npm run preview` (localhost) oder Prod-Docker. | | **Icon-Rendering** | `npm run render:icons` rendert `icon-192.png` + `icon-512.png` aus `static/icon.svg`. Nur nach SVG-Änderung erneut ausführen + committen. | +| **Gemini-Key fehlt** | Wenn `GEMINI_API_KEY` leer ist, rendert das Layout das Camera-Icon nicht, und `/new/from-photo` antwortet mit 503 (`+page.server.ts`-Gate). Graceful Degradation — kein Zombie-Button. | +| **sharp + libheif** | Im `Dockerfile`-Builder-Stage ist `vips-dev` nötig, damit `sharp` HEIC-Input (iOS) lesen kann. Runtime-Stage braucht nix zusätzlich (sharp bringt libvips prebuilt mit). | ## Dateien, die man typischerweise anfasst @@ -26,6 +28,8 @@ Selbstgehostete Rezept-PWA für die Familie Siegeln. Erreichbar unter `https://k - `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/routes/new/from-photo/+page.svelte` — Foto-Rezept-Magie (Picker → Spinner → vorbefüllter Editor) +- `src/lib/server/ai/` — Gemini-Client, Prompt-Schema, image-preprocess, rate-limit, description-phrases - `src/lib/components/RecipeView.svelte` / `RecipeEditor.svelte` — Lesen/Edit-Mode des Rezepts. Editor ist in Sub-Components aufgeteilt: `IngredientRow`, `StepList`, `ImageUploadBox`, `TimeDisplay` (+ shared types `recipe-editor-types.ts`) - `src/lib/server/search/searxng.ts` — Web-Suche + Thumbnail-Enrichment + SQLite-Cache - `src/lib/server/recipes/importer.ts` — JSON-LD → Recipe, orchestriert Bild-Download diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3494f04..83c6a7f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -58,6 +58,16 @@ src/ 4. User klickt „Speichern" → `/api/recipes/import` → Importer lädt Bild (`images/downloader.ts`), SHA256-Hash-Dedup, speichert lokal, INSERT in `recipe` + `ingredient` + `step` + `recipe_tag` 5. Redirect zu `/recipes/[id]` +### Foto-Rezept-Magie (AI-Extraktion) + +1. User klickt Camera-Icon im Header → `/new/from-photo` (nur gerendert wenn `GEMINI_API_KEY` gesetzt; disabled wenn offline) +2. File-Picker mit `capture="environment"` öffnet direkt die Rückkamera auf Mobile +3. Upload als `multipart/form-data` → `POST /api/recipes/extract-from-photo` +4. Server: MIME-Whitelist + 8 MB-Gate + Rate-Limit (10/min/IP) → `preprocessImage` (sharp, ≤1600px lange Kante, JPEG-Re-encode, Metadata-Strip) → `extractRecipeFromImage` (Gemini 2.5 Flash mit structured `responseSchema`, `temperature: 0.1`, Zod-validiert, 1× Retry bei Schema-Fehler oder 5xx) → zufällige Description aus `description-phrases.ts` (50er-Pool) → Response mit `Partial` +5. **Das Original-Foto wird nicht persistiert.** Der Server loggt keine Prompt/Response-Inhalte — nur Code, Dauer, Byte-Größe. +6. Client hält das Ergebnis im `PhotoUploadStore` und rendert ``. Weil `recipe.id === null` ist, blendet der Editor den `ImageUploadBox`-Block aus und zeigt nur den Hinweis „Bild kannst du nach dem Speichern hinzufügen." +7. User editiert und klickt „Speichern" → `POST /api/recipes` (neuer Scratch-Insert-Endpoint, wrappt `insertRecipe`) → Redirect auf `/recipes/:id` + ### Web-Suche Die gesamte Live-Search-Logik ist im `SearchStore` (`src/lib/client/search.svelte.ts`) gekapselt: Debounce, Race-Guard, Pagination, Web-Fallback, Snapshot/Restore für Back-Nav. Sowohl Header-Dropdown (`+layout.svelte`) als auch Startseite (`+page.svelte`) teilen sich die Klasse mit unterschiedlicher `filterParam`-Quelle. diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 155412c..5c595a3 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -187,3 +187,26 @@ Wichtige Config-Eigenschaften: - 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).