/* Tx OS service worker — Web Push only. * * Scope: registered with `scope: BASE_URL`, so the SW only controls * paths under the SPA's base path (e.g. `/`, or `/tx-os/` in a future * subpath deployment). Every URL the SW touches — icons, navigation * targets — is resolved against `self.registration.scope` so the same * code works at root and at a subpath without code changes. * * We deliberately do NOT cache app assets here — Tx OS is self-hosted * on a single machine, so the network is fast and reliable. Adding * cache layers would risk shipping stale React bundles after an * upgrade. */ function scopedUrl(relative) { // self.registration.scope is the full origin + base path with a // trailing slash (e.g. "https://host/" or "https://host/tx-os/"). // Strip a leading slash from `relative` so URL() treats it as a // path under scope rather than origin-relative. const cleaned = String(relative || "").replace(/^\/+/, ""); const resolved = new URL(cleaned, self.registration.scope); // Preserve the query string and hash so notification deep-links // (e.g. "/meetings?meeting=12" or "/notes?thread=7") survive the click // handler — taking only .pathname would drop them and land the user on // the generic page instead of the specific item. return resolved.pathname + resolved.search + resolved.hash; } self.addEventListener("install", () => { self.skipWaiting(); }); self.addEventListener("activate", (event) => { event.waitUntil(self.clients.claim()); }); self.addEventListener("push", (event) => { /** @type {{title?: string, body?: string, tag?: string, url?: string, type?: string}} */ let payload = {}; try { payload = event.data ? event.data.json() : {}; } catch { payload = { title: event.data ? event.data.text() : "Tx OS" }; } const title = payload.title || "Tx OS"; const iconUrl = scopedUrl("icons/icon-192.png"); const options = { body: payload.body || "", tag: payload.tag || payload.type || "tx-os", renotify: true, icon: iconUrl, badge: iconUrl, data: { url: payload.url || "/", type: payload.type || null, }, }; event.waitUntil(self.registration.showNotification(title, options)); }); self.addEventListener("notificationclick", (event) => { event.notification.close(); const rawTarget = (event.notification.data && event.notification.data.url) || "/"; const targetPath = scopedUrl(rawTarget); event.waitUntil( (async () => { const allClients = await self.clients.matchAll({ type: "window", includeUncontrolled: true, }); // Prefer an already-open Tx OS tab under our scope; navigate it // to the target and focus. for (const client of allClients) { try { await client.focus(); if ("navigate" in client) { await client.navigate(targetPath); } return; } catch { /* try next */ } } if (self.clients.openWindow) { await self.clients.openWindow(targetPath); } })(), ); });