Files
kochwas/docs/ARCHITECTURE.md
hsiegeln 31c6e5cd1f refactor(server): IMAGE_DIR/DATABASE_PATH zentralisieren + Doku-Drift fixen
src/lib/server/paths.ts: zentrale Auflösung der env-vars; vorher 6×
IMAGE_DIR und 2× DATABASE_PATH dupliziert mit identischen Defaults.

Migrierte Sites:
- src/lib/server/db/index.ts (DATABASE_PATH + IMAGE_DIR)
- src/routes/api/admin/backup/+server.ts
- src/routes/api/domains/+server.ts
- src/routes/api/domains/[id]/+server.ts
- src/routes/api/recipes/import/+server.ts
- src/routes/api/recipes/[id]/image/+server.ts
- src/routes/images/[filename]/+server.ts

ARCHITECTURE.md:
- 49 Flachwitze -> 150 (waren tatsaechlich 150)
- 'search/' Route entfernt — wurde nie als eigene Route gebaut, Suche
  laeuft direkt auf der Homepage via API-Calls

Findings aus zweiter Review-Runde (siehe OPEN-ISSUES-NEXT.md)
2026-04-18 22:41:02 +02:00

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             # 150 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
    ├── 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/previewimporter.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 + ingredient + 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:

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.

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.tsresolveStrategy({url, method})'shell' | 'swr' | 'images' | 'network-only'
  • src/lib/sw/diff-manifest.tsdiffManifest(current, cached){toAdd, toRemove}

Client-Stores (SSR-safe via typeof-Guards):

  • src/lib/client/network.svelte.tsnavigator.onLine + Events.
  • src/lib/client/sync-status.svelte.ts — SW-Message-Spiegel, lastSynced in localStorage.
  • src/lib/client/toast.svelte.ts — Toast-Queue für Offline-Fehler + Sync-Meldungen.
  • src/lib/client/install-prompt.svelte.ts — fängt beforeinstallprompt, 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