Analytics: time-series message graph with 5 time spans (1h/4h/24h/7d/30d), top-5 agents and channels leaderboards, summary cards. 4 new REST endpoints. PWA: web app manifest, service worker with cache-first static/network-only API strategy, push notifications via Web Push API with VAPID keys, push subscription management endpoints, SQLite migration for subscriptions. UX fixes: auto-resize compose textarea (3-12 lines), inline editable agent display name, editable human display name in settings, smart mention/channel highlighting (existing→link, deleted→inactive badge, unknown→plain text), font size -/+ preference (12-24px persisted in localStorage), version footer with GitHub link. MCP: 4 prompts — daily-digest, agent-health-check, channel-overview, debug-agent. Registered with prompt capabilities enabled. Code review fixes: scoped push unsubscribe to user, capped analytics limit at 100, hardened HTML strip regex, bounded SW cache, backend push unsub on disable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
90 lines
2.9 KiB
JavaScript
90 lines
2.9 KiB
JavaScript
const CACHE_NAME = 'synapbus-v1';
|
|
const STATIC_ASSETS = ['/', '/index.html'];
|
|
|
|
// Install: pre-cache app shell
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
|
|
);
|
|
self.skipWaiting();
|
|
});
|
|
|
|
// Activate: clean old caches
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then((names) =>
|
|
Promise.all(names.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n)))
|
|
)
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
// Fetch: network-only for API, cache-first for static
|
|
self.addEventListener('fetch', (event) => {
|
|
const url = new URL(event.request.url);
|
|
|
|
// Network-only for API, auth, MCP, SSE
|
|
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/auth/') ||
|
|
url.pathname.startsWith('/mcp') || url.pathname.startsWith('/a2a')) {
|
|
return; // Let the browser handle normally
|
|
}
|
|
|
|
event.respondWith(
|
|
caches.match(event.request).then((cached) => {
|
|
if (cached) return cached;
|
|
return fetch(event.request).then((response) => {
|
|
// Only cache static assets (SvelteKit chunks, icons, fonts)
|
|
if (response.ok && event.request.method === 'GET' &&
|
|
(url.pathname.startsWith('/_app/') || url.pathname.startsWith('/icons/') ||
|
|
url.pathname === '/' || url.pathname === '/index.html')) {
|
|
const clone = response.clone();
|
|
caches.open(CACHE_NAME).then(async (cache) => {
|
|
cache.put(event.request, clone);
|
|
// Evict oldest entries if cache exceeds 80 items
|
|
const keys = await cache.keys();
|
|
if (keys.length > 80) {
|
|
for (let i = 0; i < keys.length - 80; i++) {
|
|
cache.delete(keys[i]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
return response;
|
|
}).catch(() => {
|
|
// Offline fallback for navigation requests
|
|
if (event.request.mode === 'navigate') {
|
|
return caches.match('/index.html');
|
|
}
|
|
});
|
|
})
|
|
);
|
|
});
|
|
|
|
// Push notification handling
|
|
self.addEventListener('push', (event) => {
|
|
const data = event.data ? event.data.json() : {};
|
|
const title = data.title || 'SynapBus';
|
|
const options = {
|
|
body: data.body || 'New notification',
|
|
icon: '/icons/icon-192.png',
|
|
badge: '/icons/icon-192.png',
|
|
tag: data.tag || 'synapbus-notification',
|
|
data: { url: data.url || '/' }
|
|
};
|
|
event.waitUntil(self.registration.showNotification(title, options));
|
|
});
|
|
|
|
// Notification click: open the relevant page
|
|
self.addEventListener('notificationclick', (event) => {
|
|
event.notification.close();
|
|
const url = event.notification.data?.url || '/';
|
|
event.waitUntil(
|
|
clients.matchAll({ type: 'window' }).then((clientList) => {
|
|
for (const client of clientList) {
|
|
if (client.url.includes(url) && 'focus' in client) return client.focus();
|
|
}
|
|
return clients.openWindow(url);
|
|
})
|
|
);
|
|
});
|