From 5d2d8a12bc15eae2a9a9df27f56a6bfe68ad923b Mon Sep 17 00:00:00 2001 From: kimi Date: Mon, 23 Mar 2026 23:58:37 -0400 Subject: [PATCH] feat: PWA manifest + service worker for offline and install support Adds full Progressive Web App support to The Nexus: - sw.js: Service worker with cache-first strategy for local assets and stale-while-revalidate for CDN resources (Three.js, fonts) - offline.html: Styled offline fallback page with auto-reconnect - icons/nexus-icon.svg: Nexus crystal sigil icon (SVG) - icons/nexus-maskable.svg: Maskable icon for adaptive shapes - manifest.json: Complete PWA manifest with theme color #4af0c0, standalone display mode, shortcuts, and icon definitions - index.html: Service worker registration, Apple PWA meta tags, theme colors, and MS application config The Nexus now works offline after first visit and can be installed to home screen on mobile and desktop devices. Fixes #14 --- icons/nexus-icon.svg | 60 +++++++++++ icons/nexus-maskable.svg | 32 ++++++ index.html | 70 +++++++++++++ manifest.json | 63 +++++++++-- offline.html | 198 +++++++++++++++++++++++++++++++++++ sw.js | 218 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 630 insertions(+), 11 deletions(-) create mode 100644 icons/nexus-icon.svg create mode 100644 icons/nexus-maskable.svg create mode 100644 offline.html create mode 100644 sw.js diff --git a/icons/nexus-icon.svg b/icons/nexus-icon.svg new file mode 100644 index 0000000..0f7ed7b --- /dev/null +++ b/icons/nexus-icon.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/nexus-maskable.svg b/icons/nexus-maskable.svg new file mode 100644 index 0000000..ae4d623 --- /dev/null +++ b/icons/nexus-maskable.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html index 34af931..e941c22 100644 --- a/index.html +++ b/index.html @@ -5,15 +5,38 @@ Timmy's Nexus + + + + + + + + + + + + + + + + + + + + + + + @@ -27,5 +50,52 @@ + + + + + + diff --git a/manifest.json b/manifest.json index 8137537..38d5182 100644 --- a/manifest.json +++ b/manifest.json @@ -1,20 +1,61 @@ { - "name": "Timmy's Nexus", + "name": "The Nexus", "short_name": "Nexus", + "description": "Timmy's sovereign 3D world — a Three.js environment serving as the central hub for all portals", "start_url": "/", - "display": "fullscreen", - "background_color": "#050510", - "theme_color": "#050510", + "display": "standalone", + "display_override": ["fullscreen", "minimal-ui"], + "orientation": "any", + "background_color": "#0a1628", + "theme_color": "#4af0c0", + "categories": ["entertainment", "games"], + "lang": "en", + "dir": "ltr", + "scope": "/", "icons": [ { - "src": "icons/t-logo-192.png", - "sizes": "192x192", - "type": "image/png" + "src": "icons/nexus-icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" }, { - "src": "icons/t-logo-512.png", - "sizes": "512x512", - "type": "image/png" + "src": "icons/nexus-maskable.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "maskable" } - ] + ], + "screenshots": [ + { + "src": "screenshots/nexus-wide.png", + "sizes": "1280x720", + "type": "image/png", + "form_factor": "wide", + "label": "The Nexus 3D environment" + }, + { + "src": "screenshots/nexus-narrow.png", + "sizes": "750x1334", + "type": "image/png", + "form_factor": "narrow", + "label": "The Nexus on mobile" + } + ], + "shortcuts": [ + { + "name": "Enter Nexus", + "short_name": "Enter", + "description": "Jump directly into the Nexus world", + "url": "/?action=enter", + "icons": [ + { + "src": "icons/nexus-icon.svg", + "sizes": "any" + } + ] + } + ], + "related_applications": [], + "prefer_related_applications": false } diff --git a/offline.html b/offline.html new file mode 100644 index 0000000..19eee18 --- /dev/null +++ b/offline.html @@ -0,0 +1,198 @@ + + + + + + Offline — The Nexus + + + + +
+ + + + + + + + + + +

The Nexus is Dormant

+ +
+ You're offline +
+ +

+ The crystalline pathways cannot form without a connection to the sovereign network. + Check your connection and try again to enter the 3D realm. +

+ + + +

+ Core assets are cached for offline use. Some features may be limited without connectivity. +

