local: add PWA support for Android home screen install

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>
This commit is contained in:
Aamer Akhter
2026-04-04 22:15:24 -04:00
co-authored by Claude Opus 4.6
parent 7101e64800
commit 6a12a72c9c
5 changed files with 112 additions and 17 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

+3
View File
@@ -6,6 +6,9 @@
<meta name="description" content="Claude Code session manager with web interface">
<meta name="theme-color" content="#0a0a0a">
<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">
<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">
+17 -1
View File
@@ -1,8 +1,24 @@
{
"name": "Codeman",
"short_name": "Codeman",
"description": "Claude Code session manager",
"start_url": "/",
"display": "standalone",
"orientation": "any",
"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
View File
@@ -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
* OS-level notifications. Handles notification clicks to focus an existing Codeman
* tab or open a new one. Supports action buttons, per-session deep linking, and
* critical notification persistence (requireInteraction).
* 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.
*
* Lifecycle: skipWaiting on install, claim clients on activate — ensures the latest
* service worker takes control immediately without waiting for tab refresh.
* 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
* @see src/push-store.ts -- server-side VAPID key management and subscription CRUD
*/
// Codeman Service Worker — Web Push notifications
// 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.
const CACHE_NAME = 'codeman-v1';
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();
});
// --- Activate: clean old caches, claim clients ---
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) => {
if (!event.data) return;
@@ -40,8 +116,8 @@ self.addEventListener('push', (event) => {
const options = {
body: body || '',
tag: tag || 'codeman-default',
icon: '/favicon.ico',
badge: '/favicon.ico',
icon: '/icon-192.png',
badge: '/icon-192.png',
data: { sessionId, url: sessionId ? `/?session=${sessionId}` : '/' },
renotify: true,
requireInteraction: urgency === 'critical',
@@ -75,7 +151,7 @@ self.addEventListener('notificationclick', (event) => {
return client.focus();
}
}
// No existing tab — open a new one
// No existing tab -- open a new one
return self.clients.openWindow(targetUrl);
})
);