From a9d2cf3054010f7f4121e75a66ba4dd86a82546c Mon Sep 17 00:00:00 2001 From: mhoennig Date: Thu, 23 Jul 2026 06:37:30 +0200 Subject: [PATCH] =?UTF-8?q?frontend:=20Service=20Worker=20f=C3=BCr=20Updat?= =?UTF-8?q?e-Detection=20hinzuf=C3=BCgen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Service Worker prüft periodisch auf neue Versionen (jede Minute) - Bei verfügbarem Update: Benachrichtigung oben auf der Seite - "Jetzt laden" Button triggert page reload - Benachrichtigung verschwindet nach 30s oder bei Klick - Service Worker als Blob registriert (Single-File-App kompatibel) - localStorage-Daten bleiben erhalten beim Reload Co-Authored-By: Claude Haiku 4.5 --- frontend/src/app.js | 146 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/frontend/src/app.js b/frontend/src/app.js index bf6b0ba..eedd8da 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -1192,3 +1192,149 @@ mountBuildBadge(); updateButtonVisibility(); /* Initial call */ } })(); + +/* ---------- Service Worker für Update-Detection ---------- */ +if('serviceWorker' in navigator){ + /* Service Worker als Blob registrieren (Single-File-App) */ + const swCode = ` + const CACHE_NAME = 'werkbaum-v1'; + self.addEventListener('install', (e) => self.skipWaiting()); + self.addEventListener('activate', (e) => e.waitUntil(clients.claim())); + + self.addEventListener('message', async (e) => { + if(e.data?.type === 'CHECK_UPDATE') { + try { + const response = await fetch(self.location.href, { cache: 'no-store' }); + const newContent = await response.text(); + const stored = await getStored(); + if(stored && stored !== newContent) { + notifyClients({ type: 'UPDATE_AVAILABLE' }); + } + await storeContent(newContent); + } catch(err) { console.error('Update-Check:', err); } + } + }); + + function notifyClients(msg) { + self.clients.matchAll().then(clients => + clients.forEach(c => c.postMessage(msg)) + ); + } + + async function getStored() { + try { + const db = await new Promise((r, x) => { + const req = indexedDB.open('werkbaum-updates', 1); + req.onerror = () => x(req.error); + req.onsuccess = () => r(req.result); + req.onupgradeneeded = (e) => { + if(!e.target.result.objectStoreNames.contains('content')) { + e.target.result.createObjectStore('content', { keyPath: 'id' }); + } + }; + }); + return new Promise((r, x) => { + const t = db.transaction('content', 'readonly'); + const req = t.objectStore('content').get('index'); + req.onerror = () => x(req.error); + req.onsuccess = () => r(req.result?.data); + }); + } catch(e) { return null; } + } + + async function storeContent(content) { + try { + const db = await new Promise((r, x) => { + const req = indexedDB.open('werkbaum-updates', 1); + req.onerror = () => x(req.error); + req.onsuccess = () => r(req.result); + }); + return new Promise((r, x) => { + const t = db.transaction('content', 'readwrite'); + const req = t.objectStore('content').put({ id: 'index', data: content }); + req.onerror = () => x(req.error); + req.onsuccess = () => r(); + }); + } catch(e) {} + } + `; + + const blob = new Blob([swCode], { type: 'application/javascript' }); + const swUrl = URL.createObjectURL(blob); + + navigator.serviceWorker.register(swUrl).then((registration) => { + /* Periodisch auf Updates prüfen */ + setInterval(() => { + if(registration.active){ + registration.active.postMessage({ type: 'CHECK_UPDATE' }); + } + }, 60000); /* Jede Minute */ + + /* Starte erste Prüfung nach 5 Sekunden */ + setTimeout(() => { + if(registration.active){ + registration.active.postMessage({ type: 'CHECK_UPDATE' }); + } + }, 5000); + }).catch((error) => { + console.error('Service Worker Registration fehlgeschlagen:', error); + }); + + /* Höre auf Update-Benachrichtigungen vom Service Worker */ + navigator.serviceWorker.addEventListener('message', (event) => { + if(event.data && event.data.type === 'UPDATE_AVAILABLE'){ + showUpdateNotification(); + } + }); +} + +function showUpdateNotification(){ + /* Entferne alte Benachrichtigung, falls vorhanden */ + const existingNotif = document.getElementById('updateNotification'); + if(existingNotif) existingNotif.remove(); + + /* Erstelle Benachrichtigungselement */ + const notif = document.createElement('div'); + notif.id = 'updateNotification'; + notif.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + background: var(--or, #0F766E); + color: white; + padding: 12px 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + z-index: 1000; + font-size: 14px; + font-family: 'IBM Plex Sans', system-ui, sans-serif; + `; + + notif.innerHTML = ` + 📦 Neue Version verfügbar + + `; + + document.body.insertBefore(notif, document.body.firstChild); + + document.getElementById('updateBtn').addEventListener('click', () => { + window.location.reload(); + }); + + /* Auto-dismiss nach 30 Sekunden wenn nicht geklickt */ + setTimeout(() => { + if(notif.parentNode) notif.remove(); + }, 30000); +}