diff --git a/icons/icon-192.png b/icons/icon-192.png new file mode 100644 index 0000000..bce7ae5 Binary files /dev/null and b/icons/icon-192.png differ diff --git a/icons/icon-512.png b/icons/icon-512.png new file mode 100644 index 0000000..4fd6ac2 Binary files /dev/null and b/icons/icon-512.png differ diff --git a/index.html b/index.html index 3a2c6ea..46afe52 100644 --- a/index.html +++ b/index.html @@ -19,6 +19,13 @@ The Nexus — Timmy's Sovereign Home + + + + + + + @@ -33,6 +40,11 @@ + +
+ ◈ NEXUS OFFLINE MODE — Running on cached assets +
+
@@ -118,5 +130,23 @@ + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..9f7d7b3 --- /dev/null +++ b/manifest.json @@ -0,0 +1,25 @@ +{ + "name": "The Nexus — Timmy's Sovereign Home", + "short_name": "The Nexus", + "description": "Timmy's sovereign 3D space. A crystalline hub outside time.", + "start_url": "/", + "display": "standalone", + "orientation": "landscape", + "background_color": "#050510", + "theme_color": "#4af0c0", + "categories": ["games", "entertainment"], + "icons": [ + { + "src": "icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/style.css b/style.css index 519b05e..e23b187 100644 --- a/style.css +++ b/style.css @@ -359,3 +359,24 @@ canvas#nexus-canvas { display: none; } } + +/* === OFFLINE INDICATOR === */ +#offline-banner { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + padding: 6px 16px; + background: rgba(255, 170, 34, 0.15); + border-bottom: 1px solid var(--color-warning); + color: var(--color-warning); + font-family: var(--font-body); + font-size: var(--text-xs); + text-align: center; + letter-spacing: 0.08em; +} +#offline-banner.visible { + display: block; +} diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..943b030 --- /dev/null +++ b/sw.js @@ -0,0 +1,112 @@ +// ◈ The Nexus — Service Worker +// Offline-first caching for sovereign space + +const CACHE_VERSION = 'nexus-v1'; +const STATIC_CACHE = `${CACHE_VERSION}-static`; +const CDN_CACHE = `${CACHE_VERSION}-cdn`; + +// Core local assets — always cache +const STATIC_ASSETS = [ + '/', + '/index.html', + '/style.css', + '/app.js', + '/manifest.json', + '/icons/icon-192.png', + '/icons/icon-512.png', +]; + +// CDN assets for Three.js — cache on first fetch +const CDN_ORIGINS = [ + 'https://cdn.jsdelivr.net', + 'https://fonts.googleapis.com', + 'https://fonts.gstatic.com', +]; + +// ═══ INSTALL — pre-cache static assets ═══ +self.addEventListener('install', event => { + event.waitUntil( + caches.open(STATIC_CACHE) + .then(cache => cache.addAll(STATIC_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.startsWith('nexus-') && key !== STATIC_CACHE && key !== CDN_CACHE) + .map(key => caches.delete(key)) + )) + .then(() => self.clients.claim()) + ); +}); + +// ═══ FETCH — serve from cache, fall back to network ═══ +self.addEventListener('fetch', event => { + const { request } = event; + const url = new URL(request.url); + + // Skip non-GET requests + if (request.method !== 'GET') return; + + // CDN resources: cache-first with network fallback + if (CDN_ORIGINS.some(origin => request.url.startsWith(origin))) { + event.respondWith(cdnFirst(request)); + return; + } + + // Same-origin static assets: cache-first + if (url.origin === self.location.origin) { + event.respondWith(staticFirst(request)); + return; + } +}); + +// Cache-first for CDN (Three.js modules, fonts) +async function cdnFirst(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(CDN_CACHE); + cache.put(request, response.clone()); + } + return response; + } catch { + // Offline and not cached — return a minimal error response + return new Response('/* offline */', { + headers: { 'Content-Type': 'text/plain' }, + }); + } +} + +// Cache-first for local static assets +async function staticFirst(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(STATIC_CACHE); + cache.put(request, response.clone()); + } + return response; + } catch { + // Offline fallback: serve index.html for navigation requests + if (request.mode === 'navigate') { + const fallback = await caches.match('/index.html'); + if (fallback) return fallback; + } + return new Response('Nexus offline — cached assets not found.', { + status: 503, + headers: { 'Content-Type': 'text/plain' }, + }); + } +}