- 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
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>
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>
pwaStore ($lib/client/pwa.svelte.ts):
- Hängt sich an navigator.serviceWorker.ready, hört auf updatefound und
setzt updateAvailable = true, sobald ein neuer SW im Status 'installed'
ist UND es einen aktiven controller gibt (= Update eines bestehenden
Tabs, nicht die erste Installation).
- Polling alle 30 Minuten via registration.update(), damit der User den
Toast auch sieht, wenn er die Seite lange offen hat ohne zu navigieren.
- reload() ruft location.reload(); dismiss() schließt den Toast nur.
UpdateToast.svelte:
- Schwarzer Pill-Toast unten zentriert, mit Text, grünem "Neu laden"-
Button (RefreshCw-Icon) und X zum Wegklicken.
- Slide-Up-Animation beim Erscheinen.
- Responsive: auf Mobile (<420px) wird's zum vollbreiten Banner statt
Pill.
Root-Layout mountet <UpdateToast /> direkt neben <ConfirmDialog />.
onMount ruft pwaStore.init().
Status-Check der Live-Instanz https://kochwas.siegeln.net:
- manifest.webmanifest wird korrekt als JSON ausgeliefert
- service-worker.js (3.4 KB) ist verfügbar
- iOS Apple-Meta-Tags + Android theme-color sind im HTML <head>
PWA selbst funktioniert also bereits; der Toast war das fehlende Teil
für transparente User-seitige Updates.