diff --git a/frontend/public/sw.js b/frontend/public/sw.js index f31782d..965692c 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1,24 +1,56 @@ -// Service Worker – erkennt neue Builds und lädt automatisch neu -// BUILD_TIME wird beim Deploy vom Backend via /api/build-time geliefert +// Service Worker – Network-first für HTML, kein Cache für Assets +// Stellt sicher dass nach einem Deploy immer der neue Stand geladen wird -const CACHE_NAME = 'dickendock-v1'; +const CACHE_NAME = 'dickendock-v2'; self.addEventListener('install', () => { - self.skipWaiting(); // sofort aktivieren + self.skipWaiting(); // sofort aktivieren, nicht auf Tab-Schließen warten }); self.addEventListener('activate', e => { - // Alte Caches löschen + // Alle alten Caches löschen e.waitUntil( caches.keys() .then(keys => Promise.all(keys.map(k => caches.delete(k)))) - .then(() => self.clients.claim()) + .then(() => self.clients.claim()) // alle offenen Tabs übernehmen ); }); -// Alle Requests direkt ans Netzwerk – kein Caching self.addEventListener('fetch', e => { - e.respondWith(fetch(e.request)); + const url = new URL(e.request.url); + + // API-Requests immer direkt ans Netzwerk, nie cachen + if (url.pathname.startsWith('/api/')) { + e.respondWith(fetch(e.request)); + return; + } + + // index.html und Navigation: Network-first, kein Cache + // Damit nach einem Deploy immer die neue Version geladen wird + if (e.request.mode === 'navigate' || + url.pathname === '/' || + url.pathname === '/index.html') { + e.respondWith( + fetch(e.request, { cache: 'no-store' }) + .catch(() => caches.match('/index.html')) // Fallback wenn offline + ); + return; + } + + // JS/CSS/Assets: Network-first mit Cache-Fallback + // Vite baut mit Content-Hash im Dateinamen → neue Version = neue URL + e.respondWith( + fetch(e.request) + .then(response => { + // Erfolgreiche Antwort kurz cachen als Offline-Fallback + if (response.ok) { + const clone = response.clone(); + caches.open(CACHE_NAME).then(cache => cache.put(e.request, clone)); + } + return response; + }) + .catch(() => caches.match(e.request)) + ); }); // Auf Update-Nachricht vom Frontend reagieren diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index e8e9e9d..84e380f 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -2,7 +2,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' -// ── Auto-Reload bei neuem Build – nur beim Seitenwechsel ───────────────────── +// ── Auto-Reload bei neuem Build ─────────────────────────────────────────────── const STORED_KEY = 'dd_build_time'; let newBuildAvailable = false; @@ -15,7 +15,7 @@ async function checkBuildTime() { localStorage.setItem(STORED_KEY, buildTime); } else if (stored !== buildTime) { localStorage.setItem(STORED_KEY, buildTime); - newBuildAvailable = true; // merken, aber noch nicht reloaden + newBuildAvailable = true; } } catch {} } @@ -32,17 +32,25 @@ export function reloadIfNewBuild() { // ── Service Worker ──────────────────────────────────────────────────────────── if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(reg => { - if (reg.waiting) reg.waiting.postMessage('SKIP_WAITING'); + // Falls schon ein wartender SW da ist → sofort aktivieren + if (reg.waiting) { + reg.waiting.postMessage('SKIP_WAITING'); + } + reg.addEventListener('updatefound', () => { const nw = reg.installing; nw?.addEventListener('statechange', () => { - if (nw.state === 'installed' && navigator.serviceWorker.controller) { + if (nw.state === 'installed') { + // Neuer SW installiert → sofort aktivieren nw.postMessage('SKIP_WAITING'); } }); }); + + // Wenn SW-Controller gewechselt hat → sofort neu laden + // (nicht erst beim nächsten Seitenwechsel warten) navigator.serviceWorker.addEventListener('controllerchange', () => { - newBuildAvailable = true; // SW-Update → beim nächsten Wechsel laden + window.location.reload(); }); }); } diff --git a/frontend/src/tools/devtools.jsx b/frontend/src/tools/devtools.jsx index d99c32c..1b62d05 100644 --- a/frontend/src/tools/devtools.jsx +++ b/frontend/src/tools/devtools.jsx @@ -1896,12 +1896,319 @@ function RegexBuilder({ toast, mobile }) { } +// ──────────────────────────────────────────────────────────── JSON Tool ── + +// ── JSON Viewer: rekursiver Baum mit auf/zuklappen ── +function JsonNode({ data, depth, keyName }) { + const [open, setOpen] = useState(depth < 2); + const indent = depth * 16; + const isObj = data !== null && typeof data === 'object' && !Array.isArray(data); + const isArr = Array.isArray(data); + const isPrim = !isObj && !isArr; + + const typeColor = { + string: '#a8f0eb', + number: '#ffe66d', + boolean: '#ff6b9d', + null: 'rgba(255,255,255,0.3)', + }; + + function valColor(v) { + if (v === null) return typeColor.null; + return typeColor[typeof v] || '#fff'; + } + + function valLabel(v) { + if (v === null) return 'null'; + if (typeof v === 'string') return `"${v}"`; + return String(v); + } + + function typeTag(v) { + if (v === null) return 'null'; + if (Array.isArray(v)) return `Array[${v.length}]`; + if (typeof v === 'object') return `Object{${Object.keys(v).length}}`; + return typeof v; + } + + const keyStyle = {color:'#7eb8ff', fontFamily:"'Courier New',monospace", fontSize:12}; + const lineBase = {display:'flex', alignItems:'baseline', gap:4, padding:'1px 0', + marginLeft:indent, lineHeight:1.7}; + + if (isPrim) { + return ( +
+ {generatedJson}
+
+