All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
Drei fokussierte Dokumente, damit eine frische Claude-Session direkt weiterarbeiten kann, ohne den gesamten Session-Kontext zu brauchen: - CLAUDE.md (Root): "read me first" — Gotchas, Workflow, No-Gos, Quickstart, Verweise auf die Tiefen-Docs. - docs/ARCHITECTURE.md: Stack, Verzeichnisbaum, Datenfluss (Import, Web-Suche, Confirm/Alert), Design-Entscheidungen, Test-Strategie. - docs/OPERATIONS.md: Deployment-Topologie (Cloudflare → Pi → Traefik → kochwas), Gitea-CI-Pipeline, Traefik-Labels mit Wildcard-Cert, Troubleshooting (TLS, SearXNG-403, Healthcheck, Thumbnail-Cache leeren, Backup), Env-Vars. Die bestehenden Specs und Plans unter docs/superpowers/ bleiben unangetastet — die sind Planungs-Artefakte, nicht Betriebsdoku.
115 lines
6.3 KiB
Markdown
115 lines
6.3 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.
|
|
|
|
## 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
|