Commit Graph

89 Commits

Author SHA1 Message Date
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
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
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
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
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
d9490c8073 refactor(search): local search ignores domain filter
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 3m11s
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>
2026-04-21 21:59:48 +02:00
hsiegeln
3bc7fa16e2 feat(photo-upload): Limits hochschrauben fuer Tablet-Fotos
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m16s
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>
2026-04-21 13:31:34 +02:00
hsiegeln
6dab36339a test(e2e): Foto-Import Happy-Path und Offline-Icon 2026-04-21 10:50:01 +02:00
hsiegeln
bc42f35f8c feat(client): PhotoUploadStore mit idle/loading/success/error 2026-04-21 10:45:36 +02:00
hsiegeln
8c23875ba2 feat(editor): Bild-Block skip wenn recipe.id === null 2026-04-21 10:44:48 +02:00
hsiegeln
06e60afc88 feat(api): POST /api/recipes fuer Scratch-Insert aus Foto-Import 2026-04-21 10:43:30 +02:00
hsiegeln
e01f15a2a6 feat(api): POST /api/recipes/extract-from-photo 2026-04-21 10:42:46 +02:00
hsiegeln
3f259a7870 feat(ai): simpler In-Memory-Ratelimiter pro IP 2026-04-21 10:41:16 +02:00
hsiegeln
904edcb3ff feat(ai): Gemini-Client mit Timeout, 1x-Retry und Fehler-Codes 2026-04-21 10:40:58 +02:00
hsiegeln
d479fd61d8 feat(ai): Extraction-Prompt + Gemini-Schema + Zod-Validator 2026-04-21 10:40:03 +02:00
hsiegeln
0cca9a699c feat(ai): image-preprocess mit sharp (Resize + JPEG + EXIF-Strip) 2026-04-21 10:39:22 +02:00
hsiegeln
c284f4b85b feat(ai): 50er-Pool Magie-Phrasen fuer Foto-description 2026-04-21 10:38:32 +02:00
hsiegeln
633e497bdc fix(sw): network-first + 3s timeout statt SWR fuer Daten
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 30s
SWR lieferte bei jedem Cache-Hit sofort die alte Antwort und
aktualisierte das Cache nur fuer den naechsten Request. Folge:
UI zeigte stale Daten, frische Daten erst nach Refresh.

Neu: network-first mit 3 s Timeout-Fallback. Netz gewinnt bei
frischer Antwort; Timeout oder Netzwerk-Fehler fallen auf Cache
zurueck. Pre-Cache-Logik (runSync) bleibt unveraendert, Shell
und Bilder bleiben cache-first.
2026-04-20 08:29:00 +02:00
hsiegeln
6bde3909d8 polish(sections): Muelltonne statt X + Ueberschrift groesser/fetter
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m22s
- IngredientRow: Sektion-entfernen-Button nutzt Trash2 (konsistent
  mit dem Zutat-Entfernen-Button daneben)
- RecipeView: section-heading von 1rem/600 auf 1.2rem/700, mehr
  vertikaler Abstand fuer deutlichere optische Trennung
- E2E-Spec: type-inference-Trick durch APIRequestContext-Import
  ersetzt (svelte-check stolperte bei typeof test mit TestDetails-
  Overload)
- Plan-Datei der Feature-Session mitcommitet

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:26:39 +02:00
hsiegeln
c07d2f99ad test(e2e): Zutaten-Sektionen CRUD + UI-Flow auf kochwas-dev
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 40s
4 new remote specs: API roundtrip, editor add-section + view render,
section remove, empty heading -> null on save. All 46 pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 15:19:13 +02:00
hsiegeln
96cb55495e test(scaler): section_heading ueberlebt Skalierung
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 15:00:21 +02:00
hsiegeln
a1baf7f30a feat(db): section_heading roundtrip in recipe-repository
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>
2026-04-19 14:55:46 +02:00
hsiegeln
c45ef2a613 fix(search): runSearch bricht pending Debounce ab
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m20s
Enter waehrend Debounce-Fenster feuerte bislang eine zweite Fetch
fuer dieselbe Query. Race-Guard greift nicht, weil q identisch ist.
runSearch clearTimeout am Anfang behebt's, neuer Unit-Test sichert es.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:03:42 +02:00
hsiegeln
4edddc38e3 refactor(search): runDebounced ohne missweisenden Parameter
Der _q-Parameter wurde nie benutzt — Consumer sollen stattdessen
store.query im \$effect lesen, dann runDebounced() callen. Weniger
Footgun, explizitere Call-Site.

