Files
kochwas/docs/superpowers/specs/2026-04-17-kochwas-design.md
Hendrik 59dc3623fe 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) <noreply@anthropic.com>
2026-04-17 14:56:20 +02:00

440 lines
20 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 — 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 (15) 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/<sha256>.<ext>
backups/<timestamp>.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 `<script type="application/ld+json">`-Tag einer Webseite.
- **FTS5:** SQLite-Volltextsuch-Erweiterung.
- **PWA:** Progressive Web App, installierbar aus dem Browser, offline-fähig.
- **Wake-Lock:** Browser-API, die den Bildschirm während der Nutzung wachhält.
- **SearXNG:** selbstgehostete Meta-Suchmaschine, Fork von Searx.
- **Profil:** Anzeigename + Emoji eines Familienmitglieds; kein Passwort.