PWA Adjustments:

- Better iOS handling
 - Force reload on SW change
This commit is contained in:
fred.boniface committed 2026-11-08 20:42:05 +00:00
1 parent 21f91975fb
commit 06ce6cabeb
4 files changed
+80 -30

No files matched your search

+5 -1
View File
@@ -11,7 +11,11 @@
<!-- PWA Requirements -->
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
<link rel="apple-touch-icon" href="%sveltekit.assets%/icons/apple-touch-icon.png" />
<meta name="theme-color" content="#4fd1d1" />
<meta name="theme-color" content="#3c6f79" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="OwlBoard" />
<link rel="canonical" href="https://owlboard.info" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://owlboard.info" />
+27 -1
View File
@@ -31,18 +31,44 @@
// Initialize location store
LOCATIONS.init();
// Register service worker
// Register service worker with aggressive update listener
if ('serviceWorker' in navigator) {
let refreshing = false;
// Reload when active service worker updates to prevent version mixing
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (!refreshing) {
refreshing = true;
console.info('[SW] New version activated, reloading for clean state.');
window.location.reload();
}
});
navigator.serviceWorker
.register('/service-worker.js', {
type: 'module'
})
.then((registration) => {
console.info('OwlBoard Service Worker registered', registration.scope);
registration.update().catch(() => {});
})
.catch((error) => {
console.error('Service Worker installation failed: ', error);
});
// Check for updates when tab or app comes into focus
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
navigator.serviceWorker.ready
.then((registration) => registration.update())
.catch(() => {});
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}
});
+44 -24
View File
@@ -14,19 +14,36 @@ const ASSETS = [
sw.addEventListener('install', (event) => {
sw.skipWaiting();
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)));
event.waitUntil(
caches.open(CACHE_NAME).then(async (cache) => {
// Cache assets resiliently so a single transient failure does not abort installation
await Promise.all(
ASSETS.map((asset) =>
cache.add(asset).catch((err) => {
console.warn(`[SW] Failed to precache ${asset}:`, err);
})
)
);
})
);
});
sw.addEventListener('activate', (event) => {
event.waitUntil(
Promise.all([
caches.keys().then((keys) => {
return Promise.all(
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
);
}),
sw.clients.claim()
])
(async () => {
// Purge all legacy and outdated caches to ensure a clean break
const keys = await caches.keys();
await Promise.all(
keys
.filter((key) => key !== CACHE_NAME)
.map((key) => {
console.info(`[SW] Purging outdated cache: ${key}`);
return caches.delete(key);
})
);
// Take control of all open client tabs immediately
await sw.clients.claim();
})()
);
});
@@ -36,19 +53,33 @@ sw.addEventListener('fetch', (event) => {
const url = new URL(request.url);
// Uncached static assets
// 1. Uncached static assets (manifest and dynamic icons)
if (url.pathname.endsWith('manifest.json') || url.pathname.includes('/icons/')) {
event.respondWith(fetch(request));
return;
}
// Static Assets (Cache-First)
// 2. Navigation requests: Network-First with offline fallback
// Always fetch fresh HTML from the network when online so new deployments are served immediately.
// Falls back to the cached root/index page when offline.
if (request.mode === 'navigate' || request.headers.get('accept')?.includes('text/html')) {
event.respondWith(
fetch(request).catch(async () => {
const cache = await caches.open(CACHE_NAME);
return (await cache.match('/')) || ((await cache.match('index.html')) as Response);
})
);
return;
}
// 3. Static Assets: Cache-First
// Hashed assets (/_app/immutable/...) and precached assets
if (ASSETS.includes(url.pathname) || url.pathname.startsWith('/_app/')) {
event.respondWith(caches.match(request).then((res) => res || fetch(request)));
return;
}
// Cachable API Responses - stale-while-revalidate
// 4. Cachable API Responses - stale-while-revalidate
if (url.pathname === '/api/v3/locationFilter/data') {
event.respondWith(
caches.open('ob-dynamic-cache').then(async (cache) => {
@@ -88,7 +119,7 @@ sw.addEventListener('fetch', (event) => {
return;
}
// Offline API Fallback (Network-First)
// 5. Offline API Fallback (Network-First)
if (url.pathname.startsWith('/api') || url.hostname.includes('api')) {
event.respondWith(
fetch(request).catch(() => {
@@ -107,15 +138,4 @@ sw.addEventListener('fetch', (event) => {
);
return;
}
// Nav fallback - return fallback page, if offline and page requested
if (request.mode === 'navigate' || request.headers.get('accept')?.includes('text/html')) {
event.respondWith(
fetch(request).catch(() => {
// Serve svelte fallback page
return caches.match('/') as Promise<Response>;
})
);
return;
}
});
+4 -4
View File
@@ -7,13 +7,12 @@
"scope": "/",
"display": "standalone",
"display_override": [
"window-controls-overlay",
"standalone",
"minimal-ui"
],
"background_color": "#404c55",
"theme_color": "#4fd1d1",
"orientation": "portrait",
"theme_color": "#3c6f79",
"orientation": "any",
"categories": [
"transportation",
"utilities"
@@ -50,7 +49,8 @@
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192"
"sizes": "192x192",
"type": "image/png"
}
]
}