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

20 KiB
Raw Permalink Blame History

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)

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:

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.