Files
kochwas/docs/ARCHITECTURE.md
hsiegeln 0ede62dc8a
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m20s
docs(pwa): CLAUDE.md, OPERATIONS.md, ARCHITECTURE.md aktualisiert
CLAUDE.md: zwei neue Gotchas (SW nur HTTPS, Icon-Rendering) +
Erweiterung der "Dateien, die man anfasst"-Liste um SW-Pfade und
Client-Stores.

OPERATIONS.md: neuer Abschnitt "PWA / Offline-Modus" mit Caches,
Sync-Verhalten, Debug-Pfad und E2E-Test-Kommando.

ARCHITECTURE.md: neuer Abschnitt "Service Worker (PWA)" mit
Zuständigkeiten, Cache-Strategien, Message-Protokoll, Stores und
Komponenten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:09:54 +02:00

144 lines
8.1 KiB
Markdown

# 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.
### 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, `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