Tests-Rename: "mid-flight" → "cleared/changed", beschreibt was der
Test tatsaechlich absichert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:47:40 +02:00
hsiegeln
fc47c78397 fix(search): Race-Guard-Test korrekt auf in-flight abzielen
Der vorherige Test setzte query NACH dem Fetch-Abschluss und erzwang
dafuer einen setter-Side-Effect, der bei normalem Tippen die Treffer
waehrend des Debounce-Fensters fuer 300ms leer geblitzt haette.

Jetzt: echter Race-Test mit manuell aufloesbarem fetch. Setter-Nebenwirkung
entfernt, query ist wieder plain \$state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:41:43 +02:00
hsiegeln
58ce19c160 feat(search): SearchStore fuer Live-Search mit Web-Fallback
Extrahiert die duplizierte Such-Logik aus +page.svelte und
+layout.svelte in eine gemeinsame Klasse. Pure Datenschicht
mit injizierbarem fetch — UI-Concerns (URL-Sync, Dropdown,
Snapshot) bleiben in den Komponenten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:38:33 +02:00
hsiegeln
3021ccb6a9 fix(e2e): 3 Specs robuster gegen reale Runtime
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 31s
- comments: Loeschen-Button im ConfirmDialog war ambig (3 Matches —
  Rezept-Delete, Kommentar-Trash, Dialog-Bestaetigung). Locator auf
  getByRole('dialog', { name: /Kommentar löschen/i }) eingeschraenkt.
- recipe-detail Portionen: getByText(/\b750 g/) trifft nicht wegen
  Whitespace-Layout im <span class="qty">. Auf
  locator('.ing-list li', { hasText: 'Hähnchenbrustfilet' })
  .toContainText('750 g') umgestellt — robust gegenueber Svelte-
  Whitespace-Quirks.
- search empty-state: SearXNG matcht loose, "truly empty" ist nicht
  zuverlaessig reproduzierbar. Test akzeptiert jetzt "Empty-State ODER
  Web-Fallback" und prueft zusaetzlich, dass kein JS-Error fliegt.

admin/backup war eine transiente Flake — 15 Repeat-Runs alle gruen,
kein Code-Fix noetig.

Gate: 12/12 der geaenderten Specs passed local.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:21:36 +02:00
hsiegeln
a7ad159c69 test(e2e): Playwright Smoketests gegen kochwas-dev (remote)
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m42s
Automatisierte End-to-End-Tests gegen ein deployed Environment. Loest
die manuellen MCP-Playwright-Runs ab. 42 Tests in 9 Files:

- homepage: H1, Sektionen, Sort-Tabs, Console-Errors
- search: lokaler Treffer, Web-Fallback, Empty-State, Deep-Link
- profile: Switcher, Auswahl-Persistenz, Favoriten-Section, Guard-Dialog
- recipe-detail: Header, Portionen-Scaling (4->6), Favorit-Toggle,
  Rating-Persistenz ueber Reload, Gekocht-Counter, Wunschliste-Toggle
- comments: eigenen erstellen+loeschen via UI, fremder hat kein Delete
- wishlist: Seite, Sort-Tabs, Badge-Sync, requireProfile-Custom-Message
- preview: Guard ohne ?url=, echte URL parst, unparsbare zeigt error-box
- admin: alle 4 Subrouten + /admin redirect
- api-errors: parsePositiveIntParam (4x Invalid id), validateBody (4x
  Invalid body + issues), 404, Sanity /health /profiles /domains

Architektur:
- Separate playwright.remote.config.ts (getrennt von local preview)
- workers: 1 + afterEach API-Cleanup (rating, favorite, wishlist, comments)
- Hardcoded Recipe-ID 66 + Profile 1/2/3 — stabile Dev-DB-Seeds
- E2E_REMOTE_URL ueberschreibt die Ziel-URL

