Commit Graph

300 Commits

Author SHA1 Message Date
hsiegeln
f495c024c6 feat(recipe): View-Beacon beim oeffnen der Detailseite
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>
2026-04-22 14:28:32 +02:00
hsiegeln
1214b9e01d refactor(api): parsePositiveIntParam fuer view-endpoint
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>
2026-04-22 14:27:10 +02:00
hsiegeln
82d4348873 feat(api): POST /api/recipes/[id]/view fuer View-Beacon
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>
2026-04-22 14:23:36 +02:00
hsiegeln
6f54b004ca test(views): NULL-Tiebreaker explizit verifizieren
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>
2026-04-22 14:20:51 +02:00
hsiegeln
226ca5e5ed feat(search): sort=viewed in listAllRecipesPaginated
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>
2026-04-22 14:17:17 +02:00
hsiegeln
5357c9787b refactor(views): ON CONFLICT DO UPDATE statt INSERT OR REPLACE
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>
2026-04-22 14:14:44 +02:00
hsiegeln
6c8de6fa3a feat(db): recordView/listViews fuer recipe_view
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>
2026-04-22 14:10:52 +02:00
hsiegeln
866a222265 docs: plan/spec auf recipe_view (singular) angeglichen
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>
2026-04-22 14:08:54 +02:00
hsiegeln
543008b0f2 refactor(db): recipe_views -> recipe_view, TIMESTAMP-Konsistenz
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>
2026-04-22 14:08:17 +02:00
hsiegeln
2cd9b47450 feat(db): recipe_views table mit Profil-FK und Recent-Index
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>
2026-04-22 14:04:27 +02:00
hsiegeln
98894bb895 docs(plan): Implementation-Plan fuer Views-Sort + Collapsibles
10 Tasks (Migration -> Repo -> Sort-Branch -> API -> Beacon -> URL-
Helper -> Sort-Chip + Reactive Refetch -> Collapsibles -> Push&Verify)
mit TDD-Schritten, exakten Filepfaden und vollstaendigem Code in
jedem Step.

