diff --git a/ui/public/site.webmanifest b/ui/public/site.webmanifest index 8861bd64..907f6293 100644 --- a/ui/public/site.webmanifest +++ b/ui/public/site.webmanifest @@ -1,6 +1,14 @@ { + "id": "/", "name": "Paperclip", "short_name": "Paperclip", + "description": "AI-powered project management and agent coordination platform", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "any", + "theme_color": "#18181b", + "background_color": "#18181b", "icons": [ { "src": "/android-chrome-192x192.png", @@ -11,9 +19,12 @@ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } - ], - "theme_color": "#18181b", - "background_color": "#18181b", - "display": "standalone" + ] } diff --git a/ui/public/sw.js b/ui/public/sw.js new file mode 100644 index 00000000..56dd85a3 --- /dev/null +++ b/ui/public/sw.js @@ -0,0 +1,60 @@ +const CACHE_NAME = "paperclip-v1"; +const STATIC_ASSETS = ["/", "/favicon.ico", "/favicon.svg"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)) + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all( + keys + .filter((key) => key !== CACHE_NAME) + .map((key) => caches.delete(key)) + ) + ) + ); + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + const { request } = event; + const url = new URL(request.url); + + // Skip non-GET requests and API calls + if (request.method !== "GET" || url.pathname.startsWith("/api")) { + return; + } + + // Network-first for navigation requests (SPA) + if (request.mode === "navigate") { + event.respondWith( + fetch(request) + .then((response) => { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); + return response; + }) + .catch(() => caches.match("/")) + ); + return; + } + + // Cache-first for static assets + event.respondWith( + caches.match(request).then((cached) => { + if (cached) return cached; + return fetch(request).then((response) => { + if (response.ok && url.origin === self.location.origin) { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); + } + return response; + }); + }) + ); +}); diff --git a/ui/src/main.tsx b/ui/src/main.tsx index ba8a4d69..33fd6c81 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -15,6 +15,12 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import "@mdxeditor/editor/style.css"; import "./index.css"; +if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker.register("/sw.js"); + }); +} + const queryClient = new QueryClient({ defaultOptions: { queries: {