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>
Live-Test mit Fetch-Hook auf kochwas-dev hat bewiesen: bei tiefem
Endless-Scroll (60 Items) und Back-Nav fired rehydrateAll nie. Der
Logger zeigte nur limit=10&offset=0 plus IO-Trigger (10&offset=10).
Root Cause: SvelteKit ruft snapshot.restore *nach* onMount (post-mount
tick). Der vorherige Code parkte die Tiefe in pendingPagination und
liess onMount entscheiden — onMount lief aber zuerst, sah null, fiel
auf loadAllMore() zurueck. Restore setzte danach pendingPagination,
aber niemand las es mehr.
Fix: rehydrateAll direkt aus restore aufrufen (fire-and-forget).
onMount macht unkonditional loadAllMore() fuer den Fresh-Mount-Fall;
beide Pfade greifen ueber das allLoading-Flag bzw. ueber den
allRecipes-Overwrite (rehydrateAll's groesseres Result gewinnt
spaeter). Wasted-Fetch im Worst-Case: 10 Items (~2 KB) — vertretbar.
pendingPagination raus, onMount-Conditional vereinfacht.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bei tiefer Endless-Scroll (z. B. 60 nachgeladene Items) versagte der
generische scroll-restore: nach dem Back-Mount lud onMount() nur die
initialen 10 Items via loadAllMore(), das Dokument blieb ~1000px hoch
und der rAF-Poll konnte die gespeicherte scrollY (z. B. 4000) nie
erreichen — Best-Effort scrollTo clampte auf die erreichbare Hoehe.
Fix per SvelteKit-Snapshot, derselbe Mechanismus den die Page bereits
fuer SearchStore nutzt: Capture nimmt zusaetzlich allRecipes.length,
allSort und allExhausted mit. Restore setzt sort sofort und parkt die
Tiefe in pendingPagination. onMount sieht das Pending und ruft statt
loadAllMore() ein einmaliges rehydrateAll(sort, count, exhausted) —
ein Fetch mit limit=count rehydriert die ganze Liste atomar. Danach
hat das Dokument die Originalhoehe und der Layout-Restore-Poll laesst
die scrollY genau dort landen, wo der User vorher war.
API-Cap (src/routes/api/recipes/all/+server.ts) von 50 auf 200
angehoben — Recipe-Metadaten sind klein (~200 B/Stueck), 200er-
Response ~40 KB. Cap deckt realistische Scroll-Tiefen.
Reload (Cmd-R) behaelt das alte Verhalten: ohne Snapshot greift der
sort-aus-localStorage-Pfad, lade-Sequenz startet wieder bei 10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live-Test auf kochwas-dev offenbarte: bei Browser-Back hat der Browser
die History bereits gepoppt, bevor SvelteKit beforeNavigate feuert —
location.pathname war damit schon die Ziel-URL. recordScroll() schrieb
also den 0-Wert der Recipe-Page in den Slot der Home-Page und wischte
den eigentlich gemerkten Wert (z. B. 500) raus. Restore las dann 0,
fiel unter MIN_RESTORE_Y und tat nichts.
Fix: recordScroll(nav.from?.url) und restoreScroll(type, nav.to?.url).
Helper bekommen die URL explizit reingereicht — keine Abhängigkeit
mehr von location und damit kein Race mit der Browser-History.
Tests: zusätzliche Regression "does not overwrite a stored URL when
called with a different from-url" plus Skip-Pfade fuer null URLs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Zurück-Pfeil im Header war fest auf "/" verdrahtet und navigierte
forward, nicht backward. Damit ging die Scroll-Position der Origin-Seite
verloren und z. B. Wunschliste -> Rezept -> Zurück landete auf der
Startseite statt zurück auf der Wunschliste.
Jetzt: history.back() (mit goto('/') als Fallback bei leerer History).
SvelteKits eingebaute Scroll-Restoration greift dadurch wieder, und der
Pfeil tut was er optisch verspricht.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Svelte-5-Runes-Store mit uncheckedCount, recipeIds und loaded.
refresh() holt Snapshot via GET /api/shopping-list, addRecipe/
removeRecipe posten bzw. loeschen und refreshen anschliessend.
Bei Netzwerkfehler bleibt der letzte bekannte State erhalten.
Einkaufsliste-Endpunkte duerfen vom SW nie gecached werden — Liste
ist zustaendig fuer Check-States und muss immer live vom Server
gelesen werden. Test + resolveStrategy-Erweiterung analog zu den
anderen online-only-Endpunkten.
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>
Android-Chrome auf Tablet verhaelt sich zickig: mit capture="environment"
nur Kamera, ohne capture nur Datei-Picker -- nie beide. Zwei separate
Buttons (mit jeweils eigenem Input-Element) machen die Wahl explizit
und funktionieren ueberall eindeutig.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
capture="environment" zwang Mobile-Browser in den Kamera-Modus. Ohne
das Attribut zeigt der Browser auf Mobile die volle Auswahl
(Kamera / Fotomediathek / Datei) -- besser fuer Tablets und User,
die ein schon existierendes Kochbuch-Foto verwenden wollen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der bisherige Endpoint verschluckte den formData()-Fehler mit einem
generischen "Multipart erwartet" — wir wissen nicht, warum Chrome auf
dem Tablet scheitert. Jetzt wird beim Fehler Content-Type, -Length und
User-Agent geloggt, plus die konkrete Error-Message in der Response.
Kein Foto-Inhalt im Log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tablet- und iPad-Pro-Kameras liefern JPEGs/HEICs bis 15 MB. Mit den
alten 8-/10-MB-Limits scheiterte das Upload beim SvelteKit-Body-Parser
mit "Multipart erwartet" (undurchsichtiger Fehler, weil SvelteKit den
Body frueher abweist als unser Endpoint-Check).
- Endpoint MAX_BYTES: 8 -> 20 MB
- BODY_SIZE_LIMIT: 10 -> 25 MB (mit Multipart-Overhead)
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>