feat: add service worker for PWA support (offline caching, installability)

- Add sw.js with stale-while-revalidate caching for static assets
- Network-first for HTML navigation, skip API/WS requests
- Register service worker in main.tsx on page load
- Enhanced manifest.json with orientation, categories, and all icon sizes
- App is now installable as a standalone PWA on mobile and desktop
This commit is contained in:
Nicolas Varrot
2026-02-13 05:11:39 +00:00
parent 33d72d65bd
commit 5a4c5ba457
3 changed files with 84 additions and 1 deletions

View File

@@ -1,12 +1,24 @@
{
"name": "PinchChat",
"short_name": "PinchChat",
"description": "A sleek, dark-themed webchat UI for OpenClaw",
"description": "A sleek, dark-themed webchat UI for OpenClaw — monitor sessions, stream responses, and inspect tool calls in real-time.",
"start_url": "/",
"display": "standalone",
"background_color": "#1e1e24",
"theme_color": "#1e1e24",
"orientation": "any",
"categories": ["developer", "productivity", "utilities"],
"icons": [
{
"src": "/favicon-16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "/favicon-32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/logo-192.png",
"sizes": "192x192",

62
public/sw.js Normal file
View File

@@ -0,0 +1,62 @@
// PinchChat Service Worker — cache static assets for offline/instant load
const CACHE_NAME = 'pinchchat-v1';
// Cache static assets on install
self.addEventListener('install', (event) => {
self.skipWaiting();
});
// Clean old caches on activate
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
)
);
self.clients.claim();
});
// Stale-while-revalidate for static assets, network-first for API/WS
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Skip non-GET, WebSocket upgrades, and chrome-extension requests
if (event.request.method !== 'GET') return;
if (url.protocol === 'chrome-extension:') return;
// Don't cache API calls or WebSocket-related requests
if (url.pathname.startsWith('/api') || url.pathname.startsWith('/ws')) return;
// For navigation requests (HTML), always go network-first to get latest SPA shell
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.then((response) => {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
return response;
})
.catch(() => caches.match(event.request))
);
return;
}
// Static assets (JS, CSS, images, fonts): stale-while-revalidate
if (
url.pathname.match(/\.(js|css|png|jpg|jpeg|gif|webp|svg|woff2?|ttf|ico|json)$/) ||
url.pathname.startsWith('/assets/')
) {
event.respondWith(
caches.open(CACHE_NAME).then((cache) =>
cache.match(event.request).then((cached) => {
const fetchPromise = fetch(event.request).then((response) => {
if (response.ok) cache.put(event.request, response.clone());
return response;
});
return cached || fetchPromise;
})
)
);
return;
}
});

View File

@@ -5,6 +5,15 @@ import { ErrorBoundary } from './components/ErrorBoundary'
import { ThemeProvider } from './contexts/ThemeContext'
import './index.css'
// Register service worker for PWA support (offline caching, installability)
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(() => {
// SW registration failed — app works fine without it
});
});
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ErrorBoundary>