commit 59dc3623feea5eec28c810e0914d96d94616c22d Author: Hendrik Date: Fri Apr 17 14:56:20 2026 +0200 chore: initialize repo with PRD Adds .gitattributes for LF line endings and initial product/design spec. Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/docs/superpowers/specs/2026-04-17-kochwas-design.md b/docs/superpowers/specs/2026-04-17-kochwas-design.md new file mode 100644 index 0000000..d37f490 --- /dev/null +++ b/docs/superpowers/specs/2026-04-17-kochwas-design.md @@ -0,0 +1,439 @@ +# Kochwas — Produktanforderungen und Designspezifikation + +**Datum:** 2026-04-17 +**Autor:** Hendrik (mit Claude) +**Status:** Draft v1 — zur Review + +--- + +## 1. Überblick + +**Kochwas** ist eine selbstgehostete Progressive Web App (PWA) für Rezepte. Sie ermöglicht der Familie, Rezepte aus einer kuratierten Liste deutscher Koch-Websites automatisch zu importieren, lokal in einem einheitlichen Format zu speichern und mobil (Tablet/Smartphone) in der Küche zu nutzen. + +Die App läuft ausschließlich im Homelab des Anwenders. Mobile Nutzung ist der primäre Anwendungsfall. + +## 2. Problem & Motivation + +Koch-Websites im Internet haben jeweils eigenes Layout, Werbung, Cookie-Banner und wechselnde URLs. Beim Kochen am Tablet oder Smartphone ist das lästig und unzuverlässig (Seiten verschwinden, Layout ändert sich, Bildschirm geht aus). Eine eigene Sammlung mit einheitlicher Darstellung, offline-verfügbar, mit Familienbewertungen und Notizen, löst das. + +## 3. Ziele & Nicht-Ziele + +### 3.1 Ziele (MVP) +- Rezepte aus einer festgelegten Liste von Domains per Klick lokal speichern (strukturiert, nicht als Screenshot oder HTML-Dump). +- Rezepte in einheitlichem Layout darstellen (Header → Zutatenliste → Zubereitung). +- Familienmitglieder können bewerten, kommentieren, umbenennen, löschen, als Favorit markieren. +- Kochjournal: Wer hat wann was gekocht. +- Mobile-first: großer Touch-Targets, Wake-Lock, Portrait-optimiertes Layout. +- Suche: lokale Volltextsuche zuerst, dann optional Internetsuche innerhalb der Whitelist. +- Selbst gehostet per Docker Compose; Backup per Volume-Copy oder integriertem ZIP-Export. + +### 3.2 Nicht-Ziele (MVP) +- Öffentliches Hosting / Mehrmandanten-Betrieb. +- Einkaufslistenfunktion. +- Passwort-Login oder echte Authentifizierung. +- KI-gestützte Rezeptextraktion (geplant als Phase 2, siehe § 13). +- Import bestehender Rezept-Bibliotheken (Paprika, Markdown, etc.). +- Native iOS/Android-Apps (PWA ist ausreichend). +- Übersetzung fremdsprachiger Rezepte. + +## 4. Nutzer & Profile + +**Hauptnutzer:** Familie des Anwenders (mehrere Personen, gemeinsames Gerät oder persönliche Geräte, gemeinsame Homelab-Instanz). + +**Profilmodell:** „Profilauswahl ohne Login" — beim App-Start wählt man aus einer Liste der angelegten Profile. Die Auswahl wird per LocalStorage gemerkt und kann jederzeit oben rechts gewechselt werden. Kein Passwort, keine Sitzungsverwaltung. + +**Rechte:** Alle Profile haben dieselben Rechte — inklusive Verwalten der Whitelist, Löschen von Rezepten, Anlegen neuer Profile. Kein Admin-Flag. + +**Pro-Profil-Daten:** Sternbewertung je Rezept, Favoritenliste, Einträge im Kochjournal. +**Geteilte Daten:** Rezepte selbst, Kommentare (mit Autor-Zuordnung), Tags, Whitelist. + +## 5. Funktionale Anforderungen + +### 5.1 Startseite („Google-like") +- Großes, zentriertes Suchfeld. +- Profil-Switcher oben rechts. +- Optional darunter: zuletzt gekochte Rezepte des aktiven Profils als Karten. + +### 5.2 Suche — lokal +- Volltextsuche über Titel, Beschreibung, Zutaten, Tags (SQLite FTS5). +- Ranking: Titel > Tag > Zutat. +- Ergebniskarten: Bild, Titel, Domain, Durchschnittsbewertung, Datum letztes Kochen. +- Wenn keine Treffer oder Nutzer wünscht Internetsuche: Button „Im Internet suchen". + +### 5.3 Suche — Internet +- Ausgeführt gegen die selbst gehostete SearXNG-Instanz, eingeschränkt per `site:`-Filter auf die aktive Whitelist. +- Zweite Absicherung: Server filtert die Trefferliste nochmals gegen die Whitelist (schützt vor SearXNG-Fehlkonfigurationen). +- Ergebniskarten: Titel, Domain, Snippet, optional Thumbnail. + +### 5.4 Vorschau & Import +- Klick auf Web-Treffer öffnet **Vorschauseite**: + - Server lädt die Quell-URL, extrahiert JSON-LD, rendert das Rezept im einheitlichen Layout, **ohne zu speichern**. + - Oben Banner „Vorschau — noch nicht gespeichert". + - Buttons: **In meine Sammlung speichern** / **Zurück**. +- Beim Speichern wird das Rezept frisch geladen (kein stale Cache), Bild lokal abgelegt, in SQLite eingetragen, auf die Rezeptansicht weitergeleitet. +- Duplikatserkennung: dieselbe `source_url` wird nicht doppelt importiert; stattdessen zum bestehenden Eintrag weiterleiten. + +### 5.5 Rezeptansicht (einheitliches Layout) +Immer in derselben Reihenfolge, egal woher das Rezept stammt: +1. **Header:** Titel, Bild, Metadaten (Portionen, Prep/Cook/Total, Küche, Kategorie, Quelle als Link). +2. **Zutatenliste** mit Portionen-Slider (+/- Buttons und Schieberegler), skaliert Mengen automatisch; nicht-parsebare Zutaten werden als Freitext gezeigt (keine Skalierung für diese Zeilen). +3. **Zubereitung** als nummerierte Schritte. +4. **Bewertungen & Kommentare** am Ende. + +Aktionen in der Ansicht: +- Sternbewertung (1–5) des aktiven Profils. +- Herz-Icon: Favorit des aktiven Profils. +- Button „Heute gekocht" → fügt Eintrag ins Kochjournal mit `now()` und aktivem Profil. +- Kommentar hinzufügen (geteilt, mit Autor). +- Umbenennen, Löschen (mit Bestätigung). +- Drucken (öffnet `/recipes/:id/print`). +- **Wake-Lock:** beim Aufruf auf mobilen Geräten automatisch angefordert (Bildschirm bleibt an), freigegeben beim Verlassen der Seite. + +### 5.6 Druckansicht +- Eigene Route `/recipes/:id/print` mit druckoptimiertem Layout (keine Navigation, große Schrift, einspaltig, eingebettetes Bild, Quell-URL als Fußzeile). + +### 5.7 Tagging +- Freitext-Tags, geteilt, per Rezept mehrere. +- Auto-Vorschläge bereits vergebener Tags. +- Tags aus JSON-LD (`recipeCategory`, `recipeCuisine`, `keywords`) werden beim Import automatisch übernommen. + +### 5.8 Verwaltung / Admin-UI +- `/admin/domains` — Whitelist verwalten (hinzufügen/entfernen, Anzeigename setzen). +- `/admin/profiles` — Profile anlegen, umbenennen, löschen. +- `/admin/backup` — ZIP-Download (DB + images), ZIP-Restore (mit Bestätigung). + +## 6. Nicht-funktionale Anforderungen + +| Kategorie | Anforderung | +|---|---| +| Performance | Suche auf Mobil < 300 ms bei 1.000 Rezepten; Rezeptansicht-LCP < 2 s | +| Verfügbarkeit | Lokal: quasi 100 % (Docker restart policy); offline: gespeicherte Rezepte dank Service Worker abrufbar | +| Datenvolumen | Zielgröße: mehrere Tausend Rezepte + Bilder in wenigen GB | +| Browser-Support | Aktuelle Versionen von Safari (iOS/iPadOS), Chrome (Android), Firefox; Wake-Lock fällt auf Nicht-Unterstützung still durch | +| Sicherheit | Ausschließlich im Heimnetz; Import-Fetch mit Timeout 10 s, max. 10 MB, nur http(s), Ziel-URL muss Whitelist-Domain sein | +| Sprache | UI deutsch; Rezepte in Originalsprache (keine Übersetzung) | +| Zugänglichkeit | Touch-Targets ≥ 44 × 44 px, Kontrast AA, Screen-Reader-Basics | + +## 7. Architektur + +### 7.1 Container-Diagramm + +``` +┌──────────────────────────────────────────────────────────┐ +│ Browser / PWA (SvelteKit Client) │ +│ Startseite │ Suche │ Preview │ RecipeView │ Admin │ +│ Wake-Lock, Service Worker (offline für gespeicherte) │ +└─────────────────────┬────────────────────────────────────┘ + │ HTTP(S) im LAN +┌─────────────────────▼────────────────────────────────────┐ +│ SvelteKit Server (Node) │ +│ Routes/APIs, Importer, Scaler, JSON-LD-Extractor, │ +│ Ingredient-Parser, Image-Downloader │ +└──────┬──────────────────────────────────┬────────────────┘ + │ │ +┌──────▼────────────┐ ┌────────▼─────────────┐ +│ SQLite (WAL) │ │ SearXNG (Container) │ +│ /data/kochwas.db │ │ /search?format=json │ +│ /data/images/* │ │ Whitelist als site: │ +└───────────────────┘ └──────────────────────┘ +``` + +### 7.2 Tech-Stack + +- **Frontend/Backend:** SvelteKit + TypeScript (Node-Adapter) +- **Datenbank:** SQLite via `better-sqlite3` (synchron, schnell, eine Datei) +- **Scraping-Stack:** `undici` (fetch), `linkedom` oder `node-html-parser` zum JSON-LD-Herausziehen +- **Bilder:** direkter Download, optional `sharp` für Resize (max. 1200 px Längsseite) +- **Tests:** Vitest (Unit + Integration), Playwright (E2E, mit Mobile-Emulation) +- **Paketierung:** Docker (multi-stage Build), Docker Compose +- **CI:** Gitea Actions im Homelab + +### 7.3 Modulgrenzen + +Server-seitig in `src/lib/server/`: + +| Modul | Aufgabe | Schnittstelle | +|---|---|---| +| `db` | SQLite-Verbindung, Migrationen, typed Queries | Funktionen pro Tabelle | +| `recipe-importer` | URL → strukturiertes Rezept (mit/ohne Save) | `preview(url)`, `import(url)` | +| `json-ld-extractor` | HTML → schema.org/Recipe-JSON | `extract(html)` | +| `ingredient-parser` | Freitext → `{qty, unit, name, note}` | `parse(text)` | +| `image-downloader` | URL → lokale Datei (dedupliziert per SHA256) | `download(url)` | +| `search-local` | Query → Rezepte via FTS5 | `search(q, limit)` | +| `search-web` | Query → Whitelist-gefilterte Treffer | `search(q)` | +| `scaler` | Rezept × Faktor → skalierte Zutaten | `scale(recipe, factor)` | + +Jedes Modul hat einen eigenen Unit-Test mit Fixtures. + +Client-seitig: eine einzige `RecipeView`-Komponente rendert sowohl die Vorschau als auch die gespeicherte Ansicht — so ist die einheitliche Darstellung garantiert. + +### 7.4 Routen + +| Route | Methode | Zweck | +|---|---|---| +| `/` | GET | Startseite | +| `/search` | GET | Lokale Trefferliste | +| `/search/web` | GET | Web-Trefferliste | +| `/api/recipes/search` | GET `?q=` | Lokale Suche JSON | +| `/api/recipes/search/web` | GET `?q=` | Web-Suche JSON | +| `/api/recipes/preview` | GET `?url=` | Preview-Payload (nicht persistiert) | +| `/api/recipes/import` | POST `{url}` | Persistenter Import | +| `/recipes/[id]` | GET | Rezeptansicht | +| `/recipes/[id]/print` | GET | Druckansicht | +| `/api/recipes/[id]` | PATCH/DELETE | Umbenennen/Löschen | +| `/api/recipes/[id]/rating` | PUT `{profileId, stars}` | Bewerten | +| `/api/recipes/[id]/favorite` | PUT `{profileId}` / DELETE | Favorit | +| `/api/recipes/[id]/cooked` | POST `{profileId}` | Kochjournal-Eintrag | +| `/api/recipes/[id]/comments` | POST `{profileId, text}` / DELETE | Kommentare | +| `/admin/domains` | GET / POST / DELETE | Whitelist | +| `/admin/profiles` | GET / POST / PATCH / DELETE | Profile | +| `/admin/backup` | GET / POST | Backup-Export/Restore | +| `/api/health` | GET | `{ db, searxng }` | + +## 8. Datenmodell (SQLite) + +```sql +profile( + id INTEGER PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + avatar_emoji TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +recipe( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + source_url TEXT UNIQUE, + source_domain TEXT, + image_path TEXT, + servings_default INTEGER, + servings_unit TEXT, + prep_time_min INTEGER, + cook_time_min INTEGER, + total_time_min INTEGER, + cuisine TEXT, + category TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ingredient( + id INTEGER PRIMARY KEY, + recipe_id INTEGER NOT NULL REFERENCES recipe(id) ON DELETE CASCADE, + position INTEGER NOT NULL, + quantity REAL, + unit TEXT, + name TEXT NOT NULL, + note TEXT, + raw_text TEXT +); + +step( + id INTEGER PRIMARY KEY, + recipe_id INTEGER NOT NULL REFERENCES recipe(id) ON DELETE CASCADE, + position INTEGER NOT NULL, + text TEXT NOT NULL +); + +tag( + id INTEGER PRIMARY KEY, + name TEXT UNIQUE NOT NULL +); + +recipe_tag( + recipe_id INTEGER NOT NULL REFERENCES recipe(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tag(id) ON DELETE CASCADE, + PRIMARY KEY (recipe_id, tag_id) +); + +rating( + recipe_id INTEGER NOT NULL REFERENCES recipe(id) ON DELETE CASCADE, + profile_id INTEGER NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + stars INTEGER NOT NULL CHECK (stars BETWEEN 1 AND 5), + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (recipe_id, profile_id) +); + +comment( + id INTEGER PRIMARY KEY, + recipe_id INTEGER NOT NULL REFERENCES recipe(id) ON DELETE CASCADE, + profile_id INTEGER NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + text TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +favorite( + recipe_id INTEGER NOT NULL REFERENCES recipe(id) ON DELETE CASCADE, + profile_id INTEGER NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (recipe_id, profile_id) +); + +cooking_log( + id INTEGER PRIMARY KEY, + recipe_id INTEGER NOT NULL REFERENCES recipe(id) ON DELETE CASCADE, + profile_id INTEGER NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + cooked_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +allowed_domain( + id INTEGER PRIMARY KEY, + domain TEXT UNIQUE NOT NULL, + display_name TEXT, + added_by_profile_id INTEGER REFERENCES profile(id), + added_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Volltext-Index +CREATE VIRTUAL TABLE recipe_fts USING fts5( + title, description, ingredients_concat, tags_concat, + content='', tokenize='unicode61 remove_diacritics 2' +); +-- Trigger auf recipe/ingredient/tag synchronisieren recipe_fts +``` + +## 9. Schlüssel-Abläufe (Zusammenfassung) + +### 9.1 Lokale Suche (Golden Path) +1. Eingabe im Suchfeld (debounced, ab 3 Zeichen). +2. `GET /api/recipes/search?q=…` → FTS5-Abfrage mit gewichteten Feldern. +3. Trefferliste als Karten, unten Button „Im Internet suchen". + +### 9.2 Internetsuche & Vorschau & Import +1. `GET /api/recipes/search/web?q=…` → Whitelist laden → SearXNG-Query mit `site:`-Filtern → Doppel-Whitelist-Check → Liste. +2. Klick auf Treffer → `GET /api/recipes/preview?url=…` → Server holt HTML, extrahiert JSON-LD, gibt Rezept-JSON zurück (nichts in DB). +3. Client rendert Preview in `RecipeView` mit Banner „nicht gespeichert". +4. Klick „Speichern" → `POST /api/recipes/import {url}` → frisches Laden, Bild-Download, DB-Insert, Redirect zur Rezept-ID. + +### 9.3 Portionen-Skalierung +- Slider ändert Faktor `f = newServings / servings_default`. +- Jede parsebare Zutat wird als `qty × f` dargestellt, sinnvoll gerundet. +- Nicht-parsebare Zutaten (nur `raw_text`) bleiben unverändert. +- Rein Client-side (keine DB-Änderung); außer beim Druck, wo der Server direkt skaliert rendert. + +### 9.4 Fehlerbehandlung +| Fall | Verhalten | +|---|---| +| Quell-URL 404 / Timeout | Klare Fehlermeldung, kein Teileintrag | +| Kein JSON-LD vorhanden | Hinweis, dass automatische Erkennung nicht möglich (Phase 2: LLM-Fallback) | +| JSON-LD unvollständig | Best-effort speichern, Platzhalter wo nötig | +| Zutat nicht parsebar | `raw_text` füllen, aus Skalierung ausnehmen | +| SearXNG nicht erreichbar | Meldung „Suche zurzeit nicht verfügbar", lokale Ergebnisse bleiben | +| Bildquelle unerreichbar | `image_path = NULL`, Platzhalterbild | +| Offline | PWA zeigt gespeicherte Rezepte, Import/Web-Suche deaktiviert | + +## 10. UX-Prinzipien (mobile-first) + +- Portrait-optimiert; Tablets bekommen mehr Weißraum, nicht fundamental anderes Layout. +- Touch-Targets min. 44 × 44 px; Abstände groß. +- Rezeptansicht: Tabs „Zutaten" / „Zubereitung" statt langem Scrollen. +- Portionen-Slider mit +/- Buttons zusätzlich zum Ziehen. +- Wake-Lock automatisch auf der Rezeptseite, Indikator im UI. +- Bilder responsive, max. 1200 px Längsseite, mit `loading=lazy`. +- Startseite: großes Suchfeld; Autofokus nur auf Desktop (nicht auf iOS/Safari, um Keyboard-Pop-up zu vermeiden). + +## 11. Tests + +| Ebene | Werkzeug | Anteil | Inhalt | +|---|---|---|---| +| Unit | Vitest | ~60 % | `ingredient-parser`, `scaler`, `json-ld-extractor`, ISO8601-Duration | +| Integration | Vitest + SQLite-in-memory | ~25 % | Importer mit HTML-Fixtures, FTS5-Suche, CRUD + Kaskadierung, Whitelist-Filter | +| E2E | Playwright (Mobile-Emulation) | ~15 % | Golden Path, Skalierung, Kochjournal, Import-Fehlerfall | + +HTML-Fixtures der drei initialen Domains (Chefkoch, Emmi kocht einfach, Experimente aus meiner Küche) liegen unter `tests/fixtures/`. + +## 12. Deployment & Betrieb + +### 12.1 Entwicklung (lokal) +- Docker Desktop mit Compose für `searxng`-Container. +- `npm run dev` startet SvelteKit auf `localhost:5173`, liest gegen `./dev-data`-Verzeichnis. +- Gitea Actions für CI (Tests + Docker-Image-Build). + +### 12.2 Produktion (Homelab) +`docker-compose.yml`: +```yaml +services: + kochwas: + image: kochwas:latest + ports: ["3000:3000"] + volumes: + - ./data:/data + environment: + - DATABASE_PATH=/data/kochwas.db + - IMAGE_DIR=/data/images + - SEARXNG_URL=http://searxng:8080 + - NODE_ENV=production + depends_on: [searxng] + restart: unless-stopped + searxng: + image: searxng/searxng:latest + volumes: ["./searxng:/etc/searxng"] + environment: + - BASE_URL=http://searxng:8080/ + - INSTANCE_NAME=kochwas-search + restart: unless-stopped +``` + +Volume-Layout: +``` +/data/ + kochwas.db + kochwas.db-wal + images/. + backups/.zip (optional) +``` + +### 12.3 Updates & Backups +- Update: Image neu bauen/pullen, `docker compose up -d`. Schema-Migrationen laufen beim Startup. +- Backup: Volume-Copy (Homelab-Backup) oder ZIP-Export via `/admin/backup`. +- Restore: ZIP-Upload (mit Bestätigungsdialog) oder Volume-Restore. + +### 12.4 Health & Logs +- `/api/health` gibt `{ db: "ok", searxng: "ok" | "degraded" }` zurück. +- Access- und Fehler-Logs auf STDOUT (`docker logs`). +- Kein Metrics-Stack im MVP. + +### 12.5 Sicherheit +- Ausschließlicher Betrieb im Heimnetz; Remote-Zugriff (falls gewünscht) über VPN, außerhalb des App-Scopes. +- Keine Authentifizierung innerhalb der App (bewusst: niedrigschwellig für Familie). +- Import-Fetch: Timeout 10 s, max. 10 MB Body, nur `http(s)://`, Whitelist-Check vor Import, User-Agent gesetzt. + +### 12.6 Line-Endings / Repo-Hygiene +- `.gitattributes` mit `* text=auto eol=lf` (Windows-Dev-Umgebung, LF im Repo). + +## 13. MVP-Scope vs. spätere Phasen + +**MVP (Phase 1) beinhaltet:** +- Profilauswahl, Whitelist-Admin, Profil-Admin +- Lokale Suche (FTS5), Internetsuche via SearXNG +- Import via JSON-LD (Chefkoch, Emmi kocht einfach, Experimente aus meiner Küche als initiale Whitelist) +- Vorschau-Seite +- Einheitliche Rezeptansicht, Portionen-Skalierung, Druckansicht +- Bewertung, Favoriten, Kommentare, Kochjournal, Umbenennen, Löschen +- Tags (auto + manuell), Volltextsuche über Zutaten +- Bilder lokal speichern +- Wake-Lock +- Backup / Restore ZIP +- PWA-Offline-Zugriff auf gespeicherte Rezepte +- Docker-Compose-Deployment + CI + +**Phase 2 (explizit geplant, nicht MVP):** +- LLM-Fallback-Extractor für Seiten ohne JSON-LD. + +**Phase 3+ (Ideen, nicht verplant):** +- Einkaufsliste, Import aus anderen Rezept-Apps, Kochjournal-Auswertungen (Top 10, Saison-Empfehlungen), Rollen/Admin-Flags, mehrere Whitelists pro Profil. + +## 14. Offene Punkte + +Keine — alle Designentscheidungen sind im Brainstorming geklärt. + +## 15. Glossar + +- **Whitelist / Allowed Domain:** Liste der Domains, die als Rezeptquelle erlaubt sind. +- **JSON-LD:** strukturierte Metadaten (schema.org) im `