docs: Foto-Rezept-Magie in OPERATIONS/ARCHITECTURE/CLAUDE
This commit is contained in:
@@ -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`. |
|
| **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. |
|
| **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. |
|
| **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
|
## 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/+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/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/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/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/search/searxng.ts` — Web-Suche + Thumbnail-Enrichment + SQLite-Cache
|
||||||
- `src/lib/server/recipes/importer.ts` — JSON-LD → Recipe, orchestriert Bild-Download
|
- `src/lib/server/recipes/importer.ts` — JSON-LD → Recipe, orchestriert Bild-Download
|
||||||
|
|||||||
@@ -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`
|
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]`
|
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<Recipe>`
|
||||||
|
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 `<RecipeEditor recipe={extracted}>`. 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
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -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).
|
- 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.
|
**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).
|
||||||
|
|||||||
Reference in New Issue
Block a user