Ausfuehrung: npm run test:e2e:remote

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:14:04 +02:00
hsiegeln
6d9e79d4f0 feat(parser): Unicode-Brueche + Mengen-Plausibilitaet
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
2026-04-18 22:25:35 +02:00
hsiegeln
739cc2d058 feat(server): api-helpers fuer parsePositiveIntParam + validateBody
- 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
2026-04-18 22:16:00 +02:00
hsiegeln
854af2fc34 fix(pwa): Reload-Loop beim Zombie-Cleanup beseitigt
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 29s
Der Commit 1bec054 hatte einen globalen controllerchange-Listener in
init(), der bei jedem Event location.reload() auslöste. In Kombination
mit der Zombie-Aufräumung (silent SKIP_WAITING) ergab das einen
Endlos-Loop: Seite lädt → Zombie erkannt → SKIP_WAITING → controller-
change → Reload → neue Seite mit frischem Zombie → usw.

Fix: Der controllerchange-Listener wird nur noch scoped aus reload()
heraus gesetzt ({ once: true }) — also genau dann, wenn der User auf
„Neu laden" geklickt hat und einen Reload tatsächlich will. Beim
silent Zombie-Cleanup gibt es keinen Listener, die Seite läuft
einfach nahtlos unter dem neuen (funktional identischen) SW weiter.

Regression-Test sichert ab, dass fireControllerChange() nach silent
SKIP_WAITING location.reload() NICHT aufruft.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:12:19 +02:00
hsiegeln
1bec054ec6 fix(pwa): Zombie-waiting-SW via GET_VERSION erkennen (Live-Bug)
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m21s
Das reine Workbox-Handshake-Pattern aus c2074c9 reicht für dieses
Deploy nicht. Live-Analyse mit Playwright ergibt reproduzierbar nach
dem Reload-Klick:
- active-SW: Version 1776527907402
- waiting-SW: Version 1776527907402 (bit-identisch!)
- Nur ein einziger shell-Cache
- Server-Response: gleiche Version
→ Toast kommt bei jedem Reload erneut.

Vermutung: Race zwischen Chromium-SW-Update-Check (der parallel
zum SKIP_WAITING läuft) und activate. Der Browser hält den zweiten
Installation-Versuch mit identischen Bytes im waiting-Slot.

Fix: SW bekommt GET_VERSION-Handler, Client fragt via MessageChannel
active und waiting nach Version. Bei Gleichheit räumt er den Zombie
stumm auf (SKIP_WAITING ohne Toast), bei Versions-Unterschied
zeigt er den Toast. Der refreshing-Flag-Reload-Guard aus c2074c9
bleibt erhalten.

Industry-Standard-Pattern bleibt die Basis; GET_VERSION ist ein
defensiver Zusatz für einen reproduzierbaren Browser-Edge-Case,
den Workbox nicht abfängt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:06:36 +02:00
hsiegeln
c2074c9768 refactor(pwa): auf Workbox-Standard vereinfacht, refreshing-Flag
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
Der Zombie-Version-Check (858d4c1) ging über das Standard-Handshake-
Pattern hinaus. User will Industry-Standard: Workbox/web.dev-Pattern
ohne GET_VERSION-Sonderlocke.

Änderungen:
- service-worker.ts: GET_VERSION-Handler entfernt. SW reagiert nur
  noch auf SKIP_WAITING.
- pwa.svelte.ts: queryVersion + evaluateWaiting entfernt. init()
  zeigt Toast wieder schlicht bei registration.waiting (das ist
  kanonisch — bit-gleiche Bytes erzeugen keinen waiting-Slot).
- controllerchange-Listener wandert nach init() mit refreshing-Flag
  (CRA-Idiom): verhindert Doppel-Reload, wenn User zusätzlich F5
  drückt, und stellt sicher, dass der Listener in _jeder_ Session
  aktiv ist, nicht erst nach dem ersten reload()-Call.
- pwa-store.test.ts: Tests decken jetzt waiting→Toast, no-waiting→
  kein Toast, Handshake, refreshing-Flag und Sofort-Reload ab.

