Neuer Sort 'viewed' macht LEFT JOIN gegen recipe_view, ordert nach
last_viewed_at DESC mit alphabetischem Tiebreaker. NULL-Recipes (nie
angesehen) landen alphabetisch sortiert hinter den angesehenen
(CASE-NULL-last statt SQLite 3.30+ NULLS LAST).
Ohne profileId faellt der Sort auf alphabetisch zurueck — Sort-Chip
bleibt klickbar, ergibt aber sinnvolles Default-Verhalten ohne
aktiviertes Profil.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
INSERT OR REPLACE fuer idempotenten Bump des last_viewed_at Timestamps.
listViews-Helper nur fuer Tests; Sort-Query laeuft direkt in
listAllRecipesPaginated via LEFT JOIN.
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>
Idempotentes Setzen/Loeschen von shopping_cart_check-Eintraegen
ueber (name_key, unit_key). Check ueberlebt Recipe-Removals,
solange ein anderes Rezept weiterhin zur Zeile beitraegt —
verifiziert durch 3 neue Integrationstests (17 total).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Domain-Filter im Header-Dropdown wirkt ab jetzt ausschliesslich auf
die Web-Suche (SearXNG). Die Suche in gespeicherten Rezepten liefert
immer alle Treffer, unabhaengig von der Quelldomain -- wer ein Rezept
gespeichert hat, will es finden, selbst wenn er die Domain aus dem
Filter ausgeschlossen hat.
- SearchStore: filterParam -> webFilterParam, nur noch an Web-Calls
- /api/recipes/search: domains-Query-Param wird nicht mehr gelesen
- searchLocal(): domains-Parameter + SQL-Branch entfernt
- Tests entsprechend angepasst
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neue SPRACHE-Sektion weist Gemini explizit darauf hin, dass die
Texte ausschliesslich deutsch sind -- Umlaute, deutsche Zutaten,
deutsche Masseinheiten als Prior fuer die Zeichen-Rekonstruktion.
Soll die "Kontext-Detektiv"-Logik bei handgeschriebenen oder
verblassten Rezepten verbessern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auf Gemini-Empfehlung: System-Instruction als OCR-Experte fuer
kulinarische Dokumente, mit "Kontext-Detektiv"-Regel fuer schwer
lesbare Zeichen, "[?]" fuer Unleserliches und strikter "keine
Halluzination"-Regel.
User-Prompt wird jetzt als eigene text-part bei jedem Call
mitgeschickt (Bild + User-Prompt + bei Retry die Korrektur-Note).
Inline-Schema aus dem Prompt entfernt, da es mit unserem
responseSchema konfligierte (servings vs servings_default+unit,
times-nested vs flat, instructions vs steps, kein note-Feld) --
das kann die beobachteten AI_FAILED-Schema-Validation-Fehler
beguenstigt haben. Struktur wird jetzt ausschliesslich ueber
responseSchema enforced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der bisherige Log "[extract-from-photo] AI_FAILED after 43165ms,
385807 bytes" verriet nicht, ob es JSON-Parse, Schema-Validierung
oder ein SDK-Fehler war. Endpoint haengt jetzt e.message an;
gemini-client loggt den First-Attempt-Fehler vor dem Retry und
packt bei AI_FAILED beide Messages in den finalen Error.
Keine Prompt-/Response-Inhalte werden geloggt -- nur unsere eigenen
GeminiError-Messages (Zod-Pfade, "non-JSON output", SDK-toString).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ES-Dynamic-Import mit @vite-ignore reichte nicht -- adapter-node's
Rollup-Step extrahiert sharp trotzdem in einen shared chunk und
bundelt sharp's interne dynamic-requires kaputt.
createRequire(import.meta.url) plus require('sharp') ist pure Node-
Runtime-Logik, die Rollup komplett ignoriert. sharp wird regulaer aus
node_modules geladen -- inkl. seiner Plattform-.node-Binary aus
@img/sharp-linuxmusl-arm64.
Verifikation: Build-Output enthaelt 0 Vorkommen von "dynamicRequireTargets"
und "sharp.node" (waren vorher in einem 319KB shared chunk).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der vorige Versuch mit ssr.external in vite.config.ts war ein No-op:
adapter-node macht einen eigenen Rollup-Bundle-Schritt nach Vite und
ignoriert ssr.external komplett. Ergebnis: sharp's dynamic-require
fuer die native .node-Binary landet kaputt im Server-Bundle (332KB
Bundle-Chunk, 297 sharp-Referenzen).
Dynamic import mit /* @vite-ignore */ verhindert, dass Rollup sharp
aufloest — die Require geht stattdessen zur Laufzeit regulaer an
Node und findet @img/sharp-linuxmusl-arm64 in node_modules.
Ergebnis lokal: Server-Chunk von 332KB auf 14KB geschrumpft, nur noch
2 Referenzen auf den Paketnamen (der Import-String selbst).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
INSERT/SELECT in insertRecipe, replaceIngredients und getRecipeById
um section_heading ergänzt. IngredientSchema im PATCH-Endpoint sowie
Ingredient-Fixtures in search-local-, scaler- und repository-Tests
auf das neue Pflichtfeld aktualisiert.
Co-Authored-By: Claude Sonnet 4.6 <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>
ingredient.ts:
- UNICODE_FRACTION_MAP fuer ½ ¼ ¾ ⅓ ⅔ ⅕ ⅖ ⅗ ⅘ ⅙ ⅚ ⅛ ⅜ ⅝ ⅞
- clampQuantity() weist 0, negative, > 10000 als null ab
- splitUnitAndName() helper, vorher 2x dupliziert (Unicode + ASCII Pfad)
Tests:
- 13 neue Tests fuer Unicode-Brueche (mit/ohne Unit) und Bounds
- bestaetigt dass deutsches Kommadezimal (0,25 l) bereits funktioniert
Hintergrund: Apple Food App liefert haeufig ½ und ⅓ in JSON-LD
Quantity-Feldern. Vor diesem Fix wurden die Felder als unparsable
behandelt (quantity null, name = '½ TL Salz'), was den Portionen-Slider
fuer importierte Rezepte unbrauchbar machte.
Findings aus REVIEW-2026-04-18.md (Refactor D) und structure.md
Die drei [searxng]-Logs sind absichtlich produktiv (Hilfe beim
Debuggen 'warum wurde Domain X gefiltert?'). Kommentar dokumentiert
das, damit kein zukuenftiger Cleanup sie pauschal entfernt.
baseRecipe-Fixture bleibt in tests/integration/recipe-repository.test.ts —
nur dort verwendet, nicht dupliziert (Review-Annahme war falsch).
yauzl/@types/yauzl bleiben als Dependency — bereits in
session-handoff-2026-04-17.md (Phase 5b) und ARCHITECTURE.md
verankert.
Findings aus REVIEW-2026-04-18.md (Wave 5 Cleanup) und structure.md
- src/lib/server/api-helpers.ts mit parsePositiveIntParam(),
validateBody<T>() und ErrorResponse type
- 13 unit tests fuer die beiden helper (HttpError-Shape verifiziert)
- Konsolidiert spaeter 9x parseId und 11x safeParse-Bloecke aus den
+server.ts handlern
Findings aus REVIEW-2026-04-18.md (Refactor A) und redundancy.md
- Neuer Endpoint POST/DELETE /api/recipes/:id/image.
* Multipart-Upload mit Feld "file".
* Whitelist: JPG, PNG, WebP, GIF, AVIF. Max 10 MB.
* Dedupe per SHA-256-Filename analog zu downloadImage().
- updateImagePath()-Repo-Funktion ergänzt.
- RecipeEditor: neuer Block "Bild" ganz oben. Preview + Buttons
"Hochladen"/"Ersetzen"/"Entfernen". Upload passiert direkt beim
Auswählen, nicht erst bei "Speichern" — das Bild ist eigene
Ressource, Abbrechen rollt es nicht zurück (okay, da dedupliziert).
- onimagechange-Callback informiert die Detail-Ansicht, damit die
Preview im RecipeView auch nach Abbrechen aktuell bleibt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nach dem DDG-Rauswurf war Mojeek die verbleibende Lärm-Quelle im Log:
HTTP 403 pro Suche, suspended_time=180. Mojeek hat nach eigenem Muster
Pi-IPs als automatisierten Traffic klassifiziert. Brave (API) deckt die
Websuche zuverlässig ab — Mojeek ist draußen, sowohl im searxng.ts-
Query (engines=brave) als auch in der SearXNG-keep_only-Liste.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zwei Fixes gegen die hartnäckigen DDG-CAPTCHA-Fehler im SearXNG-Log:
1. searxng.ts fragt jetzt explizit `engines=brave,mojeek` an.
Vorher wurde nur `categories=general` gesetzt — dadurch wurden
alle in dieser Kategorie aktivierten Engines abgefragt, inkl. DDG
(das trotz `disabled: true` weiter antwortete).
2. settings.yml nutzt `use_default_settings.engines.keep_only` statt
einzelner `disabled: true`-Overrides. SearXNGs Merge-Semantik für
partielle Engine-Overrides (nur name + disabled ohne engine:)
greift in der aktuellen Version nicht zuverlässig, deshalb kam
DDG durch. keep_only wirft alles außer brave+mojeek vor dem Laden
raus — kein Captcha-/403-Log-Lärm mehr.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der User pastet bewusst eine URL und erwartet, dass der Import
klappt — die Whitelist-Prüfung (DOMAIN_BLOCKED) im previewRecipe
war da nur Reibung. Die Whitelist bleibt für die Web-Suche relevant
(dort muss das Crawl-Feld eingeschränkt werden), für Imports nicht
mehr.
Dropped: isDomainAllowed + whitelist.ts, DOMAIN_BLOCKED-Code in
ImporterError, die zugehörige Branch in mapImporterError. Tests
entsprechend angepasst: statt "DOMAIN_BLOCKED wenn nicht whitelisted"
prüft der Preview-Test jetzt "klappt auch ohne Whitelist-Eintrag".
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
Auf der Rezept-Detail-Seite ein neuer „Bearbeiten"-Button (Pencil-Icon)
in der Action-Bar. Klick schaltet RecipeView auf RecipeEditor um.
Im Editor:
- Titel, Beschreibung, Portionen, Vorbereitungs-/Koch-/Gesamtzeit als
inline-Inputs.
- Zutaten: pro Zeile Menge, Einheit, Name, Notiz + Trash-Icon zum
Entfernen. „+ Zutat hinzufügen"-Dashed-Button am Listenende.
- Schritte: nummerierte Textareas, Trash-Icon, „+ Schritt hinzufügen".
- Mengen akzeptieren Komma- oder Punkt-Dezimalen.
- Empty-Items werden beim Speichern automatisch aussortiert.
Backend:
- Neue Repo-Funktionen updateRecipeMeta(id, patch), replaceIngredients,
replaceSteps — letztere in einer Transaction mit delete+insert und
FTS-Refresh.
- PATCH /api/recipes/[id] akzeptiert jetzt zusätzlich description,
servings_default, servings_unit, prep_time_min, cook_time_min,
total_time_min, cuisine, category, ingredients[], steps[]. Vorher
nur title/hidden_from_recent; diese beiden bleiben als
Kurz-Fall erhalten, damit bestehende Aufrufer unverändert laufen.
- Zod-Schema mit expliziten Grenzen (max-Länge, positive Mengen).
Tests: 3 neue Cases für updateRecipeMeta, replaceIngredients (inkl.
FTS-Update), replaceSteps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Die SearXNG-Logs waren voller 403-Errors von karmasearch (video-engine)
und gelegentlich Brave. Beide gehören nicht zur general-Kategorie und
bringen für Rezeptseiten nichts — sie werden nur noch vom SearXNG-Core
angefragt, weil wir die Kategorie nicht explizit eingrenzen.
categories=general im Query beschränkt jetzt auf Text-Web-Suche; die
problematischen Video-/News-Engines werden gar nicht erst konsultiert,
und die 403-Spam in den Container-Logs verschwindet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neue Sektion unter „Zuletzt hinzugefügt": sortierbar nach Name,
Bewertung, zuletzt gekocht und Hinzugefügt. Auswahl persistiert in
localStorage (kochwas.allSort).
- Neuer Endpoint GET /api/recipes/all?sort=name&limit=10&offset=0.
- listAllRecipesPaginated(db, sort, limit, offset) im repository:
NULLS-last-Emulation per CASE für rating/cooked — funktioniert auch
auf älteren SQLite-Versionen.
- Endless Scroll per IntersectionObserver auf ein Sentinel-Element am
Listen-Ende (rootMargin 200px, damit schon vor dem harten Rand
nachgeladen wird). Pagesize 10.
- 4 neue Tests: Name-Sort, Rating-Sort, Cooked-Sort, Pagination-Offset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rezeptwelt lieferte Zubereitungs-Steps immer als einen einzigen Treffer,
oft mit vermischtem Icon-alt-Text. Zwei Ursachen, beide in der
generischen Microdata-Logik — kein rezeptwelt-spezifischer Parser nötig.
1. HowToSection wrappt HowToSteps als itemListElement, unser Parser sah
nur das erste. Jetzt: recipeInstructions-Container mit itemtype=
HowToSection werden abgestiegen, jedes itemListElement wird ein Step.
2. Ein einzelner HowToStep kann intern "1. …<br>2. …<br>3. …" enthalten.
Neuer textWithLineBreaks(el) konvertiert <br>/Block-Grenzen zu \n und
ignoriert <img>/<script>/<style>. splitStepText(raw) erkennt
nummerierte Zeilen und erzeugt einen eigenen Step pro Nummer; Fort-
setzungszeilen ohne Nummer hängen an den aktuellen Step an.
3 neue Tests: HowToSection-Kette, inline-nummerierter Multi-Step,
<img>-alt-Unterdrückung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bisher scheiterte der Import auf Seiten wie rezeptwelt.de mit „Diese Seite
enthält kein Rezept", obwohl unser Such-Filter die Treffer durchließ
(Microdata wird seit dem vorherigen Commit erkannt). Jetzt kann der
Importer die Daten auch tatsächlich extrahieren:
- extractRecipeFromMicrodata(html): parst [itemtype=schema.org/Recipe]-
Scopes per linkedom, sammelt itemprop-Werte unter Beachtung der
verschachtelten itemscope-Grenzen (HowToStep-Texts landen nicht im
Haupt-Scope).
- Übernimmt Content-Attribute auf <meta>/<time> (z.B. prepTime="PT20M"),
src auf <img>, textContent als Fallback — die Standard-Microdata-
Value-Regeln.
- Behandelt HowToStep-Items UND einfache <li>/<ol>-Listen als
recipeInstructions.
- extractRecipeFromHtml ruft JSON-LD zuerst, fällt nur bei null auf
Microdata zurück — damit bleibt bestehendes Verhalten stabil.
Tests: Königsberger-Klopse-Fixture mit HowToSteps, einfache ol/li-
Variante und Priorität-JSON-LD-über-Microdata-Check.
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>
Domain-Admin-Seite bekommt jetzt ein Favicon-Icon vor jedem Eintrag,
einen Pencil-Button zum Inline-Editieren von Domain und Anzeigename,
und Save/Cancel-Buttons. Beim Ändern des Domain-Namens wird das Favicon
zurückgesetzt und beim Speichern frisch nachgeladen (den Filter-Dropdown-
Icons reicht der neue favicon_path automatisch zu).
Der Filter-Button auf der Hauptseite sitzt jetzt IM weißen Suchfeld-
Container (neuer .search-box-Wrapper mit Border) statt daneben, analog
zum Referenz-Screenshot von rezeptwelt.de. Neue inline-Prop an
SearchFilter schaltet eigenen Border/Background ab und setzt stattdessen
einen vertikalen Divider nach rechts.
- Neuer PATCH /api/domains/[id] mit zod-Schema.
- Repository: updateDomain(id, patch) + getDomainById(id).
domain-Change nullt favicon_path → Caller lädt neu.
- Tests für updateDomain-Fälle und getDomainById.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Statt eine separate Debug-Seite zu bauen: bei jeder Web-Suche werden
zwei kompakte Log-Zeilen nach stdout geschrieben, die den Filter-Verlust
pro Pipeline-Schritt zeigen. In den Pi-Docker-Logs (docker compose logs
kochwas) leicht über grep '[searxng]' zu finden.
Format:
[searxng] q="…" pageno=1 domains=3 raw=12 non_whitelist=2
non_recipe_url=4 dup=0 kept_pre_enrich=6
[searxng] q="…" pageno=1 enrich=6 dropped_non_recipe=3 final=3
Damit lässt sich gezielt sehen, ob rezeptwelt-Treffer am looksLikeRecipePage-
Filter, am hasRecipe-Check oder schon bei SearXNG selbst verloren gehen.
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>
Links im großen Suchfeld ein Slider-Icon mit Badge („Alle" oder „2/5"),
das ein Dropdown-Menü mit allen Whitelist-Domains als Checkboxen öffnet.
Auswahl wird per localStorage persistiert und gilt global — Header-Such-
Dropdown konsumiert den gleichen Store und sendet den domains-Parameter
bei jedem Fetch mit.
Leere Menge heißt „alle aktiv", damit neu vom Admin freigeschaltete
Domains automatisch dabei sind. Aktive Auswahl landet als explizite
Intersection mit der Whitelist serverseitig.
- searchLocal nimmt jetzt optional string[] domains → `source_domain IN (…)`.
- searchWeb nimmt jetzt opts.domains → site:-Filter auf die Auswahl
eingeschränkt. Nicht-Whitelist-Einträge werden ignoriert.
- API-Endpoints: `?domains=a.de,b.de`.
- Neuer Client-Store $lib/client/search-filter.svelte.ts.
- Neue Komponente $lib/components/SearchFilter.svelte (mobile-tauglich,
44px Touch-Targets, Badge auf engen Screens versteckt).
Home-Seite re-runt die Suche bei Filter-Änderung automatisch (150ms debounce),
ohne dass der User neu tippen muss.
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>