From f72fe64d8e0398b2e1dcd39cdc53f1fca4554a8b Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:38:00 +0200 Subject: [PATCH] feat(pwa): Update-Toast zeigt neue Version an MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 direkt neben . 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 PWA selbst funktioniert also bereits; der Toast war das fehlende Teil für transparente User-seitige Updates. --- src/lib/client/pwa.svelte.ts | 52 ++++++++++++ src/lib/components/UpdateToast.svelte | 110 ++++++++++++++++++++++++++ src/routes/+layout.svelte | 4 + 3 files changed, 166 insertions(+) create mode 100644 src/lib/client/pwa.svelte.ts create mode 100644 src/lib/components/UpdateToast.svelte diff --git a/src/lib/client/pwa.svelte.ts b/src/lib/client/pwa.svelte.ts new file mode 100644 index 0000000..bc125ac --- /dev/null +++ b/src/lib/client/pwa.svelte.ts @@ -0,0 +1,52 @@ +class PwaStore { + updateAvailable = $state(false); + private registration: ServiceWorkerRegistration | null = null; + private pollTimer: ReturnType | null = null; + + async init(): Promise { + if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return; + try { + this.registration = await navigator.serviceWorker.ready; + } catch { + return; + } + if (!this.registration) return; + + // Wenn beim Mount schon ein neuer SW installiert und aktiv wartet, + // zeigen wir den Toast direkt an. + if (this.registration.waiting) { + this.updateAvailable = true; + } + + this.registration.addEventListener('updatefound', () => this.onUpdateFound()); + + // Alle 30 Minuten aktiv nach Updates fragen, damit der User sie auch + // mitbekommt, wenn er die Seite lange offen lässt ohne zu navigieren. + this.pollTimer = setInterval(() => { + void this.registration?.update().catch(() => {}); + }, 30 * 60_000); + } + + private onUpdateFound(): void { + const installing = this.registration?.installing; + if (!installing) return; + installing.addEventListener('statechange', () => { + // 'installed' UND ein laufender controller = Update für bestehenden Tab. + // (Ohne controller wäre das die erste Installation, kein Update.) + if (installing.state === 'installed' && navigator.serviceWorker.controller) { + this.updateAvailable = true; + } + }); + } + + reload(): void { + this.updateAvailable = false; + location.reload(); + } + + dismiss(): void { + this.updateAvailable = false; + } +} + +export const pwaStore = new PwaStore(); diff --git a/src/lib/components/UpdateToast.svelte b/src/lib/components/UpdateToast.svelte new file mode 100644 index 0000000..bcf383b --- /dev/null +++ b/src/lib/components/UpdateToast.svelte @@ -0,0 +1,110 @@ + + +{#if pwaStore.updateAvailable} +
+ Neue Kochwas-Version verfügbar + + +
+{/if} + + diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 0866220..2d247b4 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,9 +5,11 @@ import { Settings, CookingPot, Globe, Utensils } from 'lucide-svelte'; import { profileStore } from '$lib/client/profile.svelte'; import { wishlistStore } from '$lib/client/wishlist.svelte'; + import { pwaStore } from '$lib/client/pwa.svelte'; import ProfileSwitcher from '$lib/components/ProfileSwitcher.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import SearchLoader from '$lib/components/SearchLoader.svelte'; + import UpdateToast from '$lib/components/UpdateToast.svelte'; import type { SearchHit } from '$lib/server/recipes/search-local'; import type { WebHit } from '$lib/server/search/searxng'; @@ -112,6 +114,7 @@ onMount(() => { profileStore.load(); void wishlistStore.refresh(); + void pwaStore.init(); document.addEventListener('click', handleClickOutside); document.addEventListener('keydown', handleKey); return () => { @@ -122,6 +125,7 @@ +