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>
This commit is contained in:
439
docs/superpowers/specs/2026-04-17-kochwas-design.md
Normal file
439
docs/superpowers/specs/2026-04-17-kochwas-design.md
Normal file
@@ -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/<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.
|
||||
Reference in New Issue
Block a user