Files
kochwas/docs/ARCHITECTURE.md
hsiegeln b5c01b950e
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m14s
chore(release): v1.2.0 + Doku-Aktualisierung
Release-Bundle fuer v1.2.0. Inhaltliche Highlights seit v1.1.0:
- Post-Review-Roadmap: API-Helper, Trash-Kommentar-Delete, Preview-
  Guard, untrack()-Snapshots, CSS-Var --pill-radius, asyncFetch-
  Wrapper, requireProfile(message), Code-Cleanup
- Remote-E2E-Suite (tests/e2e/remote/) gegen kochwas-dev.siegeln.net
  inkl. CRUD, Profile-Fixtures, API-Cleanup-Helpers, serviceWorkers-
  block fuer Chromium-Stabilitaet
- SearchStore (src/lib/client/search.svelte.ts) — gemeinsamer
  Live-Search-Store fuer Header-Dropdown und Startseite mit Debounce,
  Race-Guard, Pagination, Web-Fallback, Snapshot/Restore
- Editor-Split: RecipeEditor in IngredientRow, StepList,
  ImageUploadBox, TimeDisplay + recipe-editor-types zerlegt
- Zutaten-Sektionen: Migration 012 + section_heading-Feld,
  Inline-Insert-Button im Editor, Heading-Rendering in RecipeView,
  4 neue Remote-E2E-Tests mit CRUD-Coverage

Doku-Updates:
- ARCHITECTURE.md: Component-Liste, SearchStore-Erwaehnung,
  section_heading-Semantik, Test-Strategie um E2E local+remote
- OPERATIONS.md: Dev-System kochwas-dev.siegeln.net dokumentiert
- CLAUDE.md: Datei-Map auf Sub-Components ausgeweitet, Stand-
  Abschnitt auf aktuelle Roadmap-Stufen aktualisiert
- package.json / package-lock.json: 0.1.0 -> 1.2.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:34:01 +02:00

10 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/               # reaktive Stores (Profile, Search, Wishlist, PWA, Network, Sync, Toast, Install, Confirm, API-Fetch-Wrapper)
│   ├── components/           # Svelte-Komponenten:
│   │                         #   - Recipe: RecipeView, RecipeEditor + Editor-Sub-Components
│   │                         #     (IngredientRow, StepList, ImageUploadBox, TimeDisplay, recipe-editor-types)
│   │                         #   - UI-Shell: ConfirmDialog, ProfileSwitcher, SyncIndicator, Toast, UpdateToast
│   │                         #   - Search: SearchFilter, SearchLoader, StarRating
│   ├── 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

Die gesamte Live-Search-Logik ist im SearchStore (src/lib/client/search.svelte.ts) gekapselt: Debounce, Race-Guard, Pagination, Web-Fallback, Snapshot/Restore für Back-Nav. Sowohl Header-Dropdown (+layout.svelte) als auch Startseite (+page.svelte) teilen sich die Klasse mit unterschiedlicher filterParam-Quelle.

  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. Form-lokale Snapshots in Edit-Komponenten mit untrack() aus svelte, damit Prop-Updates nicht laufende Edits überschreiben.
  • Zutaten-Sektionen (ab Migration 012, v1.2): ingredient.section_heading TEXT NULL. Ist das Feld gesetzt, startet an dieser Zeile eine neue Sektion — folgende Zutaten gehören dazu, bis die nächste Zeile wieder ein Heading hat. Kein zweites Tabellen-Modell, Ordnung bleibt position. Importer setzt immer null (schema.org/Recipe hat das Konzept nicht). Editor erlaubt Inline-Insert via Abschnitt hinzufügen-Button vor jeder Zeile; leeres Heading wird beim Save zu null normalisiert.
  • 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 + Client-Stores via jsdom (json-ld-recipe, iso8601-duration, quotes-random, scaler, ingredient-parser, SearchStore, PWA/Toast/Sync-Stores, SW-Logik).
  • Integration: tests/integration/ — mit openInMemoryForTest() fresh SQLite pro Test. Externe HTTP via node:http-TestServer auf Port 0 gemockt.
  • E2E local: tests/e2e/ — Playwright gegen npm run preview, deckt PWA-Offline-Lifecycle ab (offline.spec.ts).
  • E2E remote: tests/e2e/remote/ — Playwright gegen kochwas-dev.siegeln.net via playwright.remote.config.ts (workers:1, serviceWorkers:block). Testet Live-API-Verhalten, inkl. destruktiver CRUD-Flows (Recipes, Kommentare, Favoriten). Run: npm run test:e2e:remote. Siehe tests/e2e/remote/fixtures/ für Profile-Setup + idempotente API-Cleanup-Helper.
  • Keine Svelte-Component-Unit-Tests (bewusst, Aufwand/Nutzen stimmt nicht; UI wird per E2E und manuell getestet).
  • Vor Commit: npm test && npm run check muss grün sein. Vor Merge zu main: zusätzlich npm run test:e2e:remote.

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