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>
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>
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>
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>
Liest KOCHWAS_TAG via +layout.server.ts aus $env/dynamic/private
und zeigt den Tag als kleine graue Zeile unter dem Brand-Text auf
der Startseite. Fallback "dev" wenn nicht gesetzt. Auf engen
Screens mit ausgeblendetem Brand verschwindet auch die Version.
docker-compose.prod.yml reicht die Host-Env-Variable jetzt in den
Container durch (vorher nur fuers Image-Tag-Binding interpoliert).
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>
Entfernt die duplizierten $state-Felder, runSearch, loadMore und
beide Debounce-Effekte. URL-Sync, Snapshot und Filter-Re-Search
bleiben hier — delegieren aber an den Store. All-Recipes-Infinite-
Scroll unberuehrt (separate UI-Concern).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ersetzt die 10 lokalen $state-Felder, den Debounce-$effect und die
lokalen Search-Funktionen durch eine SearchStore-Instanz. Nav-Open-
Toggle, Click-outside und Menu-State bleiben lokal — UI-Concerns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der DELETE-Endpunkt fuer Kommentare existierte schon, hatte aber
keine UI-Exposition — Nutzer konnten ihre eigenen Kommentare nur
via API-Call loeschen. Das war beim UAT 2026-04-19 aufgefallen.
Jetzt: pro Kommentar wird nur fuer den Autor (comment.profile_id
=== profileStore.active.id) ein kleiner Trash2-Button in der
Ecke angezeigt. Mit confirmAction-Dialog, weil das Loeschen
nicht undo-bar ist.
Nutzt asyncFetch fuer den DELETE-Call — konsistent mit dem
Rest des Page-Scripts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/preview ohne Query zeigte endlos "Vorschau wird geladen…", weil
loading initial true war und der $effect bei leerem u nichts tat.
Jetzt: beim leeren u wird errored gesetzt (mit Hinweis, dass das
der falsche Einstieg in die Route ist), so zeigt die bestehende
error-box den passenden Text an.
Im UAT 2026-04-19 aufgefallen, dort als MINOR eingeordnet.
Hier direkt mitgenommen weil 6-Zeilen-Fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Option B aus dem Roadmap-Plan. requireProfile bekommt einen optionalen
message-Parameter mit dem bisherigen Text als Default — die 5 Bestands-
Aufrufe aendern sich nicht, die Wunschliste nutzt die Custom-Message
„um mitzuwünschen" sauber ueber den Helper statt mit dupliziertem
alertAction-Block.
Netto: -3 Zeilen in wishlist/+page.svelte, eine Duplikation weniger,
Helper dokumentiert jetzt explizit den Message-Override-Use-Case.
Gate: svelte-check 0 Warnings, 184/184 Tests, Wunschliste zeigt
korrekte Message beim Klick ohne Profil.
Refs docs/superpowers/plans/2026-04-19-post-review-roadmap.md Item G.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
border-radius: 999px war 15x im CSS dupliziert. Ausgelagert als
:root --pill-radius Variable im globalen :root-Block in +layout.svelte,
Call-Sites auf var(--pill-radius) umgestellt.
Bewusst NICHT angefasst (plan war "nur Werte die mehrfach vorkommen"):
- z-index: 10 Distinct Values in 14 Sites, bilden ein implizites
Layer-System. Konsolidieren = behavior-change-Risiko ohne konkreten
Nutzen. Wenn kuenftig einheitliche Modal-/Popover-Layer noetig,
separate Phase.
- setTimeout(): 3 Sites, jeder mit eigener Semantik (Debounce/Print/
Spinner). Kein DRY-Nutzen durch Extraktion.
Gate: svelte-check 0 Warnings, 184/184 Tests, Build clean, kein
sichtbarer Unterschied (einzige Aenderung: selber Wert ueber Variable).
Refs docs/superpowers/plans/2026-04-19-post-review-roadmap.md Item F.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Alle 10 pre-existing svelte-check WARNINGs ("state_referenced_locally")
in RecipeEditor.svelte und recipes/[id]/+page.svelte addressiert.
Die betroffenen `let foo = $state(recipe.bar)`-Pattern sind
intentional Snapshots: der Editor soll User-Edits behalten und nicht
von prop-Updates ueberschrieben werden. untrack() macht die Intent
explizit und silenced die Warnung sauber statt sie unter den Teppich
zu kehren.
Scope: imagePath, title, description, servings, prepMin, cookMin,
totalMin, ingredients, steps (RecipeEditor) + recipeState
(recipes/[id]/+page).
Gate: svelte-check 0 Warnings (war 10), Tests 184/184.
Refs docs/superpowers/plans/2026-04-19-post-review-roadmap.md Item I.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 +server.ts handler nutzen jetzt parsePositiveIntParam und
validateBody statt jeweils lokaler parseId/safeParse-Bloecke.
Konsequenzen:
- 9 lokale parseId/parsePositiveInt Definitionen geloescht
- 11 safeParse + manueller error()-Throws ersetzt
- domains/[id], domains, profiles: catch-Block reicht jetzt HttpError
durch (isHttpError) — vorher wurde ein 404 vom updateDomain als 409
re-emittiert
- recipes/[id]/image: kein function-clutter mehr neben den FormData-Pfaden
- Konsistente Error-Bodies: validateBody schickt {message, issues},
parsePositiveIntParam {message: 'Missing X' / 'Invalid X'}
Findings aus REVIEW-2026-04-18.md (Refactor A) und redundancy.md
- src/lib/constants.ts: SW_VERSION_QUERY_TIMEOUT_MS, SW_UPDATE_POLL_INTERVAL_MS
- pwa.svelte.ts: nutzt die Konstanten statt 1500/30*60_000
- cache-strategy.ts / diff-manifest.ts: RequestShape/ManifestDiff entkapselt
(intern statt export, da nirgends extern importiert)
- recipes/[id]/image: deutsche Fehlermeldungen auf Englisch (Konsistenz
mit allen anderen Endpoints)
Findings aus REVIEW-2026-04-18.md (Quick-Wins 6+7) und dead-code.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>
Das Schema hatte positive() statt nonnegative() — damit schlug
jedes Save fehl, bei dem der Importer keine Portionsangabe finden
konnte und 0 eingesetzt hatte (z.B. bei rezeptwelt.de-Rezepten).
Alle anderen Int-Felder im gleichen Schema nutzen nonnegative()
konsistent; servings_default war der Ausreißer. DB-Spalte erlaubt
0 ohnehin, insertRecipe akzeptiert 0 → nur die PATCH-Validierung
hat unnötig blockiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neuer vierter Admin-Tab (Smartphone-Icon) mit drei Karten:
1. Installieren — fängt beforeinstallprompt (Android), zeigt
iOS-Teilen-Hinweis, sonst Info "nicht verfügbar".
2. Offline-Synchronisation — Status + "Jetzt synchronisieren"-
Button, disabled wenn offline.
3. Cache — "Offline-Cache leeren" löscht alle kochwas-*-Caches
via caches.keys() + delete.
install-prompt.svelte.ts hält das deferred-Event und die Plattform
(android/ios/other) per UA-Detection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>