# 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) 1. User klickt auf Web-Hit → `/preview?url=...` 2. `/api/recipes/preview` → `importer.ts` lädt HTML, `parseHTML` von linkedom, `json-ld-recipe.ts` extrahiert `Recipe`-Objekt mit **externer** Bild-URL 3. Preview-Seite rendert das `Recipe` via `RecipeView.svelte` (erkennt externe URL und lädt direkt vom Original-CDN) 4. User klickt „Speichern" → `/api/recipes/import` → Importer lädt Bild (`images/downloader.ts`), SHA256-Hash-Dedup, speichert lokal, INSERT in `recipe` + `recipe_ingredient` + `recipe_step` + `recipe_tag` 5. Redirect zu `/recipes/[id]` ### Web-Suche 1. User tippt → 300 ms Debounce → `/api/recipes/search?q=...` (lokal FTS5) 2. Wenn 0 Treffer: automatisch `/api/recipes/search/web?q=...` 3. `searxng.ts` → SearXNG-API mit `site:domain OR site:domain2 ...`-Filter aus Whitelist 4. Filtert Non-Recipe-Pfade (Foren, Magazin, Listings) via `NON_RECIPE_PATH_PATTERNS` 5. 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_cache` persistieren (30 Tage TTL) - **Überschreibt** bestehendes SearXNG-Thumbnail, weil das meist LowRes ist ### Confirm / Alert Promise-basiert statt `window.confirm`/`window.alert`: ```ts 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/Recipe` im 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: 1. Neue Datei `src/lib/server/db/migrations/00N_beschreibung.sql` — nächste freie Nummer 2. SQL sollte nicht-destruktiv sein (nur `CREATE`, `ALTER ADD`); keine `DROP` auf bestehende Daten 3. `migrate.ts` liest via Vite-Glob und führt neue Einträge aus (über `schema_migrations`-Tabelle getrackt) 4. Tests anpassen: `db idempotent` zä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/` — mit `openInMemoryForTest()` fresh SQLite pro Test. Externe HTTP via `node: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 check` muss grün sein. ## 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