diff --git a/web/index.html b/web/index.html index 8749601..7d656d6 100644 --- a/web/index.html +++ b/web/index.html @@ -14,6 +14,13 @@ rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%231a1d24'/%3E%3Ccircle cx='16' cy='16' r='6' fill='%235fe3a1'/%3E%3Ccircle cx='16' cy='16' r='10' fill='none' stroke='%235fe3a1' stroke-opacity='0.4' stroke-width='1.5'/%3E%3C/svg%3E" /> + + + + + + +
diff --git a/web/public/apple-touch-icon.png b/web/public/apple-touch-icon.png new file mode 100644 index 0000000..872e2b3 Binary files /dev/null and b/web/public/apple-touch-icon.png differ diff --git a/web/public/icon-192.png b/web/public/icon-192.png new file mode 100644 index 0000000..ceefb25 Binary files /dev/null and b/web/public/icon-192.png differ diff --git a/web/public/icon-512.png b/web/public/icon-512.png new file mode 100644 index 0000000..27c9d98 Binary files /dev/null and b/web/public/icon-512.png differ diff --git a/web/public/icon.svg b/web/public/icon.svg new file mode 100644 index 0000000..ec5ae5c --- /dev/null +++ b/web/public/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/public/manifest.webmanifest b/web/public/manifest.webmanifest new file mode 100644 index 0000000..dbe56ba --- /dev/null +++ b/web/public/manifest.webmanifest @@ -0,0 +1,38 @@ +{ + "name": "maestro", + "short_name": "maestro", + "description": "Unified terminal-session orchestrator — see and attach to every shell and agent across your machines.", + "id": "/", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#1a1d24", + "theme_color": "#1a1d24", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + } + ] +} diff --git a/web/public/sw.js b/web/public/sw.js new file mode 100644 index 0000000..9b48878 --- /dev/null +++ b/web/public/sw.js @@ -0,0 +1,72 @@ +// maestro service worker. +// +// Goal: make the app installable and let the shell load offline. It is NOT a +// data cache — anything under /api (HTTP and WebSocket upgrades) is always left +// to the network, since a terminal orchestrator is useless without a live +// daemon. Static assets are cached at runtime so we never need to know Vite's +// hashed filenames ahead of time. + +const CACHE = "maestro-shell-v1"; + +self.addEventListener("install", (event) => { + // Warm the cache with the app entry so a cold offline launch still paints. + event.waitUntil( + caches + .open(CACHE) + .then((cache) => cache.addAll(["/", "/manifest.webmanifest"])) + .catch(() => {}), + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + // Drop caches from previous versions, then take control immediately. + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))), + ) + .then(() => self.clients.claim()), + ); +}); + +self.addEventListener("fetch", (event) => { + const req = event.request; + if (req.method !== "GET") return; + + const url = new URL(req.url); + // Only handle same-origin GETs; never touch the daemon API. + if (url.origin !== self.location.origin) return; + if (url.pathname.startsWith("/api/")) return; + + // SPA navigations: network-first, fall back to the cached shell offline. + if (req.mode === "navigate") { + event.respondWith( + fetch(req) + .then((res) => { + const copy = res.clone(); + caches.open(CACHE).then((c) => c.put("/", copy)).catch(() => {}); + return res; + }) + .catch(() => caches.match("/").then((r) => r || caches.match(req))), + ); + return; + } + + // Static assets (Vite emits content-hashed names): stale-while-revalidate. + event.respondWith( + caches.match(req).then((cached) => { + const network = fetch(req) + .then((res) => { + if (res.ok && res.type === "basic") { + const copy = res.clone(); + caches.open(CACHE).then((c) => c.put(req, copy)).catch(() => {}); + } + return res; + }) + .catch(() => cached); + return cached || network; + }), + ); +}); diff --git a/web/src/main.ts b/web/src/main.ts index f4eb6cb..9cd615d 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -9,6 +9,19 @@ import "./styles.css"; initTheme(); installViewportSizing(); +registerServiceWorker(); + +// Register the PWA service worker so the app is installable and the shell loads +// offline. Production only — in dev the SW would fight Vite's HMR. +function registerServiceWorker(): void { + if (import.meta.env.DEV) return; + if (!("serviceWorker" in navigator)) return; + window.addEventListener("load", () => { + void navigator.serviceWorker.register("/sw.js").catch(() => { + // Best-effort: a failed registration must never break the app. + }); + }); +} const rootEl = document.getElementById("app"); if (!rootEl) {