teecup/app/push.py

64 lines
2.4 KiB
Python
Raw Normal View History

Update Todos 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.
2026-07-28 22:34:16 +02:00
"""
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()