diff --git a/app/static/css/style.css b/app/static/css/style.css index f57f988..3725d14 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -766,3 +766,10 @@ textarea, select { transition-property: all; } + +/* Offline mode label */ +.offline-label { + font-size: 0.75rem; + color: var(--offline, #888); + font-weight: 600; +} diff --git a/app/static/js/app.js b/app/static/js/app.js index 618740f..f054d83 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -44,6 +44,17 @@ const NextSnap = { indicator.classList.add('offline'); indicator.title = 'Offline'; } + + // Show/hide "Offline Mode" label + const offlineLabel = document.getElementById('offlineLabel'); + if (offlineLabel) { + offlineLabel.style.display = this.isOnline ? 'none' : ''; + } + + // Show/hide online-only nav items (Files, Admin) + document.querySelectorAll('[data-online-only]').forEach(el => { + el.style.display = this.isOnline ? '' : 'none'; + }); }, setSyncingStatus(isSyncing) { diff --git a/app/static/sw.js b/app/static/sw.js index f475515..6cf799b 100644 --- a/app/static/sw.js +++ b/app/static/sw.js @@ -1,27 +1,54 @@ // NextSnap Service Worker // Provides offline-first caching for the app shell -const CACHE_VERSION = 'nextsnap-v22'; -const APP_SHELL_CACHE = 'nextsnap-shell-v18'; -const RUNTIME_CACHE = 'nextsnap-runtime-v18'; +const CACHE_VERSION = 'nextsnap-v25'; +const APP_SHELL_CACHE = 'nextsnap-shell-v21'; +const RUNTIME_CACHE = 'nextsnap-runtime-v21'; -// Offline fallback page (served when network fails and no cached version exists) +// Offline fallback page with bottom nav bar so user can navigate to cached pages const OFFLINE_PAGE = ` - + + Offline - NextSnap -

You're Offline

Check your internet connection and try again.

+
+

NextSnap

+
Offline Mode
+
+
+

You're Offline

+

This page requires an internet connection.
Capture and Queue are available offline.

+
+ `; // Assets to cache on install @@ -73,6 +100,16 @@ self.addEventListener('activate', (event) => { }) ); }) + .then(() => { + // Purge cached online-only pages so they show offline fallback + return caches.open(RUNTIME_CACHE).then((cache) => { + return Promise.all( + ['/browser', '/admin', '/reviewer'].map(path => + cache.delete(new Request(path)).catch(() => {}) + ) + ); + }); + }) .then(() => { console.log('[SW] Service worker activated'); return self.clients.claim(); // Take control immediately @@ -115,12 +152,17 @@ self.addEventListener('fetch', (event) => { return; } + // Pages that require connectivity - never cache, always show offline page + const ONLINE_ONLY = ['/browser', '/admin', '/reviewer']; + const isOnlineOnly = ONLINE_ONLY.includes(url.pathname); + // Everything else (pages, API) - network-first with cache fallback event.respondWith( fetch(request) .then((response) => { // Cache successful GET responses for offline fallback - if (response.status === 200) { + // Skip online-only pages - they need live data + if (response.status === 200 && !isOnlineOnly) { const responseClone = response.clone(); caches.open(RUNTIME_CACHE).then((cache) => { // Don't cache file uploads or large binary responses @@ -133,6 +175,13 @@ self.addEventListener('fetch', (event) => { return response; }) .catch(() => { + // Online-only pages go straight to offline fallback + if (isOnlineOnly) { + return new Response(OFFLINE_PAGE, { + status: 200, + headers: { 'Content-Type': 'text/html' } + }); + } // Network failed - try cache return caches.match(request) .then((cachedResponse) => { diff --git a/app/templates/base.html b/app/templates/base.html index 0cd955d..0224c28 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -25,6 +25,7 @@

NextSnap

+ @@ -48,12 +49,12 @@ 📤 Queue - + 📁 Files {% if is_admin %} - + ⚙️ Admin