Der Zombie-Edge-Case (Browser-Quirk mit bit-identischem waiting-SW)
wird sich nach einmaligem Klick auflösen — erwarteter Trade-off
gegenüber der eingesparten Komplexität.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:57:51 +02:00
hsiegeln
858d4c1622 fix(pwa): Zombie-Waiting-SW erkennen und stumm aufräumen
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m23s
Der vorige Fix (3d6f639) hat den Endlos-Toast "Neue Kochwas-Version
verfügbar" im Happy-Path beseitigt, aber das Grundproblem blieb:
pwaStore.init() hat blind `registration.waiting` als Update-Signal
verwendet.

Beobachtet auf der Live-PWA: Nach dem Reload-Klick existiert
registration.waiting weiter — als bit-identischer Zombie zum aktiven
SW (nur ein einziger shell-Cache `kochwas-shell-<version>`, Server-
Fetch liefert dieselbe Version-Konstante wie der active-SW). Der
Browser räumt diesen waiting-Slot nicht von selbst auf. Ergebnis:
beim nächsten init() steht `registration.waiting` wieder, Toast
kommt wieder.

Fix: SW bekommt einen GET_VERSION-MessageHandler. pwaStore fragt
active und waiting per MessageChannel nach ihrer Version. Sind sie
gleich, schickt er SKIP_WAITING silent an den Zombie und zeigt
KEINEN Toast. Nur bei echter Versions-Differenz erscheint das Update-
Angebot. Der alte onUpdateFound-Pfad geht den gleichen Weg.

Regression-Test: tests/unit/pwa-store.test.ts deckt Zombie-, Echt-
Update- und Fallback-Fall (alter SW ohne GET_VERSION) ab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:48:55 +02:00
hsiegeln
1a4f7b5f20 test(pwa): E2E für Offline-Navigation, -Toast, -Indikator
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 28s
Playwright-Spec mit drei Tests:
- Offline-Navigation zu einem Rezept-Detail (Cache-Lesefall)
- Schreib-Aktion offline zeigt Toast (Favorit-Klick → Fehler-Toast)
- SyncIndicator zeigt "Offline"-Pill bei deaktiviertem Netzwerk

Seed-Fixture legt per /api/recipes/blank ein Rezept an, falls die
DB leer ist. waitForSync pollt navigator.serviceWorker.ready und
wartet zusätzlich 3 s für den initialen Pre-Cache.

Profil-Fixture (worker-scoped) erstellt bei Bedarf ein Test-Profil
und setzt es per addInitScript in localStorage, damit der Favorit-
Button den requireOnline-Guard erreicht (statt alertAction-Dialog).

SyncIndicator-Test ohne Reload: network.svelte.ts lauscht direkt auf
den 'offline'-Browser-Event, der bei context.setOffline(true) feuert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:07:21 +02:00
hsiegeln
528508a304 chore(test): Playwright für PWA-E2E-Tests aufgesetzt
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m33s
@playwright/test + chromium als devDep. playwright.config.ts
startet npm run preview (production build nötig für SW), baseURL
localhost:4173, fullyParallel off (SW-Installations-Timing). Ein
Smoke-Test smoke.spec.ts öffnet / und prüft den Titel. test-results/
und playwright-report/ in .gitignore ergänzt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:01:39 +02:00
hsiegeln
7c8edb9b92 feat(pwa): Cache-Manifest-Diff-Funktion + Tests
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
Pure Funktion diffManifest(current, cached) → {toAdd, toRemove}.
Vom SW beim Update-Sync genutzt: neue Rezept-IDs nachladen,
gelöschte aus dem Cache räumen. 5 Tests decken add/remove/
beides/unchanged/empty-cache ab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:34:39 +02:00
hsiegeln
d38992661c feat(pwa): Cache-Strategy-Entscheider + Unit-Tests
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Reine TS-Funktion resolveStrategy({url, method}) → 'shell' | 'swr'
| 'images' | 'network-only'. Kernregel:
  - Schreib-Methoden + import/preview/search/web → network-only
  - /images/* → images (cache-first)
  - /_app/, manifest, Icons, favicon, robots → shell (cache-first)
  - Alles andere same-origin-GET → swr
7 Tests decken alle Buckets ab. Wird vom SW in Task 8 aufgerufen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:32:30 +02:00