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).
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.
- 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>
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>
Fuegt das nullable Feld section_heading zur ingredient-Tabelle hinzu
(Migration 012), erweitert den Ingredient-Typ und aktualisiert alle drei
Return-Stellen in parseIngredient. Downstream-Sites (repository, Editor,
Tests) bleiben rot – werden in Task 2+ behoben.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
timeSummary-Formatierung in eine wiederverwendbare Component
gezogen. RecipeView liefert nur noch die drei Werte — zukuenftige
Call-Sites (Preview, Hover-Cards) koennen dieselbe Logik reusen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zubereitungs-Liste mit Add + Remove als Sub-Component. Parent steuert
nur noch den Wrapper und reicht steps + die zwei Callbacks rein.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
IngredientRow rendert eine einzelne editierbare Zutat-Zeile. DraftIng
und DraftStep sind jetzt in recipe-editor-types.ts, damit Parent und
Sub-Components auf dieselbe Form referenzieren.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Isoliert den Bild-Upload-Flow (File-Input, Preview, Entfernen-Dialog)
aus dem RecipeEditor. Parent haelt nur noch den <section>-Wrapper und
reicht recipe.id + image_path rein, kriegt Aenderungen per onchange
callback zurueck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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 _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>
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>
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>
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>
RecipeEditor war noch die letzte Stelle im UI, die das
handgeschriebene "if (!res.ok) { alertAction(...) }"-Pattern
benutzte, welches wir in review-fixes-2026-04-18 ueberall sonst
durch asyncFetch() ersetzt hatten.
Netto: -14 Zeilen, konsistenter Fehlermessage-Fallback (body.message
> res.status), eine Import-Zeile weniger (alertAction raus, asyncFetch
rein).
Gate: svelte-check clean, 184/184 Tests, Upload/Delete-Flow per
Hand zu testen beim naechsten Editor-Touch.
Refs docs/superpowers/plans/2026-04-19-post-review-roadmap.md Item H.
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>
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
Die drei [searxng]-Logs sind absichtlich produktiv (Hilfe beim
Debuggen 'warum wurde Domain X gefiltert?'). Kommentar dokumentiert
das, damit kein zukuenftiger Cleanup sie pauschal entfernt.
baseRecipe-Fixture bleibt in tests/integration/recipe-repository.test.ts —
nur dort verwendet, nicht dupliziert (Review-Annahme war falsch).
yauzl/@types/yauzl bleiben als Dependency — bereits in
session-handoff-2026-04-17.md (Phase 5b) und ARCHITECTURE.md
verankert.
Findings aus REVIEW-2026-04-18.md (Wave 5 Cleanup) und structure.md
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/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
- 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>
- Dekorativer GripVertical raus, stattdessen zwei Pfeil-Buttons (↑/↓)
pro Zeile. An erster/letzter Stelle sind die Buttons disabled.
- moveIngredient() vertauscht Zeile mit Nachbarn; simpel und
tastatur-/touch-freundlich ohne Drag-and-Drop-Abhängigkeit.
- Grid-Spalten von 1fr 90px (Zutat/Notiz) auf 1fr 1fr — beide Felder
sind jetzt gleich breit, wie im Family-Feedback gewünscht.
- Mobile-Layout behält gestaffelte Note-Zeile, Move-Spalte rutscht
als eigene Spalte links daneben.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auf dem 10"-Tablet war die Schrift in Zutaten und Zubereitung zu klein.
Im 2-Spalten-Layout (>=820px) bumpen wir jetzt:
- Zutaten-Zeilen und Step-Text auf 1.2rem (vorher 1rem)
- qty-Spalte breiter (6rem statt 5rem)
- Portionen-Zahl größer
- Step-Badge auf 2.4rem + 1.1rem Font
Mobile bleibt unverändert — Lesedistanz ist dort anders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nach dem DDG-Rauswurf war Mojeek die verbleibende Lärm-Quelle im Log:
HTTP 403 pro Suche, suspended_time=180. Mojeek hat nach eigenem Muster
Pi-IPs als automatisierten Traffic klassifiziert. Brave (API) deckt die
Websuche zuverlässig ab — Mojeek ist draußen, sowohl im searxng.ts-
Query (engines=brave) als auch in der SearXNG-keep_only-Liste.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zwei Fixes gegen die hartnäckigen DDG-CAPTCHA-Fehler im SearXNG-Log:
1. searxng.ts fragt jetzt explizit `engines=brave,mojeek` an.
Vorher wurde nur `categories=general` gesetzt — dadurch wurden
alle in dieser Kategorie aktivierten Engines abgefragt, inkl. DDG
(das trotz `disabled: true` weiter antwortete).
2. settings.yml nutzt `use_default_settings.engines.keep_only` statt
einzelner `disabled: true`-Overrides. SearXNGs Merge-Semantik für
partielle Engine-Overrides (nur name + disabled ohne engine:)
greift in der aktuellen Version nicht zuverlässig, deshalb kam
DDG durch. keep_only wirft alles außer brave+mojeek vor dem Laden
raus — kein Captcha-/403-Log-Lärm mehr.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
Der SW rief bisher im Install-Handler self.skipWaiting() auf —
der neue SW übersprang damit die "waiting"-Phase und aktivierte
sofort. pwaStore.onUpdateFound feuerte trotzdem auf statechange=
"installed" + vorhandenem controller und setzte updateAvailable=
true. Ergebnis: Toast erschien, obwohl der SW bereits übernommen
hatte, und der Klick auf "Neu laden" löste durch das Timing einen
neuen Update-Zyklus aus → Endlosschleife, v.a. im Incognito-Mode
wo jede Session neu installiert.
Jetzt klassisches Pattern: SW wartet in "installed"-Zustand bis
der User den Toast bestätigt; pwaStore.reload() postet
SKIP_WAITING an den wartenden SW, lauscht auf controllerchange
und reloadet dann erst. Ohne diese Trennung ist der Toast
semantisch kaputt.
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>
Neuer Helper requireOnline(action) prüft vor jedem Schreib-Fetch
den Online-Status. Offline: ein Toast erscheint ("Die Aktion braucht
eine Internet-Verbindung."), Aktion bricht sauber ab. Der Button-
State bleibt unverändert (kein optimistisches Update, das gleich
wieder zurückgedreht werden müsste).
Eingebaut in Rezept-Detail (8 Handler), Register (2), Wunschliste
(2), Admin Domains/Profile/Backup, Home-Dismiss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorher: saveCachedIds(currentIds) hat alle Server-IDs gespeichert,
auch wenn cacheRecipe still fehlschlug — beim nächsten sync-check
wurden die fehlenden übersprungen, offline gab's 404. Jetzt:
cacheRecipe + addToCache geben boolean zurück, nur die wirklich
erfolgreich gecachten IDs landen im Manifest. Bei Update-Sync wird
das Manifest aus (cached - toRemove + successful) berechnet.
Zusätzlich console.warn in addToCache, damit Cache-Misses auf dem
Pi debuggbar sind.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Message-Handler für sync-start (initial: alle Rezepte cachen) und
sync-check (delta: nur neue nachladen, gelöschte räumen). Vor dem
Sync ein Storage-Quota-Check (<100 MB frei → abbrechen mit Fehler-
Broadcast). Concurrency-Pool mit 4 parallelen Downloads pro
Rezept (HTML, API-JSON, Bild). Fortschritt per postMessage an
alle Clients, die über den sync-status-Store den SyncIndicator
füllen. Das Cache-Manifest wird als JSON-Response unter
/__cache-manifest__ im kochwas-meta Cache persistiert.
Client triggert beim App-Start entweder sync-check (bereits
kontrollierter SW) oder sync-start (erstmaliger SW-Install).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/service-worker.ts installiert die App-Shell-Assets (build +
files aus $service-worker) beim install-Event in kochwas-shell-
<version>, räumt alte Shell-Caches beim activate und dispatcht
jeden Fetch via resolveStrategy — shell/images cache-first, swr
stale-while-revalidate, network-only unangetastet. Pre-Cache-
Orchestrator kommt in Task 9.
Client-seitig registriert sw-register.ts den SW und verdrahtet
Messages vom SW in den sync-status-Store.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>