mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
Merge pull request #59 from aakhter/feat/pwa-support
feat: add PWA support for Android/iOS home screen install
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
@@ -6,6 +6,9 @@
|
|||||||
<meta name="description" content="Claude Code session manager with web interface">
|
<meta name="description" content="Claude Code session manager with web interface">
|
||||||
<meta name="theme-color" content="#0a0a0a">
|
<meta name="theme-color" content="#0a0a0a">
|
||||||
<meta name="google" content="notranslate">
|
<meta name="google" content="notranslate">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<link rel="apple-touch-icon" href="icon-192.png">
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<title>Codeman</title>
|
<title>Codeman</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%2360a5fa'/%3E%3Cstop offset='100%25' stop-color='%233b82f6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' rx='6' fill='%230a0a0a'/%3E%3Cpath d='M18 4L8 18h6l-2 10 10-14h-6z' fill='url(%23g)'/%3E%3C/svg%3E">
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%2360a5fa'/%3E%3Cstop offset='100%25' stop-color='%233b82f6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' rx='6' fill='%230a0a0a'/%3E%3Cpath d='M18 4L8 18h6l-2 10 10-14h-6z' fill='url(%23g)'/%3E%3C/svg%3E">
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "Codeman",
|
"name": "Codeman",
|
||||||
"short_name": "Codeman",
|
"short_name": "Codeman",
|
||||||
|
"description": "Claude Code session manager",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
|
"orientation": "any",
|
||||||
"background_color": "#0a0a0a",
|
"background_color": "#0a0a0a",
|
||||||
"theme_color": "#0a0a0a"
|
"theme_color": "#0a0a0a",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-16
@@ -1,30 +1,106 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview Service worker for Web Push notifications.
|
* @fileoverview Service worker for PWA install + Web Push notifications.
|
||||||
*
|
*
|
||||||
* Receives push events from the Codeman server (via web-push library) and displays
|
* App-shell caching: on install, precaches the core UI assets so the app
|
||||||
* OS-level notifications. Handles notification clicks to focus an existing Codeman
|
* launches instantly and works offline (or on flaky connections). Uses a
|
||||||
* tab or open a new one. Supports action buttons, per-session deep linking, and
|
* network-first strategy for navigation and API calls, cache-first for
|
||||||
* critical notification persistence (requireInteraction).
|
* static assets.
|
||||||
*
|
*
|
||||||
* Lifecycle: skipWaiting on install, claim clients on activate — ensures the latest
|
* Push notifications: receives push events from the Codeman server (via
|
||||||
* service worker takes control immediately without waiting for tab refresh.
|
* 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)
|
* @dependency None (runs in ServiceWorkerGlobalScope, isolated from page scripts)
|
||||||
* @see src/push-store.ts — server-side VAPID key management and subscription CRUD
|
* @see src/push-store.ts -- server-side VAPID key management and subscription CRUD
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Codeman Service Worker — Web Push notifications
|
const CACHE_NAME = 'codeman-v1';
|
||||||
// This service worker receives push events from the server and displays OS-level notifications.
|
|
||||||
// It also handles notification clicks to focus or open the Codeman tab.
|
|
||||||
|
|
||||||
self.addEventListener('install', () => {
|
// 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();
|
self.skipWaiting();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Activate: clean old caches, claim clients ---
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
event.waitUntil(self.clients.claim());
|
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) => {
|
self.addEventListener('push', (event) => {
|
||||||
if (!event.data) return;
|
if (!event.data) return;
|
||||||
|
|
||||||
@@ -40,8 +116,8 @@ self.addEventListener('push', (event) => {
|
|||||||
const options = {
|
const options = {
|
||||||
body: body || '',
|
body: body || '',
|
||||||
tag: tag || 'codeman-default',
|
tag: tag || 'codeman-default',
|
||||||
icon: '/favicon.ico',
|
icon: '/icon-192.png',
|
||||||
badge: '/favicon.ico',
|
badge: '/icon-192.png',
|
||||||
data: { sessionId, url: sessionId ? `/?session=${sessionId}` : '/' },
|
data: { sessionId, url: sessionId ? `/?session=${sessionId}` : '/' },
|
||||||
renotify: true,
|
renotify: true,
|
||||||
requireInteraction: urgency === 'critical',
|
requireInteraction: urgency === 'critical',
|
||||||
@@ -75,7 +151,7 @@ self.addEventListener('notificationclick', (event) => {
|
|||||||
return client.focus();
|
return client.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// No existing tab — open a new one
|
// No existing tab -- open a new one
|
||||||
return self.clients.openWindow(targetUrl);
|
return self.clients.openWindow(targetUrl);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user