From d0ead09f80fad360211c6be37a4f4979ed86e663 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 23:05:26 -0400 Subject: [PATCH] feat: add PWA manifest and service worker (#485) - Add manifest.json with app name, theme color, display=fullscreen, and icon references - Add sw.js with cache-first strategy for assets and network-first for API calls - Update index.html with and SW registration script - Cache key nexus-v3, precaches core assets from jsdelivr CDN - Network-first for Gitea API and WebSocket requests Fixes #485 --- index.html | 8 +++++ manifest.json | 20 +++++++++++ sw.js | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 manifest.json create mode 100644 sw.js diff --git a/index.html b/index.html index dd4d42d..a3eee3c 100644 --- a/index.html +++ b/index.html @@ -19,6 +19,8 @@ The Nexus — Timmy's Sovereign Home + + @@ -172,6 +174,12 @@ + +
{ + event.waitUntil( + caches.open(ASSET_CACHE).then((cache) => cache.addAll(CORE_ASSETS)) + .then(() => self.skipWaiting()) + ); +}); + +// Activate: clean up old caches +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all( + keys + .filter((key) => key !== CACHE_NAME && key !== ASSET_CACHE) + .map((key) => caches.delete(key)) + ) + ).then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + // Network-first for API calls (Gitea / WebSocket upgrades / portals.json live data) + if ( + url.pathname.startsWith('/api/') || + url.hostname.includes('143.198.27.163') || + request.headers.get('Upgrade') === 'websocket' + ) { + event.respondWith(networkFirst(request)); + return; + } + + // Cache-first for everything else (local assets + CDN) + event.respondWith(cacheFirst(request)); +}); + +async function cacheFirst(request) { + const cached = await caches.match(request); + if (cached) return cached; + + try { + const response = await fetch(request); + if (response.ok) { + const cache = await caches.open(ASSET_CACHE); + cache.put(request, response.clone()); + } + return response; + } catch { + // Offline and not cached — return a minimal fallback for navigation + if (request.mode === 'navigate') { + const fallback = await caches.match('/index.html'); + if (fallback) return fallback; + } + return new Response('Offline', { status: 503, statusText: 'Service Unavailable' }); + } +} + +async function networkFirst(request) { + try { + const response = await fetch(request); + if (response.ok) { + const cache = await caches.open(CACHE_NAME); + cache.put(request, response.clone()); + } + return response; + } catch { + const cached = await caches.match(request); + return cached || new Response('Offline', { status: 503, statusText: 'Service Unavailable' }); + } +} -- 2.43.0