Der Profile-Switch-Refetch-Effect las allLoading in der sync tracking-
Phase. Der await fetch beendete die Sync-Phase, das finale
allLoading = false im finally lief ausserhalb → wurde als externer
Write interpretiert → Effect rerun → naechster Fetch → Endlosschleife.
2136 GETs auf /api/recipes/all?sort=viewed in 8s beobachtet.
Fix: nur profileStore.active bleibt tracked (der tatsaechliche
Trigger). allSort/allLoading werden in untrack() gelesen — die Writes
auf allLoading im finally triggern damit keinen Effect-Rerun mehr.
Verifiziert lokal: 1 Request statt 2000+ bei mount mit allSort=viewed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tag v1.4.0 ist schon gesetzt (auf 2b0bd4d), der synchrone Version-Bump
in package.json und package-lock.json wurde dabei vergessen. Mit dem
Pattern aus vorigen Releases (v1.2.0/v1.3.0) wieder konsistent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live-Test auf kochwas-dev: bei Hard-Reload/Cold-Start (nicht SPA-Click)
landete kein view-Eintrag in der DB. Ursache: Recipe-Page-onMount
feuert vor Layout-onMount, profileStore.load() laeuft aber im Layout-
onMount und macht erst danach einen async fetch — zum Zeitpunkt des
Beacons war profileStore.active noch null.
Loesung: Beacon im \$effect, das auf profileStore.active reagiert.
viewBeaconSent-Flag verhindert duplicate POSTs wenn der User waehrend
der Page-Lifetime das Profil wechselt — der ursprueglich getrackte
Profil-View bleibt der "richtige" fuer dieses Page-Open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-Review zu commit 2216c89: button hatte keinen :focus-visible
Outline (Safari zeigt sonst gar nichts an) — Pattern aus dem Rest
der Page uebernommen (#2b6a3d Outline). Globale .chev-Selektoren
unter .section-head gescoped, damit andere Komponenten den Klassen-
Namen kuenftig wiederverwenden koennen ohne Konflikte.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Header als <button> mit Chevron + Count-Pill, slide-Transition (180ms).
State in localStorage unter kochwas.collapsed.sections — JSON-Map
{favorites, recent}, default beide offen, corrupt-JSON faellt auf
Default zurueck.
Alle Rezepte bleibt absichtlich nicht-collapsibel — Hauptliste, immer
sichtbar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neuer Wert 'viewed' im AllSort-Enum + ALL_SORTS-Array. localStorage-
Whitelist ergaenzt. Reactive $effect lauscht auf profileStore.active
und refetcht offset=0 nur wenn aktueller Sort 'viewed' ist — andere
Sortierungen sind profilunabhaengig.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
buildAllUrl-Helper haengt profile_id an wenn ein Profil aktiv ist;
nutzt es loadAllMore, setAllSort und rehydrateAll. Voraussetzung fuer
sort=viewed (Server braucht profile_id fuer den View-Join).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VALID_SORTS um 'viewed' erweitert. parseProfileId-Helper analog zu
/api/wishlist. Wert wird an listAllRecipesPaginated als 5. Param
durchgereicht.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fire-and-forget POST /api/recipes/[id]/view in onMount, nur wenn
profileStore.active gesetzt. Schreibt last_viewed_at fuers Profil —
Voraussetzung fuer den Sort 'Zuletzt angesehen' auf der Hauptseite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-Review-Finding zu commit 82d4348: das Sibling-Endpoint
recipes/[id]/+server.ts nutzt schon parsePositiveIntParam aus
api-helpers.ts. Der neue View-Endpoint hatte die Logik inline
nachgebaut — jetzt aufgeraeumt fuer Konsistenz.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Body { profile_id } via zod validiert. FK-Violation (unbekanntes
Profil oder Rezept) wird zu 404 normalisiert. Erfolg liefert 204
ohne Body — fire-and-forget vom Client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-Review-Finding zu commit 226ca5e: vorheriges Test seedete nur ein
NULL-Recipe, der alphabetische Tiebreaker fuer ungesehene Eintraege
wurde nicht exerciert. Zweites ungesehenes Rezept mit anderer
Einsatzreihenfolge ergaenzt — beweist dass Donauwelle vor
Zwiebelkuchen kommt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Tabellen-Konvention im Repo ist singular — siehe Code-Review-Findings
zu Task 1 (commit 543008b). Plan und Spec angeglichen damit weitere
Tasks nicht mit dem alten Plural arbeiten.
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>
Spec fuer zwei Hauptseite-Features aus Brainstorming am 2026-04-22:
1) Neue Sort-Option "Zuletzt angesehen" fuer "Alle Rezepte". Tracking
per Profil in neuer SQLite-Tabelle recipe_views, beim Laden der
Detail-Seite per Beacon (POST /api/recipes/[id]/view) gesetzt.
Server-Sort macht LEFT JOIN mit ORDER BY last_viewed_at DESC NULLS
LAST, alphabetischer Tiebreaker.
2) "Deine Favoriten" und "Zuletzt hinzugefuegt" auf-/zuklappbar.
Default offen, User-Wahl persistiert in localStorage pro Device.
Header als button mit Chevron + Count-Pill, slide-Transition.
"Alle Rezepte" bleibt nicht-collapsibel (Hauptliste).
Spec deckt Schema, API-Endpoint, DB-Layer, Markup-Pattern,
Reactive-Refetch bei Profile-Switch, Snapshot-Kompatibilitaet (rehydrate
muss profile_id mitbekommen), Test-Strategie und Reihenfolge.
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>
In 0bfeba2 sind via git add -A versehentlich zwei lokale Artefakte
mitgewandert: ci-log.txt (lokaler CI-Log-Dump) und
.claude/scheduled_tasks.lock (Claude-Code Session-Lock). Beide gehören
nicht ins Repo. Per git rm --cached entfernt und in .gitignore
geblacklistet.
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>