Spec-Migrationsnummer auf 014 korrigiert (war 010 — letzte aktuelle
Migration ist 013_shopping_list).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:00:22 +02:00
hsiegeln
363ea6fbe7 docs(spec): Hauptseite Zuletzt-angesehen-Sort + Collapsibles
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>
2026-04-22 13:54:52 +02:00
hsiegeln
005c3ea7b5 fix(home): rehydrate-Trigger aus snapshot.restore, nicht ueber onMount
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m16s
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>
2026-04-22 09:59:11 +02:00
hsiegeln
1d7731edbb chore: ci-log.txt und .claude/ aus dem Repo werfen
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m39s
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>
2026-04-22 09:42:53 +02:00
hsiegeln
0bfeba2c0a feat(home): Pagination-Tiefe per Snapshot, scroll-restore deep-scroll-fest
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
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>
2026-04-22 09:42:16 +02:00
hsiegeln
f3e2cebfb4 fix(nav): scroll-restore key auf nav.from.url, nicht location
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m51s
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>
2026-04-22 09:28:20 +02:00
hsiegeln
442076a278 fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m44s
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>
2026-04-22 09:18:00 +02:00
hsiegeln
4afc597689 fix(nav): Header-Back-Pfeil als echtes history.back()
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 3m19s
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>
2026-04-22 09:09:45 +02:00
hsiegeln
42b1aed023 fix(shopping-e2e): beforeEach-Cleanup + checked-Count statt first-Row
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 32s
2026-04-22 08:57:17 +02:00
hsiegeln
a15390f4b8 fix(shopping): requireOnline-Guards + 2-space indent
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 3m14s
2026-04-21 23:59:14 +02:00
hsiegeln
52bb83cbd5 fix(shopping-e2e): exact match fuer Leeren-Confirm-Button
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 36s
2026-04-21 23:52:49 +02:00
hsiegeln
4e902b1d98 test(shopping): E2E-Spec + clearShoppingCart-Fixture
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 33s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:50:05 +02:00
hsiegeln
0346a699b9 feat(shopping): Footer-Actions (Erledigte entfernen, Liste leeren)
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 23:47:30 +02:00
hsiegeln
f4eac4d9c3 feat(shopping): Rezept-Chips mit Portions-Stepper
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 23:45:32 +02:00
hsiegeln
3c30d1f35a feat(shopping): Zutaten-Rows mit Abhaken
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m17s
2026-04-21 23:43:00 +02:00
hsiegeln
943a645095 feat(shopping): Einkaufslisten-Seite mit Empty-State
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 23:40:52 +02:00
hsiegeln
7fa1079125 refactor(wishlist): horizontale Actions + Einkaufswagen-Button
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m15s
2026-04-21 23:38:26 +02:00
hsiegeln
0e6d2c93a6 feat(shopping): Header-Badge mit Einkaufswagen-Icon
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m14s
2026-04-21 23:34:57 +02:00
hsiegeln
1bd5dd106f feat(shopping): ShoppingCartStore (Client)
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m16s
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.
2026-04-21 23:31:29 +02:00
hsiegeln
dc15cf04a9 feat(shopping): Service-Worker network-only fuer /api/shopping-list/*
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m25s
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>
2026-04-21 23:28:17 +02:00
hsiegeln
e53cdc96fe feat(shopping): DELETE /api/shopping-list/checked (Erledigte entfernen)
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 23:26:32 +02:00
hsiegeln
a500a5623e feat(shopping): POST/DELETE /api/shopping-list/check
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 23:25:05 +02:00
hsiegeln
2750c298e9 feat(shopping): PATCH/DELETE /api/shopping-list/recipe/[id]
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 23:23:21 +02:00
hsiegeln
7baf60f422 feat(shopping): POST /api/shopping-list/recipe
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 23:21:35 +02:00
hsiegeln
e176b8c3f2 style(shopping): GET/DELETE endpoint 2-space indent
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 23:20:03 +02:00
hsiegeln
8570d41f53 feat(shopping): GET /api/shopping-list + DELETE (Liste leeren)
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 23:18:24 +02:00
hsiegeln
76864a6034 feat(shopping): formatQuantity-Utility
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 23:16:23 +02:00
hsiegeln
2c61d82935 feat(shopping): clearCart
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m14s
2026-04-21 23:13:58 +02:00
hsiegeln
974227590f feat(shopping): clearCheckedItems + Orphan-Cleanup
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m13s
2026-04-21 23:11:25 +02:00
hsiegeln
1889b0dea0 feat(shopping): toggleCheck (idempotent)
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m20s
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>
2026-04-21 23:08:20 +02:00
hsiegeln
494b672e8d fix(shopping): NULLIF-Guard gegen servings_default=0 in Aggregation
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 23:06:19 +02:00
hsiegeln
c31a9c6110 feat(shopping): listShoppingList mit Aggregation + Skalierung
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m14s
2026-04-21 23:02:05 +02:00
hsiegeln
85bf197084 feat(shopping): setCartServings mit Positiv-Validation
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m15s
2026-04-21 22:59:12 +02:00
hsiegeln
83fe95ac76 feat(shopping): removeRecipeFromCart
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m27s
2026-04-21 22:56:26 +02:00
hsiegeln
95ba14ad6f refactor(shopping): DEFAULT_SERVINGS-Konstante + Kommentare
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
2026-04-21 22:54:54 +02:00
hsiegeln
8ceb5e95d7 feat(shopping): addRecipeToCart (idempotent via ON CONFLICT)
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m20s
2026-04-21 22:50:58 +02:00
hsiegeln
7dab267033 feat(shopping): Repository-Skeleton mit Types
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m16s
2026-04-21 22:47:21 +02:00
hsiegeln
45223df86d chore(db): Migration 013 fuer Einkaufsliste-Tabellen
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m20s
2026-04-21 22:43:50 +02:00
hsiegeln
fd5d759336 docs(plan): Implementierungs-Plan fuer Einkaufsliste
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 31s
24 Tasks, TDD, bite-sized. Reihenfolge: Migration -> Repository -> Quantity-Formatter
-> API -> Service-Worker -> Client-Store -> Header-Badge -> Wunschlisten-Relayout
-> Shopping-List-Seite -> E2E.

E2E-Tests laufen erst nach Deploy gegen kochwas-dev.siegeln.net.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:37:04 +02:00
hsiegeln
956357d5ca docs(spec): Einkaufsliste-Design
Neue Spec fuer das Einkaufslisten-Feature:
- Globale (haushaltsweite) Einkaufsliste, aus Rezepten der Wunschliste gefuellt
- Portionen zentral auf der Listen-Seite skalierbar
- Flache Aggregation via (LOWER(TRIM(name)), LOWER(TRIM(unit)))
- Abhaken persistiert, Cleanup manuell
- Header-Badge zaehlt nicht-abgehakte Zeilen
- Relayout der Wunschlisten-Karte: Action-Icons horizontal oben, Quell-Domain raus
- Kein Fuzzy-Matching, keine manuellen Eintraege (YAGNI fuer v1)

E2E-Tests erst nach Deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:25:46 +02:00