- ARCHITECTURE.md: ingredient/step (waren faelschlich recipe_*) - OPERATIONS.md: IMAGE_DIR (statt IMAGES_PATH) - session-handoff: /api/recipes/[id]/image POST/DELETE ergaenzt Findings aus REVIEW-2026-04-18.md / docs-vs-code.md
8.1 KiB
Kochwas — Architektur
Stack
- SvelteKit 2 + Svelte 5 Runes (
$state,$derived,$effect,$props) - TypeScript strict
- SQLite über
better-sqlite3(synchron, native Binding arm64) - FTS5 Virtual Tables mit BM25-Ranking für Volltext-Suche
- linkedom für HTML-Parsing (JSON-LD-Extraktion, og:image-Enrichment)
- zod für API-Schema-Validierung
- Adapter:
@sveltejs/adapter-node→ Node 22 Alpine im Container
Top-Level-Struktur
src/
├── app.html, app.d.ts # Shell + Env-Types
├── service-worker.ts # PWA-Shell
├── lib/
│ ├── client/ # clientseitig: Profil-Store, Confirm-Dialog
│ ├── components/ # Svelte-Komponenten (RecipeView, StarRating, ConfirmDialog, ProfileSwitcher)
│ ├── recipes/ # shared: Portionen-Scaler (Client UND Server)
│ ├── server/ # nur Server-Code (nie in Client-Bundle!)
│ │ ├── db/ # openDb, Migrations, DB-Singleton
│ │ ├── domains/ # Whitelist-Repo
│ │ ├── http.ts # fetch-Wrapper mit Timeout / maxBytes / extraHeaders
│ │ ├── images/ # Download, SHA256-Dedup, Save
│ │ ├── parsers/ # json-ld-recipe.ts, iso8601-duration.ts
│ │ ├── profiles/ # Profile-Repo
│ │ ├── recipes/ # importer, actions, repository, search-local
│ │ ├── search/ # searxng.ts (Web-Suche + Thumbnail-Cache)
│ │ ├── wishlist/ # Repo
│ │ └── backup/ # ZIP-Export via archiver, Import via yauzl
│ ├── quotes.ts # 49 Flachwitze für die Homepage
│ └── types.ts # shared types
└── routes/
├── +layout.svelte # Header, Confirm-Dialog-Mount, Header-Search-Dropdown
├── +page.svelte # Home: Hero + Live-Search + Zuletzt-hinzugefügt
├── recipes/[id]/ # Rezept-Detail
├── preview/ # Vorschau vor dem Speichern
├── search/ # /search (lokal), /search/web (Internet)
├── wishlist/
├── admin/ # Whitelist, Profile, Backup/Restore
├── images/[filename] # Statische Auslieferung lokaler Bilder
└── api/ # REST-Endpoints
Datenfluss
Import (User klickt auf Web-Treffer)
- User klickt auf Web-Hit →
/preview?url=... /api/recipes/preview→importer.tslädt HTML,parseHTMLvon linkedom,json-ld-recipe.tsextrahiertRecipe-Objekt mit externer Bild-URL- Preview-Seite rendert das
RecipeviaRecipeView.svelte(erkennt externe URL und lädt direkt vom Original-CDN) - User klickt „Speichern" →
/api/recipes/import→ Importer lädt Bild (images/downloader.ts), SHA256-Hash-Dedup, speichert lokal, INSERT inrecipe+ingredient+step+recipe_tag - Redirect zu
/recipes/[id]
Web-Suche
- User tippt → 300 ms Debounce →
/api/recipes/search?q=...(lokal FTS5) - Wenn 0 Treffer: automatisch
/api/recipes/search/web?q=... searxng.ts→ SearXNG-API mitsite:domain OR site:domain2 ...-Filter aus Whitelist- Filtert Non-Recipe-Pfade (Foren, Magazin, Listings) via
NON_RECIPE_PATH_PATTERNS - Pro Treffer: parallel (max 6)
enrichThumbnail:- SQLite-Cache hit → return
- Sonst: Seite holen (max 512 KB, 4 s),
extractPageImage: og:image → link rel=image_src → JSON-LD → erstes Content-img - Ergebnis (auch null) in
thumbnail_cachepersistieren (30 Tage TTL) - Überschreibt bestehendes SearXNG-Thumbnail, weil das meist LowRes ist
Confirm / Alert
Promise-basiert statt window.confirm/window.alert:
import { confirmAction, alertAction } from '$lib/client/confirm.svelte';
if (await confirmAction({ title: 'Löschen?', destructive: true })) { /* ... */ }
await alertAction({ title: 'Fehler', message: 'xyz' });
Gemeinsame Komponente ConfirmDialog.svelte wird im Root-Layout einmal gemountet. Store (confirmStore) hält die Promise-Resolve-Funktion, Komponente rendert nur wenn pending !== null.
Design-Entscheidungen
- Kein Login, nur Profile: Profile werden beim Start gewählt, in localStorage persistiert. Actions (Rating, Favorit, Cooked, Kommentar) brauchen aktives Profil → sonst Custom-Alert „Bitte Profil wählen".
- FTS5 als Haupt-Suche: statt externer Search-Engine-DB. Passt zu SQLite-only-Stack.
- JSON-LD first: Alle drei Ziel-Domains (Chefkoch, Emmi, Experimente) liefern
schema.org/Recipeim JSON-LD. LLM-Fallback war geplant, aktuell nicht nötig. - SearXNG als Such-Engine: Self-hosted, daher keine API-Keys. Das Bot-Detection-Theater wird mit gesetzten
X-Forwarded-For-Headern aus Docker-IPs umgangen. - Thumbnail-Cache in SQLite: 30 Tage TTL (per
KOCHWAS_THUMB_TTL_DAYS). Negative Einträge (Seite ohne Bild) werden auch gecacht. - Svelte 5 Runes — kein
$:mehr, keine alten Stores außer$app/stores. Neue Stores via Klasse mit$state-Feldern. - Service Worker rein zum Shell-Cachen für Offline-First-PWA, kein intelligentes Cache-Matching (keine externe Rezept-Seiten).
Migrations-Workflow
Bei Schema-Änderung:
- Neue Datei
src/lib/server/db/migrations/00N_beschreibung.sql— nächste freie Nummer - SQL sollte nicht-destruktiv sein (nur
CREATE,ALTER ADD); keineDROPauf bestehende Daten migrate.tsliest via Vite-Glob und führt neue Einträge aus (überschema_migrations-Tabelle getrackt)- Tests anpassen:
db idempotentzählt vorher/nachher — bleibt automatisch grün
Test-Strategie
- Unit:
tests/unit/— pure Funktionen (json-ld-recipe, iso8601-duration, quotes-random, smoke) - Integration:
tests/integration/— mitopenInMemoryForTest()fresh SQLite pro Test. Externe HTTP vianode:http-TestServer auf Port 0 gemockt. - Keine Svelte-Component-Tests (bewusst, Aufwand/Nutzen stimmt nicht; UI wird manuell getestet)
- Vor Commit:
npm test && npm run checkmuss grün sein.
Service Worker (PWA)
src/service-worker.ts ist SvelteKits eingebauter SW-Slot. Er nutzt $service-worker (build, files, version) für den App-Shell-Cache und implementiert eigene Logik für:
- Pre-Cache (alle Rezepte + Bilder beim Initial-Sync), über paginierten Fetch von
/api/recipes/all. - Delta-Sync beim App-Start (diff vs. Cache-Manifest, nur Delta laden).
- Drei Cache-Strategien (dispatcht per
resolveStrategy): Shell = cache-first, Daten = SWR, Bilder = cache-first. - Message-Protokoll (
sync-start,sync-progress,sync-done,sync-error) zwischen SW und Client.
Reine Logik-Einheiten (testbar, Unit-Tests in tests/unit/):
src/lib/sw/cache-strategy.ts—resolveStrategy({url, method})→'shell' | 'swr' | 'images' | 'network-only'src/lib/sw/diff-manifest.ts—diffManifest(current, cached)→{toAdd, toRemove}
Client-Stores (SSR-safe via typeof-Guards):
src/lib/client/network.svelte.ts—navigator.onLine+ Events.src/lib/client/sync-status.svelte.ts— SW-Message-Spiegel,lastSyncedin localStorage.src/lib/client/toast.svelte.ts— Toast-Queue für Offline-Fehler + Sync-Meldungen.src/lib/client/install-prompt.svelte.ts— fängtbeforeinstallprompt, erkennt Plattform.src/lib/client/sw-register.ts— registriert den SW, leitet Messages an den Sync-Status-Store.src/lib/client/require-online.ts— Helper für Schreib-Aktionen (Toast statt stillem Fail).
UI-Komponenten:
src/lib/components/SyncIndicator.svelte— Pill unten rechts (Sync-Fortschritt / Offline-Status).src/lib/components/Toast.svelte— Top-Center-Toast-Renderer.
Admin-UI: src/routes/admin/app/+page.svelte mit Install-Button, manuellem Sync-Trigger, Cache-Reset.
E2E-Tests: tests/e2e/offline.spec.ts — Playwright setzt das Netzwerk offline und prüft Navigation/Toast/Indikator-Verhalten.
Was später kommt (laut Spec, aktuell nicht implementiert)
- LLM-Fallback für nicht-JSON-LD-Seiten
- Print-View ist nur rudimentär, könnte ein eigenes Print-CSS bekommen
- Tag-Editor im Admin
- Export-Format JSON zusätzlich zu ZIP