From 36a53301a72012d28cd8db3f17f8753d4b78a9d0 Mon Sep 17 00:00:00 2001 From: kamaji Date: Sat, 7 Feb 2026 23:00:34 -0600 Subject: [PATCH] Show offline mode UI: hide online-only nav items, show offline label When offline, hide Files and Admin from the bottom nav and display "Offline Mode" next to the status dot. The SW offline fallback page also only shows Capture and Queue nav items. Online-only pages (browser, admin, reviewer) are never cached and go straight to the offline fallback when the network is unavailable. Co-Authored-By: Claude Opus 4.6 --- app/static/css/style.css | 7 +++++ app/static/js/app.js | 11 +++++++ app/static/sw.js | 65 +++++++++++++++++++++++++++++++++++----- app/templates/base.html | 5 ++-- 4 files changed, 78 insertions(+), 10 deletions(-) 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