PageSourceSearch

https://cellcog.ai/sw.js

js cellcog.ai collected 2026-10-02 15:34:56 UTC 5,796 bytes, 106 lines download raw bytes

1/* eslint-disable no-restricted-globals -- a service worker's global IS `self`; CRA's config bans it for window code */
2/* CellCog service worker (CEL-2450, 2026-09-09; one-sound rule + tap tracking CEL-2490, same evening): Web Push ONLY.
3   No caching and no fetch interception: the app keeps loading exactly as before. This file turns a push message
4   into a system notification and opens (or focuses) the chat when the notification is tapped.
5   Payload shape (services/web_push_service.py): { title, body, url, tag, silent }.
6   EVERY push shows a notification, or is skipped only because the person is looking at that very page. Chrome budgets
7   pushes that show nothing and then shows its own "This site has been updated in the background" card; the
8   cross-device clear that did exactly that was removed 2026-09-09. Never add a push branch that shows nothing. */
9
10self.addEventListener('install', () => self.skipWaiting());
11self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));
12
13const TAP_PARAM = 'src';
14const TAP_VALUE = 'push';
15const TAP_MESSAGE_TYPE = 'cc-push-tapped';
16
17const samePath = (clientUrl, targetPath) => {
18  try { return new URL(clientUrl).pathname === targetPath; } catch (e) { return false; }
19};
20const kindFor = (path) => (path.indexOf('/ai_employee/') === 0 ? 'employee' : 'chat');
21const withTapParam = (href) => {
22  const u = new URL(href);
23  u.searchParams.set(TAP_PARAM, TAP_VALUE);
24  return u.href;
25};
26
27self.addEventListener('push', (event) => {
28  let data = {};
29  try {
30    data = event.data ? event.data.json() : {};
31  } catch (e) {
32    data = { title: 'CellCog', body: event.data ? event.data.text() : '' };
33  }
34  const title = data.title || 'CellCog';
35  const url = data.url || '/';
36  const targetPath = new URL(url, self.location.origin).pathname;
37  const baseOptions = {
38    body: data.body || '',
39    icon: '/logo192.png',
40    badge: '/logo192.png',
41    // Dedupe (Sep 9 2026): one notification per tag (chat id, or employee id for AI employees); a newer one REPLACES
42    // the older, so cards never stack. renotify stays TRUE (Nitish): a card the person never dismissed can sit in the
43    // center for hours, and a new "needs you" must still alert; each real stop is one alert, never a pile.
44    tag: data.tag || 'cellcog',
45    renotify: true,
46    data: { url },
47  };
48  event.waitUntil(
49    self.clients.matchAll({ type: 'window', includeUncontrolled: true })
50      .then((clients) => {
51        // A tab that is FOCUSED and already on the target page is the person looking at it: no notification.
52        const viewing = clients.some((c) => c.focused && c.visibilityState === 'visible' && samePath(c.url, targetPath));
53        if (viewing) return null;
54        // One sound per device (journey review Sep 9 2026, gap 4): while ANY cellcog.ai tab is open, that tab owns
55        // the audio (the leader tab rings, honoring the mute), so the banner is silent. With no tab open the banner
56        // carries the sound, unless the account is muted (payload.silent, "Mute all sound notifications").
57        const silent = clients.length > 0 ? true : !!data.silent;
58        return self.registration.showNotification(title, Object.assign({}, baseOptions, { silent }));
59      })
60      .catch(() => self.registration.showNotification(title, baseOptions))
61  );
62});
63
64self.addEventListener('notificationclick', (event) => {
65  event.notification.close();
66  const target = new URL((event.notification.data && event.notification.data.url) || '/', self.location.origin).href;
67  const targetPath = new URL(target).pathname;
68  const kind = kindFor(targetPath);
69  // Sep 9 2026 tap bug: a tab this worker does NOT control (the page loaded before the worker registered) makes
70  // client.navigate() reject, and the old chain swallowed that rejection: tap did nothing. Order now: focus a tab
71  // already on the target PATH (query string ignored) and tell it about the tap; else try navigating a controlled
72  // tab; on ANY failure, or no tab at all, open a new window. openWindow needs no control over anything. A page that
73  // is opened or navigated learns about the tap from ?src=push (tracking, hooks/usePushAccountSync).
74  event.waitUntil(
75    self.clients.matchAll({ type: 'window', includeUncontrolled: true })
76      .then((clients) => {
77        const onTarget = clients.find((c) => samePath(c.url, targetPath));
78        if (onTarget && 'focus' in onTarget) {
79          return onTarget.focus().then((focused) => {
80            try { (focused || onTarget).postMessage({ type: TAP_MESSAGE_TYPE, kind }); } catch (e) { /* tracking only */ }
81            return focused;
82          });
83        }
84        const navigable = clients.find((c) => 'navigate' in c && 'focus' in c);
85        if (navigable) {
86          return navigable.navigate(withTapParam(target))
87            .then((c) => ((c || navigable).focus ? (c || navigable).focus() : null))
88            .catch(() => self.clients.openWindow(withTapParam(target)));
89        }
90        return self.clients.openWindow(withTapParam(target));
91      })
92      .catch(() => self.clients.openWindow(withTapParam(target)))
93  );
94});
95
96// The push service can rotate a subscription (new endpoint). Keep this browser subscribed with the same server key;
97// the app registers the new endpoint under the signed-in account on its next load (utils/webPush.ensureSubscribed) and
98// the old row prunes itself on the push service's 410.
99self.addEventListener('pushsubscriptionchange', (event) => {
100  const previous = event.oldSubscription || event.newSubscription;
101  const key = previous && previous.options && previous.options.applicationServerKey;
102  if (!key) return;
103  event.waitUntil(
104    self.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key }).catch(() => null)
105  );
106});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.