Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner) Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1) Design og bygg scramble/greensome-statistikk (valgt utslag per spiller) Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker) Scratch-verifiser backend for alle fire (27/27 sjekker gjort) Browserverifiser alle fire i nettleser mot scratch-backend Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md Alle fire er ferdigstilt, verifisert og live. Kort oppsummert: PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten. Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database. Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen). Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet. 27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
63 lines
2.4 KiB
Python
63 lines
2.4 KiB
Python
"""
|
|
Push-varsler til telefonens OS (Web Push/VAPID), FEATURE_BACKLOG.md
|
|
"Varsler: push til telefon". Bevisst helt frittstående fra
|
|
app/realtime.py (WebSocket-sanntid for åpne faner) -- push når appen ikke
|
|
er åpen i det hele tatt, helt andre leveringsmekanisme og bibliotek.
|
|
|
|
Valgfritt (PUSH_CONFIGURED, app/config.py) -- ingen nøkler satt betyr
|
|
push rett og slett ikke forsøkes, resten av varslingssystemet (in-app +
|
|
e-post) fungerer helt uendret. Samme grasiøs-degraderings-/feil-
|
|
isolasjons-mønster som app/email.py: en driftsfeil i selve utsendingen
|
|
lekker ALDRI til kalleren (create_notification skal alltid lykkes med å
|
|
skrive selve in-app-varselet, uansett hva som skjer her).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import traceback
|
|
|
|
from pywebpush import WebPushException, webpush
|
|
|
|
from .config import settings
|
|
|
|
|
|
def _send_sync(subscription_info: dict, payload: dict) -> None:
|
|
webpush(
|
|
subscription_info=subscription_info,
|
|
data=json.dumps(payload),
|
|
vapid_private_key=settings.VAPID_PRIVATE_KEY,
|
|
vapid_claims={"sub": settings.VAPID_SUBJECT},
|
|
)
|
|
|
|
|
|
async def send_push_to_user(conn, user_id: str, title: str, body: str, link_path: str) -> None:
|
|
"""Sender til ALLE enheter brukeren har abonnert (flere nettlesere/
|
|
telefoner er normalt). Et utløpt/tilbakekalt abonnement (404/410 fra
|
|
push-tjenesten) ryddes bort her -- ikke behandlet som en feil, bare
|
|
forventet livssyklus for et push-abonnement."""
|
|
if not settings.PUSH_CONFIGURED:
|
|
return
|
|
rows = await conn.fetch(
|
|
"SELECT id::text AS id, endpoint, p256dh, auth FROM push_subscription WHERE user_id = $1",
|
|
user_id,
|
|
)
|
|
if not rows:
|
|
return
|
|
payload = {"title": title, "body": body, "link_path": link_path}
|
|
for row in rows:
|
|
subscription_info = {
|
|
"endpoint": row["endpoint"],
|
|
"keys": {"p256dh": row["p256dh"], "auth": row["auth"]},
|
|
}
|
|
try:
|
|
await asyncio.to_thread(_send_sync, subscription_info, payload)
|
|
except WebPushException as exc:
|
|
status = exc.response.status_code if exc.response is not None else None
|
|
if status in (404, 410):
|
|
await conn.execute("DELETE FROM push_subscription WHERE id = $1", row["id"])
|
|
else:
|
|
traceback.print_exc()
|
|
except Exception:
|
|
traceback.print_exc()
|