Files
clawmates/frontend/public/sw.js
T
Omar SobhandClaude Fable 5 ace66d7ffb P6 complete: PWA, route motion, dex browser-flow OAuth, release pipeline
- PWA (§16): hand-rolled 60-line service worker (network-first pages with
  offline fallback, cache-first hashed statics, /api NEVER touched — SSE
  and approvals stay live), app manifest with §2 identity, stdlib-
  generated coral claw icons, prod-only registration. E2E asserts
  manifest, real PNG icons, an ACTIVATED service worker, and the /api
  bypass. (Serwist was tried and dropped: its webpack plugin fights
  Next 16's Turbopack builds; sixty lines we own beat a plugin we fight.)
- Route motion (§3): (workspace) template re-mounts per navigation with a
  quiet fade-rise, zeroed under prefers-reduced-motion. The a11y sweep
  now settles running animations before scanning — axe was reading
  mid-fade opacity as contrast failures
- OAuth browser flow vs REAL dex: the e2e harness boots dexidp/dex with
  static client + password; the journey drives the actual dex login form
  from /api/apps/oauth/start through the callback 303 and asserts the
  app reads connected (closing the P4 deferral honestly)
- release.yml: tag-triggered — builds all four images + postgres, saves
  tarballs, assembles the SIGNED air-gapped bundle (compose, config,
  migrations, seccomp profile, installer, bundler binary), derives the
  public key via the new Could not find command "pubkey". subcommand (tested), verifies
  the bundle customer-style with the public half only, attaches tarball
  + public key to the GitHub release

153 Rust + 63 frontend tests + 29 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 09:57:21 -05:00

61 lines
1.8 KiB
JavaScript

// TeamClaw service worker (§16 PWA). Hand-rolled on purpose: ~50 lines we
// fully control beats a build-tool plugin. Strategy:
// /api/** -> NEVER touched (approvals + SSE must be live)
// /_next/static/** -> cache-first (content-hashed, immutable)
// navigations -> network-first, cache fallback for offline shell
const STATIC_CACHE = "tc-static-v1";
const PAGE_CACHE = "tc-pages-v1";
self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const keep = [STATIC_CACHE, PAGE_CACHE];
for (const key of await caches.keys()) {
if (!keep.includes(key)) await caches.delete(key);
}
await self.clients.claim();
})(),
);
});
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.origin !== self.location.origin) return;
if (url.pathname.startsWith("/api/")) return;
if (url.pathname.startsWith("/_next/static/")) {
event.respondWith(
(async () => {
const cache = await caches.open(STATIC_CACHE);
const hit = await cache.match(event.request);
if (hit) return hit;
const response = await fetch(event.request);
if (response.ok) cache.put(event.request, response.clone());
return response;
})(),
);
return;
}
if (event.request.mode === "navigate") {
event.respondWith(
(async () => {
const cache = await caches.open(PAGE_CACHE);
try {
const response = await fetch(event.request);
if (response.ok) cache.put(event.request, response.clone());
return response;
} catch {
const hit = await cache.match(event.request);
if (hit) return hit;
throw new Error("offline and not cached");
}
})(),
);
}
});