v1.0.5: Offline mode with sync queue, fix symbol/text rotation on compass, polygon area display

This commit is contained in:
Pepe Ziberi
2026-02-21 23:09:15 +01:00
parent 0376e71066
commit e3f8f14f6a
4 changed files with 279 additions and 18 deletions

View File

@@ -1,6 +1,10 @@
const TILE_CACHE = 'lageplan-tiles-v2'
const STATIC_CACHE = 'lageplan-static-v2'
const APP_CACHE = 'lageplan-app-v2'
const TILE_CACHE = 'lageplan-tiles-v3'
const STATIC_CACHE = 'lageplan-static-v3'
const APP_CACHE = 'lageplan-app-v3'
const API_CACHE = 'lageplan-api-v3'
// API routes that should be cached for offline use
const CACHEABLE_API = ['/api/icons', '/api/hose-types', '/api/dictionary']
// Pre-cache essential app shell on install
self.addEventListener('install', (event) => {
@@ -8,6 +12,8 @@ self.addEventListener('install', (event) => {
caches.open(APP_CACHE).then((cache) =>
cache.addAll([
'/app',
'/login',
'/',
'/logo.svg',
'/logo-icon.png',
'/manifest.json',
@@ -17,7 +23,6 @@ self.addEventListener('install', (event) => {
self.skipWaiting()
})
// Cache strategy: Network First for API, Cache First for tiles, Stale While Revalidate for static assets
self.addEventListener('fetch', (event) => {
const url = event.request.url
const { pathname } = new URL(url)
@@ -25,19 +30,68 @@ self.addEventListener('fetch', (event) => {
// Skip non-GET requests
if (event.request.method !== 'GET') return
// API requests: network only (don't cache dynamic data)
// Cacheable API routes: Network First with cache fallback (icons, hose-types, dictionary)
if (CACHEABLE_API.some(p => pathname.startsWith(p))) {
event.respondWith(
caches.open(API_CACHE).then((cache) =>
fetch(event.request).then((response) => {
if (response.ok) cache.put(event.request, response.clone())
return response
}).catch(() =>
cache.match(event.request).then((cached) => cached || new Response('{"error":"offline"}', {
status: 503, headers: { 'Content-Type': 'application/json' }
}))
)
)
)
return
}
// Projects API: Network First with cache fallback
if (pathname === '/api/projects' || pathname.match(/^\/api\/projects\/[^/]+$/)) {
event.respondWith(
caches.open(API_CACHE).then((cache) =>
fetch(event.request).then((response) => {
if (response.ok) cache.put(event.request, response.clone())
return response
}).catch(() =>
cache.match(event.request).then((cached) => cached || new Response('{"error":"offline"}', {
status: 503, headers: { 'Content-Type': 'application/json' }
}))
)
)
)
return
}
// Features API: Network First with cache fallback
if (pathname.match(/^\/api\/projects\/[^/]+\/features$/)) {
event.respondWith(
caches.open(API_CACHE).then((cache) =>
fetch(event.request).then((response) => {
if (response.ok) cache.put(event.request, response.clone())
return response
}).catch(() =>
cache.match(event.request).then((cached) => cached || new Response('{"error":"offline"}', {
status: 503, headers: { 'Content-Type': 'application/json' }
}))
)
)
)
return
}
// Other API requests: network only
if (pathname.startsWith('/api/')) return
// Cache map tiles from OpenStreetMap (Cache First)
// Cache map tiles from OpenStreetMap / MapTiler (Cache First — tiles don't change)
if (url.includes('tile.openstreetmap.org') || url.includes('api.maptiler.com')) {
event.respondWith(
caches.open(TILE_CACHE).then((cache) =>
cache.match(event.request).then((cached) => {
if (cached) return cached
return fetch(event.request).then((response) => {
if (response.ok) {
cache.put(event.request, response.clone())
}
if (response.ok) cache.put(event.request, response.clone())
return response
}).catch(() => new Response('', { status: 503 }))
})
@@ -46,7 +100,23 @@ self.addEventListener('fetch', (event) => {
return
}
// Static assets (JS, CSS, images): Stale While Revalidate
// Next.js build chunks (_next/static): Cache First (hashed filenames = immutable)
if (pathname.startsWith('/_next/static/')) {
event.respondWith(
caches.open(STATIC_CACHE).then((cache) =>
cache.match(event.request).then((cached) => {
if (cached) return cached
return fetch(event.request).then((response) => {
if (response.ok) cache.put(event.request, response.clone())
return response
}).catch(() => new Response('', { status: 503 }))
})
)
)
return
}
// Other static assets (JS, CSS, images, fonts): Stale While Revalidate
if (pathname.match(/\.(js|css|png|jpg|jpeg|svg|ico|woff2?)$/)) {
event.respondWith(
caches.open(STATIC_CACHE).then((cache) =>
@@ -62,8 +132,8 @@ self.addEventListener('fetch', (event) => {
return
}
// App pages: Network First with cache fallback
if (pathname === '/app' || pathname === '/' || pathname.startsWith('/app')) {
// App pages / navigation: Network First with cache fallback
if (event.request.mode === 'navigate' || pathname === '/app' || pathname === '/' || pathname.startsWith('/app')) {
event.respondWith(
fetch(event.request).then((response) => {
if (response.ok) {
@@ -81,7 +151,7 @@ self.addEventListener('fetch', (event) => {
// Clean old caches on activation
self.addEventListener('activate', (event) => {
const currentCaches = [TILE_CACHE, STATIC_CACHE, APP_CACHE]
const currentCaches = [TILE_CACHE, STATIC_CACHE, APP_CACHE, API_CACHE]
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
@@ -92,3 +162,23 @@ self.addEventListener('activate', (event) => {
).then(() => self.clients.claim())
)
})
// Listen for sync events (Background Sync for queued saves)
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-saves') {
event.waitUntil(syncQueuedSaves())
}
})
// Process queued saves from IndexedDB/localStorage
async function syncQueuedSaves() {
try {
const clients = await self.clients.matchAll()
clients.forEach(client => client.postMessage({ type: 'SYNC_START' }))
// Read queue from a BroadcastChannel or let the main thread handle it
clients.forEach(client => client.postMessage({ type: 'FLUSH_SYNC_QUEUE' }))
} catch (e) {
console.error('[SW] Sync error:', e)
}
}