Files
bambuddy/static/sw.js
T
maziggy 16da533c9a fix(static): serve /fonts/*.woff2 — self-hosted Inter font (#1460 follow-up)
The browser console logged "downloadable font: rejected by sanitizer"
  for inter-latin.woff2 on every load. The #1460 PWA fix added @font-face
  rules pointing at /fonts/inter-latin.woff2 and bundled the woff2 files
  into static/fonts/, but main.py only mounts /assets, /img and /icons as
  static directories. With no /fonts mount, /fonts/*.woff2 fell through to
  the SPA catch-all and returned index.html with 200 OK; the browser's
  OpenType sanitizer rejected the HTML-as-a-font.

  Add a /fonts StaticFiles mount alongside /img and /icons. The woff2
  files themselves are valid (verified — Inter variable, latin and
  latin-ext subsets).

  Also bump the service worker STATIC_CACHE version (v26 -> v27). sw.js
  lists the two font URLs in STATIC_ASSETS, and cache.addAll() treats the
  200 OK HTML as a successful fetch — so it had cached index.html under
  the font URLs and served it cache-first. The version bump makes the
  activate handler purge the poisoned cache and re-fetch the real fonts.
2026-05-22 10:43:27 +02:00

220 lines
6.0 KiB
JavaScript

// Bambuddy Service Worker
const CACHE_NAME = 'bambuddy-v28';
const STATIC_CACHE = 'bambuddy-static-v27';
// Static assets to cache on install
const STATIC_ASSETS = [
'/',
'/manifest.json',
'/img/favicon.png',
'/img/favicon-16x16.png',
'/img/favicon-32x32.png',
'/img/android-chrome-192x192.png',
'/img/android-chrome-512x512.png',
'/img/apple-touch-icon.png',
'/img/bambuddy_logo_dark.png',
// Self-hosted Inter font (#1460) - cached so the UI renders offline.
'/fonts/inter-latin.woff2',
'/fonts/inter-latin-ext.woff2',
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => {
console.log('[SW] Caching static assets');
return cache.addAll(STATIC_ASSETS);
})
);
// Activate immediately
self.skipWaiting();
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('[SW] Activating service worker...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME && name !== STATIC_CACHE)
.map((name) => {
console.log('[SW] Deleting old cache:', name);
return caches.delete(name);
})
);
})
);
// Take control immediately
self.clients.claim();
});
// Fetch event - network-first for API, cache-first for static
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
// Skip cross-origin requests - let the browser handle them directly.
// Without this the catch-all HTML branch below would answer a failed
// cross-origin request with our cached index.html, so e.g. a blocked
// Google Fonts request came back as text/html (#1460).
if (url.origin !== self.location.origin) {
return;
}
// Skip WebSocket connections
if (url.protocol === 'ws:' || url.protocol === 'wss:') {
return;
}
// Skip camera stream/snapshot requests - Safari has issues with streaming through SW
if (url.pathname.includes('/camera/stream') || url.pathname.includes('/camera/snapshot')) {
return;
}
// API requests - network first, no cache (real-time data is critical)
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request).catch(() => {
// Return offline response for API failures
return new Response(
JSON.stringify({ error: 'offline', message: 'You are currently offline' }),
{
status: 503,
headers: { 'Content-Type': 'application/json' },
}
);
})
);
return;
}
// Static assets - cache first, then network
if (
url.pathname.startsWith('/img/') ||
url.pathname.startsWith('/icons/') ||
url.pathname.startsWith('/fonts/') ||
url.pathname.endsWith('.png') ||
url.pathname.endsWith('.jpg') ||
url.pathname.endsWith('.svg') ||
url.pathname.endsWith('.ico') ||
url.pathname.endsWith('.woff2')
) {
event.respondWith(
caches.match(request).then((cached) => {
if (cached) {
return cached;
}
return fetch(request).then((response) => {
// Cache successful responses
if (response.ok) {
const clone = response.clone();
caches.open(STATIC_CACHE).then((cache) => {
cache.put(request, clone);
});
}
return response;
});
})
);
return;
}
// JS/CSS assets - network first (Vite content-hashes filenames, so
// cache-busting is built in; network-first ensures new builds load immediately)
if (
url.pathname.startsWith('/assets/') ||
url.pathname.endsWith('.js') ||
url.pathname.endsWith('.css')
) {
event.respondWith(
fetch(request)
.then((response) => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, clone);
});
}
return response;
})
.catch(() => {
return caches.match(request);
})
);
return;
}
// HTML pages - network first, fall back to cache
event.respondWith(
fetch(request)
.then((response) => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, clone);
});
}
return response;
})
.catch(() => {
return caches.match(request).then((cached) => {
if (cached) {
return cached;
}
// Return cached index for SPA navigation
return caches.match('/');
});
})
);
});
// Handle push notifications (for future use)
self.addEventListener('push', (event) => {
if (!event.data) return;
const data = event.data.json();
const options = {
body: data.body || 'New notification from Bambuddy',
icon: '/img/android-chrome-192x192.png',
badge: '/img/favicon-32x32.png',
vibrate: [100, 50, 100],
data: {
url: data.url || '/',
},
};
event.waitUntil(
self.registration.showNotification(data.title || 'Bambuddy', options)
);
});
// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = event.notification.data?.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
// Check if there's already a window open
for (const client of windowClients) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
client.navigate(url);
return client.focus();
}
}
// Open a new window if none exists
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});