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 @@
+