/*! trentpower.fr · /sw.js · generated · signed via /integrity.json */ var CACHE = 'tp-2026-05-09.fceaf1e4-f33ab2eb-edition-2026-05-09-perf-arch-rev2'; // Critical precache — pages, core CSS/JS, manifest, favicon. Failure // to cache any of these aborts install: an offline visit could not // render a coherent page without them. var CRITICAL_PRECACHE = [ '/', '/privacy/', '/integrity/', '/integrity/releases/', '/security/', '/security/acknowledgments/', '/verify/', '/source/', '/403.html', '/404.html', '/500.html', '/maintenance.html', '/styles.css', '/print.css', '/fonts-full.css', '/app.js', '/app-enhance.js', '/cite.js', '/i18n-core.js', '/verify/verify.js', '/verify/verification-data.js', '/manifest.webmanifest', '/favicon.svg' ]; // Optional precache — fonts, platform icons (apple-touch / 192 / 512), // architecture diagrams, QR codes. Failure to cache any of these is // logged but never breaks install. Pages render with the CSS fallback // stack if fonts are absent; missing platform icons only affect // install/share UI, not the site itself. var OPTIONAL_PRECACHE = [ '/fonts/subsets/signifier-light-hero.woff2', '/fonts/subsets/soehne-kraftig-nav.woff2', '/fonts/subsets/soehne-mono-buch-labels.woff2', '/fonts/signifier-light.woff2', '/fonts/signifier-light-italic.woff2', '/fonts/signifier-regular.woff2', '/fonts/signifier-regular-italic.woff2', '/fonts/soehne-buch.woff2', '/fonts/soehne-kraftig.woff2', '/fonts/soehne-mono-buch.woff2', '/fonts/soehne-mono-kraftig.woff2', '/favicon.ico', '/apple-touch-icon.png', '/icon-192.png', '/icon-512.png', '/images/architecture/architecture.svg', '/images/architecture/architecture-mobile.svg', '/images/qr/print-qr-trentpower.svg', '/images/qr/qr-home.svg', '/images/qr/qr-privacy.svg', '/images/qr/qr-integrity.svg', '/images/qr/qr-security.svg', '/images/qr/qr-source.svg', '/images/qr/qr-verify.svg', '/images/qr/qr-releases.svg' ]; // Combined surface — install · activate · fetch var PRECACHE = CRITICAL_PRECACHE.concat(OPTIONAL_PRECACHE); var NEVER_CACHE = [ '/integrity.json', '/integrity.json.sig', '/site-metadata.json', '/llms.txt', '/sw-reset/', '/sw-reset/index.html' ]; var NEVER_CACHE_PREFIX = [ '/.well-known/' ]; function canonicalPath(pathname) { if (pathname === '/') return '/'; if (pathname.indexOf('.') === -1 && pathname.charAt(pathname.length - 1) !== '/') { return pathname + '/'; } return pathname; } function isNeverCache(pathname) { if (NEVER_CACHE.indexOf(pathname) !== -1) return true; for (var i = 0; i < NEVER_CACHE_PREFIX.length; i++) { if (pathname.indexOf(NEVER_CACHE_PREFIX[i]) === 0) return true; } return false; } // Install — two-tier precache. // CRITICAL: fail loud. A single missing URL aborts install. Pages, // core CSS/JS, manifest, favicon — without these the offline // experience is incoherent. // OPTIONAL: best-effort. Each URL caches independently; failures are // logged via console.warn but never reject the install promise. // Fonts / platform icons / diagrams / QR codes — pages render // without them. function _putReload(cache, u) { return fetch(u, { cache: 'reload', credentials: 'same-origin' }) .then(function (r) { if (!r.ok) { throw new Error('precache failed: ' + u + ' (' + r.status + ')'); } return cache.put(u, r); }); } self.addEventListener('install', function (e) { e.waitUntil( caches.open(CACHE).then(function (cache) { // Critical first — Promise.all rejects on any single failure. return Promise.all(CRITICAL_PRECACHE.map(function (u) { return _putReload(cache, u); })).then(function () { // Optional second — each URL gets its own catch so optional // failures never reject the outer promise. Promise.all here // resolves once all optional fetches have either succeeded // or quietly failed. return Promise.all(OPTIONAL_PRECACHE.map(function (u) { return _putReload(cache, u).catch(function (err) { console.warn('[sw] optional precache skipped:', u, err && err.message); return null; }); })); }); }).then(function () { return self.skipWaiting(); }) ); }); // Activate — drop old caches, claim clients immediately so the next // navigation is served by this generation. self.addEventListener('activate', function (e) { e.waitUntil( caches.keys().then(function (names) { return Promise.all( names.filter(function (n) { return n !== CACHE; }) .map(function (n) { return caches.delete(n); }) ); }).then(function () { return self.clients.claim(); }) ); }); // Fetch — route by request type. self.addEventListener('fetch', function (e) { var url = new URL(e.request.url); if (e.request.method !== 'GET') return; if (url.origin !== self.location.origin) return; // Frozen archive assets: cache-first, immutable. if (/^\/integrity\/releases\/\d{4}-\d{2}\/assets\//.test(url.pathname)) { e.respondWith( caches.match(e.request, { ignoreSearch: true }).then(function (cached) { return cached || fetch(e.request).then(function (response) { var clone = response.clone(); caches.open(CACHE).then(function (cache) { cache.put(e.request, clone); }); return response; }); }) ); return; } // Network-only: identity + signed manifest + .well-known. if (isNeverCache(url.pathname)) return; // Navigation: network-first, fallback to cached canonical, then '/'. if (e.request.mode === 'navigate') { var cacheKey = canonicalPath(url.pathname); e.respondWith( fetch(e.request).then(function (response) { var clone = response.clone(); caches.open(CACHE).then(function (cache) { cache.put(cacheKey, clone); }); return response; }).catch(function () { return caches.match(cacheKey, { ignoreSearch: true }).then(function (cached) { return cached || caches.match('/', { ignoreSearch: true }); }); }) ); return; } // Active assets: network-first; cache fallback with ignoreSearch // so ?v={asset_version} cache-bust on the URL still resolves to // the bare precached entry on first offline visit. e.respondWith( fetch(e.request).then(function (response) { var clone = response.clone(); caches.open(CACHE).then(function (cache) { cache.put(e.request, clone); }); return response; }).catch(function () { return caches.match(e.request, { ignoreSearch: true }); }) ); });