mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 05:59:43 +02:00
Add app icons, update manifest with icon entries, and add app-shell caching to service worker for offline/instant startup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
159 lines
4.4 KiB
JavaScript
159 lines
4.4 KiB
JavaScript
/**
|
|
* @fileoverview Service worker for PWA install + Web Push notifications.
|
|
*
|
|
* App-shell caching: on install, precaches the core UI assets so the app
|
|
* launches instantly and works offline (or on flaky connections). Uses a
|
|
* network-first strategy for navigation and API calls, cache-first for
|
|
* static assets.
|
|
*
|
|
* Push notifications: receives push events from the Codeman server (via
|
|
* web-push library) and displays OS-level notifications. Handles notification
|
|
* clicks to focus an existing Codeman tab or open a new one.
|
|
*
|
|
* Lifecycle: skipWaiting on install, claim clients on activate -- ensures the
|
|
* latest service worker takes control immediately without waiting for tab
|
|
* refresh.
|
|
*
|
|
* @dependency None (runs in ServiceWorkerGlobalScope, isolated from page scripts)
|
|
* @see src/push-store.ts -- server-side VAPID key management and subscription CRUD
|
|
*/
|
|
|
|
const CACHE_NAME = 'codeman-v1';
|
|
|
|
// Core app shell -- cached on install for instant startup
|
|
const APP_SHELL = [
|
|
'/',
|
|
'/styles.css',
|
|
'/mobile.css',
|
|
'/constants.js',
|
|
'/app.js',
|
|
'/api-client.js',
|
|
'/terminal-ui.js',
|
|
'/session-ui.js',
|
|
'/settings-ui.js',
|
|
'/panels-ui.js',
|
|
'/notification-manager.js',
|
|
'/mobile-handlers.js',
|
|
'/keyboard-accessory.js',
|
|
'/voice-input.js',
|
|
'/vendor/xterm.min.js',
|
|
'/vendor/xterm-addon-fit.min.js',
|
|
'/vendor/xterm-addon-unicode11.min.js',
|
|
'/vendor/xterm-zerolag-input.js',
|
|
'/vendor/xterm.css',
|
|
'/icon-192.png',
|
|
'/icon-512.png',
|
|
'/manifest.json',
|
|
];
|
|
|
|
// --- Install: precache app shell ---
|
|
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|
// Use addAll but don't fail install if some assets 404 (hashed filenames)
|
|
return Promise.allSettled(
|
|
APP_SHELL.map((url) => cache.add(url).catch(() => {}))
|
|
);
|
|
})
|
|
);
|
|
self.skipWaiting();
|
|
});
|
|
|
|
// --- Activate: clean old caches, claim clients ---
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then((keys) =>
|
|
Promise.all(
|
|
keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
|
|
)
|
|
).then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
// --- Fetch: network-first for API/navigation, cache-first for static ---
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
|
|
// Skip non-GET, WebSocket upgrades, and SSE streams
|
|
if (request.method !== 'GET') return;
|
|
if (request.headers.get('upgrade') === 'websocket') return;
|
|
if (request.headers.get('accept') === 'text/event-stream') return;
|
|
if (request.url.includes('/api/')) return;
|
|
|
|
event.respondWith(
|
|
caches.match(request).then((cached) => {
|
|
// Return cache immediately, refresh in background (stale-while-revalidate)
|
|
const fetchPromise = fetch(request).then((response) => {
|
|
if (response && response.ok) {
|
|
const clone = response.clone();
|
|
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
|
}
|
|
return response;
|
|
}).catch(() => cached);
|
|
|
|
return cached || fetchPromise;
|
|
})
|
|
);
|
|
});
|
|
|
|
// --- Push notifications ---
|
|
|
|
self.addEventListener('push', (event) => {
|
|
if (!event.data) return;
|
|
|
|
let payload;
|
|
try {
|
|
payload = event.data.json();
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
const { title, body, tag, sessionId, urgency, actions } = payload;
|
|
|
|
const options = {
|
|
body: body || '',
|
|
tag: tag || 'codeman-default',
|
|
icon: '/icon-192.png',
|
|
badge: '/icon-192.png',
|
|
data: { sessionId, url: sessionId ? `/?session=${sessionId}` : '/' },
|
|
renotify: true,
|
|
requireInteraction: urgency === 'critical',
|
|
};
|
|
|
|
if (actions && actions.length > 0) {
|
|
options.actions = actions;
|
|
}
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification(title || 'Codeman', options)
|
|
);
|
|
});
|
|
|
|
self.addEventListener('notificationclick', (event) => {
|
|
event.notification.close();
|
|
|
|
const { sessionId, url } = event.notification.data || {};
|
|
const targetUrl = url || '/';
|
|
|
|
event.waitUntil(
|
|
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
|
|
// Try to find an existing Codeman tab
|
|
for (const client of clients) {
|
|
if (client.url.includes(self.location.origin)) {
|
|
client.postMessage({
|
|
type: 'notification-click',
|
|
sessionId,
|
|
action: event.action || null,
|
|
});
|
|
return client.focus();
|
|
}
|
|
}
|
|
// No existing tab -- open a new one
|
|
return self.clients.openWindow(targetUrl);
|
|
})
|
|
);
|
|
});
|