Optimierung automatische Updates der Webanwendung bei Aktualisierung der Anwendung auf dem Server

This commit is contained in:
2026-08-10 11:04:00 +02:00
parent a917c5f64e
commit 7b99a2d4b5
5 changed files with 229 additions and 12 deletions

View File

@@ -1577,3 +1577,59 @@ select.category-select {
man sehen, ohne hinzusehen. */
select.market-select:not([data-empty]),
select.category-select:not([data-empty]) { border-color: var(--accent); }
/* ===================================================================
Hinweis auf eine neue Fassung
=================================================================== */
.update-banner {
position: fixed;
z-index: 80;
left: 1rem;
right: 1rem;
bottom: calc(1rem + env(safe-area-inset-bottom));
max-width: 28rem;
margin: 0 auto;
display: flex;
align-items: center;
gap: .6rem;
background: var(--card);
border: 1px solid var(--accent);
border-radius: var(--radius);
padding: .7rem .8rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, .22);
font-size: .92rem;
}
.update-text { flex: 1; min-width: 0; }
button.update-apply {
flex: 0 0 auto;
background: var(--accent);
color: var(--accent-fg);
border: none;
border-radius: var(--radius);
padding: .45rem .8rem;
font: inherit;
font-weight: 600;
cursor: pointer;
}
button.update-later {
flex: 0 0 auto;
background: none;
border: none;
color: var(--muted);
font-size: 1.2rem;
line-height: 1;
padding: .2rem .4rem;
cursor: pointer;
}
/* Über dem Sucher und über Dialogen hat der Hinweis nichts zu suchen. */
body.scanning .update-banner,
body.modal-open .update-banner { display: none; }
@media print { .update-banner { display: none !important; } }

View File

@@ -24,6 +24,7 @@ import { manageView } from "./views/manage.js";
import { closeMenu } from "./appbar.js";
import * as db from "./db.js";
import { installTriggers, stopWatching } from "./sync.js";
import { watchForUpdates } from "./update.js";
import { publicView } from "./views/public.js";
import { acceptInviteView, shareView } from "./views/share.js";
@@ -276,13 +277,9 @@ async function start() {
await route();
if ("serviceWorker" in navigator) {
try {
await navigator.serviceWorker.register("/sw.js", { scope: "/" });
} catch (err) {
console.warn("Service Worker nicht registriert:", err.message);
}
}
// Registriert den Service Worker und meldet sich, wenn eine neue
// Fassung bereitliegt.
watchForUpdates();
}
start();

119
web/html/js/update.js Normal file
View File

