Code-Review-Finding zu commit 6c8de6f: INSERT OR REPLACE ist intern
DELETE+INSERT, das wuerde eventuelle FK-Children kuenftig stillschweigend
mitloeschen. ON CONFLICT DO UPDATE bumpt nur das Timestamp-Feld und
matcht den Stil der anderen Repos (shopping/repository.ts:43).
Migration-Dateiname zu recipe_view (singular) angeglichen, matcht
jetzt den Tabellennamen aus 543008b.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-Review-Findings nachgezogen: Tabellen-Konvention im Repo ist
singular (profile, recipe, favorite, cooking_log, thumbnail_cache),
deshalb recipe_view statt recipe_views; Index analog umbenannt.
last_viewed_at auf TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
gewechselt — matcht den Rest des Schemas. Header-Kommentar +
notnull-Assertion fuer recipe_id ergaenzt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tracking-Tabelle fuer Sort-Option Zuletzt angesehen. Composite-PK
(profile_id, recipe_id) erlaubt INSERT OR REPLACE per Default-Timestamp.
Index nach profile_id + last_viewed_at DESC fuer Sort-Query.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fuegt das nullable Feld section_heading zur ingredient-Tabelle hinzu
(Migration 012), erweitert den Ingredient-Typ und aktualisiert alle drei
Return-Stellen in parseIngredient. Downstream-Sites (repository, Editor,
Tests) bleiben rot – werden in Task 2+ behoben.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Emmi kocht einfach (und viele andere WordPress-Seiten) liefert unter
/favicon.ico ein Hoster-Default — Zahnrad-Artige Grafik — während das
eigentliche Site-Icon nur per <link rel="icon"> im <head> auftaucht.
Jetzt ziehen wir die Icon-Kandidaten erst aus der Homepage, sortieren
nach "sweet spot" 32–192 px und fallen bei Fehlschlag wie bisher auf
/favicon.ico und dann Google s2/favicons zurück.
Migration 011 setzt alle favicon_path=NULL, damit existierende
(falsche) Favicons beim nächsten Start neu geladen werden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aus dem Log (q="Königsberger klopse"): 11 rezeptwelt-Treffer kamen durch
alle URL-Filter, wurden aber von hasRecipeJsonLd als non-recipe gedroppt.
Ursache: rezeptwelt.de nutzt Microdata (itemtype=schema.org/Recipe) statt
application/ld+json.
- hasRecipeJsonLd → hasRecipeMarkup: prüft jetzt zusätzlich per Regex
auf itemtype=(https?://)schema.org/Recipe. Alter Export bleibt als
Deprecated-Weiterleitung erhalten.
- Log zeigt jetzt auch die ersten 3 gedropten URLs als dropped samples,
damit neue Problem-Domains einfach zu diagnostizieren sind.
- Migration 010 räumt alle thumbnail_cache-Einträge mit has_recipe=0 aus
— die waren mit dem alten Check falsch-negativ und müssen neu
klassifiziert werden.
Tests: 4 neue Cases für hasRecipeMarkup (JSON-LD, http/https Microdata,
Negativ-Fall).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Für jede Whitelist-Domain wird das Favicon jetzt einmalig geladen und
im image-Verzeichnis abgelegt. SearchFilter zeigt das Icon neben dem
Domain-Namen im Filter-Dropdown.
- Migration 009: allowed_domain.favicon_path (NULL = noch nicht geladen).
- Neues Modul $lib/server/domains/favicons.ts:
fetchAndStoreFavicon(domain, imageDir) + ensureFavicons(db, imageDir)
für Bulk-Nachzug; 8 parallele Worker mit 3s-Timeout.
- Reihenfolge: erst /favicon.ico der Domain, Fallback Google-Service.
- GET /api/domains zieht fehlende Favicons auf Abruf nach;
POST /api/domains lädt direkt im selben Call.
- .ico + .svg jetzt in der /images/[filename]-Route erlaubt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorher warf fetchText einen Fehler, sobald eine Seite >512 KB war —
bei modernen Rezeptseiten (eingebettete Bundles, base64-Bilder) läuft
das praktisch immer voll. Der Catch-Block hat dann hasRecipe auf NULL
gelassen, und der Treffer ging ungefiltert durch.
Neue FetchOptions.allowTruncate: true → wir bekommen die ersten 512 KB
(das reicht für <head> mit og:image und JSON-LD) statt eines Throws.
Timeout auf 8s erhöht, weil der Pi manchmal langsamer ist.
Migration 008 räumt alte NULL-has_recipe-Einträge aus dem Cache, damit
sie beim nächsten Search frisch klassifiziert werden statt weitere
30 Tage falsch gecached zu bleiben.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wir fetchen die Trefferseite sowieso schon fürs Thumbnail — prüfen
jetzt in der gleichen HTML-Parse-Runde, ob überhaupt ein
schema.org/Recipe JSON-LD vorhanden ist. Fehlt es, wird der Treffer
aus der Liste entfernt, weil der Importer auf dieser Seite später
sowieso mit „Diese Seite enthält kein Rezept" scheitern würde.
- Migration 007: thumbnail_cache.has_recipe (NULL=unbekannt, 0=nein, 1=ja).
- Fetch-Fehler hinterlassen NULL → Treffer bleibt konservativ sichtbar.
- Neue export `hasRecipeJsonLd(html)` in json-ld-recipe.ts.
- Alle Cache-Reads/Writes nehmen den neuen Wert mit.
Tests: +2 für Filter/Failover, bestehende Thumbnail-Tests mit
Recipe-JSON-LD-Stub ergänzt, damit sie nicht selber rausgefiltert
werden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Profil-Modal:
- Default-Emoji '🍳' im "Neues Profil"-Input entfernt (war ein ver-
sehentlicher Platzhalter, den die meisten nicht überschrieben haben
→ alle Profile sahen gleich aus). Jetzt leer, mit 🙂 als Hint im
placeholder.
- Profil-Liste: avatar_emoji wird nur gezeigt, wenn wirklich gesetzt.
Sonst CircleUser-Lucide statt 🙂-Fallback.
Migration 006_clear_default_profile_emoji.sql räumt bestehende DB-
Einträge auf: UPDATE profile SET avatar_emoji = NULL WHERE avatar_emoji
= '🍳'. User, die wirklich einen Pfannen-Avatar wollten, können das in
/admin/profiles neu setzen.
Wunschliste-Header-Icon: Heart → CookingPot. Der Kontext ist "was wir
essen wollen", also passt ein Topf besser als ein Herz. Heart bleibt
im Rezept als "Favorit" und in der Wunschliste als "ich will auch"-
Toggle, keine Kollision.
Ungenutzten Heart-Import aus +layout.svelte entfernt.
Schema-Änderung (Migration 005):
- Tabelle wishlist umgestellt auf PK (recipe_id, profile_id)
- wishlist_like-Tabelle zusammengelegt — Liken WAR schon "will ich auch",
also werden alle bestehenden Likes Memberships auf der neuen Tabelle.
- Alt-Einträge mit added_by_profile_id werden migriert, anonyme gehen
verloren (war inkonsistent, jetzt erzwingen wir profile_id NOT NULL).
Repository:
- listWishlist aggregiert pro Rezept: wanted_by_count, wanted_by_names
(kommagetrennt), on_my_wishlist für das aktive Profil
- listWishlistProfileIds(recipeId) für den Recipe-Page-Loader
- countWishlistRecipes für das Header-Badge (DISTINCT recipe_id)
- addToWishlist/removeFromWishlist/isOnMyWishlist alle mit profile_id
als Pflicht
API:
- POST /api/wishlist: profile_id jetzt Pflicht (nullable raus)
- DELETE /api/wishlist/[recipe_id]?profile_id=X (nur eigenes Entry)
- /api/wishlist/[recipe_id]/like komplett entfernt (Konzept obsolet)
- Neu: GET /api/wishlist/count → { count: <distinct recipes> }
UI:
- Header-Heart bekommt rotes Badge mit Zahl der Wunschliste-Rezepte.
wishlistStore in $lib/client/wishlist.svelte.ts hält den Count reaktiv;
Refresh auf Mount, nach Add/Remove, beim Öffnen der Wunschliste.
- Recipe-Detail: Loader liefert wishlist_profile_ids; onMyWishlist ist
ein $derived. Toggle fragt aktives Profil (alertAction sonst), mutiert
die lokale Liste + ruft wishlistStore.refresh.
- Wunschliste-Seite: Heart toggelt eigenen Wunsch, Count zeigt Gesamt-
wünsche, kommagetrennte Namen zeigen "wer will". Trash-Button
entfernt — Heart-off reicht jetzt.
Tests (99 → 99, 8 neu geschrieben):
- Per-User-Add/Remove, aggregierte Counts, on_my_wishlist, Cascades bei
Recipe/Profile-Delete, countWishlistRecipes = DISTINCT.
Homepage:
- Neue Sektion "Deine Favoriten" über "Zuletzt hinzugefügt" (alphabetisch
sortiert, lädt wenn Profil aktiv ist; versteckt sonst)
- Jede Karte in "Zuletzt hinzugefügt" hat jetzt oben-rechts ein X-Icon
zum Ausblenden. Das Rezept selbst bleibt in der DB — nur die
Anzeige in der Recent-Liste wird per recipe.hidden_from_recent = 1
unterdrückt. Section versteckt sich, wenn die Liste leer wird.
DB:
- Neue Migration 004_recipe_hidden_from_recent.sql (+Index)
- listFavoritesForProfile in search-local.ts (ORDER BY title NOCASE)
- setRecipeHiddenFromRecent in actions.ts
API:
- GET /api/recipes/favorites?profile_id=X
- PATCH /api/recipes/[id] akzeptiert jetzt title und/oder
hidden_from_recent (Zod-Schema mit refine)
Rezept-Detail:
- Titel ist jetzt inline editierbar: kleines Stift-Icon rechts neben
H1. Click öffnet Input, Enter speichert (PATCH), Escape bricht ab.
Kein location.reload() mehr.
- RecipeView bekommt neuen Snippet-Prop titleSlot für Title-Override.
- Neue Aktionsreihenfolge:
Zeile 1: Favorit | Wunschliste | Drucken
Zeile 2: Heute gekocht | Löschen
(Umbenennen ist jetzt am Titel statt in der Leiste.)
Icons (lucide-svelte, neues Dep):
- Emoji-Icons durch Lucide-SVGs ersetzt auf Startseite, Header,
Rezept-Detail, Wunschliste, Header-Dropdown:
🍽️→Heart/Utensils, ⚙️→Settings, 🥘→CookingPot, 🌐→Globe,
♥/♡→Heart(filled), 🖨→Printer, ✎→Pencil, 🗑→Trash2, ✓→Check,
🍳→ChefHat, X→X
- Header-Brand-Badge auf Mobile behält sein 🍳 (ist im ::after-Pseudo,
Lucide käme da nicht sauber rein).
- SearchLoader-Emojis bleiben — die sind Teil der Animations-Charme.
Tests: 99/99 grün (bestehend), Typecheck 0 Fehler.
Vorher: In-Memory-Map, TTL 30 Minuten. Container-Neustart verwarf den
kompletten Cache, also musste nach jedem Deploy jede Suche wieder alle
Seiten laden.
Jetzt:
- Neue Tabelle thumbnail_cache (url PK, image, expires_at)
- Default-TTL 30 Tage, per Env KOCHWAS_THUMB_TTL_DAYS konfigurierbar
(7, 365, was der User will — is alles ok laut Nutzer)
- Negative Cache: Seiten ohne Bild werden mit image=NULL gespeichert,
damit wir nicht jede Suche die gleiche kaputte Seite wieder laden
- Lazy-Cleanup: pro searchWeb-Aufruf werden abgelaufene Zeilen via
DELETE ... WHERE expires_at <= now() weggeräumt (Index-Scan, billig)
Migration 003_thumbnail_cache.sql: nicht-destruktiv, nur neue Tabelle.
Bestehende DB bekommt sie beim nächsten Start automatisch dazu.
Tests (99/99):
- Neuer Cache-Test: zweiter searchWeb für dieselbe URL macht keinen
Page-Fetch mehr und liest die image-Spalte aus SQLite.
Each recipe appears at most once on the wishlist. Any profile can add,
remove, like, and unlike. Ratings and cooking log stay independent.
Data model: wishlist(recipe_id PK, added_by_profile_id, added_at)
wishlist_like(recipe_id, profile_id, created_at)
Why: 'das will ich essen' — family members pick candidates, everyone
can +1 to signal agreement, cook decides based on popularity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>