// TeeCup service worker (ADR-028) -- håndskrevet, ingen next-pwa/workbox- // avhengighet. To ansvar, begge begrenset med vilje: // // 1) Cache side-NAVIGASJON (nettverk først, cache som fallback, offline.html // som siste utvei) -- en tidligere besøkt side kan åpnes på nytt uten // nett. // 2) Cache GET-kall under /orgs/* (nettverk først, cache som fallback) -- // slik at data for en allerede åpnet side (f.eks. et scorekort) fortsatt // er LESBAR offline. Bevisst IKKE stale-while-revalidate: scorekortet // leses på nytt rett etter hver innsending, og må da alltid prøve ekte // fersk data først -- en umiddelbart utdatert cache ville vist feil // matchstatus/hull-tall rett etter en vellykket innsending. // // Selve SKRIVINGEN (hole-scores/hole-results) fanges ALDRI her -- det // håndteres i lib/offline-queue.ts, med en egen IndexedDB-kø i klientkoden, // hvor UI-et kan gi umiddelbar, presis tilbakemelding ("lagret lokalt, // venter på synk"). /auth/* og /public/* caches bevisst ikke -- caching er // begrenset til akkurat det scoreregistrerings-flyten trenger. // // Kjent, akseptert begrensning: Cache Storage er nøklet på URL, ikke på // innlogget bruker/sesjon -- deler flere kontoer samme enhet/nettleser, // kan en offline-fallback vise data cachet av en TIDLIGERE innlogget // bruker på samme enhet. Ikke løst i v1 (samme tillitsnivå som resten av // appen, se team_authz.py sin trusselmodell-begrunnelse), ingen // cache-tømming ved utlogging bygget ennå. // v2 (2026-08-02): "nettverk først" var i praksis "nettleser-cache først" -- // et rått fetch(req) her respekterer fortsatt HTTP Cache-Control på selve // requesten, så en side lastet før en ny utrulling kunne forbli synlig // lenge etter deploy uten at brukeren merket noe (ingen feilmelding, bare // stille utdatert innhold). explicit cache:"reload" tvinger et ekte // nettverksoppslag hver gang, samme prinsipp begge steder under. const RUNTIME_CACHE = "teecup-runtime-v2" const OFFLINE_URL = "/offline.html" self.addEventListener("install", (event) => { event.waitUntil( caches .open(RUNTIME_CACHE) .then((cache) => cache.add(OFFLINE_URL)) .then(() => self.skipWaiting()), ) }) self.addEventListener("activate", (event) => { event.waitUntil( caches .keys() .then((keys) => Promise.all(keys.filter((k) => k !== RUNTIME_CACHE).map((k) => caches.delete(k)))) .then(() => self.clients.claim()), ) }) self.addEventListener("fetch", (event) => { const req = event.request if (req.method !== "GET") return // skriving går aldri via SW-en if (req.mode === "navigate") { event.respondWith( fetch(req, { cache: "reload" }) .then((res) => { const copy = res.clone() caches.open(RUNTIME_CACHE).then((cache) => cache.put(req, copy)) return res }) .catch(async () => (await caches.match(req)) || (await caches.match(OFFLINE_URL))), ) return } const url = new URL(req.url) if (url.origin === self.location.origin && url.pathname.startsWith("/orgs/")) { event.respondWith( fetch(req, { cache: "reload" }) .then((res) => { if (res.ok) caches.open(RUNTIME_CACHE).then((cache) => cache.put(req, res.clone())) return res }) .catch(() => caches.match(req)), ) } }) // Push-varsler til telefonens OS (Web Push/VAPID, 2026-07-28). Payloaden er // alltid JSON {title, body, link_path} (se app/push.py) -- viser en native // systemvarsling selv når ingen fane er åpen. notificationclick fokuserer en // allerede åpen fane på samme origin hvis én finnes, ellers åpner en ny. self.addEventListener("push", (event) => { let data = { title: "TeeCup", body: "Du har et nytt varsel.", link_path: "/my-notifications" } try { if (event.data) data = { ...data, ...event.data.json() } } catch { // Behold standardteksten hvis payloaden mot formodning ikke er JSON. } event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: "/icons/icon-192.png", badge: "/icons/icon-192.png", data: { link_path: data.link_path }, }), ) }) self.addEventListener("notificationclick", (event) => { event.notification.close() const linkPath = event.notification.data?.link_path || "/my-notifications" event.waitUntil( self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((clients) => { for (const client of clients) { if (client.url.includes(self.location.origin) && "focus" in client) { client.navigate(linkPath) return client.focus() } } return self.clients.openWindow(linkPath) }), ) })