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.
+
+
+
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
+
Offline Mode
0
@@ -48,12 +49,12 @@
📤
Queue
-
+
📁
Files
{% if is_admin %}
-
+
⚙️
Admin