Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
"""
|
2026-07-28 20:09:21 +02:00
|
|
|
In-app varslingssenter (FEATURE_BACKLOG.md "Varsler"-runden, 2026-07-25) +
|
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
|
|
|
e-post-fallback per type (2026-07-28 oppfølging, migrasjon 033) + push til
|
|
|
|
|
telefonens OS (2026-07-28, migrasjon 037, se app/push.py). Push har -- ulikt
|
|
|
|
|
e-post -- INGEN egen per-type opt-in: selve det å abonnere (gi
|
|
|
|
|
nettleser-tillatelse) ER samtykket, en abonnert enhet får push for ALLE
|
|
|
|
|
typer create_notification() skriver.
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
|
|
|
|
|
Eid av BRUKER (mottaker), ikke organisasjon -- plain_connection(), samme
|
|
|
|
|
mønster som personlig profil/HCP-historikk/venner. `create_notification()`
|
2026-07-28 20:09:21 +02:00
|
|
|
er den eneste skrivevegen inn -- kalt fra andre routere (friends.py,
|
|
|
|
|
rounds.py) ved konkrete hendelser, ikke noe generisk event-system.
|
|
|
|
|
|
|
|
|
|
Trygg standard (samme filosofi som visibility_mode='private' ellers i
|
|
|
|
|
appen): en bruker som ikke har gjort noe valg får ALDRI e-post for noen
|
|
|
|
|
type -- kun `user_notification_email_pref`-raden EN BRUKER selv setter
|
|
|
|
|
(via PUT /notifications/email-prefs) slår e-post PÅ for akkurat den typen.
|
|
|
|
|
E-post-utsendingen skjer INNI `create_notification()` selv (ikke i hvert
|
|
|
|
|
enkelt kallsted) -- driftsfeil i utsendingen skal ALDRI hindre selve
|
|
|
|
|
in-app-varselet fra å bli opprettet, samme mønster (SMTP_CONFIGURED-sjekk
|
|
|
|
|
+ try/except + traceback.print_exc()) som all annen e-post i appen
|
|
|
|
|
(app/routers/auth.py, organizations.py).
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-07-28 20:09:21 +02:00
|
|
|
import traceback
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
from typing import Literal
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends
|
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
|
|
|
|
from ..auth import CurrentUser, get_current_user
|
2026-07-28 20:09:21 +02:00
|
|
|
from ..config import settings
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
from ..db import plain_connection
|
2026-07-28 20:09:21 +02:00
|
|
|
from ..email import send_notification_email
|
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
|
|
|
from ..errors import app_error
|
|
|
|
|
from ..push import send_push_to_user
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
|
|
|
|
|
router = APIRouter(tags=["notifications"])
|
|
|
|
|
|
|
|
|
|
NotificationType = Literal["friend", "tournament", "round", "result"]
|
2026-07-28 20:09:21 +02:00
|
|
|
_NOTIFICATION_TYPES: tuple[NotificationType, ...] = ("friend", "tournament", "round", "result")
|
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_TITLES: dict[str, str] = {
|
|
|
|
|
"friend": "Venner",
|
|
|
|
|
"round": "Runde",
|
|
|
|
|
"result": "Resultat",
|
|
|
|
|
"tournament": "Turnering",
|
|
|
|
|
}
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def create_notification(conn, *, user_id: str, type: NotificationType, message: str, link_path: str) -> None:
|
|
|
|
|
"""Kalles fra andre routere sin egen `plain_connection()`/transaksjon --
|
2026-07-28 20:09:21 +02:00
|
|
|
tar en allerede-åpen `conn`, åpner ikke en egen. Sender i tillegg en
|
|
|
|
|
e-post-fallback hvis (og kun hvis) mottakeren selv har valgt inn for
|
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
|
|
|
akkurat DENNE typen, OG et push-varsel til enhver enhet mottakeren har
|
|
|
|
|
abonnert (uavhengig av e-post-valget -- se _push_for_notification)."""
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
await conn.execute(
|
|
|
|
|
"INSERT INTO notification (user_id, type, message, link_path) VALUES ($1, $2, $3, $4)",
|
|
|
|
|
user_id,
|
|
|
|
|
type,
|
|
|
|
|
message,
|
|
|
|
|
link_path,
|
|
|
|
|
)
|
|
|
|
|
|
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
|
|
|
await _push_for_notification(conn, user_id=user_id, type=type, message=message, link_path=link_path)
|
|
|
|
|
|
2026-07-28 20:09:21 +02:00
|
|
|
wants_email = await conn.fetchval(
|
|
|
|
|
"SELECT EXISTS(SELECT 1 FROM user_notification_email_pref WHERE user_id = $1 AND type = $2)",
|
|
|
|
|
user_id,
|
|
|
|
|
type,
|
|
|
|
|
)
|
|
|
|
|
if not wants_email:
|
|
|
|
|
return
|
|
|
|
|
recipient = await conn.fetchrow(
|
|
|
|
|
"SELECT email, preferred_locale FROM app_user WHERE id = $1 AND email IS NOT NULL",
|
|
|
|
|
user_id,
|
|
|
|
|
)
|
|
|
|
|
if recipient is None:
|
|
|
|
|
return
|
|
|
|
|
if settings.SMTP_CONFIGURED:
|
|
|
|
|
try:
|
|
|
|
|
await send_notification_email(recipient["email"], message, link_path, recipient["preferred_locale"])
|
|
|
|
|
except Exception:
|
|
|
|
|
# Se modul-docstring: driftsfeil i utsendingen skal aldri
|
|
|
|
|
# hindre selve in-app-varselet, som allerede er skrevet over.
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
elif settings.DEV_LOG_MAGIC_LINKS:
|
|
|
|
|
print(f"[DEV] Varsel-e-post til {recipient['email']} ({type}): {message}", flush=True)
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
async def _push_for_notification(conn, *, user_id: str, type: NotificationType, message: str, link_path: str) -> None:
|
|
|
|
|
"""Kalt fra create_notification() -- egen liten funksjon KUN for at
|
|
|
|
|
push (uavhengig av e-post-preferansen over) alltid forsøkes for enhver
|
|
|
|
|
abonnert enhet. Feiler aldri synlig for kalleren, se app/push.py."""
|
|
|
|
|
try:
|
|
|
|
|
await send_push_to_user(conn, user_id, _PUSH_TITLES.get(type, "TeeCup"), message, link_path)
|
|
|
|
|
except Exception:
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 20:09:21 +02:00
|
|
|
class NotificationEmailPrefsOut(BaseModel):
|
|
|
|
|
types: list[str]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NotificationEmailPrefsUpdate(BaseModel):
|
|
|
|
|
types: list[NotificationType]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/notifications/email-prefs", response_model=NotificationEmailPrefsOut)
|
|
|
|
|
async def get_email_prefs(user: CurrentUser = Depends(get_current_user)) -> NotificationEmailPrefsOut:
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
"SELECT type FROM user_notification_email_pref WHERE user_id = $1",
|
|
|
|
|
user.user_id,
|
|
|
|
|
)
|
|
|
|
|
return NotificationEmailPrefsOut(types=[r["type"] for r in rows])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/notifications/email-prefs", response_model=NotificationEmailPrefsOut)
|
|
|
|
|
async def set_email_prefs(
|
|
|
|
|
body: NotificationEmailPrefsUpdate, user: CurrentUser = Depends(get_current_user)
|
|
|
|
|
) -> NotificationEmailPrefsOut:
|
|
|
|
|
"""Full-erstatning, samme mønster som PUT /friends/{id}/categories --
|
|
|
|
|
sender med en tom liste slår e-post AV for alle typer igjen."""
|
|
|
|
|
types = sorted(set(body.types))
|
|
|
|
|
async with plain_connection() as conn, conn.transaction():
|
|
|
|
|
await conn.execute("DELETE FROM user_notification_email_pref WHERE user_id = $1", user.user_id)
|
|
|
|
|
for t in types:
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"INSERT INTO user_notification_email_pref (user_id, type) VALUES ($1, $2)",
|
|
|
|
|
user.user_id,
|
|
|
|
|
t,
|
|
|
|
|
)
|
|
|
|
|
return NotificationEmailPrefsOut(types=types)
|
|
|
|
|
|
Varsler (fra zip 20): in-app varslingssenter er live — bjelle med uleste-tall i dashbord-headeren, /my-notifications-side. Trigges i dag ved venneforespørsel sendt/akseptert; flere hendelser kan kobles på senere.
Rundeleaderboard: ny GET /rounds/{id}/leaderboard-backend er live (rangering, thru-tall, brutto+netto til par, håndterer 1 til 15+ deltakere). V0-prompten for selve visningen ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app — send meg zip-en når du har den, så kobler jeg den på (foreslått rute /my-rounds/[id]/leaderboard, lenket fra rundesiden).
Begge deler scratch-verifisert (39/39 sjekker, inkl. en uavhengig kryssjekk av netto-beregningen mot handicap_engine direkte), rullet ut mot ekte teecup_db/containere, teeoff.no upåvirket.
2026-07-26 06:53:58 +02:00
|
|
|
|
|
|
|
|
class NotificationOut(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
type: str
|
|
|
|
|
message: str
|
|
|
|
|
link_path: str
|
|
|
|
|
created_at: str
|
|
|
|
|
read_at: str | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/notifications", response_model=list[NotificationOut])
|
|
|
|
|
async def list_notifications(user: CurrentUser = Depends(get_current_user)) -> list[NotificationOut]:
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
"""
|
|
|
|
|
SELECT id::text AS id, type, message, link_path, created_at, read_at
|
|
|
|
|
FROM notification WHERE user_id = $1
|
|
|
|
|
ORDER BY created_at DESC
|
|
|
|
|
LIMIT 100
|
|
|
|
|
""",
|
|
|
|
|
user.user_id,
|
|
|
|
|
)
|
|
|
|
|
return [
|
|
|
|
|
NotificationOut(
|
|
|
|
|
id=r["id"],
|
|
|
|
|
type=r["type"],
|
|
|
|
|
message=r["message"],
|
|
|
|
|
link_path=r["link_path"],
|
|
|
|
|
created_at=r["created_at"].isoformat(),
|
|
|
|
|
read_at=r["read_at"].isoformat() if r["read_at"] else None,
|
|
|
|
|
)
|
|
|
|
|
for r in rows
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/notifications/unread-count")
|
|
|
|
|
async def unread_count(user: CurrentUser = Depends(get_current_user)) -> dict[str, int]:
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
count = await conn.fetchval(
|
|
|
|
|
"SELECT COUNT(*) FROM notification WHERE user_id = $1 AND read_at IS NULL",
|
|
|
|
|
user.user_id,
|
|
|
|
|
)
|
|
|
|
|
return {"count": count}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/notifications/{notification_id}/read")
|
|
|
|
|
async def mark_read(notification_id: str, user: CurrentUser = Depends(get_current_user)) -> dict[str, bool]:
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"UPDATE notification SET read_at = now() WHERE id = $1 AND user_id = $2 AND read_at IS NULL",
|
|
|
|
|
notification_id,
|
|
|
|
|
user.user_id,
|
|
|
|
|
)
|
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/notifications/read-all")
|
|
|
|
|
async def mark_all_read(user: CurrentUser = Depends(get_current_user)) -> dict[str, bool]:
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"UPDATE notification SET read_at = now() WHERE user_id = $1 AND read_at IS NULL",
|
|
|
|
|
user.user_id,
|
|
|
|
|
)
|
|
|
|
|
return {"ok": True}
|
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, migrasjon 037). Offentlig
|
|
|
|
|
# nøkkel er nettopp det -- offentlig, ingen auth nødvendig for GET-en (samme
|
|
|
|
|
# begrunnelse som ethvert annet "her er min offentlige nøkkel"-endepunkt).
|
|
|
|
|
# Abonner/avbryt krever derimot en ekte sesjon -- abonnementet er alltid
|
|
|
|
|
# knyttet til AKKURAT den innloggede brukeren.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VapidPublicKeyOut(BaseModel):
|
|
|
|
|
public_key: str | None
|
|
|
|
|
configured: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/push/vapid-public-key", response_model=VapidPublicKeyOut)
|
|
|
|
|
async def get_vapid_public_key() -> VapidPublicKeyOut:
|
|
|
|
|
return VapidPublicKeyOut(public_key=settings.VAPID_PUBLIC_KEY, configured=settings.PUSH_CONFIGURED)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PushSubscriptionKeys(BaseModel):
|
|
|
|
|
p256dh: str
|
|
|
|
|
auth: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PushSubscriptionCreate(BaseModel):
|
|
|
|
|
endpoint: str
|
|
|
|
|
keys: PushSubscriptionKeys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/push/subscribe")
|
|
|
|
|
async def subscribe_push(
|
|
|
|
|
body: PushSubscriptionCreate, user: CurrentUser = Depends(get_current_user)
|
|
|
|
|
) -> dict[str, bool]:
|
|
|
|
|
if not settings.PUSH_CONFIGURED:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Push er ikke konfigurert på serveren.")
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
# Samme endpoint kan i prinsippet re-abonneres (f.eks. nøklene
|
|
|
|
|
# rotert av nettleseren) -- oppdater i stedet for å feile på
|
|
|
|
|
# unik-constrainten.
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"""
|
|
|
|
|
INSERT INTO push_subscription (user_id, endpoint, p256dh, auth)
|
|
|
|
|
VALUES ($1, $2, $3, $4)
|
|
|
|
|
ON CONFLICT (endpoint) DO UPDATE SET user_id = $1, p256dh = $3, auth = $4
|
|
|
|
|
""",
|
|
|
|
|
user.user_id,
|
|
|
|
|
body.endpoint,
|
|
|
|
|
body.keys.p256dh,
|
|
|
|
|
body.keys.auth,
|
|
|
|
|
)
|
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PushUnsubscribe(BaseModel):
|
|
|
|
|
endpoint: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/push/subscribe")
|
|
|
|
|
async def unsubscribe_push(
|
|
|
|
|
body: PushUnsubscribe, user: CurrentUser = Depends(get_current_user)
|
|
|
|
|
) -> dict[str, bool]:
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"DELETE FROM push_subscription WHERE endpoint = $1 AND user_id = $2",
|
|
|
|
|
body.endpoint,
|
|
|
|
|
user.user_id,
|
|
|
|
|
)
|
|
|
|
|
return {"ok": True}
|