feat: PWA update strategy with hashed assets and cache control
Move frontend sources to frontend/, build content-hashed assets at deploy, network-first SW for shell/JS, immutable cache for /assets/, version.json update banner, and Cache-Control middleware. Docs in docs/PWA-STRATEGY.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
let deferredInstallPrompt = null;
|
||||
|
||||
function isStandalone() {
|
||||
return window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true;
|
||||
}
|
||||
|
||||
export function wirePwaInstall({ sectionId = "pwaInstallSection", buttonId = "installPwa" } = {}) {
|
||||
const section = document.getElementById(sectionId);
|
||||
const button = document.getElementById(buttonId);
|
||||
if (!section || !button) return;
|
||||
|
||||
const hide = () => { section.hidden = true; };
|
||||
if (isStandalone()) { hide(); return; }
|
||||
hide();
|
||||
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
e.preventDefault();
|
||||
deferredInstallPrompt = e;
|
||||
section.hidden = false;
|
||||
});
|
||||
|
||||
window.addEventListener("appinstalled", () => {
|
||||
deferredInstallPrompt = null;
|
||||
hide();
|
||||
});
|
||||
|
||||
button.addEventListener("click", async () => {
|
||||
if (!deferredInstallPrompt) return;
|
||||
deferredInstallPrompt.prompt();
|
||||
await deferredInstallPrompt.userChoice;
|
||||
deferredInstallPrompt = null;
|
||||
hide();
|
||||
});
|
||||
}
|
||||
|
||||
export async function registerPwa() {
|
||||
if (!("serviceWorker" in navigator)) return;
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.register("/sw.js");
|
||||
if ("sync" in ServiceWorkerRegistration.prototype) {
|
||||
navigator.serviceWorker.ready.then((readyReg) => {
|
||||
readyReg.sync?.register("sync-logs").catch(() => {});
|
||||
});
|
||||
}
|
||||
return reg;
|
||||
} catch (e) {
|
||||
console.error("SW registration failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function subscribePush(vapidPublicKey) {
|
||||
if (!("PushManager" in window)) throw new Error("Push nicht unterstützt");
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
let sub = await reg.pushManager.getSubscription();
|
||||
if (!sub) {
|
||||
sub = await reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
|
||||
});
|
||||
}
|
||||
return sub.toJSON();
|
||||
}
|
||||
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||
const raw = atob(base64);
|
||||
return Uint8Array.from([...raw].map((c) => c.charCodeAt(0)));
|
||||
}
|
||||
Reference in New Issue
Block a user