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.
6.3 KiB
6.3 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 # 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)
- User klickt auf Web-Hit →
/preview?url=... /api/recipes/preview→importer.tslädt HTML,parseHTMLvon linkedom,json-ld-recipe.tsextrahiertRecipe-Objekt mit externer Bild-URL- Preview-Seite rendert das
RecipeviaRecipeView.svelte(erkennt externe URL und lädt direkt vom Original-CDN) - User klickt „Speichern" →
/api/recipes/import→ Importer lädt Bild (images/downloader.ts), SHA256-Hash-Dedup, speichert lokal, INSERT inrecipe+recipe_ingredient+recipe_step+recipe_tag - Redirect zu
/recipes/[id]
Web-Suche
- User tippt → 300 ms Debounce →
/api/recipes/search?q=...(lokal FTS5) - Wenn 0 Treffer: automatisch
/api/recipes/search/web?q=... searxng.ts→ SearXNG-API mitsite:domain OR site:domain2 ...-Filter aus Whitelist- Filtert Non-Recipe-Pfade (Foren, Magazin, Listings) via
NON_RECIPE_PATH_PATTERNS - 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_cachepersistieren (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/Recipeim 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:
- Neue Datei
src/lib/server/db/migrations/00N_beschreibung.sql— nächste freie Nummer - SQL sollte nicht-destruktiv sein (nur
CREATE,ALTER ADD); keineDROPauf bestehende Daten migrate.tsliest via Vite-Glob und führt neue Einträge aus (überschema_migrations-Tabelle getrackt)- Tests anpassen:
db idempotentzä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/— mitopenInMemoryForTest()fresh SQLite pro Test. Externe HTTP vianode: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 checkmuss 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