@@ -0,0 +1,119 @@
// Erkennt neue Fassungen der Anwendung und wendet sie an.
//
// Warum das nötig ist: Eine installierte PWA wird oft wochenlang nicht
// vollständig geschlossen. Der Browser lädt zwar den Service Worker im
// Hintergrund neu, aber der neue bleibt im Wartezustand, bis alle
// Fenster geschlossen sind - und die laufende Seite arbeitet weiter mit
// dem alten JavaScript. Für den Nutzer sieht es aus, als käme das
// Update nie an.
//
// Ablauf hier:
// 1. Regelmäßig nachfragen, ob eine neue Fassung bereitliegt
// 2. Wenn ja: Hinweis einblenden, nicht ungefragt neu laden
// 3. Auf Wunsch den Wechsel anfordern und danach EINMAL neu laden
"use strict";
import { el } from "./dom.js";
// Wie oft im laufenden Betrieb nachgefragt wird. Der Browser prüft von
// sich aus nur bei Navigationen - in einer installierten PWA kann das
// sehr lange dauern.
const CHECK_INTERVAL_MS = 30 * 60 * 1000;
let reloading = false;
let banner = null;
function showBanner(onApply) {
if (banner) return;
banner = el("div.update-banner", { role: "status" },
el("span.update-text", {},
"Eine neue Fassung der App ist bereit."),
el("button.update-apply", { type: "button", onclick: onApply },
"Jetzt neu laden"),
el("button.update-later", {
type: "button",
"aria-label": "Hinweis ausblenden",
title: "Später die neue Fassung wird beim nächsten Start aktiv",
onclick: () => {
banner.remove();
banner = null;
},
}, "×")
);
document.body.append(banner);
}
/**
* Registriert den Service Worker und überwacht Aktualisierungen.
*
* Gibt nichts zurück; Fehler werden geschluckt, weil eine fehlende
* Aktualisierungsprüfung die Anwendung nicht unbenutzbar machen darf.
*/
export async function watchForUpdates() {
if (!("serviceWorker" in navigator)) return;
let registration;
try {
registration = await navigator.serviceWorker.register("/sw.js", { scope: "/" });
} catch (err) {
console.warn("Service Worker nicht registriert:", err.message);
return;
}
/** Fordert den wartenden Service Worker auf, zu übernehmen. Das
* eigentliche Neuladen löst danach das controllerchange-Ereignis
* aus - so wird auch in anderen offenen Fenstern neu geladen. */
function apply() {
const waiting = registration.waiting;
if (!waiting) {
// Sollte nicht vorkommen; dann eben unmittelbar neu laden.
window.location.reload();
return;
}
waiting.postMessage({ type: "SKIP_WAITING" });
}
function considerWaiting() {
// Ohne controller ist es die Erstinstallation - da gibt es nichts
// zu ersetzen, und ein Hinweis wäre verwirrend.
if (registration.waiting && navigator.serviceWorker.controller) {
showBanner(apply);
}
}
// Beim Start: Liegt schon etwas bereit?
considerWaiting();
registration.addEventListener("updatefound", () => {
const incoming = registration.installing;
if (!incoming) return;
incoming.addEventListener("statechange", () => {
if (incoming.state === "installed") considerWaiting();
});
});
// Genau einmal neu laden, wenn der neue Service Worker übernimmt.
navigator.serviceWorker.addEventListener("controllerchange", () => {
if (reloading) return;
reloading = true;
window.location.reload();
});
const check = () => {
// Ohne Verbindung wäre die Abfrage nur ein Fehler im Protokoll.
if (!navigator.onLine) return;
registration.update().catch(() => {});
};
// Beim Zurückkehren in den Vordergrund - der häufigste Moment, in dem
// eine installierte PWA nach Tagen wieder benutzt wird.
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") check();
});
window.addEventListener("online", check);
setInterval(check, CHECK_INTERVAL_MS);
check();
}

View File

@@ -5,7 +5,7 @@
// Bei jeder Änderung an den Dateien unten hochzählen. Der Wert entscheidet,
// wann alte Zwischenspeicher verworfen werden.
const VERSION = "v30";
const VERSION = "v31";
const SHELL_CACHE = `einkaufsapp-shell-${VERSION}`;
const SHELL = [
@@ -18,6 +18,7 @@ const SHELL = [
"/js/dom.js",
"/js/store.js",
"/js/appbar.js",
"/js/update.js",
"/js/db.js",
"/js/sync.js",
"/js/push.js",
@@ -41,13 +42,28 @@ const SHELL = [
];
self.addEventListener("install", (event) => {
// KEIN skipWaiting hier.
//
// Vorher stand es an dieser Stelle - mit der Folge, dass der neue
// Service Worker sofort die Kontrolle übernahm, während die bereits
// geladene Seite mit dem alten JavaScript weiterlief. Ergebnis:
// gemischte Stände, und aktualisiert wurde trotzdem erst beim
// nächsten vollständigen Schließen der App.
//
// Jetzt wartet er, bis die Anwendung ihn ausdrücklich dazu auffordert
// (Nachricht "SKIP_WAITING") oder bis alle Fenster geschlossen sind.
event.waitUntil(
caches.open(SHELL_CACHE)
.then((cache) => cache.addAll(SHELL))
.then(() => self.skipWaiting())
caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL))
);
});
// Die Anwendung fordert den Wechsel an, sobald der Nutzer zugestimmt hat.
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys()