teecup/frontend/public/sw.js
Erol Haagenrud d395f86753 Update Todos
Skrive migrasjon 052 (guest_first_name/guest_last_name + backfill)

Bygge backend: splitt navn i ParticipantCreate/Update, auto-synk guest_name

Bygge backend: GET known-guest-oppslag (e-post-basert, scoped til egen bruker)

Bygge backend: retroaktiv e-post-kobling ved innlogging (auth.py)

Bygge backend: HTML-e-post-støtte i email.py (multipart/alternative)

Bygge backend: scorekort/statistikk-e-post ved complete_round (individuell + delt-ball)

Frontend: e-post-felt + navnesplitt + autofyll fra søk i AddGuestForm

Frontend: "vi kjenner igjen denne e-posten"-forslag i AddGuestForm

Scratch-verifisere hele kjeden (migrasjon+backend+frontend+e-post) end-to-end

Kjøre migrasjon mot ekte teecup_db og rulle ut (etter bekreftelse)
Alt er bygget og scratch-verifisert. Før jeg ruller ut mot ekte teecup_db, her er planen — trenger din bekreftelse per sikkerhetsregelen:

Migrasjon 052_guest_name_split.sql (rent additiv, ingen sletting):

ALTER TABLE round_participant ADD COLUMN guest_first_name text; + guest_last_name text
Backfiller eksisterende guest_name-rader ved samme "første ord/resten"-heuristikk som frontend bruker til autofyll
Deretter: docker compose up -d --build teecup_api teecup_frontend

Hva som er bygget:

E-post som valgfritt felt i "Legg til uten konto (gjest)" (fantes i backend fra før, manglet i selve skjemaet)
Navn splittet i for-/etternavn (etternavn valgfritt)
Retroaktiv kobling: spiller runder som gjest før du har konto → runden dukker opp i "Egne runder" når du logger inn med samme e-post, men teller ikke automatisk mot faktisk HCP (må bekreftes selv — unngår at en fremmed kan påvirke HCP-en din uten samtykke)
E-post med scorekort + statistikk + invitasjon sendes automatisk når runden fullføres, til enhver gjest med registrert e-post — første HTML-e-post i appen
Autofyll fra søkefeltet inn i gjesteskjemaet
"Vi kjenner igjen denne e-posten"-forslag, scoped til dine egne tidligere gjester (ikke globalt — unngår en personvernlekkasje)
2026-08-03 06:24:57 +02:00

119 lines
4.6 KiB
JavaScript

// 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)
}),
)
})