+
+ + + + diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..239f688 --- /dev/null +++ b/sw.js @@ -0,0 +1,218 @@ +/** + * The Nexus Service Worker + * Provides offline capability and home screen install support + * Strategy: Cache-first for local assets, stale-while-revalidate for CDN + */ + +const CACHE_VERSION = 'nexus-v1'; +const STATIC_CACHE = `${CACHE_VERSION}-static`; +const CDN_CACHE = `${CACHE_VERSION}-cdn`; +const OFFLINE_PAGE = '/offline.html'; + +// Core local assets that must be cached +const CORE_ASSETS = [ + '/', + '/index.html', + '/style.css', + '/app.js', + '/manifest.json', + '/icons/nexus-icon.svg', + '/icons/nexus-maskable.svg', + OFFLINE_PAGE +]; + +// CDN resources that benefit from caching but can be stale +const CDN_PATTERNS = [ + /^https:\/\/unpkg\.com/, + /^https:\/\/cdn\.jsdelivr\.net/, + /^https:\/\/fonts\.googleapis\.com/, + /^https:\/\/fonts\.gstatic\.com/, + /^https:\/\/cdn\.threejs\.org/ +]; + +/** + * Check if a URL matches any CDN pattern + */ +function isCdnResource(url) { + return CDN_PATTERNS.some(pattern => pattern.test(url)); +} + +/** + * Install event - cache core assets + */ +self.addEventListener('install', (event) => { + console.log('[Nexus SW] Installing...'); + + event.waitUntil( + caches.open(STATIC_CACHE) + .then(cache => { + console.log('[Nexus SW] Caching core assets'); + return cache.addAll(CORE_ASSETS); + }) + .then(() => { + console.log('[Nexus SW] Core assets cached'); + return self.skipWaiting(); + }) + .catch(err => { + console.error('[Nexus SW] Cache failed:', err); + }) + ); +}); + +/** + * Activate event - clean up old caches + */ +self.addEventListener('activate', (event) => { + console.log('[Nexus SW] Activating...'); + + event.waitUntil( + caches.keys() + .then(cacheNames => { + return Promise.all( + cacheNames + .filter(name => name.startsWith('nexus-') && name !== STATIC_CACHE && name !== CDN_CACHE) + .map(name => { + console.log('[Nexus SW] Deleting old cache:', name); + return caches.delete(name); + }) + ); + }) + .then(() => { + console.log('[Nexus SW] Activated'); + return self.clients.claim(); + }) + ); +}); + +/** + * Fetch event - handle requests with appropriate strategy + */ +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + // Skip non-GET requests + if (request.method !== 'GET') { + return; + } + + // Skip cross-origin requests that aren't CDN resources + if (url.origin !== self.location.origin && !isCdnResource(url.href)) { + return; + } + + // Handle CDN resources with stale-while-revalidate + if (isCdnResource(url.href)) { + event.respondWith(handleCdnRequest(request)); + return; + } + + // Handle local assets with cache-first strategy + event.respondWith(handleLocalRequest(request)); +}); + +/** + * Cache-first strategy for local assets + * Fastest response, updates cache in background + */ +async function handleLocalRequest(request) { + try { + const cache = await caches.open(STATIC_CACHE); + const cached = await cache.match(request); + + // Return cached version immediately if available + if (cached) { + // Revalidate in background for next time + fetch(request) + .then(response => { + if (response.ok) { + cache.put(request, response.clone()); + } + }) + .catch(() => { + // Network failed, cached version is already being used + }); + + return cached; + } + + // Not in cache - fetch from network + const response = await fetch(request); + + if (response.ok) { + cache.put(request, response.clone()); + } + + return response; + } catch (error) { + console.error('[Nexus SW] Local request failed:', error); + + // Return offline page for navigation requests + if (request.mode === 'navigate') { + const cache = await caches.open(STATIC_CACHE); + const offlinePage = await cache.match(OFFLINE_PAGE); + if (offlinePage) { + return offlinePage; + } + } + + throw error; + } +} + +/** + * Stale-while-revalidate strategy for CDN resources + * Serves stale content while updating in background + */ +async function handleCdnRequest(request) { + const cache = await caches.open(CDN_CACHE); + const cached = await cache.match(request); + + // Always try to fetch fresh version + const fetchPromise = fetch(request) + .then(response => { + if (response.ok) { + cache.put(request, response.clone()); + } + return response; + }) + .catch(error => { + console.error('[Nexus SW] CDN fetch failed:', error); + throw error; + }); + + // Return cached version immediately if available, otherwise wait for network + if (cached) { + // Return stale but revalidate for next time + fetchPromise.catch(() => {}); // Swallow errors, we have cached version + return cached; + } + + // No cache - must wait for network + return fetchPromise; +} + +/** + * Message handler for runtime cache updates + */ +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } + + if (event.data && event.data.type === 'GET_VERSION') { + event.ports[0].postMessage({ version: CACHE_VERSION }); + } +}); + +/** + * Background sync for deferred actions (future enhancement) + */ +self.addEventListener('sync', (event) => { + if (event.tag === 'nexus-sync') { + console.log('[Nexus SW] Background sync triggered'); + // Future: sync chat messages, state updates, etc. + } +}); + +console.log('[Nexus SW] Service worker loaded'); -- 2.43.0