Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
"""
|
|
|
|
|
|
Frittstående rundeføring med detaljert statistikk (ADR-033).
|
|
|
|
|
|
|
|
|
|
|
|
Eid av en BRUKER (`app_user.id`), ikke en organisasjon -- INGEN RLS på
|
|
|
|
|
|
`round`/`round_participant`/`round_hole`/`personal_course*` (Beslutning A).
|
|
|
|
|
|
Samme mønster som personlig profil/HCP-historikk/sekundær e-post: kun
|
|
|
|
|
|
`plain_connection()`, autorisasjon håndheves eksplisitt her med
|
|
|
|
|
|
`WHERE owner_user_id = $1` (eller via round_id -> owner-kjeden).
|
|
|
|
|
|
|
|
|
|
|
|
Banedata (Beslutning C): offisielle teeoff-baner slås opp LIVE ved hver
|
|
|
|
|
|
runde-opprettelse/deltaker-tilføyelse (ingen lokal kopi) via samme
|
|
|
|
|
|
`teeoff_client` som den org-scopede import-flyten i courses.py bruker --
|
|
|
|
|
|
men her INGEN `course`/`hole`/`tee`-rader skrives, kun et navn-snapshot
|
|
|
|
|
|
på selve runden og et rating-snapshot per deltaker.
|
|
|
|
|
|
|
|
|
|
|
|
v1-avgrensning (eksplisitt notert i ADR-033, ikke løst her): en deltaker
|
|
|
|
|
|
med `user_id` satt får IKKE egen tilgang til runden -- derfor støtter
|
|
|
|
|
|
DENNE runden av API-et kun GJEST-deltakere (fritekstnavn), ikke ekte
|
|
|
|
|
|
kontokobling. `round_participant.user_id`-kolonnen brukes fortsatt for
|
|
|
|
|
|
runde-EIEREN (alltid en ekte konto), bare ikke for andre i flighten ennå.
|
|
|
|
|
|
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
HCP: to atskilte tall (ADR-038). `app_user.handicap_index` er MANUELT
|
|
|
|
|
|
satt, uendret betydning (brukes til Course Handicap i enhver runde med
|
|
|
|
|
|
mindre eksplisitt overstyrt). `app_user.computed_handicap_index` er det
|
|
|
|
|
|
FAKTISKE, automatisk beregnede WHS Handicap Index (Rule 5.2) --
|
|
|
|
|
|
oppdateres av `_recompute_computed_handicap_index()` ved fullføring, ved
|
|
|
|
|
|
eksklusjons-endring på en allerede fullført runde, og ved fjerning/
|
|
|
|
|
|
sletting av en tellende deltaker/runde. Kun frittstående runder teller
|
|
|
|
|
|
med i v1 (se `_gather_qualifying_differentials` for skjøtepunktet mot en
|
|
|
|
|
|
fremtidig turnering-kilde, ADR-038 Beslutning E).
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
import json
|
2026-07-24 11:21:39 +02:00
|
|
|
|
import math
|
2026-08-03 06:24:57 +02:00
|
|
|
|
import secrets
|
|
|
|
|
|
import traceback
|
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
|
|
|
|
import uuid
|
2026-08-03 06:24:57 +02:00
|
|
|
|
from datetime import date, datetime, timedelta, timezone
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
from typing import Literal
|
|
|
|
|
|
|
2026-07-26 08:49:37 +02:00
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
2026-07-26 09:42:06 +02:00
|
|
|
|
from pydantic import BaseModel, EmailStr, Field
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
from .. import teeoff_client
|
2026-07-28 19:25:37 +02:00
|
|
|
|
from ..auth import CurrentUser, get_current_user, get_current_user_from_websocket, get_current_user_optional
|
2026-08-03 06:24:57 +02:00
|
|
|
|
from ..config import settings
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
from ..db import plain_connection
|
2026-08-03 06:24:57 +02:00
|
|
|
|
from ..email import RoundSummaryHole, send_round_summary_email
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
from ..errors import app_error, translate_db_errors
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
from ..handicap import SIDE_IS_UNIT, parse_allowance_config
|
2026-07-26 08:49:37 +02:00
|
|
|
|
from ..realtime import broadcast_round_update, live_sockets_for_round
|
2026-08-03 06:24:57 +02:00
|
|
|
|
from .auth import _hash_secret
|
Update Todos
Les add_participant, complete_round, friend_categorization-skjema for design
Design + skriv backend for tre rundevarsler (medspiller lagt til, venn starter synlig runde, tilkoblet runde fullført)
Scratch-verifiser alle tre varseltypene
Oppdater CLAUDE.md status + rull ut mot ekte systemer (med bekreftelse)
Live. Rundevarsler er nå koblet på, ren backend-endring (varslingssenteret hadde allerede round/result-typene klare, bare ingenting skrev dem):
Medspiller lagt til — varsel med lenke til /my-rounds/{id} (full tilgang)
En venn starter en synlig runde — varsel til aksepterte venner som faktisk kan se den (offentlig, eller riktig kategori), lenke til /watch/{id}
Tilkoblet runde fullført — medspillere og tredjeparts-venner varsles, aldri personen som selv fullførte
28/28 nye sjekker + full regresjon av tre eksisterende testsuiter (191 sjekker) grønt. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
2026-07-28 19:54:05 +02:00
|
|
|
|
from .notifications import create_notification
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
from handicap_engine import (
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
HoleResult,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
adjusted_gross_score,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
allocate_over_played_holes,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
allocate_strokes_by_index,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
apply_index_caps,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
bbb_points_for_hole,
|
|
|
|
|
|
compute_bbb,
|
|
|
|
|
|
compute_copenhagen_detail,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
compute_match_state,
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
compute_skins_detail,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
course_handicap,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
flag_result,
|
|
|
|
|
|
high_low_high_points_for_hole,
|
|
|
|
|
|
high_low_high_running_score,
|
|
|
|
|
|
money_ball_hole_score,
|
|
|
|
|
|
shamble_hole_score,
|
|
|
|
|
|
stableford_points_for_hole,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
course_handicap_raw,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
handicap_index_from_differentials,
|
|
|
|
|
|
low_handicap_index,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
match_play_strokes,
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
max_hole_score_for_handicap,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
round_counts_for_handicap,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
round_half_up,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
score_differential,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
_RESULT_LABEL = {HoleResult.SIDE_A: "a", HoleResult.SIDE_B: "b", HoleResult.HALVED: "halved"}
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
# Samme faste kategori-sett som friends.py sin Category (ADR-036 fase 1),
|
|
|
|
|
|
# duplisert her fremfor importert for å unngå en tvers-router-avhengighet
|
|
|
|
|
|
# for én enkelt type -- samme pragmatiske valg som skjemaets doble CHECK.
|
|
|
|
|
|
_FriendCategory = Literal[
|
|
|
|
|
|
"spouse", "close_family", "extended_family", "close_friends",
|
|
|
|
|
|
"golf_friends", "colleagues", "business", "classmates",
|
|
|
|
|
|
"acquaintances", "other",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
router = APIRouter(tags=["rounds"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Global banekatalog for egendefinerte (ikke-teeoff) baner
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class PersonalCourseHoleIn(BaseModel):
|
|
|
|
|
|
hole_number: int = Field(ge=1, le=18)
|
|
|
|
|
|
par: int = Field(ge=3, le=6)
|
|
|
|
|
|
stroke_index: int = Field(ge=1, le=18)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PersonalCourseTeeRatingIn(BaseModel):
|
|
|
|
|
|
gender: Literal["m", "f"]
|
|
|
|
|
|
course_rating: float
|
|
|
|
|
|
slope_rating: int = Field(ge=55, le=155)
|
|
|
|
|
|
par: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PersonalCourseTeeIn(BaseModel):
|
|
|
|
|
|
name: str
|
|
|
|
|
|
ratings: list[PersonalCourseTeeRatingIn] = Field(min_length=1, max_length=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PersonalCourseCreate(BaseModel):
|
|
|
|
|
|
name: str
|
|
|
|
|
|
holes: list[PersonalCourseHoleIn] = Field(min_length=18, max_length=18)
|
|
|
|
|
|
tees: list[PersonalCourseTeeIn] = Field(min_length=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PersonalCourseOut(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
class TeeOption(BaseModel):
|
|
|
|
|
|
name: str
|
|
|
|
|
|
genders: list[Literal["m", "f"]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PersonalCourseDetail(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
tees: list[TeeOption]
|
|
|
|
|
|
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
@router.get("/personal-courses", response_model=list[PersonalCourseOut])
|
|
|
|
|
|
async def search_personal_courses(
|
|
|
|
|
|
q: str = "",
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> list[PersonalCourseOut]:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id, name FROM personal_course WHERE name ILIKE $1 ORDER BY name LIMIT 20",
|
|
|
|
|
|
f"%{q.strip()}%",
|
|
|
|
|
|
)
|
|
|
|
|
|
return [PersonalCourseOut(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
@router.get("/personal-courses/{personal_course_id}", response_model=PersonalCourseDetail)
|
|
|
|
|
|
async def get_personal_course(
|
|
|
|
|
|
personal_course_id: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> PersonalCourseDetail:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
course_row = await conn.fetchrow("SELECT id::text AS id, name FROM personal_course WHERE id = $1", personal_course_id)
|
|
|
|
|
|
if course_row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Den egendefinerte banen finnes ikke.")
|
|
|
|
|
|
tee_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT t.name AS tee_name, r.gender
|
|
|
|
|
|
FROM personal_course_tee t
|
|
|
|
|
|
JOIN personal_course_tee_rating r ON r.personal_course_tee_id = t.id
|
|
|
|
|
|
WHERE t.personal_course_id = $1
|
|
|
|
|
|
ORDER BY t.name
|
|
|
|
|
|
""",
|
|
|
|
|
|
personal_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
tees: dict[str, list[str]] = {}
|
|
|
|
|
|
for r in tee_rows:
|
|
|
|
|
|
tees.setdefault(r["tee_name"], []).append(r["gender"])
|
|
|
|
|
|
return PersonalCourseDetail(
|
|
|
|
|
|
id=course_row["id"],
|
|
|
|
|
|
name=course_row["name"],
|
|
|
|
|
|
tees=[TeeOption(name=name, genders=genders) for name, genders in tees.items()],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
@router.post("/personal-courses", response_model=PersonalCourseOut, status_code=201)
|
|
|
|
|
|
async def create_personal_course(
|
|
|
|
|
|
body: PersonalCourseCreate,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> PersonalCourseOut:
|
|
|
|
|
|
numbers = sorted(h.hole_number for h in body.holes)
|
|
|
|
|
|
indexes = sorted(h.stroke_index for h in body.holes)
|
|
|
|
|
|
if numbers != list(range(1, 19)) or indexes != list(range(1, 19)):
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Alle 18 hullnumre og alle 18 stroke-indekser må være unike, 1-18.")
|
|
|
|
|
|
|
|
|
|
|
|
async with plain_connection() as conn, translate_db_errors():
|
|
|
|
|
|
async with conn.transaction():
|
|
|
|
|
|
course_row = await conn.fetchrow(
|
|
|
|
|
|
"INSERT INTO personal_course (name, created_by_user_id) VALUES ($1, $2) RETURNING id::text AS id, name",
|
|
|
|
|
|
body.name.strip(),
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
for h in body.holes:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"INSERT INTO personal_course_hole (personal_course_id, hole_number, par, stroke_index) VALUES ($1, $2, $3, $4)",
|
|
|
|
|
|
course_row["id"],
|
|
|
|
|
|
h.hole_number,
|
|
|
|
|
|
h.par,
|
|
|
|
|
|
h.stroke_index,
|
|
|
|
|
|
)
|
|
|
|
|
|
for t in body.tees:
|
|
|
|
|
|
tee_row = await conn.fetchrow(
|
|
|
|
|
|
"INSERT INTO personal_course_tee (personal_course_id, name) VALUES ($1, $2) RETURNING id",
|
|
|
|
|
|
course_row["id"],
|
|
|
|
|
|
t.name.strip(),
|
|
|
|
|
|
)
|
|
|
|
|
|
for r in t.ratings:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO personal_course_tee_rating
|
|
|
|
|
|
(personal_course_tee_id, gender, course_rating, slope_rating, par)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
|
|
|
|
""",
|
|
|
|
|
|
tee_row["id"],
|
|
|
|
|
|
r.gender,
|
|
|
|
|
|
r.course_rating,
|
|
|
|
|
|
r.slope_rating,
|
|
|
|
|
|
r.par,
|
|
|
|
|
|
)
|
|
|
|
|
|
return PersonalCourseOut(**dict(course_row))
|
|
|
|
|
|
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Offisiell bane fra teeoff (ADR-019/ADR-033 Beslutning C) -- IKKE org-scopet,
|
|
|
|
|
|
# ulikt courses.py sine tilsvarende endepunkter (frittstående runder har
|
|
|
|
|
|
# ingen organisasjon). Rent lese-søk, ingen import/persistering av banen.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class OfficialFacility(BaseModel):
|
|
|
|
|
|
slug: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
city: str | None
|
|
|
|
|
|
county: str | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OfficialCourseOption(BaseModel):
|
|
|
|
|
|
teeoff_course_id: int
|
|
|
|
|
|
name: str
|
|
|
|
|
|
is_main_course: bool
|
|
|
|
|
|
tees: list[TeeOption]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OfficialFacilityDetail(BaseModel):
|
|
|
|
|
|
slug: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
courses: list[OfficialCourseOption]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/rounds/official-search", response_model=list[OfficialFacility])
|
|
|
|
|
|
async def search_official_courses_for_round(
|
|
|
|
|
|
q: str = "",
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> list[OfficialFacility]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
facilities = await teeoff_client.search_facilities(q)
|
|
|
|
|
|
except teeoff_client.TeeoffUnavailableError:
|
|
|
|
|
|
raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baner fra teeoff akkurat nå.")
|
|
|
|
|
|
return [
|
|
|
|
|
|
OfficialFacility(slug=f["slug"], name=f["name"], city=f.get("city"), county=f.get("county"))
|
|
|
|
|
|
for f in facilities
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
class NearbyFacility(OfficialFacility):
|
|
|
|
|
|
distance_km: float
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _haversine_km(lat1: float, lng1: float, lat2: float, lng2: float) -> float:
|
|
|
|
|
|
r = 6371.0
|
|
|
|
|
|
phi1, phi2 = math.radians(lat1), math.radians(lat2)
|
|
|
|
|
|
dphi = math.radians(lat2 - lat1)
|
|
|
|
|
|
dlambda = math.radians(lng2 - lng1)
|
|
|
|
|
|
a = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2) ** 2
|
|
|
|
|
|
return 2 * r * math.asin(math.sqrt(a))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# MERK: MÅ registreres FØR /rounds/official-search/{slug} under, ellers
|
|
|
|
|
|
# tolker FastAPI "nearby" som en ugyldig slug-verdi til den ruten (samme
|
|
|
|
|
|
# lærdom som ADR-020s "by-code" måtte registreres før {tournament_id}).
|
|
|
|
|
|
@router.get("/rounds/official-search/nearby", response_model=list[NearbyFacility])
|
|
|
|
|
|
async def nearby_official_courses_for_round(
|
|
|
|
|
|
lat: float = Query(...),
|
|
|
|
|
|
lng: float = Query(...),
|
|
|
|
|
|
limit: int = Query(default=5, ge=1, le=20),
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> list[NearbyFacility]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
facilities = await teeoff_client.search_facilities("")
|
|
|
|
|
|
except teeoff_client.TeeoffUnavailableError:
|
|
|
|
|
|
raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baner fra teeoff akkurat nå.")
|
|
|
|
|
|
with_coords = [f for f in facilities if f.get("lat") is not None and f.get("lng") is not None]
|
|
|
|
|
|
scored = sorted(with_coords, key=lambda f: _haversine_km(lat, lng, f["lat"], f["lng"]))
|
|
|
|
|
|
return [
|
|
|
|
|
|
NearbyFacility(
|
|
|
|
|
|
slug=f["slug"], name=f["name"], city=f.get("city"), county=f.get("county"),
|
|
|
|
|
|
distance_km=round(_haversine_km(lat, lng, f["lat"], f["lng"]), 1),
|
|
|
|
|
|
)
|
|
|
|
|
|
for f in scored[:limit]
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
@router.get("/rounds/official-search/{slug}", response_model=OfficialFacilityDetail)
|
|
|
|
|
|
async def get_official_facility_for_round(
|
|
|
|
|
|
slug: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> OfficialFacilityDetail:
|
|
|
|
|
|
try:
|
|
|
|
|
|
facility = await teeoff_client.get_facility(slug)
|
|
|
|
|
|
except teeoff_client.TeeoffNotFoundError:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Anlegget finnes ikke i teeoff.")
|
|
|
|
|
|
except teeoff_client.TeeoffUnavailableError:
|
|
|
|
|
|
raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baneinfo fra teeoff akkurat nå.")
|
|
|
|
|
|
|
|
|
|
|
|
courses: list[OfficialCourseOption] = []
|
|
|
|
|
|
for c in facility.get("courses", []):
|
|
|
|
|
|
if len(c.get("holes") or []) != 18:
|
|
|
|
|
|
continue
|
|
|
|
|
|
tees: list[TeeOption] = []
|
|
|
|
|
|
for t in c.get("tees") or []:
|
|
|
|
|
|
genders: list[str] = []
|
|
|
|
|
|
if t.get("cr_men") is not None and t.get("slope_men") is not None:
|
|
|
|
|
|
genders.append("m")
|
|
|
|
|
|
if t.get("cr_women") is not None and t.get("slope_women") is not None:
|
|
|
|
|
|
genders.append("f")
|
|
|
|
|
|
if genders:
|
|
|
|
|
|
tees.append(TeeOption(name=t.get("name") or "Tee", genders=genders))
|
|
|
|
|
|
courses.append(
|
|
|
|
|
|
OfficialCourseOption(
|
|
|
|
|
|
teeoff_course_id=c["id"],
|
|
|
|
|
|
name=c["name"],
|
|
|
|
|
|
is_main_course=bool(c.get("is_main_course")),
|
|
|
|
|
|
tees=tees,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return OfficialFacilityDetail(slug=facility["slug"], name=facility["name"], courses=courses)
|
|
|
|
|
|
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Banedata-oppslag (delt mellom opprett-runde og legg-til-deltaker)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
#
|
|
|
|
|
|
# Returnerer (par_per_hole, stroke_index_per_hole, tee_names) og en
|
|
|
|
|
|
# funksjon for å slå opp (course_rating, slope_rating, par) for en gitt
|
|
|
|
|
|
# tee+kjønn -- felles for BÅDE teeoff (live) og personal_course (lagret).
|
|
|
|
|
|
|
|
|
|
|
|
class _ResolvedCourse:
|
|
|
|
|
|
def __init__(self, holes: list[tuple[int, int, int]], course_name: str):
|
|
|
|
|
|
# holes: liste av (hole_number, par, stroke_index), sortert 1..18
|
|
|
|
|
|
self.holes = holes
|
|
|
|
|
|
self.course_name = course_name
|
|
|
|
|
|
self._ratings: dict[tuple[str, str], tuple[float, int, int]] = {}
|
|
|
|
|
|
|
|
|
|
|
|
def add_rating(self, tee_name: str, gender: str, course_rating: float, slope_rating: int, par: int) -> None:
|
|
|
|
|
|
self._ratings[(tee_name, gender)] = (course_rating, slope_rating, par)
|
|
|
|
|
|
|
|
|
|
|
|
def rating_for(self, tee_name: str, gender: str) -> tuple[float, int, int] | None:
|
|
|
|
|
|
return self._ratings.get((tee_name, gender))
|
|
|
|
|
|
|
2026-07-26 09:18:52 +02:00
|
|
|
|
def tee_options(self) -> list["TeeOption"]:
|
|
|
|
|
|
names: dict[str, list[Literal["m", "f"]]] = {}
|
|
|
|
|
|
for tee_name, gender in self._ratings:
|
|
|
|
|
|
names.setdefault(tee_name, []).append(gender) # type: ignore[arg-type]
|
|
|
|
|
|
return [TeeOption(name=n, genders=sorted(g)) for n, g in names.items()]
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
async def _resolve_teeoff_course(facility_slug: str, teeoff_course_id: int) -> _ResolvedCourse:
|
|
|
|
|
|
try:
|
|
|
|
|
|
facility = await teeoff_client.get_facility(facility_slug)
|
|
|
|
|
|
except teeoff_client.TeeoffNotFoundError:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Anlegget finnes ikke i teeoff.")
|
|
|
|
|
|
except teeoff_client.TeeoffUnavailableError:
|
|
|
|
|
|
raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baneinfo fra teeoff akkurat nå.")
|
|
|
|
|
|
|
|
|
|
|
|
course_data = next((c for c in facility.get("courses", []) if c.get("id") == teeoff_course_id), None)
|
|
|
|
|
|
if course_data is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Banen finnes ikke på dette anlegget i teeoff.")
|
|
|
|
|
|
|
|
|
|
|
|
raw_holes = course_data.get("holes") or []
|
|
|
|
|
|
if len(raw_holes) != 18:
|
|
|
|
|
|
raise app_error(400, "EXTERNAL_DATA_INCOMPLETE", "Banen har ikke 18 registrerte hull i teeoff ennå.")
|
|
|
|
|
|
for h in raw_holes:
|
|
|
|
|
|
if h.get("par") is None or h.get("hcp_index") is None:
|
|
|
|
|
|
raise app_error(400, "EXTERNAL_DATA_INCOMPLETE", "Banen mangler par eller HCP-index på ett eller flere hull.")
|
|
|
|
|
|
|
2026-07-25 06:29:09 +02:00
|
|
|
|
# Samme "kun ett banenavn -- hopp over duplikat anleggsnavn"-regel som
|
|
|
|
|
|
# `courses.py` sin `import_official_course` (2026-07-25).
|
|
|
|
|
|
if len(facility.get("courses", [])) == 1:
|
|
|
|
|
|
course_name = facility.get("name") or "Bane"
|
|
|
|
|
|
else:
|
|
|
|
|
|
course_name = f"{facility.get('name')} – {course_data.get('name') or 'Bane'}"
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
resolved = _ResolvedCourse(
|
|
|
|
|
|
holes=sorted((h["hole_number"], h["par"], h["hcp_index"]) for h in raw_holes),
|
|
|
|
|
|
course_name=course_name,
|
|
|
|
|
|
)
|
|
|
|
|
|
for t in course_data.get("tees") or []:
|
|
|
|
|
|
name = t.get("name") or "Tee"
|
|
|
|
|
|
if t.get("cr_men") is not None and t.get("slope_men") is not None:
|
|
|
|
|
|
resolved.add_rating(name, "m", float(t["cr_men"]), int(t["slope_men"]), sum(h[1] for h in resolved.holes))
|
|
|
|
|
|
if t.get("cr_women") is not None and t.get("slope_women") is not None:
|
|
|
|
|
|
resolved.add_rating(name, "f", float(t["cr_women"]), int(t["slope_women"]), sum(h[1] for h in resolved.holes))
|
|
|
|
|
|
return resolved
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _resolve_personal_course(personal_course_id: str) -> _ResolvedCourse:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
course_row = await conn.fetchrow("SELECT name FROM personal_course WHERE id = $1", personal_course_id)
|
|
|
|
|
|
if course_row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Den egendefinerte banen finnes ikke.")
|
|
|
|
|
|
hole_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT hole_number, par, stroke_index FROM personal_course_hole WHERE personal_course_id = $1",
|
|
|
|
|
|
personal_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
tee_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT t.name AS tee_name, r.gender, r.course_rating, r.slope_rating, r.par
|
|
|
|
|
|
FROM personal_course_tee t
|
|
|
|
|
|
JOIN personal_course_tee_rating r ON r.personal_course_tee_id = t.id
|
|
|
|
|
|
WHERE t.personal_course_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
personal_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
resolved = _ResolvedCourse(
|
|
|
|
|
|
holes=sorted((h["hole_number"], h["par"], h["stroke_index"]) for h in hole_rows),
|
|
|
|
|
|
course_name=course_row["name"],
|
|
|
|
|
|
)
|
|
|
|
|
|
for t in tee_rows:
|
|
|
|
|
|
resolved.add_rating(t["tee_name"], t["gender"], float(t["course_rating"]), t["slope_rating"], t["par"])
|
|
|
|
|
|
return resolved
|
|
|
|
|
|
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
async def _resolve_round_course(round_row) -> _ResolvedCourse:
|
|
|
|
|
|
if round_row["course_source"] == "teeoff":
|
|
|
|
|
|
return await _resolve_teeoff_course(round_row["teeoff_facility_slug"], int(round_row["teeoff_course_id"]))
|
|
|
|
|
|
return await _resolve_personal_course(round_row["personal_course_id"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Sider og match-play-håndtering (ADR-039) -- porterer det allerede
|
|
|
|
|
|
# bevisste, produksjonskjørte mønsteret fra `app/handicap.py` sin
|
|
|
|
|
|
# `compute_and_store_side_handicaps`/`relative_strokes_for_match`
|
|
|
|
|
|
# (org-scopede turnering-matcher) til frittstående runder. SAMME motor
|
|
|
|
|
|
# (handicap_engine.py), samme allowance-familie -- ingen ny regnelogikk
|
|
|
|
|
|
# for match/fourball/foursome/greensome/scramble, kun andre tabeller.
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
#
|
|
|
|
|
|
# round.allowance_override (migrasjon 051, "det store grepet" 2026-08-02):
|
|
|
|
|
|
# HCP-prosenten (strategy) er alltid justerbar, og Match-HCP (use_matchplay_
|
|
|
|
|
|
# handicap -- differensial-fordeling mellom sidene) kan slås av/på, per
|
|
|
|
|
|
# brukerens eksplisitte bekreftelse. Hentes friskt fra `round`-raden ved
|
|
|
|
|
|
# hvert kall, samme mønster som scoring.py/matches.py sin session.
|
|
|
|
|
|
# allowance_override -- ikke cachet i selve config-objektet.
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
async def _round_allowance_override(conn, round_id: str) -> dict | None:
|
|
|
|
|
|
raw = await conn.fetchval("SELECT allowance_override::text FROM round WHERE id = $1", round_id)
|
|
|
|
|
|
return json.loads(raw) if raw else None
|
|
|
|
|
|
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
async def _recompute_side_handicaps(conn, round_id: str, play_format: str) -> None:
|
|
|
|
|
|
"""Beregn og lagre round_participant.playing_handicap for begge sider.
|
|
|
|
|
|
|
|
|
|
|
|
Rører ALDRI course_handicap_snapshot (den absolutte, individuelle WHS-
|
|
|
|
|
|
verdien -- brukt uendret av slagspill-pipelinen) -- playing_handicap er
|
|
|
|
|
|
en egen, ny kolonne kun for den format-relative match-play-enheten.
|
|
|
|
|
|
"""
|
|
|
|
|
|
engine_format = _ROUND_FORMAT_TO_ENGINE_FORMAT[play_format]
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
config = parse_allowance_config(engine_format, await _round_allowance_override(conn, round_id))
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
expected = _SIDE_PLAYER_COUNT[play_format]
|
|
|
|
|
|
|
|
|
|
|
|
sides = await conn.fetch("SELECT id::text AS id FROM round_side WHERE round_id = $1", round_id)
|
|
|
|
|
|
for side in sides:
|
|
|
|
|
|
participants = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id, course_handicap_snapshot FROM round_participant WHERE round_side_id = $1",
|
|
|
|
|
|
side["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
if len(participants) != expected or any(p["course_handicap_snapshot"] is None for p in participants):
|
|
|
|
|
|
continue # ikke komplett ennå, eller mangler HCP-sporing for noen
|
|
|
|
|
|
|
|
|
|
|
|
if not config.use_handicap:
|
|
|
|
|
|
for p in participants:
|
|
|
|
|
|
await conn.execute("UPDATE round_participant SET playing_handicap = 0 WHERE id = $1", p["id"])
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
course_handicaps = [float(p["course_handicap_snapshot"]) for p in participants]
|
|
|
|
|
|
if engine_format in SIDE_IS_UNIT:
|
|
|
|
|
|
playing = config.strategy.playing_handicap(course_handicaps)
|
|
|
|
|
|
for p in participants:
|
|
|
|
|
|
await conn.execute("UPDATE round_participant SET playing_handicap = $1 WHERE id = $2", playing, p["id"])
|
|
|
|
|
|
else:
|
|
|
|
|
|
for p, ch in zip(participants, course_handicaps):
|
|
|
|
|
|
playing = config.strategy.playing_handicap([ch])
|
|
|
|
|
|
await conn.execute("UPDATE round_participant SET playing_handicap = $1 WHERE id = $2", playing, p["id"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _relative_strokes_for_round(conn, round_id: str, play_format: str) -> dict[str, int]:
|
|
|
|
|
|
"""Relative slag per scoringsenhet (porterer relative_strokes_for_match).
|
|
|
|
|
|
|
|
|
|
|
|
Nøkkel = round_participant_id for match/fourball (fourball: begge sidene
|
|
|
|
|
|
samlet i ÉN liste før match_play_strokes, jf. R&A-regelen om at laveste
|
|
|
|
|
|
av alle fire spiller av scratch). Nøkkel = round_side_id for foursome/
|
|
|
|
|
|
greensome/scramble. Returnerer {} hvis playing_handicap ikke er klart
|
|
|
|
|
|
for én eller flere enheter ennå.
|
|
|
|
|
|
"""
|
|
|
|
|
|
engine_format = _ROUND_FORMAT_TO_ENGINE_FORMAT[play_format]
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
config = parse_allowance_config(engine_format, await _round_allowance_override(conn, round_id))
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
|
|
|
|
|
if engine_format in SIDE_IS_UNIT:
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT DISTINCT ON (round_side_id) round_side_id::text AS unit, playing_handicap
|
|
|
|
|
|
FROM round_participant WHERE round_id = $1 AND round_side_id IS NOT NULL
|
|
|
|
|
|
ORDER BY round_side_id
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS unit, playing_handicap FROM round_participant "
|
|
|
|
|
|
"WHERE round_id = $1 AND round_side_id IS NOT NULL",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not rows or any(r["playing_handicap"] is None for r in rows):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
absolutes = [r["playing_handicap"] for r in rows]
|
|
|
|
|
|
relative = match_play_strokes(absolutes) if config.use_matchplay_handicap else absolutes
|
|
|
|
|
|
return {r["unit"]: v for r, v in zip(rows, relative)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _check_side_capacity(
|
|
|
|
|
|
conn, side_id: str, play_format: str, exclude_participant_id: str | None = None
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""Avvis tydelig FØR en deltaker tildeles en side som allerede har
|
|
|
|
|
|
forventet antall spillere for formatet (2026-07-28 oppfølging) -- i
|
|
|
|
|
|
stedet for å oppdage overtallet først ved fullføring. `exclude_
|
|
|
|
|
|
participant_id` unngår at en deltaker som allerede ER på siden (f.eks.
|
|
|
|
|
|
en no-op-reassignment til samme side) teller seg selv ut av plass."""
|
|
|
|
|
|
expected = _SIDE_PLAYER_COUNT[play_format]
|
|
|
|
|
|
current_count = await conn.fetchval(
|
|
|
|
|
|
"SELECT count(*) FROM round_participant WHERE round_side_id = $1 AND id != $2",
|
|
|
|
|
|
side_id, exclude_participant_id or "00000000-0000-0000-0000-000000000000",
|
|
|
|
|
|
)
|
|
|
|
|
|
if current_count >= expected:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
409, "SIDE_FULL",
|
|
|
|
|
|
f"Denne siden har allerede {expected} spiller(e) -- maks for {play_format}.",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _format_setup_status(conn, round_id: str, play_format: str) -> tuple[bool, str | None]:
|
|
|
|
|
|
"""Er rundens deltaker-/side-oppsett komplett for FORMATET (2026-07-28
|
|
|
|
|
|
oppfølging): match/skins/fourball/foursome/greensome/scramble krever
|
|
|
|
|
|
flere spillere enn slagspill -- ingenting fanget dette opp før nå.
|
|
|
|
|
|
Returnerer (komplett, forklarende melding hvis ikke)."""
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
if play_format in ("stroke", "stableford", "flag"):
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
return True, None
|
|
|
|
|
|
|
|
|
|
|
|
if play_format == "skins":
|
|
|
|
|
|
total = await conn.fetchval("SELECT count(*) FROM round_participant WHERE round_id = $1", round_id)
|
|
|
|
|
|
if total < _SKINS_MIN_PARTICIPANTS:
|
|
|
|
|
|
return False, f"Skins krever minst {_SKINS_MIN_PARTICIPANTS} spillere -- runden har {total}."
|
|
|
|
|
|
return True, None
|
|
|
|
|
|
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
if play_format == "copenhagen":
|
|
|
|
|
|
total = await conn.fetchval("SELECT count(*) FROM round_participant WHERE round_id = $1", round_id)
|
|
|
|
|
|
if total != _COPENHAGEN_PLAYER_COUNT:
|
|
|
|
|
|
return False, f"Københavner krever nøyaktig {_COPENHAGEN_PLAYER_COUNT} spillere -- runden har {total}."
|
|
|
|
|
|
return True, None
|
|
|
|
|
|
|
|
|
|
|
|
if play_format == "bbb":
|
|
|
|
|
|
total = await conn.fetchval("SELECT count(*) FROM round_participant WHERE round_id = $1", round_id)
|
|
|
|
|
|
if total < _BBB_MIN_PARTICIPANTS:
|
|
|
|
|
|
return False, f"Bingo Bango Bongo krever minst {_BBB_MIN_PARTICIPANTS} spillere -- runden har {total}."
|
|
|
|
|
|
return True, None
|
|
|
|
|
|
|
|
|
|
|
|
if play_format == "shamble":
|
|
|
|
|
|
# Ett lag = HELE rundens deltakersett (bekreftet av bruker, INGEN
|
|
|
|
|
|
# round_side her) -- 2-4 spillere, fleksibel størrelse.
|
|
|
|
|
|
total = await conn.fetchval("SELECT count(*) FROM round_participant WHERE round_id = $1", round_id)
|
|
|
|
|
|
if not (2 <= total <= 4):
|
|
|
|
|
|
return False, f"Shamble krever 2-4 spillere -- runden har {total}."
|
|
|
|
|
|
best_n = await conn.fetchval("SELECT shamble_best_n FROM round WHERE id = $1", round_id)
|
|
|
|
|
|
if best_n is None:
|
|
|
|
|
|
return False, "Velg hvor mange av lagets resultater som skal telle per hull (\"beste N av M\")."
|
|
|
|
|
|
if not (1 <= best_n <= total):
|
|
|
|
|
|
return False, f"\"Beste N av M\" må være mellom 1 og {total} -- er satt til {best_n}."
|
|
|
|
|
|
return True, None
|
|
|
|
|
|
|
|
|
|
|
|
if play_format == "money_ball":
|
|
|
|
|
|
# Ett lag = HELE rundens deltakersett (samme struktur som Shamble),
|
|
|
|
|
|
# men FAST 4 -- ikke fleksibel størrelse.
|
|
|
|
|
|
total = await conn.fetchval("SELECT count(*) FROM round_participant WHERE round_id = $1", round_id)
|
|
|
|
|
|
if total != 4:
|
|
|
|
|
|
return False, f"Money Ball krever nøyaktig 4 spillere -- runden har {total}."
|
|
|
|
|
|
distinct_orders = await conn.fetchval(
|
|
|
|
|
|
"SELECT count(DISTINCT lineup_order) FROM round_participant WHERE round_id = $1 AND lineup_order IS NOT NULL",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if distinct_orders != 4:
|
|
|
|
|
|
return False, "Alle fire spillerne må ha en tildelt, distinkt plass i rekkefølgen (0-3)."
|
|
|
|
|
|
return True, None
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# To-sidede formater (match/fourball/foursome/greensome/scramble_*).
|
|
|
|
|
|
expected = _SIDE_PLAYER_COUNT[play_format]
|
|
|
|
|
|
sides = await conn.fetch("SELECT id::text AS id, label FROM round_side WHERE round_id = $1 ORDER BY id", round_id)
|
|
|
|
|
|
if len(sides) != 2:
|
|
|
|
|
|
return False, "Denne spilleformen krever nøyaktig to sider -- opprett begge før runden fullføres."
|
|
|
|
|
|
|
|
|
|
|
|
unassigned = await conn.fetchval(
|
|
|
|
|
|
"SELECT count(*) FROM round_participant WHERE round_id = $1 AND round_side_id IS NULL", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if unassigned > 0:
|
|
|
|
|
|
return False, f"{unassigned} deltaker(e) er ikke tildelt noen side ennå."
|
|
|
|
|
|
|
|
|
|
|
|
for side in sides:
|
|
|
|
|
|
count = await conn.fetchval("SELECT count(*) FROM round_participant WHERE round_side_id = $1", side["id"])
|
|
|
|
|
|
if count != expected:
|
|
|
|
|
|
label = side["label"] or "En side"
|
|
|
|
|
|
return False, f"{label} har {count} spiller(e), men trenger nøyaktig {expected}."
|
|
|
|
|
|
return True, None
|
|
|
|
|
|
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Runder
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-07-24 06:27:35 +02:00
|
|
|
|
# Statistikknivå per deltaker (2026-07-24): kun slag er strengt tatt
|
|
|
|
|
|
# nødvendig for resultat/HCP -- putter og "flere detaljer" er valgfritt,
|
|
|
|
|
|
# av som default. GIR krever putts og vises derfor ikke i strokes_only.
|
|
|
|
|
|
StatLevel = Literal["strokes_only", "strokes_and_putts", "full"]
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# ADR-039: formater som bruker et to-siders-konsept (Beslutning A). "match"
|
|
|
|
|
|
# er rundens eget navn for singles match play -- mappes til motorens eget
|
|
|
|
|
|
# Format-enum-navn ("singles") kun når handicap_engine.py/app.handicap
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
# kalles, ikke noe sted ellers. "chapman" tilføyd 2026-07-30 (Chapman/
|
|
|
|
|
|
# Pinehurst/"Try all", strukturelt identisk med foursome -- se
|
|
|
|
|
|
# _ROUND_FORMAT_TO_ENGINE_FORMAT og app/handicap.py sin ENGINE_FORMAT_ALIASES).
|
|
|
|
|
|
_TWO_SIDED_FORMATS = {
|
|
|
|
|
|
"match", "fourball", "foursome", "greensome", "scramble_2", "scramble_4", "chapman", "high_low_high",
|
|
|
|
|
|
}
|
2026-07-29 15:46:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _viewer_match_status_text(lead: int, holes_remaining: int, is_closed: bool) -> str:
|
|
|
|
|
|
"""Kort, VIEWER-relativ matchstatus (2026-07-29, dashbord-rundeboks-
|
|
|
|
|
|
runden) -- positivt lead betyr alltid "spørrende bruker leder", uansett
|
|
|
|
|
|
hvilken side de faktisk sitter på. Speiler MatchState.describe() i
|
|
|
|
|
|
handicap_engine.py, men uten bokstavelig "(A)"/"(B)" og med et eksplisitt
|
|
|
|
|
|
Vunnet/Tapt-prefiks for et avgjort resultat (siden "2&1" alene ikke
|
|
|
|
|
|
sier hvem sin side det gjaldt når det ikke lenger er en ren leder-visning)."""
|
|
|
|
|
|
margin = abs(lead)
|
|
|
|
|
|
if is_closed:
|
|
|
|
|
|
suffix = f"{margin}&{holes_remaining}" if holes_remaining > 0 else f"{margin} UP"
|
|
|
|
|
|
return f"Vunnet {suffix}" if lead > 0 else f"Tapt {suffix}"
|
|
|
|
|
|
if lead == 0:
|
|
|
|
|
|
return "AS"
|
|
|
|
|
|
return f"{margin} UP" if lead > 0 else f"{margin} DOWN"
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
_ROUND_FORMAT_TO_ENGINE_FORMAT = {
|
|
|
|
|
|
"match": "singles",
|
|
|
|
|
|
"fourball": "fourball",
|
|
|
|
|
|
"foursome": "foursome",
|
|
|
|
|
|
"greensome": "greensome",
|
|
|
|
|
|
"scramble_2": "scramble_2",
|
|
|
|
|
|
"scramble_4": "scramble_4",
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
# Chapman/Pinehurst ("Try all", 2026-07-30) -- delt ball, identisk med
|
|
|
|
|
|
# foursome (se app/handicap.py sin ENGINE_FORMAT_ALIASES for samme
|
|
|
|
|
|
# oppløsning på org-lagturnering-siden).
|
|
|
|
|
|
"chapman": "foursome",
|
|
|
|
|
|
# High-low-high (2026-07-30) -- 2 lag à 2, individuell ball, samme
|
|
|
|
|
|
# "alle fire samlet i én match_play_strokes-pool"-beregning som
|
|
|
|
|
|
# fourball (R&A-regelen), gjenbrukt uendret for selve HCP-delen --
|
|
|
|
|
|
# selve poeng-rangeringen (high/low) er en HELT NY, egen beregning
|
|
|
|
|
|
# (se _build_high_low_high_result), ikke del av denne aliaseringen.
|
|
|
|
|
|
"high_low_high": "fourball",
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
# Antall SPILLERE forventet per side -- IKKE det samme som app.handicap sin
|
|
|
|
|
|
# FORMAT_UNIT_SIZE (som er HCP-BEREGNINGS-enheten: fourball beregnes
|
|
|
|
|
|
# individuelt der, men har likevel 2 spillere per side for selve
|
|
|
|
|
|
# matchresultat-sammenligningen).
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
_SIDE_PLAYER_COUNT = {
|
|
|
|
|
|
"match": 1, "fourball": 2, "foursome": 2, "greensome": 2, "scramble_2": 2, "scramble_4": 4,
|
|
|
|
|
|
"chapman": 2, "high_low_high": 2,
|
|
|
|
|
|
}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# Bekreftet av bruker (2026-07-28): 2 spillere gjør skins i praksis
|
|
|
|
|
|
# identisk med en vanlig match -- 3+ er der en oppsamlet, uavgjort pott
|
|
|
|
|
|
# faktisk gir mening som eget spillformat.
|
|
|
|
|
|
_SKINS_MIN_PARTICIPANTS = 3
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
# Københavner (2026-07-30) -- flatt felt, NØYAKTIG 3 (ikke "minst 3" som
|
|
|
|
|
|
# skins) -- 6-poengs-fordelingen (copenhagen_points_for_hole) er definert
|
|
|
|
|
|
# for nøyaktig 3 rangerte spillere, ikke et vilkårlig antall.
|
|
|
|
|
|
_COPENHAGEN_PLAYER_COUNT = 3
|
|
|
|
|
|
# Bingo Bango Bongo (2026-07-30) -- flatt felt, minst 2 (med kun 1 spiller
|
|
|
|
|
|
# vinner de alle tre kategoriene hvert hull, meningsløst som konkurranse).
|
|
|
|
|
|
_BBB_MIN_PARTICIPANTS = 2
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
2026-07-24 06:27:35 +02:00
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
class RoundCreate(BaseModel):
|
|
|
|
|
|
course_source: Literal["teeoff", "custom"]
|
|
|
|
|
|
teeoff_facility_slug: str | None = None
|
|
|
|
|
|
teeoff_course_id: int | None = None
|
|
|
|
|
|
personal_course_id: str | None = None
|
|
|
|
|
|
tee_name: str
|
2026-07-25 06:29:09 +02:00
|
|
|
|
# Valgfritt eget navn på runden (2026-07-25) -- mangler det, faller
|
|
|
|
|
|
# visningen tilbake til course_name_snapshot, som alltid er satt.
|
|
|
|
|
|
name: str | None = Field(default=None, max_length=200)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
played_at: date
|
|
|
|
|
|
start_hole: int = Field(default=1, ge=1, le=18)
|
|
|
|
|
|
holes_planned: Literal[9, 18] = 18
|
2026-07-24 06:27:35 +02:00
|
|
|
|
stat_level: StatLevel = "strokes_only"
|
2026-07-24 11:21:39 +02:00
|
|
|
|
# Utslagstidspunkt (2026-07-24) -- valgfritt, brukt til å beregne
|
|
|
|
|
|
# tidsbruk (completed_at - started_at) når runden fullføres.
|
|
|
|
|
|
started_at: datetime | None = None
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# Selvdeklarert spilleform (ADR-038 Beslutning D, utvidet ADR-039) --
|
|
|
|
|
|
# ingen egen "oppdag selv"-motor for frittstående runder, så dette taes
|
|
|
|
|
|
# brukerens ord for. match/fourball/foursome/greensome/scramble_*
|
|
|
|
|
|
# trenger sider (opprettes separat via POST .../sides, se ADR-039
|
|
|
|
|
|
# Beslutning A) -- IKKE noe som settes opp i selve opprett-kallet.
|
|
|
|
|
|
play_format: Literal[
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
"stroke", "match", "skins", "fourball", "foursome", "greensome", "scramble_2", "scramble_4", "stableford",
|
|
|
|
|
|
"chapman", "copenhagen", "bbb", "flag", "shamble", "money_ball", "high_low_high",
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
] = "stroke"
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
# Frontend forhåndsutfyller denne til True når play_format='match'
|
|
|
|
|
|
# (anbefalt, men overstyrbart) -- eierens EGEN rad ved opprettelse.
|
|
|
|
|
|
exclude_owner_from_handicap: bool = False
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
# HCP-prosent + Match-HCP, migrasjon 051 ("det store grepet",
|
|
|
|
|
|
# 2026-08-02) -- ADR-014s "fire brytere", samme JSON-form som org-
|
|
|
|
|
|
# turneringers session.allowance_override (`{use_handicap,
|
|
|
|
|
|
# use_course_handicap, use_matchplay_handicap, strategy:{type,
|
|
|
|
|
|
# percentage|...}}`), se app/handicap.py sin parse_allowance_config.
|
|
|
|
|
|
# Manglende nøkler = standardverdi for formatet. Gir kun mening for
|
|
|
|
|
|
# to-sidede formater (match/fourball/foursome/greensome/scramble/
|
|
|
|
|
|
# chapman/shamble/money_ball/high_low_high) -- ingen validering her
|
|
|
|
|
|
# avviser den for andre, siden den da rett og slett aldri leses.
|
|
|
|
|
|
allowance_override: dict | None = None
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# KUN meningsfulle (og påkrevd) når play_format='skins' -- ADR-039
|
|
|
|
|
|
# Beslutning D. Brukeren som setter opp runden velger begge uavhengig.
|
|
|
|
|
|
skins_scoring: Literal["net", "gross"] | None = None
|
|
|
|
|
|
skins_tie_handling: Literal["carry", "split"] | None = None
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
# KUN meningsfullt når play_format='bbb' -- valgfri organisator-
|
|
|
|
|
|
# innstilling (bekreftet av bruker), ikke fast påslått.
|
|
|
|
|
|
bbb_sweep_bonus_enabled: bool = False
|
|
|
|
|
|
# KUN meningsfullt (og påkrevd) når play_format='shamble' -- antall av
|
|
|
|
|
|
# lagets individuelle resultater som teller per hull ("beste N av M").
|
|
|
|
|
|
shamble_best_n: int | None = Field(default=None, ge=1)
|
2026-07-28 19:25:37 +02:00
|
|
|
|
# ADR-036 Beslutning B (fase 2, 2026-07-28) -- styrer KUN tredjeparts
|
|
|
|
|
|
# innsyn (venner/offentligheten). Trygg standard 'private', samme
|
|
|
|
|
|
# filosofi som RLS-policyenes "se ingenting" ved manglende kontekst.
|
|
|
|
|
|
# En lagt-til medspiller ser ALLTID runden uansett, uendret av dette.
|
|
|
|
|
|
visibility_mode: Literal["public", "private", "friends"] = "private"
|
|
|
|
|
|
# KUN meningsfullt (og bør være ikke-tom) når visibility_mode='friends'
|
|
|
|
|
|
# -- hvilke av EGNE private kategorier (se friends.py sin Category) som
|
|
|
|
|
|
# får se runden. Tomt/utelatt for public/private.
|
|
|
|
|
|
visible_categories: list[_FriendCategory] = []
|
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
|
|
|
|
# Flere flighter i én "vanlig" runde (migrasjon 035) -- klient-generert
|
|
|
|
|
|
# UUID som binder flere `round`-rader sammen. Utelatt/null for en vanlig
|
|
|
|
|
|
# solo-/enkeltflight-runde. Klienten genererer denne selv (kun ETT sted:
|
|
|
|
|
|
# round-detail.tsx sin "Legg til en flight til"), enten en FERSK verdi
|
|
|
|
|
|
# (første gang en andre flight legges til en tidligere gruppeløs runde,
|
|
|
|
|
|
# satt på DEN runden via PATCH FØRST) eller en allerede eksisterende
|
|
|
|
|
|
# gruppes id (tredje+ flight). Se GET .../flight-group.
|
|
|
|
|
|
flight_group_id: str | None = None
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoundParticipantOut(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
user_id: str | None
|
|
|
|
|
|
guest_name: str | None
|
2026-08-03 06:24:57 +02:00
|
|
|
|
# Kilden til sannhet for redigering (2026-08-03) -- guest_name over
|
|
|
|
|
|
# forblir det auto-synkroniserte, VISTE fulle navnet (samme mønster som
|
|
|
|
|
|
# app_user.display_name synkes fra first_name/last_name). guest_last_name
|
|
|
|
|
|
# er valgfritt (en gjest kan legges til med kun fornavn).
|
|
|
|
|
|
guest_first_name: str | None
|
|
|
|
|
|
guest_last_name: str | None
|
2026-07-26 09:42:06 +02:00
|
|
|
|
# Valgfritt kontaktfelt, KUN for gjester (2026-07-26) -- ingen betydning
|
|
|
|
|
|
# for en lenket bruker, som allerede har sin egen kontos e-post.
|
|
|
|
|
|
guest_email: str | None
|
2026-07-26 08:22:41 +02:00
|
|
|
|
# Alltid utfylt -- guest_name for gjester, ellers en LEVENDE oppslått
|
|
|
|
|
|
# app_user.display_name for enhver lenket bruker (eier ELLER medspiller,
|
|
|
|
|
|
# ADR-036 fase 3-utvidelsen 2026-07-26) -- ikke en snapshot, samme
|
|
|
|
|
|
# "vis gjeldende navn"-mønster som org-medlemslister/chat-forfatter.
|
|
|
|
|
|
display_name: str
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
is_owner: bool
|
|
|
|
|
|
gender: str
|
2026-07-26 09:18:52 +02:00
|
|
|
|
# Per-deltaker utslagssted (2026-07-26) -- kan avvike fra en annen
|
|
|
|
|
|
# deltakers, f.eks. et kjønnsdelt utslag eller et bevisst enklere valg.
|
|
|
|
|
|
# Redigerbar av eieren via PATCH .../participants/{id}, kun for DENNE
|
|
|
|
|
|
# runden (endrer aldri spillerens profil).
|
|
|
|
|
|
tee_name_snapshot: str
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
handicap_index_snapshot: float | None
|
|
|
|
|
|
course_handicap_snapshot: int | None
|
|
|
|
|
|
counts_for_handicap: bool
|
|
|
|
|
|
score_differential: float | None
|
2026-07-24 06:27:35 +02:00
|
|
|
|
stat_level: StatLevel
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
# ADR-038 Beslutning C -- manuell opt-out, uavhengig av
|
|
|
|
|
|
# counts_for_handicap. Kontrolleres av DENNE deltakeren selv (eller
|
|
|
|
|
|
# runde-eieren), se update_participant.
|
|
|
|
|
|
exclude_from_handicap: bool
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# ADR-039 Beslutning A -- hvilken side deltakeren er på (match/fourball/
|
|
|
|
|
|
# foursome/greensome/scramble_*), null for slagspill/skins (ingen sider).
|
|
|
|
|
|
round_side_id: str | None
|
|
|
|
|
|
# ADR-039 Beslutning B -- den format-avledede, RELATIVE match-play-
|
|
|
|
|
|
# enheten (fra AllowanceStrategy), IKKE samme som course_handicap_
|
|
|
|
|
|
# snapshot (som forblir den absolutte, individuelle WHS-verdien).
|
|
|
|
|
|
playing_handicap: int | None
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
# Money Ball (2026-07-30) -- 0-3, null for alle andre formater.
|
|
|
|
|
|
lineup_order: int | None
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoundSideOut(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
label: str | None
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoundOut(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
course_source: str
|
2026-07-25 06:29:09 +02:00
|
|
|
|
name: str | None
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
course_name_snapshot: str
|
|
|
|
|
|
tee_name_snapshot: str
|
|
|
|
|
|
played_at: date
|
|
|
|
|
|
start_hole: int
|
|
|
|
|
|
holes_planned: int
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
play_format: str
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
skins_scoring: str | None
|
|
|
|
|
|
skins_tie_handling: str | None
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
# KUN meningsfullt for play_format='bbb' (2026-07-30).
|
|
|
|
|
|
bbb_sweep_bonus_enabled: bool
|
|
|
|
|
|
# KUN meningsfullt for play_format='shamble' (2026-07-30).
|
|
|
|
|
|
shamble_best_n: int | None
|
2026-07-24 11:21:39 +02:00
|
|
|
|
started_at: str | None
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
completed_at: str | None
|
|
|
|
|
|
participants: list[RoundParticipantOut]
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# ADR-039 Beslutning A -- tom liste for formater uten sider (slagspill/skins).
|
|
|
|
|
|
sides: list[RoundSideOut]
|
|
|
|
|
|
# 2026-07-28 oppfølging -- match/skins/fourball/foursome/greensome/
|
|
|
|
|
|
# scramble krever flere spillere enn slagspill, håndhevet i
|
|
|
|
|
|
# complete_round. setup_message er None når setup_complete er sann.
|
|
|
|
|
|
setup_complete: bool
|
|
|
|
|
|
setup_message: str | None
|
2026-07-26 08:22:41 +02:00
|
|
|
|
# Den SPØRRENDE brukerens (eier ELLER en lenket medspiller, ADR-036
|
|
|
|
|
|
# fase 3-utvidelsen 2026-07-26) egen fremdrift/score, utledet fra
|
|
|
|
|
|
# round_hole (aldri lagret) -- omdøpt fra `owner_*` til `my_*` samme
|
|
|
|
|
|
# dag, siden feltet nå er VIEWER-relativt, ikke alltid eierens.
|
|
|
|
|
|
# `my_score_to_par` er None helt til minst ett hull er registrert.
|
|
|
|
|
|
my_holes_played: int
|
|
|
|
|
|
my_total_score: int | None
|
|
|
|
|
|
my_score_to_par: int | None
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
# 2026-07-29 (dashbord-rundeboks-runden), utvidet 2026-07-29 til også
|
|
|
|
|
|
# 'stableford' (migrasjon 038): netto til-par for slagspill/Stableford,
|
|
|
|
|
|
# kun når HCP faktisk spores for spilleren (course_handicap_snapshot
|
|
|
|
|
|
# satt). Samme allokeringsalgoritme som list_holes/update_hole -- ikke
|
|
|
|
|
|
# en egen utregning.
|
2026-07-29 15:46:49 +02:00
|
|
|
|
my_net_score_to_par: int | None
|
|
|
|
|
|
# Viewer-relativ løpende matchstatus for to-sidede formater (match/
|
|
|
|
|
|
# fourball/foursome/greensome/scramble), None for slagspill/skins eller
|
|
|
|
|
|
# før sidene er komplette. "1 UP"/"AS"/"1 DOWN"/"Vunnet 2&1"/"Tapt 3&2"
|
|
|
|
|
|
# -- ALDRI backend sin bokstavelige "(A)"/"(B)", speiler alltid
|
|
|
|
|
|
# SPØRRENDE bruker sin egen side. my_match_lead er samme fortegn (>0 =
|
|
|
|
|
|
# leder, <0 = ligger under) -- brukt av frontend til fargelegging uten
|
|
|
|
|
|
# å måtte parse teksten.
|
|
|
|
|
|
my_match_status: str | None
|
|
|
|
|
|
my_match_lead: int | None
|
2026-07-28 19:25:37 +02:00
|
|
|
|
# ADR-036 Beslutning B (fase 2, 2026-07-28) -- kun eieren (eller en
|
|
|
|
|
|
# medspiller, som ser dette som ren informasjon) trenger å se/endre
|
|
|
|
|
|
# disse -- eksponert her siden dette ER den autentiserte eier-/
|
|
|
|
|
|
# medspiller-endepunktet (GET /rounds/{id}), ikke tredjepartsvisningen.
|
|
|
|
|
|
visibility_mode: str
|
|
|
|
|
|
visible_categories: list[str]
|
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
|
|
|
|
# Flere flighter (migrasjon 035) -- null for en vanlig enkeltflight-runde.
|
|
|
|
|
|
flight_group_id: str | None
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
# HCP-prosent + Match-HCP (migrasjon 051) -- null = formatets standard.
|
|
|
|
|
|
allowance_override: dict | None
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-26 08:22:41 +02:00
|
|
|
|
async def _load_round_out(conn, round_id: str, viewer_user_id: str) -> RoundOut:
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
2026-07-25 06:29:09 +02:00
|
|
|
|
SELECT id::text AS id, course_source, name, course_name_snapshot, tee_name_snapshot,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
played_at, start_hole, holes_planned, play_format, skins_scoring, skins_tie_handling,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
started_at, completed_at, visibility_mode, flight_group_id::text AS flight_group_id,
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
bbb_sweep_bonus_enabled, shamble_best_n, allowance_override::text AS allowance_override
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
FROM round WHERE id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
2026-07-28 19:25:37 +02:00
|
|
|
|
visible_category_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT category FROM round_visible_category WHERE round_id = $1", round_id
|
|
|
|
|
|
)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
participant_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
2026-08-03 06:24:57 +02:00
|
|
|
|
SELECT rp.id::text AS id, rp.user_id::text AS user_id, rp.guest_name,
|
|
|
|
|
|
rp.guest_first_name, rp.guest_last_name, rp.guest_email,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS display_name,
|
2026-07-26 09:18:52 +02:00
|
|
|
|
rp.is_owner, rp.gender, rp.tee_name_snapshot,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
rp.handicap_index_snapshot::float AS handicap_index_snapshot,
|
|
|
|
|
|
rp.course_handicap_snapshot, rp.counts_for_handicap,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
rp.score_differential::float AS score_differential, rp.stat_level,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
rp.exclude_from_handicap, rp.round_side_id::text AS round_side_id,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
rp.playing_handicap, rp.lineup_order
|
2026-07-26 08:22:41 +02:00
|
|
|
|
FROM round_participant rp
|
|
|
|
|
|
LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.round_id = $1 ORDER BY rp.is_owner DESC, rp.created_at
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
side_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id, label FROM round_side WHERE round_id = $1 ORDER BY id", round_id
|
|
|
|
|
|
)
|
2026-07-26 08:22:41 +02:00
|
|
|
|
# Viewer-relativt (ADR-036 fase 3-utvidelsen 2026-07-26): en lenket
|
|
|
|
|
|
# medspiller som ser SIN EGEN "Egne runder"-liste skal se SIN EGEN
|
|
|
|
|
|
# fremdrift her, ikke alltid eierens. Faller tilbake til eierens rad
|
|
|
|
|
|
# i det usannsynlige tilfellet spørreren ikke har noen egen
|
|
|
|
|
|
# deltaker-rad (bør ikke skje, siden tilgang allerede er gatet til
|
|
|
|
|
|
# eier-eller-deltaker av kallerne).
|
|
|
|
|
|
viewer_participant_id = next((r["id"] for r in participant_rows if r["user_id"] == viewer_user_id), None)
|
|
|
|
|
|
target_id = viewer_participant_id or next((r["id"] for r in participant_rows if r["is_owner"]), None)
|
|
|
|
|
|
viewer_agg = await conn.fetchrow(
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
"""
|
|
|
|
|
|
SELECT COUNT(*) FILTER (WHERE played) AS played_count,
|
|
|
|
|
|
COALESCE(SUM(score) FILTER (WHERE played), 0) AS total_score,
|
|
|
|
|
|
COALESCE(SUM(par) FILTER (WHERE played), 0) AS total_par
|
|
|
|
|
|
FROM round_hole WHERE round_participant_id = $1
|
|
|
|
|
|
""",
|
2026-07-26 08:22:41 +02:00
|
|
|
|
target_id,
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
)
|
2026-07-26 08:22:41 +02:00
|
|
|
|
my_holes_played = viewer_agg["played_count"] if viewer_agg else 0
|
|
|
|
|
|
my_total_score = viewer_agg["total_score"] if my_holes_played > 0 else None
|
|
|
|
|
|
my_score_to_par = (
|
|
|
|
|
|
viewer_agg["total_score"] - viewer_agg["total_par"] if my_holes_played > 0 else None
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
)
|
2026-07-29 15:46:49 +02:00
|
|
|
|
|
|
|
|
|
|
# Netto til-par (2026-07-29, dashbord-rundeboks-runden) -- kun slagspill,
|
|
|
|
|
|
# kun når HCP faktisk spores for spilleren. Bruker samme allokerings-
|
|
|
|
|
|
# algoritme som list_holes/update_hole -- ingen ny utregningsmåte.
|
|
|
|
|
|
my_net_score_to_par: int | None = None
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
if round_row["play_format"] in ("stroke", "stableford") and my_holes_played > 0:
|
2026-07-29 15:46:49 +02:00
|
|
|
|
target_row = next((r for r in participant_rows if r["id"] == target_id), None)
|
|
|
|
|
|
if target_row is not None and target_row["course_handicap_snapshot"] is not None:
|
|
|
|
|
|
hole_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT hole_number, stroke_index, played, score FROM round_hole "
|
|
|
|
|
|
"WHERE round_participant_id = $1 ORDER BY hole_number",
|
|
|
|
|
|
target_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if len(hole_rows) == 18:
|
|
|
|
|
|
allocation = allocate_strokes_by_index(
|
|
|
|
|
|
target_row["course_handicap_snapshot"], [r["stroke_index"] for r in hole_rows]
|
|
|
|
|
|
)
|
|
|
|
|
|
strokes_by_hole = dict(zip(range(1, 19), allocation))
|
|
|
|
|
|
total_strokes_received = sum(
|
|
|
|
|
|
strokes_by_hole[r["hole_number"]] for r in hole_rows if r["played"] and r["score"] is not None
|
|
|
|
|
|
)
|
|
|
|
|
|
my_net_score_to_par = (my_score_to_par or 0) - total_strokes_received
|
|
|
|
|
|
|
|
|
|
|
|
# Viewer-relativ matchstatus (2026-07-29) -- to-sidede formater KUN.
|
|
|
|
|
|
# Gjenbruker _build_format_result uendret (samme sannhetskilde som selve
|
|
|
|
|
|
# scorekortet) og flipper fortegnet slik at positivt ALLTID betyr
|
|
|
|
|
|
# "spørrende bruker leder", uansett hvilken side de faktisk sitter på.
|
|
|
|
|
|
my_match_status: str | None = None
|
|
|
|
|
|
my_match_lead: int | None = None
|
|
|
|
|
|
if round_row["play_format"] in _TWO_SIDED_FORMATS:
|
|
|
|
|
|
target_row = next((r for r in participant_rows if r["id"] == target_id), None)
|
|
|
|
|
|
viewer_side_id = target_row["round_side_id"] if target_row is not None else None
|
|
|
|
|
|
if viewer_side_id is not None and len(side_rows) == 2:
|
|
|
|
|
|
fr = await _build_format_result(conn, round_id)
|
|
|
|
|
|
if fr.ready and fr.match_lead is not None and fr.match_holes_played:
|
|
|
|
|
|
sorted_side_ids = sorted(r["id"] for r in side_rows)
|
|
|
|
|
|
viewer_is_side_a = viewer_side_id == sorted_side_ids[0]
|
|
|
|
|
|
lead = fr.match_lead if viewer_is_side_a else -fr.match_lead
|
|
|
|
|
|
my_match_lead = lead
|
|
|
|
|
|
my_match_status = _viewer_match_status_text(
|
|
|
|
|
|
lead, fr.match_holes_remaining or 0, fr.match_is_closed or False
|
|
|
|
|
|
)
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
setup_complete, setup_message = await _format_setup_status(conn, round_id, round_row["play_format"])
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
return RoundOut(
|
|
|
|
|
|
id=round_row["id"],
|
|
|
|
|
|
course_source=round_row["course_source"],
|
2026-07-25 06:29:09 +02:00
|
|
|
|
name=round_row["name"],
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
course_name_snapshot=round_row["course_name_snapshot"],
|
|
|
|
|
|
tee_name_snapshot=round_row["tee_name_snapshot"],
|
|
|
|
|
|
played_at=round_row["played_at"],
|
|
|
|
|
|
start_hole=round_row["start_hole"],
|
|
|
|
|
|
holes_planned=round_row["holes_planned"],
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
play_format=round_row["play_format"],
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
skins_scoring=round_row["skins_scoring"],
|
|
|
|
|
|
skins_tie_handling=round_row["skins_tie_handling"],
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
bbb_sweep_bonus_enabled=round_row["bbb_sweep_bonus_enabled"],
|
|
|
|
|
|
shamble_best_n=round_row["shamble_best_n"],
|
2026-07-24 11:21:39 +02:00
|
|
|
|
started_at=round_row["started_at"].isoformat() if round_row["started_at"] else None,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
completed_at=round_row["completed_at"].isoformat() if round_row["completed_at"] else None,
|
|
|
|
|
|
participants=[RoundParticipantOut(**dict(r)) for r in participant_rows],
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
sides=[RoundSideOut(**dict(r)) for r in side_rows],
|
|
|
|
|
|
setup_complete=setup_complete,
|
|
|
|
|
|
setup_message=setup_message,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
my_holes_played=my_holes_played,
|
|
|
|
|
|
my_total_score=my_total_score,
|
|
|
|
|
|
my_score_to_par=my_score_to_par,
|
2026-07-29 15:46:49 +02:00
|
|
|
|
my_net_score_to_par=my_net_score_to_par,
|
|
|
|
|
|
my_match_status=my_match_status,
|
|
|
|
|
|
my_match_lead=my_match_lead,
|
2026-07-28 19:25:37 +02:00
|
|
|
|
visibility_mode=round_row["visibility_mode"],
|
|
|
|
|
|
visible_categories=[r["category"] for r in visible_category_rows],
|
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
|
|
|
|
flight_group_id=round_row["flight_group_id"],
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
allowance_override=json.loads(round_row["allowance_override"]) if round_row["allowance_override"] else None,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _create_participant(
|
|
|
|
|
|
conn,
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
resolved: _ResolvedCourse,
|
|
|
|
|
|
tee_name: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
user_id: str | None,
|
|
|
|
|
|
guest_name: str | None,
|
2026-08-03 06:24:57 +02:00
|
|
|
|
guest_first_name: str | None = None,
|
|
|
|
|
|
guest_last_name: str | None = None,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
is_owner: bool,
|
|
|
|
|
|
gender: str,
|
|
|
|
|
|
handicap_index: float | None,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
stat_level: StatLevel = "strokes_only",
|
2026-07-26 09:42:06 +02:00
|
|
|
|
guest_email: str | None = None,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
exclude_from_handicap: bool = False,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
round_side_id: str | None = None,
|
|
|
|
|
|
individual_round_hole: bool = True,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
) -> str:
|
|
|
|
|
|
rating = resolved.rating_for(tee_name, gender)
|
|
|
|
|
|
course_rating = slope_rating = tee_par = None
|
|
|
|
|
|
course_handicap_snapshot = None
|
|
|
|
|
|
if rating is not None:
|
|
|
|
|
|
course_rating, slope_rating, tee_par = rating
|
|
|
|
|
|
if handicap_index is not None:
|
|
|
|
|
|
course_handicap_snapshot = course_handicap(handicap_index, slope_rating, course_rating, tee_par)
|
|
|
|
|
|
|
|
|
|
|
|
participant_row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO round_participant
|
2026-08-03 06:24:57 +02:00
|
|
|
|
(round_id, user_id, guest_name, guest_first_name, guest_last_name, guest_email, is_owner, gender,
|
|
|
|
|
|
tee_name_snapshot, handicap_index_snapshot,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
course_rating_snapshot, slope_rating_snapshot, tee_par_snapshot, course_handicap_snapshot,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
stat_level, exclude_from_handicap, round_side_id)
|
2026-08-03 06:24:57 +02:00
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
RETURNING id::text AS id
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
user_id,
|
|
|
|
|
|
guest_name,
|
2026-08-03 06:24:57 +02:00
|
|
|
|
guest_first_name,
|
|
|
|
|
|
guest_last_name,
|
2026-07-26 09:42:06 +02:00
|
|
|
|
guest_email,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
is_owner,
|
|
|
|
|
|
gender,
|
2026-07-26 09:18:52 +02:00
|
|
|
|
tee_name,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
handicap_index,
|
|
|
|
|
|
course_rating,
|
|
|
|
|
|
slope_rating,
|
|
|
|
|
|
tee_par,
|
|
|
|
|
|
course_handicap_snapshot,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
stat_level,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
exclude_from_handicap,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
round_side_id,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
)
|
|
|
|
|
|
participant_id = participant_row["id"]
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# ADR-039 Beslutning C -- delt-ball-formater (foursome/greensome/
|
|
|
|
|
|
# scramble) lagrer scoren PÅ SIDEN (round_hole.round_side_id, opprettet
|
|
|
|
|
|
# samtidig med selve siden, se create_round_side), ikke per deltaker --
|
|
|
|
|
|
# ingen individuelle round_hole-rader for disse.
|
|
|
|
|
|
if individual_round_hole:
|
|
|
|
|
|
for hole_number, par, stroke_index in resolved.holes:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"INSERT INTO round_hole (round_participant_id, hole_number, par, stroke_index) VALUES ($1, $2, $3, $4)",
|
|
|
|
|
|
participant_id,
|
|
|
|
|
|
hole_number,
|
|
|
|
|
|
par,
|
|
|
|
|
|
stroke_index,
|
|
|
|
|
|
)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
return participant_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/rounds", response_model=RoundOut, status_code=201)
|
|
|
|
|
|
async def create_round(
|
|
|
|
|
|
body: RoundCreate,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> RoundOut:
|
|
|
|
|
|
if body.course_source == "teeoff":
|
|
|
|
|
|
if not body.teeoff_facility_slug or not body.teeoff_course_id:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "teeoff_facility_slug og teeoff_course_id er påkrevd.")
|
|
|
|
|
|
resolved = await _resolve_teeoff_course(body.teeoff_facility_slug, body.teeoff_course_id)
|
|
|
|
|
|
else:
|
|
|
|
|
|
if not body.personal_course_id:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "personal_course_id er påkrevd.")
|
|
|
|
|
|
resolved = await _resolve_personal_course(body.personal_course_id)
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# ADR-039 Beslutning D -- skins-konfigurasjon er påkrevd for skins, og
|
|
|
|
|
|
# gir ikke mening for noe annet format.
|
|
|
|
|
|
if body.play_format == "skins":
|
|
|
|
|
|
if body.skins_scoring is None or body.skins_tie_handling is None:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED",
|
|
|
|
|
|
"skins_scoring og skins_tie_handling er påkrevd når play_format='skins'.",
|
|
|
|
|
|
)
|
|
|
|
|
|
elif body.skins_scoring is not None or body.skins_tie_handling is not None:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "skins_scoring/skins_tie_handling gir kun mening for play_format='skins'.")
|
|
|
|
|
|
|
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
|
|
|
|
if body.flight_group_id is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
uuid.UUID(body.flight_group_id)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "flight_group_id er ikke en gyldig UUID.")
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
owner_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT gender, handicap_index::float AS handicap_index FROM app_user WHERE id = $1",
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if owner_row is None or owner_row["gender"] is None:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Fullfør profilen din (kjønn/HCP) før du registrerer en runde.")
|
|
|
|
|
|
|
|
|
|
|
|
if resolved.rating_for(body.tee_name, owner_row["gender"]) is None:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED",
|
|
|
|
|
|
"Valgt utslag har ingen rating for ditt registrerte kjønn på denne banen.",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async with conn.transaction(), translate_db_errors():
|
|
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO round
|
|
|
|
|
|
(owner_user_id, course_source, teeoff_facility_slug, teeoff_course_id,
|
|
|
|
|
|
personal_course_id, course_name_snapshot, tee_name_snapshot, played_at,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
start_hole, holes_planned, started_at, name, play_format,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
skins_scoring, skins_tie_handling, visibility_mode, flight_group_id,
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
bbb_sweep_bonus_enabled, shamble_best_n, allowance_override)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20::jsonb)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
RETURNING id::text AS id
|
|
|
|
|
|
""",
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
body.course_source,
|
|
|
|
|
|
body.teeoff_facility_slug,
|
|
|
|
|
|
str(body.teeoff_course_id) if body.teeoff_course_id is not None else None,
|
|
|
|
|
|
body.personal_course_id,
|
|
|
|
|
|
resolved.course_name,
|
|
|
|
|
|
body.tee_name,
|
|
|
|
|
|
body.played_at,
|
|
|
|
|
|
body.start_hole,
|
|
|
|
|
|
body.holes_planned,
|
2026-07-24 11:21:39 +02:00
|
|
|
|
body.started_at,
|
2026-07-25 06:29:09 +02:00
|
|
|
|
body.name.strip() if body.name and body.name.strip() else None,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
body.play_format,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
body.skins_scoring,
|
|
|
|
|
|
body.skins_tie_handling,
|
2026-07-28 19:25:37 +02:00
|
|
|
|
body.visibility_mode,
|
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
|
|
|
|
body.flight_group_id,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
body.bbb_sweep_bonus_enabled,
|
|
|
|
|
|
body.shamble_best_n,
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
json.dumps(body.allowance_override) if body.allowance_override is not None else None,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
)
|
|
|
|
|
|
round_id = round_row["id"]
|
|
|
|
|
|
|
|
|
|
|
|
await _create_participant(
|
|
|
|
|
|
conn, round_id, resolved, body.tee_name,
|
|
|
|
|
|
user_id=user.user_id, guest_name=None, is_owner=True,
|
|
|
|
|
|
gender=owner_row["gender"], handicap_index=owner_row["handicap_index"],
|
2026-07-24 06:27:35 +02:00
|
|
|
|
stat_level=body.stat_level,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
exclude_from_handicap=body.exclude_owner_from_handicap,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
individual_round_hole=_ROUND_FORMAT_TO_ENGINE_FORMAT.get(body.play_format, body.play_format)
|
|
|
|
|
|
not in SIDE_IS_UNIT,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
if body.visibility_mode == "friends" and body.visible_categories:
|
|
|
|
|
|
for category in sorted(set(body.visible_categories)):
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"INSERT INTO round_visible_category (round_id, category) VALUES ($1, $2)",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
category,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
Update Todos
Les add_participant, complete_round, friend_categorization-skjema for design
Design + skriv backend for tre rundevarsler (medspiller lagt til, venn starter synlig runde, tilkoblet runde fullført)
Scratch-verifiser alle tre varseltypene
Oppdater CLAUDE.md status + rull ut mot ekte systemer (med bekreftelse)
Live. Rundevarsler er nå koblet på, ren backend-endring (varslingssenteret hadde allerede round/result-typene klare, bare ingenting skrev dem):
Medspiller lagt til — varsel med lenke til /my-rounds/{id} (full tilgang)
En venn starter en synlig runde — varsel til aksepterte venner som faktisk kan se den (offentlig, eller riktig kategori), lenke til /watch/{id}
Tilkoblet runde fullført — medspillere og tredjeparts-venner varsles, aldri personen som selv fullførte
28/28 nye sjekker + full regresjon av tre eksisterende testsuiter (191 sjekker) grønt. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
2026-07-28 19:54:05 +02:00
|
|
|
|
# Rundevarsler (2026-07-28 oppfølging av ADR-036 fase 2) -- kun
|
|
|
|
|
|
# meningsfullt for 'public'/'friends' ('private' gir alltid en tom
|
|
|
|
|
|
# liste). Sendt EN GANG, her, ikke gjenberegnet ved hver visning.
|
|
|
|
|
|
if body.visibility_mode != "private":
|
|
|
|
|
|
watcher_ids = await _friends_who_can_see_round(conn, round_id, user.user_id)
|
|
|
|
|
|
if watcher_ids:
|
|
|
|
|
|
owner_name = await conn.fetchval("SELECT display_name FROM app_user WHERE id = $1", user.user_id)
|
|
|
|
|
|
round_label = (body.name.strip() if body.name and body.name.strip() else None) or resolved.course_name
|
|
|
|
|
|
for friend_id in watcher_ids:
|
|
|
|
|
|
await create_notification(
|
|
|
|
|
|
conn,
|
|
|
|
|
|
user_id=friend_id,
|
|
|
|
|
|
type="round",
|
|
|
|
|
|
message=f"{owner_name} startet en runde du kan følge ({round_label}).",
|
|
|
|
|
|
link_path=f"/watch/{round_id}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-26 08:22:41 +02:00
|
|
|
|
return await _load_round_out(conn, round_id, user.user_id)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/rounds", response_model=list[RoundOut])
|
|
|
|
|
|
async def list_rounds(user: CurrentUser = Depends(get_current_user)) -> list[RoundOut]:
|
|
|
|
|
|
async with plain_connection() as conn:
|
2026-07-26 08:22:41 +02:00
|
|
|
|
# ADR-036 fase 3-utvidelsen (2026-07-26): en lenket medspiller ser nå
|
|
|
|
|
|
# runden i SIN EGEN liste også, ikke bare eieren -- bekreftet av
|
|
|
|
|
|
# brukeren som ønsket allerede 2026-07-25.
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
ids = await conn.fetch(
|
2026-07-26 08:22:41 +02:00
|
|
|
|
"""
|
|
|
|
|
|
SELECT r.id::text AS id
|
|
|
|
|
|
FROM round r
|
|
|
|
|
|
WHERE r.owner_user_id = $1
|
|
|
|
|
|
OR EXISTS(SELECT 1 FROM round_participant WHERE round_id = r.id AND user_id = $1)
|
|
|
|
|
|
ORDER BY r.played_at DESC, r.created_at DESC
|
|
|
|
|
|
""",
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
user.user_id,
|
|
|
|
|
|
)
|
2026-07-26 08:22:41 +02:00
|
|
|
|
return [await _load_round_out(conn, r["id"], user.user_id) for r in ids]
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
class RoundStatsSummary(BaseModel):
|
|
|
|
|
|
rounds_completed: int
|
|
|
|
|
|
avg_score_to_par: float | None
|
|
|
|
|
|
avg_putts_per_18: float | None
|
|
|
|
|
|
putts_tracked_rounds: int
|
|
|
|
|
|
fairway_hit_pct: float | None
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
fairway_left_pct: float | None
|
|
|
|
|
|
fairway_right_pct: float | None
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
fairway_tracked_holes: int
|
|
|
|
|
|
gir_pct: float | None
|
|
|
|
|
|
gir_tracked_holes: int
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
green_miss_long_pct: float | None
|
|
|
|
|
|
green_miss_short_pct: float | None
|
|
|
|
|
|
green_miss_left_pct: float | None
|
|
|
|
|
|
green_miss_right_pct: float | None
|
|
|
|
|
|
green_miss_tracked_holes: int
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
one_putt_pct: float | None
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
putt_dist_one_pct: float | None
|
|
|
|
|
|
putt_dist_two_pct: float | None
|
|
|
|
|
|
putt_dist_three_plus_pct: float | None
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
scrambling_pct: float | None
|
|
|
|
|
|
sand_save_pct: float | None
|
|
|
|
|
|
avg_chip_per_round: float | None
|
|
|
|
|
|
avg_bunker_per_round: float | None
|
|
|
|
|
|
avg_penalty_per_round: float | None
|
|
|
|
|
|
avg_anyway_per_round: float | None
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
# Til-par splittet på om en hendelse inntraff på HULLET -- pooler
|
|
|
|
|
|
# enkelthull på tvers av alle runder (ikke per-runde-snitt, siden dette
|
|
|
|
|
|
# er hull-nivå-betingelser), samme formel som `round-stats.tsx` sin
|
|
|
|
|
|
# `avgToParWithGir`/`avgToParFairwayHit`/`avgToParWithBunker` bruker
|
|
|
|
|
|
# for én runde.
|
|
|
|
|
|
avg_to_par_with_gir: float | None
|
|
|
|
|
|
avg_to_par_without_gir: float | None
|
|
|
|
|
|
avg_to_par_fairway_hit: float | None
|
|
|
|
|
|
avg_to_par_fairway_miss: float | None
|
|
|
|
|
|
avg_to_par_with_bunker: float | None
|
|
|
|
|
|
avg_to_par_without_bunker: float | None
|
|
|
|
|
|
avg_to_par_with_anyway: float | None
|
|
|
|
|
|
avg_to_par_without_anyway: float | None
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
class RoundStatsWindowSummary(BaseModel):
|
|
|
|
|
|
window: str
|
|
|
|
|
|
current: RoundStatsSummary
|
|
|
|
|
|
previous: RoundStatsSummary
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
|
|
|
|
|
|
StatsWindow = Literal["last_round", "last_5", "last_10", "month", "year", "last_year", "all"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_stats_window(window: StatsWindow, ordered_rounds: list[tuple[str, date]]) -> tuple[set[str], set[str]]:
|
|
|
|
|
|
"""`ordered_rounds` er (round_id, played_at) sortert NYEST FØRST.
|
|
|
|
|
|
Returnerer (gjeldende periode, forrige periode) som round_id-mengder.
|
|
|
|
|
|
|
|
|
|
|
|
Forrige periode er ALLTID perioden av SAMME LENGDE rett før gjeldende
|
|
|
|
|
|
periodes start -- rullerende for antall-baserte vinduer (samme antall
|
|
|
|
|
|
runder rett før), rullerende for dato-baserte vinduer (samme antall
|
|
|
|
|
|
DAGER rett før startdatoen). Én ensartet regel for alle vindutyper,
|
|
|
|
|
|
ingen kalender-spesialtilfeller (unngår å måtte definere "forrige
|
|
|
|
|
|
måned"/"forrige år" ulikt fra "forrige periode" for et rullerende
|
|
|
|
|
|
vindu som "siste 365 dager").
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
"""
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
if window == "all":
|
|
|
|
|
|
return {r[0] for r in ordered_rounds}, set()
|
|
|
|
|
|
|
|
|
|
|
|
if window in ("last_round", "last_5", "last_10"):
|
|
|
|
|
|
n = {"last_round": 1, "last_5": 5, "last_10": 10}[window]
|
|
|
|
|
|
current = ordered_rounds[:n]
|
|
|
|
|
|
previous = ordered_rounds[n : 2 * n]
|
|
|
|
|
|
return {r[0] for r in current}, {r[0] for r in previous}
|
|
|
|
|
|
|
|
|
|
|
|
today = date.today()
|
|
|
|
|
|
if window == "month":
|
|
|
|
|
|
start = today.replace(day=1)
|
|
|
|
|
|
elif window == "year":
|
|
|
|
|
|
start = today.replace(month=1, day=1)
|
|
|
|
|
|
elif window == "last_year":
|
|
|
|
|
|
start = today - timedelta(days=365)
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError(f"Ukjent vindu: {window}")
|
|
|
|
|
|
|
|
|
|
|
|
length_days = (today - start).days + 1
|
|
|
|
|
|
prev_end = start - timedelta(days=1)
|
|
|
|
|
|
prev_start = prev_end - timedelta(days=length_days - 1)
|
|
|
|
|
|
current = {r[0] for r in ordered_rounds if r[1] >= start}
|
|
|
|
|
|
previous = {r[0] for r in ordered_rounds if prev_start <= r[1] <= prev_end}
|
|
|
|
|
|
return current, previous
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
|
|
|
|
|
|
def _summarize_rounds(round_ids: set[str], by_round: dict[str, list], stat_level_by_round: dict[str, str]) -> RoundStatsSummary:
|
|
|
|
|
|
"""Samme formler som `round-stats.tsx` sin `computeStats()` bruker per
|
|
|
|
|
|
enkeltrunde, pooler kun holdene fra `round_ids` -- gjenbrukt for BÅDE
|
|
|
|
|
|
gjeldende og forrige periode (se `_resolve_stats_window`)."""
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
|
|
|
|
|
|
def avg(values: list[float]) -> float | None:
|
|
|
|
|
|
return sum(values) / len(values) if values else None
|
|
|
|
|
|
|
|
|
|
|
|
to_par_values: list[float] = []
|
|
|
|
|
|
putt_totals: list[float] = []
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
for round_id in round_ids:
|
|
|
|
|
|
holes = by_round.get(round_id, [])
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
played = [h for h in holes if h["played"] and h["score"] is not None]
|
|
|
|
|
|
if played:
|
|
|
|
|
|
to_par_values.append(sum(h["score"] - h["par"] for h in played))
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
if stat_level_by_round.get(round_id) != "strokes_only":
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
putt_totals.append(sum(h["putts"] if h["putts"] is not None else 2 for h in holes))
|
|
|
|
|
|
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
all_played = [h for rid in round_ids for h in by_round.get(rid, []) if h["played"] and h["score"] is not None]
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
|
|
|
|
|
|
fairway_tracked = [h for h in all_played if h["par"] != 3 and h["tee_shot_result"] is not None]
|
|
|
|
|
|
fairway_hit_pct = avg([100.0 if h["tee_shot_result"] == "fairway" else 0.0 for h in fairway_tracked])
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
fairway_left_pct = avg([100.0 if h["tee_shot_result"] == "left" else 0.0 for h in fairway_tracked])
|
|
|
|
|
|
fairway_right_pct = avg([100.0 if h["tee_shot_result"] == "right" else 0.0 for h in fairway_tracked])
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
|
|
|
|
|
|
def is_gir(h) -> bool:
|
|
|
|
|
|
return h["score"] - h["putts"] <= h["par"] - 2
|
|
|
|
|
|
|
|
|
|
|
|
gir_eligible = [h for h in all_played if h["putts"] is not None]
|
|
|
|
|
|
gir_pct = avg([100.0 if is_gir(h) else 0.0 for h in gir_eligible])
|
|
|
|
|
|
one_putt_pct = avg([100.0 if h["putts"] <= 1 else 0.0 for h in gir_eligible])
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
# Puttfordeling (donut-vennlig, 3 gjensidig utelukkende bøtter) -- egne
|
|
|
|
|
|
# eksakte grenser (==1/==2/>=3), SAMME bøtter som `round-stats.tsx` sin
|
|
|
|
|
|
# `puttCategories` bruker per enkeltrunde, ikke samme grense som
|
|
|
|
|
|
# `one_putt_pct` over (som bevisst bruker <=1 -- en litt annen,
|
|
|
|
|
|
# allerede etablert "hvor ofte putter du bra"-rate, ikke en distribusjon).
|
|
|
|
|
|
putt_dist_one_pct = avg([100.0 if h["putts"] == 1 else 0.0 for h in gir_eligible])
|
|
|
|
|
|
putt_dist_two_pct = avg([100.0 if h["putts"] == 2 else 0.0 for h in gir_eligible])
|
|
|
|
|
|
putt_dist_three_plus_pct = avg([100.0 if h["putts"] >= 3 else 0.0 for h in gir_eligible])
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
# Retning på bomskudd mot green -- eget spor (approach_result), ikke
|
|
|
|
|
|
# avledet av putte-basert GIR: en spiller kan registrere retning uten
|
|
|
|
|
|
# noensinne å registrere putter (og omvendt), samme uavhengighet som
|
|
|
|
|
|
# `round-stats.tsx` sin `missedGreen`/`missDir`.
|
|
|
|
|
|
green_missed = [h for h in all_played if h["approach_result"] is not None and h["approach_result"] != "hit"]
|
|
|
|
|
|
|
|
|
|
|
|
def miss_pct(direction: str) -> float | None:
|
|
|
|
|
|
return avg([100.0 if h["approach_result"] == direction else 0.0 for h in green_missed])
|
|
|
|
|
|
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
gir_missed = [h for h in gir_eligible if not is_gir(h)]
|
|
|
|
|
|
scrambling_pct = avg([100.0 if (h["score"] - h["par"]) <= 0 else 0.0 for h in gir_missed])
|
|
|
|
|
|
sand_eligible = [h for h in gir_missed if h["bunker_shot_count"] is not None and h["bunker_shot_count"] >= 1]
|
|
|
|
|
|
sand_save_pct = avg([100.0 if (h["score"] - h["par"]) <= 0 else 0.0 for h in sand_eligible])
|
|
|
|
|
|
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
# Til-par splittet på hull-nivå-hendelser (brukerens eksplisitte
|
|
|
|
|
|
# instruks 2026-07-28) -- pooler ENKELTHULL på tvers av alle runder i
|
|
|
|
|
|
# perioden, samme `diff()`-formel som `round-stats.tsx` sin
|
|
|
|
|
|
# `avgToParWithGir`/`avgToParFairwayHit`/`avgToParWithBunker` (portert
|
|
|
|
|
|
# uendret, anywayslag-splitten er en ny, konsekvent utvidelse av samme
|
|
|
|
|
|
# mønster -- fantes ikke fra før for enkeltrunder).
|
|
|
|
|
|
def diff(h) -> int:
|
|
|
|
|
|
return h["score"] - h["par"]
|
|
|
|
|
|
|
|
|
|
|
|
avg_to_par_with_gir = avg([diff(h) for h in gir_eligible if is_gir(h)])
|
|
|
|
|
|
avg_to_par_without_gir = avg([diff(h) for h in gir_eligible if not is_gir(h)])
|
|
|
|
|
|
|
|
|
|
|
|
avg_to_par_fairway_hit = avg([diff(h) for h in fairway_tracked if h["tee_shot_result"] == "fairway"])
|
|
|
|
|
|
avg_to_par_fairway_miss = avg([diff(h) for h in fairway_tracked if h["tee_shot_result"] in ("left", "right")])
|
|
|
|
|
|
|
|
|
|
|
|
bunker_eligible = [h for h in all_played if h["bunker_shot_count"] is not None]
|
|
|
|
|
|
avg_to_par_with_bunker = avg([diff(h) for h in bunker_eligible if h["bunker_shot_count"] >= 1])
|
|
|
|
|
|
avg_to_par_without_bunker = avg([diff(h) for h in bunker_eligible if h["bunker_shot_count"] == 0])
|
|
|
|
|
|
|
|
|
|
|
|
anyway_eligible = [h for h in all_played if h["anyway_strokes"] is not None]
|
|
|
|
|
|
avg_to_par_with_anyway = avg([diff(h) for h in anyway_eligible if h["anyway_strokes"] >= 1])
|
|
|
|
|
|
avg_to_par_without_anyway = avg([diff(h) for h in anyway_eligible if h["anyway_strokes"] == 0])
|
|
|
|
|
|
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
def avg_per_round(field: str) -> float | None:
|
|
|
|
|
|
totals = []
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
for round_id in round_ids:
|
|
|
|
|
|
eligible = [h for h in by_round.get(round_id, []) if h["played"] and h["score"] is not None and h[field] is not None]
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
if eligible:
|
|
|
|
|
|
totals.append(sum(h[field] for h in eligible))
|
|
|
|
|
|
return avg(totals)
|
|
|
|
|
|
|
|
|
|
|
|
return RoundStatsSummary(
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
rounds_completed=len(round_ids),
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
avg_score_to_par=avg(to_par_values),
|
|
|
|
|
|
avg_putts_per_18=avg(putt_totals),
|
|
|
|
|
|
putts_tracked_rounds=len(putt_totals),
|
|
|
|
|
|
fairway_hit_pct=fairway_hit_pct,
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
fairway_left_pct=fairway_left_pct,
|
|
|
|
|
|
fairway_right_pct=fairway_right_pct,
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
fairway_tracked_holes=len(fairway_tracked),
|
|
|
|
|
|
gir_pct=gir_pct,
|
|
|
|
|
|
gir_tracked_holes=len(gir_eligible),
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
green_miss_long_pct=miss_pct("long"),
|
|
|
|
|
|
green_miss_short_pct=miss_pct("short"),
|
|
|
|
|
|
green_miss_left_pct=miss_pct("left"),
|
|
|
|
|
|
green_miss_right_pct=miss_pct("right"),
|
|
|
|
|
|
green_miss_tracked_holes=len(green_missed),
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
one_putt_pct=one_putt_pct,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
putt_dist_one_pct=putt_dist_one_pct,
|
|
|
|
|
|
putt_dist_two_pct=putt_dist_two_pct,
|
|
|
|
|
|
putt_dist_three_plus_pct=putt_dist_three_plus_pct,
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
scrambling_pct=scrambling_pct,
|
|
|
|
|
|
sand_save_pct=sand_save_pct,
|
|
|
|
|
|
avg_chip_per_round=avg_per_round("chip_count"),
|
|
|
|
|
|
avg_bunker_per_round=avg_per_round("bunker_shot_count"),
|
|
|
|
|
|
avg_penalty_per_round=avg_per_round("penalty_strokes"),
|
|
|
|
|
|
avg_anyway_per_round=avg_per_round("anyway_strokes"),
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
avg_to_par_with_gir=avg_to_par_with_gir,
|
|
|
|
|
|
avg_to_par_without_gir=avg_to_par_without_gir,
|
|
|
|
|
|
avg_to_par_fairway_hit=avg_to_par_fairway_hit,
|
|
|
|
|
|
avg_to_par_fairway_miss=avg_to_par_fairway_miss,
|
|
|
|
|
|
avg_to_par_with_bunker=avg_to_par_with_bunker,
|
|
|
|
|
|
avg_to_par_without_bunker=avg_to_par_without_bunker,
|
|
|
|
|
|
avg_to_par_with_anyway=avg_to_par_with_anyway,
|
|
|
|
|
|
avg_to_par_without_anyway=avg_to_par_without_anyway,
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
All shipped, tested, and browser-verified. Summary of this round:
GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical).
Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks.
Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page.
"vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side.
Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far).
2026-07-28 07:43:47 +02:00
|
|
|
|
@router.get("/rounds/stats/summary", response_model=RoundStatsWindowSummary)
|
|
|
|
|
|
async def get_rounds_stats_summary(
|
|
|
|
|
|
window: StatsWindow = Query("all"),
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> RoundStatsWindowSummary:
|
|
|
|
|
|
"""Aggregert statistikk over brukerens fullførte runder (egen
|
|
|
|
|
|
deltaker-rad, ikke medspilleres), for et valgt tidsvindu -- pluss samme
|
|
|
|
|
|
tall for FORRIGE periode av samme lengde til sammenligning (se
|
|
|
|
|
|
`_resolve_stats_window`). Ingen paginering -- samme "hent alt, aggreger
|
|
|
|
|
|
i Python"-mønster som `list_rounds`, forsvarlig i volum siden
|
|
|
|
|
|
`round_hole` alltid har nøyaktig 18 rader per deltaker uansett
|
|
|
|
|
|
holes_planned (se `_create_participant`).
|
|
|
|
|
|
|
|
|
|
|
|
Putt/18-hull-regelen (brukerens eksplisitte instruks 2026-07-28): for
|
|
|
|
|
|
hver fullført runde der puttsporing faktisk var på (stat_level ≠
|
|
|
|
|
|
'strokes_only') telles ALLE 18 lagrede hull -- et hull uten registrert
|
|
|
|
|
|
putt-verdi (uspilt, eller utenfor et 9-hulls spilleomfang) telles som 2
|
|
|
|
|
|
putter. Runder uten puttsporing holdes helt utenfor putt-tallet.
|
|
|
|
|
|
Alle andre andelstall (fairway/GIR/én-putt/scrambling/sand save/miss-
|
|
|
|
|
|
retning) pooler kun FAKTISK registrerte hull -- samme formler som
|
|
|
|
|
|
`round-stats.tsx` sin `computeStats()` bruker per enkeltrunde (portert
|
|
|
|
|
|
hit, ikke en ny definisjon), ingen padding der brukeren ikke ba om det.
|
|
|
|
|
|
"""
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.round_id::text AS round_id, rp.stat_level, r.played_at,
|
|
|
|
|
|
rh.par, rh.played, rh.score, rh.putts,
|
|
|
|
|
|
rh.tee_shot_result, rh.approach_result, rh.chip_count,
|
|
|
|
|
|
rh.bunker_shot_count, rh.penalty_strokes, rh.anyway_strokes
|
|
|
|
|
|
FROM round_participant rp
|
|
|
|
|
|
JOIN round r ON r.id = rp.round_id
|
|
|
|
|
|
JOIN round_hole rh ON rh.round_participant_id = rp.id
|
|
|
|
|
|
WHERE rp.user_id = $1 AND r.completed_at IS NOT NULL
|
|
|
|
|
|
ORDER BY r.played_at DESC, rh.hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
by_round: dict[str, list] = {}
|
|
|
|
|
|
stat_level_by_round: dict[str, str] = {}
|
|
|
|
|
|
played_at_by_round: dict[str, date] = {}
|
|
|
|
|
|
for r in rows:
|
|
|
|
|
|
by_round.setdefault(r["round_id"], []).append(r)
|
|
|
|
|
|
stat_level_by_round[r["round_id"]] = r["stat_level"]
|
|
|
|
|
|
played_at_by_round[r["round_id"]] = r["played_at"]
|
|
|
|
|
|
|
|
|
|
|
|
# Distinkte runder, nyest først -- rekkefølgen `rows` allerede har
|
|
|
|
|
|
# (ORDER BY r.played_at DESC) bevares siden Python-dicts er innsettings-
|
|
|
|
|
|
# ordnet, ingen ny sortering nødvendig.
|
|
|
|
|
|
ordered_rounds = [(rid, played_at_by_round[rid]) for rid in by_round]
|
|
|
|
|
|
|
|
|
|
|
|
current_ids, previous_ids = _resolve_stats_window(window, ordered_rounds)
|
|
|
|
|
|
|
|
|
|
|
|
return RoundStatsWindowSummary(
|
|
|
|
|
|
window=window,
|
|
|
|
|
|
current=_summarize_rounds(current_ids, by_round, stat_level_by_round),
|
|
|
|
|
|
previous=_summarize_rounds(previous_ids, by_round, stat_level_by_round),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 06:24:57 +02:00
|
|
|
|
class KnownGuestOut(BaseModel):
|
|
|
|
|
|
guest_first_name: str
|
|
|
|
|
|
guest_last_name: str | None
|
|
|
|
|
|
gender: Literal["m", "f", "x"] | None
|
|
|
|
|
|
handicap_index: float | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/rounds/guests/known", response_model=KnownGuestOut | None)
|
|
|
|
|
|
async def find_known_guest(email: str, user: CurrentUser = Depends(get_current_user)) -> KnownGuestOut | None:
|
|
|
|
|
|
""""Gjenkjenn denne e-posten"-oppslag (2026-08-03) -- bevisst scoped til
|
|
|
|
|
|
DENNE brukerens EGNE tidligere registrerte gjester, ikke globalt på
|
|
|
|
|
|
tvers av alle organisatorer. Et globalt oppslag ville latt hvem som
|
|
|
|
|
|
helst skrive inn en tilfeldig e-post og se navn/kjønn/HCP en HELT ANNEN
|
|
|
|
|
|
organisator har registrert -- en reell personvernlekkasje. Returnerer
|
|
|
|
|
|
den NYESTE treffende gjeste-raden (spillerens opplysninger kan ha
|
|
|
|
|
|
endret seg siden forrige runde)."""
|
|
|
|
|
|
email = email.strip()
|
|
|
|
|
|
if not email:
|
|
|
|
|
|
return None
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.guest_first_name, rp.guest_last_name, rp.gender,
|
|
|
|
|
|
rp.handicap_index_snapshot::float AS handicap_index
|
|
|
|
|
|
FROM round_participant rp
|
|
|
|
|
|
JOIN round r ON r.id = rp.round_id
|
|
|
|
|
|
WHERE r.owner_user_id = $1
|
|
|
|
|
|
AND rp.guest_first_name IS NOT NULL
|
|
|
|
|
|
AND lower(rp.guest_email) = lower($2)
|
|
|
|
|
|
ORDER BY r.played_at DESC
|
|
|
|
|
|
LIMIT 1
|
|
|
|
|
|
""",
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
email,
|
|
|
|
|
|
)
|
|
|
|
|
|
return KnownGuestOut(**dict(row)) if row is not None else None
|
|
|
|
|
|
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
async def _get_owned_round_or_404(conn, round_id: str, user_id: str):
|
2026-07-26 08:22:41 +02:00
|
|
|
|
"""Strengt eier-only -- for runde-forvaltning (rediger/slett metadata,
|
|
|
|
|
|
legge til/fjerne deltakere). IKKE for lesing/scoreregistrering, se
|
|
|
|
|
|
`_get_accessible_round_or_404`."""
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
row = await conn.fetchrow("SELECT owner_user_id::text AS owner_user_id FROM round WHERE id = $1", round_id)
|
|
|
|
|
|
if row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Runden finnes ikke.")
|
|
|
|
|
|
if row["owner_user_id"] != user_id:
|
|
|
|
|
|
raise app_error(403, "NOT_AUTHORIZED", "Du eier ikke denne runden.")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-26 08:22:41 +02:00
|
|
|
|
async def _get_accessible_round_or_404(conn, round_id: str, user_id: str):
|
|
|
|
|
|
"""Eier ELLER en lenket medspiller (round_participant.user_id) --
|
|
|
|
|
|
ADR-036 fase 3-utvidelsen (2026-07-26): en medspiller kan se runden og
|
|
|
|
|
|
registrere score for HELE flighten (bekreftet regel), men kan IKKE
|
|
|
|
|
|
forvalte runden (se `_get_owned_round_or_404` for det)."""
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT r.owner_user_id::text AS owner_user_id,
|
|
|
|
|
|
EXISTS(
|
|
|
|
|
|
SELECT 1 FROM round_participant
|
|
|
|
|
|
WHERE round_id = r.id AND user_id = $2
|
|
|
|
|
|
) AS is_linked_participant
|
|
|
|
|
|
FROM round r WHERE r.id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Runden finnes ikke.")
|
|
|
|
|
|
if row["owner_user_id"] != user_id and not row["is_linked_participant"]:
|
|
|
|
|
|
raise app_error(403, "NOT_AUTHORIZED", "Du har ikke tilgang til denne runden.")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Tredjeparts leseadgang (ADR-036 fase 2, 2026-07-28) -- venner/offentlig-
|
|
|
|
|
|
# heten sitt innsyn i en runde de IKKE selv er eier eller deltaker på.
|
|
|
|
|
|
# Bevisst ADSKILT fra _get_accessible_round_or_404 over: den dekker eier +
|
|
|
|
|
|
# faktisk medspiller (som alltid har full lese-/skrivetilgang uansett
|
|
|
|
|
|
# visibility_mode), denne dekker KUN lesing for alle andre, styrt av
|
|
|
|
|
|
# visibility_mode + (for 'friends') eierens private kategorisering av
|
|
|
|
|
|
# viewer. `viewer_user_id=None` er en gyldig, forventet verdi (anonym
|
|
|
|
|
|
# leser) -- gir tilgang KUN når visibility_mode='public'.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
async def _can_view_round(conn, round_id: str, viewer_user_id: str | None) -> bool:
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT owner_user_id::text AS owner_user_id, visibility_mode FROM round WHERE id = $1",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if row is None:
|
|
|
|
|
|
return False
|
|
|
|
|
|
if viewer_user_id is not None and row["owner_user_id"] == viewer_user_id:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if viewer_user_id is not None:
|
|
|
|
|
|
is_participant = await conn.fetchval(
|
|
|
|
|
|
"SELECT EXISTS(SELECT 1 FROM round_participant WHERE round_id = $1 AND user_id = $2)",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
viewer_user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if is_participant:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if row["visibility_mode"] == "public":
|
|
|
|
|
|
return True
|
|
|
|
|
|
if row["visibility_mode"] == "private":
|
|
|
|
|
|
return False
|
|
|
|
|
|
# 'friends' -- krever et AKSEPTERT vennskap MED eieren OG at EIERENS
|
|
|
|
|
|
# kategorisering av viewer (ikke omvendt -- det er eieren som
|
2026-07-29 12:44:51 +02:00
|
|
|
|
# begrenser, basert på egen gruppering, ADR-036 Beslutning B).
|
|
|
|
|
|
#
|
|
|
|
|
|
# Presisert av bruker 2026-07-29: "ikke vise" overstyrer "vise" -- er
|
|
|
|
|
|
# vennen satt i FLERE kategorier, holder det IKKE at én av dem er
|
|
|
|
|
|
# synlig for runden; ALLE vennens kategorier må være i rundens synlige
|
|
|
|
|
|
# sett. En venn med INGEN kategorier i det hele tatt vises ALDRI (samme
|
|
|
|
|
|
# runde: kategorisering er nå obligatorisk fra vennskapet inngås, se
|
|
|
|
|
|
# friends.py -- en ukategorisert venn er derfor alltid en igjenværende
|
|
|
|
|
|
# unntakstilstand fra FØR den regelen, ikke en normal, forventet sti).
|
2026-07-28 19:25:37 +02:00
|
|
|
|
if viewer_user_id is None:
|
|
|
|
|
|
return False
|
|
|
|
|
|
owner_id = row["owner_user_id"]
|
|
|
|
|
|
is_friend = await conn.fetchval(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT EXISTS(
|
|
|
|
|
|
SELECT 1 FROM friendship
|
|
|
|
|
|
WHERE status = 'accepted'
|
|
|
|
|
|
AND ((requester_user_id = $1 AND addressee_user_id = $2)
|
|
|
|
|
|
OR (requester_user_id = $2 AND addressee_user_id = $1))
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
owner_id,
|
|
|
|
|
|
viewer_user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not is_friend:
|
|
|
|
|
|
return False
|
|
|
|
|
|
return await conn.fetchval(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT EXISTS(
|
2026-07-29 12:44:51 +02:00
|
|
|
|
SELECT 1 FROM friend_categorization
|
|
|
|
|
|
WHERE owner_user_id = $1 AND friend_user_id = $2
|
|
|
|
|
|
)
|
|
|
|
|
|
AND NOT EXISTS(
|
2026-07-28 19:25:37 +02:00
|
|
|
|
SELECT 1 FROM friend_categorization fc
|
2026-07-29 12:44:51 +02:00
|
|
|
|
WHERE fc.owner_user_id = $1 AND fc.friend_user_id = $2
|
|
|
|
|
|
AND fc.category NOT IN (SELECT category FROM round_visible_category WHERE round_id = $3)
|
2026-07-28 19:25:37 +02:00
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
owner_id,
|
|
|
|
|
|
viewer_user_id,
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _get_viewable_round_or_404(conn, round_id: str, viewer_user_id: str | None) -> None:
|
|
|
|
|
|
exists = await conn.fetchval("SELECT 1 FROM round WHERE id = $1", round_id)
|
|
|
|
|
|
if exists is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Runden finnes ikke.")
|
|
|
|
|
|
if not await _can_view_round(conn, round_id, viewer_user_id):
|
|
|
|
|
|
raise app_error(403, "NOT_AUTHORIZED", "Du har ikke tilgang til denne runden.")
|
|
|
|
|
|
|
|
|
|
|
|
|
Update Todos
Les add_participant, complete_round, friend_categorization-skjema for design
Design + skriv backend for tre rundevarsler (medspiller lagt til, venn starter synlig runde, tilkoblet runde fullført)
Scratch-verifiser alle tre varseltypene
Oppdater CLAUDE.md status + rull ut mot ekte systemer (med bekreftelse)
Live. Rundevarsler er nå koblet på, ren backend-endring (varslingssenteret hadde allerede round/result-typene klare, bare ingenting skrev dem):
Medspiller lagt til — varsel med lenke til /my-rounds/{id} (full tilgang)
En venn starter en synlig runde — varsel til aksepterte venner som faktisk kan se den (offentlig, eller riktig kategori), lenke til /watch/{id}
Tilkoblet runde fullført — medspillere og tredjeparts-venner varsles, aldri personen som selv fullførte
28/28 nye sjekker + full regresjon av tre eksisterende testsuiter (191 sjekker) grønt. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
2026-07-28 19:54:05 +02:00
|
|
|
|
async def _friends_who_can_see_round(conn, round_id: str, owner_user_id: str) -> list[str]:
|
|
|
|
|
|
"""Rundevarsler (FEATURE_BACKLOG.md, 2026-07-28 oppfølging av ADR-036
|
|
|
|
|
|
fase 2) -- aksepterte venner av EIEREN som kan se DENNE runden akkurat
|
|
|
|
|
|
nå (offentlig, eller riktig venne-kategori), brukt til fan-out-varsling
|
|
|
|
|
|
ved opprettelse OG ved fullføring. Bevisst GJENBEREGNET hver gang (ikke
|
|
|
|
|
|
en lagret mottakerliste) -- samme regel som selve tilgangskontrollen i
|
|
|
|
|
|
`_can_view_round`, aldri ute av synk med den."""
|
|
|
|
|
|
row = await conn.fetchrow("SELECT visibility_mode FROM round WHERE id = $1", round_id)
|
|
|
|
|
|
if row is None or row["visibility_mode"] == "private":
|
|
|
|
|
|
return []
|
|
|
|
|
|
friend_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT (CASE WHEN requester_user_id = $1 THEN addressee_user_id ELSE requester_user_id END)::text AS friend_id
|
|
|
|
|
|
FROM friendship
|
|
|
|
|
|
WHERE status = 'accepted' AND (requester_user_id = $1 OR addressee_user_id = $1)
|
|
|
|
|
|
""",
|
|
|
|
|
|
owner_user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
friend_ids = [r["friend_id"] for r in friend_rows]
|
|
|
|
|
|
if not friend_ids:
|
|
|
|
|
|
return []
|
|
|
|
|
|
if row["visibility_mode"] == "public":
|
|
|
|
|
|
return friend_ids
|
2026-07-29 12:44:51 +02:00
|
|
|
|
# Samme "alle kategorier må være synlige, ikke bare én"-regel som
|
|
|
|
|
|
# _can_view_round -- en venn med minst én ikke-synlig kategori
|
|
|
|
|
|
# ekskluderes fra fan-out-varselet, selv om en annen av kategoriene
|
|
|
|
|
|
# deres er valgt.
|
Update Todos
Les add_participant, complete_round, friend_categorization-skjema for design
Design + skriv backend for tre rundevarsler (medspiller lagt til, venn starter synlig runde, tilkoblet runde fullført)
Scratch-verifiser alle tre varseltypene
Oppdater CLAUDE.md status + rull ut mot ekte systemer (med bekreftelse)
Live. Rundevarsler er nå koblet på, ren backend-endring (varslingssenteret hadde allerede round/result-typene klare, bare ingenting skrev dem):
Medspiller lagt til — varsel med lenke til /my-rounds/{id} (full tilgang)
En venn starter en synlig runde — varsel til aksepterte venner som faktisk kan se den (offentlig, eller riktig kategori), lenke til /watch/{id}
Tilkoblet runde fullført — medspillere og tredjeparts-venner varsles, aldri personen som selv fullførte
28/28 nye sjekker + full regresjon av tre eksisterende testsuiter (191 sjekker) grønt. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
2026-07-28 19:54:05 +02:00
|
|
|
|
matched = await conn.fetch(
|
|
|
|
|
|
"""
|
2026-07-29 12:44:51 +02:00
|
|
|
|
SELECT fc.friend_user_id::text AS friend_id
|
Update Todos
Les add_participant, complete_round, friend_categorization-skjema for design
Design + skriv backend for tre rundevarsler (medspiller lagt til, venn starter synlig runde, tilkoblet runde fullført)
Scratch-verifiser alle tre varseltypene
Oppdater CLAUDE.md status + rull ut mot ekte systemer (med bekreftelse)
Live. Rundevarsler er nå koblet på, ren backend-endring (varslingssenteret hadde allerede round/result-typene klare, bare ingenting skrev dem):
Medspiller lagt til — varsel med lenke til /my-rounds/{id} (full tilgang)
En venn starter en synlig runde — varsel til aksepterte venner som faktisk kan se den (offentlig, eller riktig kategori), lenke til /watch/{id}
Tilkoblet runde fullført — medspillere og tredjeparts-venner varsles, aldri personen som selv fullførte
28/28 nye sjekker + full regresjon av tre eksisterende testsuiter (191 sjekker) grønt. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
2026-07-28 19:54:05 +02:00
|
|
|
|
FROM friend_categorization fc
|
2026-07-29 12:44:51 +02:00
|
|
|
|
WHERE fc.owner_user_id = $1 AND fc.friend_user_id = ANY($2::uuid[])
|
|
|
|
|
|
GROUP BY fc.friend_user_id
|
|
|
|
|
|
HAVING COUNT(*) FILTER (
|
|
|
|
|
|
WHERE fc.category NOT IN (SELECT category FROM round_visible_category WHERE round_id = $3)
|
|
|
|
|
|
) = 0
|
Update Todos
Les add_participant, complete_round, friend_categorization-skjema for design
Design + skriv backend for tre rundevarsler (medspiller lagt til, venn starter synlig runde, tilkoblet runde fullført)
Scratch-verifiser alle tre varseltypene
Oppdater CLAUDE.md status + rull ut mot ekte systemer (med bekreftelse)
Live. Rundevarsler er nå koblet på, ren backend-endring (varslingssenteret hadde allerede round/result-typene klare, bare ingenting skrev dem):
Medspiller lagt til — varsel med lenke til /my-rounds/{id} (full tilgang)
En venn starter en synlig runde — varsel til aksepterte venner som faktisk kan se den (offentlig, eller riktig kategori), lenke til /watch/{id}
Tilkoblet runde fullført — medspillere og tredjeparts-venner varsles, aldri personen som selv fullførte
28/28 nye sjekker + full regresjon av tre eksisterende testsuiter (191 sjekker) grønt. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
2026-07-28 19:54:05 +02:00
|
|
|
|
""",
|
|
|
|
|
|
owner_user_id,
|
2026-07-29 12:44:51 +02:00
|
|
|
|
friend_ids,
|
Update Todos
Les add_participant, complete_round, friend_categorization-skjema for design
Design + skriv backend for tre rundevarsler (medspiller lagt til, venn starter synlig runde, tilkoblet runde fullført)
Scratch-verifiser alle tre varseltypene
Oppdater CLAUDE.md status + rull ut mot ekte systemer (med bekreftelse)
Live. Rundevarsler er nå koblet på, ren backend-endring (varslingssenteret hadde allerede round/result-typene klare, bare ingenting skrev dem):
Medspiller lagt til — varsel med lenke til /my-rounds/{id} (full tilgang)
En venn starter en synlig runde — varsel til aksepterte venner som faktisk kan se den (offentlig, eller riktig kategori), lenke til /watch/{id}
Tilkoblet runde fullført — medspillere og tredjeparts-venner varsles, aldri personen som selv fullførte
28/28 nye sjekker + full regresjon av tre eksisterende testsuiter (191 sjekker) grønt. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
2026-07-28 19:54:05 +02:00
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return [r["friend_id"] for r in matched]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 21:15:07 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# "Venner på banen" (dashbord-redesign 2026-08-01) -- venner som enten
|
|
|
|
|
|
# spiller en aktiv runde AKKURAT NÅ, eller fullførte en innen de siste 24
|
|
|
|
|
|
# timene. Motsatt retning av `_friends_who_can_see_round` (der starter vi
|
|
|
|
|
|
# fra EIEREN og finner vennene deres) -- her starter vi fra VIEWEREN og
|
|
|
|
|
|
# finner vennenes runder, filtrert gjennom SAMME synlighetsregel som
|
|
|
|
|
|
# `_can_view_round` sin 'friends'-gren (kopiert inn i SQL-en under for å
|
|
|
|
|
|
# gjøre dette til ett enkelt sett-oppslag i stedet for N kall til
|
|
|
|
|
|
# `_can_view_round` per kandidatrunde -- reelt greit på denne skalaen,
|
|
|
|
|
|
# siden en brukers vennlisteantall er lite).
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FriendOnCourseEntry(BaseModel):
|
|
|
|
|
|
round_id: str
|
|
|
|
|
|
friend_user_id: str
|
|
|
|
|
|
friend_name: str
|
|
|
|
|
|
course_name: str
|
|
|
|
|
|
play_format: str
|
|
|
|
|
|
status: Literal["playing", "recently_finished"]
|
|
|
|
|
|
holes_played: int
|
|
|
|
|
|
holes_planned: int
|
|
|
|
|
|
score_to_par: int | None
|
|
|
|
|
|
started_at: str | None
|
|
|
|
|
|
completed_at: str | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/friends/on-course", response_model=list[FriendOnCourseEntry])
|
|
|
|
|
|
async def list_friends_on_course(user: CurrentUser = Depends(get_current_user)) -> list[FriendOnCourseEntry]:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT r.id::text AS round_id, r.owner_user_id::text AS owner_user_id,
|
|
|
|
|
|
COALESCE(au.first_name || ' ' || au.last_name, au.display_name) AS friend_name,
|
|
|
|
|
|
r.course_name_snapshot AS course_name, r.play_format,
|
|
|
|
|
|
r.holes_planned, r.started_at, r.completed_at,
|
|
|
|
|
|
COUNT(rh.*) FILTER (WHERE rh.played) AS holes_played,
|
|
|
|
|
|
COALESCE(SUM(rh.score) FILTER (WHERE rh.played), 0) AS total_score,
|
|
|
|
|
|
COALESCE(SUM(rh.par) FILTER (WHERE rh.played), 0) AS total_par
|
|
|
|
|
|
FROM round r
|
|
|
|
|
|
JOIN app_user au ON au.id = r.owner_user_id
|
|
|
|
|
|
JOIN friendship f ON f.status = 'accepted'
|
|
|
|
|
|
AND ((f.requester_user_id = $1 AND f.addressee_user_id = r.owner_user_id)
|
|
|
|
|
|
OR (f.requester_user_id = r.owner_user_id AND f.addressee_user_id = $1))
|
|
|
|
|
|
JOIN round_participant rp ON rp.round_id = r.id AND rp.user_id = r.owner_user_id
|
|
|
|
|
|
LEFT JOIN round_hole rh ON rh.round_participant_id = rp.id
|
|
|
|
|
|
WHERE r.owner_user_id != $1
|
|
|
|
|
|
AND (
|
|
|
|
|
|
(r.started_at IS NOT NULL AND r.completed_at IS NULL)
|
|
|
|
|
|
OR (r.completed_at IS NOT NULL AND r.completed_at >= now() - interval '24 hours')
|
|
|
|
|
|
)
|
|
|
|
|
|
AND (
|
|
|
|
|
|
r.visibility_mode = 'public'
|
|
|
|
|
|
OR (
|
|
|
|
|
|
r.visibility_mode = 'friends'
|
|
|
|
|
|
AND EXISTS(
|
|
|
|
|
|
SELECT 1 FROM friend_categorization fc
|
|
|
|
|
|
WHERE fc.owner_user_id = r.owner_user_id AND fc.friend_user_id = $1
|
|
|
|
|
|
)
|
|
|
|
|
|
AND NOT EXISTS(
|
|
|
|
|
|
SELECT 1 FROM friend_categorization fc
|
|
|
|
|
|
WHERE fc.owner_user_id = r.owner_user_id AND fc.friend_user_id = $1
|
|
|
|
|
|
AND fc.category NOT IN (
|
|
|
|
|
|
SELECT category FROM round_visible_category WHERE round_id = r.id
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
GROUP BY r.id, r.owner_user_id, au.first_name, au.last_name, au.display_name,
|
|
|
|
|
|
r.course_name_snapshot, r.play_format, r.holes_planned, r.started_at, r.completed_at
|
|
|
|
|
|
ORDER BY COALESCE(r.started_at, r.completed_at) DESC
|
|
|
|
|
|
""",
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
out: list[FriendOnCourseEntry] = []
|
|
|
|
|
|
for r in rows:
|
|
|
|
|
|
holes_played = r["holes_played"]
|
|
|
|
|
|
score_to_par = (
|
|
|
|
|
|
r["total_score"] - r["total_par"]
|
|
|
|
|
|
if holes_played > 0 and r["play_format"] in ("stroke", "stableford")
|
|
|
|
|
|
else None
|
|
|
|
|
|
)
|
|
|
|
|
|
out.append(
|
|
|
|
|
|
FriendOnCourseEntry(
|
|
|
|
|
|
round_id=r["round_id"],
|
|
|
|
|
|
friend_user_id=r["owner_user_id"],
|
|
|
|
|
|
friend_name=r["friend_name"],
|
|
|
|
|
|
course_name=r["course_name"],
|
|
|
|
|
|
play_format=r["play_format"],
|
|
|
|
|
|
status="playing" if r["completed_at"] is None else "recently_finished",
|
|
|
|
|
|
holes_played=holes_played,
|
|
|
|
|
|
holes_planned=r["holes_planned"],
|
|
|
|
|
|
score_to_par=score_to_par,
|
|
|
|
|
|
started_at=r["started_at"].isoformat() if r["started_at"] else None,
|
|
|
|
|
|
completed_at=r["completed_at"].isoformat() if r["completed_at"] else None,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Faktisk (beregnet) HCP -- ADR-038. v1 KUN fra frittstående runder --
|
|
|
|
|
|
# `_gather_qualifying_differentials` er det bevisste skjøtepunktet for en
|
|
|
|
|
|
# fremtidig UNION mot turnering-/organisasjonsscoring (ADR-037), ikke bygget
|
|
|
|
|
|
# ennå. Ingen ny, generell "scoring record"-tabell -- for tidlig abstraksjon
|
|
|
|
|
|
# for en kilde som ikke finnes.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
async def _gather_qualifying_differentials(conn, user_id: str) -> list[tuple[date, float]]:
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT r.played_at, rp.score_differential::float AS score_differential
|
|
|
|
|
|
FROM round_participant rp
|
|
|
|
|
|
JOIN round r ON r.id = rp.round_id
|
|
|
|
|
|
WHERE rp.user_id = $1
|
|
|
|
|
|
AND rp.counts_for_handicap
|
|
|
|
|
|
AND NOT rp.exclude_from_handicap
|
|
|
|
|
|
AND rp.score_differential IS NOT NULL
|
|
|
|
|
|
ORDER BY r.played_at DESC
|
|
|
|
|
|
""",
|
|
|
|
|
|
user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return [(row["played_at"], row["score_differential"]) for row in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _recompute_computed_handicap_index(conn, user_id: str) -> None:
|
|
|
|
|
|
"""Rule 5.2 (beste 8-av-≤20) + Rule 5.7/5.8 (Low HI-cap). Ingen tidligere
|
|
|
|
|
|
'computed'-historikk å låse mot -- capping hoppes bevisst over (Low HI
|
|
|
|
|
|
er udefinert før en første indeks er etablert), ikke en feil."""
|
|
|
|
|
|
differentials = await _gather_qualifying_differentials(conn, user_id)
|
|
|
|
|
|
new_index = handicap_index_from_differentials([d for _, d in differentials][:20])
|
|
|
|
|
|
|
|
|
|
|
|
if new_index is None:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"UPDATE app_user SET computed_handicap_index = NULL, computed_handicap_index_updated_at = now() "
|
|
|
|
|
|
"WHERE id = $1",
|
|
|
|
|
|
user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
history_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT recorded_at::date AS d, handicap_index::float AS idx
|
|
|
|
|
|
FROM handicap_history WHERE user_id = $1 AND source = 'computed'
|
|
|
|
|
|
ORDER BY recorded_at
|
|
|
|
|
|
""",
|
|
|
|
|
|
user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
low = low_handicap_index([(row["d"], row["idx"]) for row in history_rows], date.today())
|
|
|
|
|
|
final_index = apply_index_caps(new_index, low) if low is not None else new_index
|
|
|
|
|
|
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"UPDATE app_user SET computed_handicap_index = $2, computed_handicap_index_updated_at = now() WHERE id = $1",
|
|
|
|
|
|
user_id, final_index,
|
|
|
|
|
|
)
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"INSERT INTO handicap_history (user_id, handicap_index, source) VALUES ($1, $2, 'computed')",
|
|
|
|
|
|
user_id, final_index,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
@router.get("/rounds/{round_id}", response_model=RoundOut)
|
|
|
|
|
|
async def get_round(round_id: str, user: CurrentUser = Depends(get_current_user)) -> RoundOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
2026-07-26 08:22:41 +02:00
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
return await _load_round_out(conn, round_id, user.user_id)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
class RoundUpdate(BaseModel):
|
|
|
|
|
|
# Bane-bytte er ATOMISK -- enten oppgis course_source+tee_name (pluss de
|
|
|
|
|
|
# kildespesifikke feltene) sammen, eller ingen av dem. holes_planned/
|
|
|
|
|
|
# start_hole kan endres uavhengig, i samme kall eller alene -- disse
|
|
|
|
|
|
# rører aldri round_hole, kun visnings-/navigasjonsmetadata.
|
|
|
|
|
|
course_source: Literal["teeoff", "custom"] | None = None
|
|
|
|
|
|
teeoff_facility_slug: str | None = None
|
|
|
|
|
|
teeoff_course_id: int | None = None
|
|
|
|
|
|
personal_course_id: str | None = None
|
|
|
|
|
|
tee_name: str | None = None
|
2026-07-25 06:29:09 +02:00
|
|
|
|
# Rent metadata-felt -- kan endres/fjernes uansett fullført-status,
|
|
|
|
|
|
# samme begrunnelse som start_hole/started_at/completed_at under. Sendes
|
|
|
|
|
|
# feltet med tom streng, tolkes det som "fjern navnet" (NULL); mangler
|
|
|
|
|
|
# feltet i kallet, røres eksisterende navn ikke (samme mønster som de
|
|
|
|
|
|
# andre feltene her -- kun `None`/utelatt betyr "ikke rør").
|
|
|
|
|
|
name: str | None = Field(default=None, max_length=200)
|
2026-07-24 11:21:39 +02:00
|
|
|
|
holes_planned: Literal[9, 18] | None = None
|
|
|
|
|
|
start_hole: int | None = Field(default=None, ge=1, le=18)
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
# Ren metadata (ADR-038 Beslutning D) -- rører aldri AGS/differensial-
|
2026-08-03 14:32:48 +02:00
|
|
|
|
# matematikken selv, kan derfor endres uansett fullført-status. Utvidet
|
|
|
|
|
|
# 2026-08-03 til samme fulle sett som RoundCreate.play_format (var
|
|
|
|
|
|
# blitt hengende igjen på kun de tre opprinnelige formatene fra FØR de
|
|
|
|
|
|
# åtte nye formatene -- ADR-039 -- ble bygget, en ren forglemmelse, ikke
|
|
|
|
|
|
# en bevisst begrensning). Et format som krever ekstra oppsett (sider/
|
|
|
|
|
|
# best_n/lineup_order) som ikke finnes ennå håndteres allerede av
|
|
|
|
|
|
# eksisterende "sett opp X først"-meldinger andre steder, ikke noe nytt
|
|
|
|
|
|
# å validere her.
|
|
|
|
|
|
play_format: Literal[
|
|
|
|
|
|
"stroke", "match", "skins", "fourball", "foursome", "greensome", "scramble_2", "scramble_4", "stableford",
|
|
|
|
|
|
"chapman", "copenhagen", "bbb", "flag", "shamble", "money_ball", "high_low_high",
|
|
|
|
|
|
] | None = None
|
2026-07-24 11:21:39 +02:00
|
|
|
|
# Utslagstidspunkt og "Ferdig"-tidspunkt -- begge kan justeres i
|
|
|
|
|
|
# etterkant (f.eks. glemte å trykke "Fullfør runde" før flere timer
|
|
|
|
|
|
# senere, og vil rette opp tidsbruken). completed_at kan KUN settes på
|
|
|
|
|
|
# en runde som allerede er fullført (se validering under) -- denne
|
|
|
|
|
|
# PATCH-en fullfører aldri runden selv, kun korrigerer et tidspunkt som
|
|
|
|
|
|
# /complete allerede har satt.
|
|
|
|
|
|
started_at: datetime | None = None
|
|
|
|
|
|
completed_at: datetime | None = None
|
2026-07-28 19:25:37 +02:00
|
|
|
|
# ADR-036 Beslutning B (fase 2, 2026-07-28) -- ren metadata, endrer
|
|
|
|
|
|
# aldri HCP-beregningen, kan derfor justeres uansett fullført-status,
|
|
|
|
|
|
# samme begrunnelse som start_hole/started_at over.
|
|
|
|
|
|
# visible_categories er en FULL ERSTATNING av settet når sendt (samme
|
|
|
|
|
|
# "erstatt hele settet"-kontrakt som PUT /friends/{id}/categories) --
|
|
|
|
|
|
# utelatt/None rører ikke eksisterende kategorier, tom liste [] tømmer
|
|
|
|
|
|
# dem.
|
|
|
|
|
|
visibility_mode: Literal["public", "private", "friends"] | None = None
|
|
|
|
|
|
visible_categories: list[_FriendCategory] | None = None
|
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
|
|
|
|
# Flere flighter (migrasjon 035) -- SETTER gruppe-id (None = ikke rør,
|
|
|
|
|
|
# samme konvensjon som resten av denne modellen). Kun brukt til å binde
|
|
|
|
|
|
# en tidligere gruppeløs runde til en fersk gruppe idet en andre flight
|
|
|
|
|
|
# legges til (round-detail.tsx). Ingen vei til å FJERNE en runde fra en
|
|
|
|
|
|
# gruppe igjen i denne runden -- ikke etterspurt.
|
|
|
|
|
|
flight_group_id: str | None = None
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
# HCP-prosent + Match-HCP (migrasjon 051) -- samme "None = ikke rør"-
|
|
|
|
|
|
# konvensjon som resten av denne modellen (ingen vei til å eksplisitt
|
|
|
|
|
|
# NULLSTILLE tilbake til formatets standard i v1 -- ikke etterspurt).
|
|
|
|
|
|
# Rører ALDRI course_handicap_snapshot, men regner playing_handicap på
|
|
|
|
|
|
# nytt for allerede tildelte sider (se update_round).
|
|
|
|
|
|
allowance_override: dict | None = None
|
2026-07-24 11:21:39 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/rounds/{round_id}", response_model=RoundOut)
|
|
|
|
|
|
async def update_round(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
body: RoundUpdate,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> RoundOut:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Retter opp feil bane/utslag/antall hull/starthull ETTER at runden er
|
|
|
|
|
|
opprettet (2026-07-24) -- f.eks. hvis man oppdager underveis at feil
|
|
|
|
|
|
bane ble valgt, eller vil justere utslags-/fullført-tidspunktet i
|
|
|
|
|
|
etterkant. Endrer ALDRI allerede registrerte slag/putter/etc. i
|
|
|
|
|
|
round_hole -- kun rating-grunnlaget (par/stroke-index-snapshot +
|
|
|
|
|
|
course/slope-rating) hullene regnes ut fra. Bane-bytte og
|
|
|
|
|
|
holes_planned er bevisst avvist etter at runden er fullført
|
|
|
|
|
|
(differensialen er da allerede beregnet fra det gamle grunnlaget, og
|
|
|
|
|
|
en re-beregning etter fullføring er utenfor omfang her -- ulikt
|
|
|
|
|
|
tilsvarende bane-bytte for turnering-økter, som bevisst TILLATER dette
|
|
|
|
|
|
selv etter avgjørelse). start_hole/started_at/completed_at er derimot
|
|
|
|
|
|
ren metadata som ALDRI påvirker HCP-beregningen, og kan derfor justeres
|
|
|
|
|
|
uansett fullført-status.
|
|
|
|
|
|
"""
|
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
|
|
|
|
if body.flight_group_id is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
uuid.UUID(body.flight_group_id)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "flight_group_id er ikke en gyldig UUID.")
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_owned_round_or_404(conn, round_id, user.user_id)
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT started_at, completed_at, play_format FROM round WHERE id = $1", round_id
|
|
|
|
|
|
)
|
2026-07-24 11:21:39 +02:00
|
|
|
|
is_completed = round_row["completed_at"] is not None
|
|
|
|
|
|
|
|
|
|
|
|
if is_completed and (body.course_source is not None or body.holes_planned is not None):
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
409, "ALREADY_COMPLETED",
|
|
|
|
|
|
"Runden er allerede fullført -- kan ikke endre bane eller antall hull i etterkant.",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if body.completed_at is not None and not is_completed:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED",
|
|
|
|
|
|
"Runden må fullføres via \"Fullfør runde\" først -- deretter kan tidspunktet justeres.",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
effective_started_at = body.started_at if body.started_at is not None else round_row["started_at"]
|
|
|
|
|
|
effective_completed_at = body.completed_at if body.completed_at is not None else round_row["completed_at"]
|
|
|
|
|
|
if (
|
|
|
|
|
|
effective_started_at is not None
|
|
|
|
|
|
and effective_completed_at is not None
|
|
|
|
|
|
and effective_completed_at <= effective_started_at
|
|
|
|
|
|
):
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Fullført-tidspunktet må være etter utslagstidspunktet.")
|
|
|
|
|
|
|
|
|
|
|
|
resolved: _ResolvedCourse | None = None
|
|
|
|
|
|
participants = None
|
|
|
|
|
|
if body.course_source is not None:
|
|
|
|
|
|
if not body.tee_name:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "tee_name er påkrevd ved bane-bytte.")
|
|
|
|
|
|
if body.course_source == "teeoff":
|
|
|
|
|
|
if not body.teeoff_facility_slug or not body.teeoff_course_id:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "teeoff_facility_slug og teeoff_course_id er påkrevd.")
|
|
|
|
|
|
resolved = await _resolve_teeoff_course(body.teeoff_facility_slug, body.teeoff_course_id)
|
|
|
|
|
|
else:
|
|
|
|
|
|
if not body.personal_course_id:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "personal_course_id er påkrevd.")
|
|
|
|
|
|
resolved = await _resolve_personal_course(body.personal_course_id)
|
|
|
|
|
|
|
|
|
|
|
|
participants = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT id::text AS id, gender, handicap_index_snapshot::float AS handicap_index_snapshot
|
|
|
|
|
|
FROM round_participant WHERE round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
# Valider FOR ALLE deltakere FØR noe skrives -- hele bane-byttet
|
|
|
|
|
|
# avvises tydelig hvis ÉN eneste deltaker ville mistet HCP-
|
|
|
|
|
|
# sporing, ingen delvis anvendt endring.
|
|
|
|
|
|
for p in participants:
|
|
|
|
|
|
if resolved.rating_for(body.tee_name, p["gender"]) is None:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED",
|
|
|
|
|
|
"Den nye banen/utslaget mangler rating for én eller flere av deltakernes kjønn -- ingenting er endret.",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async with conn.transaction():
|
|
|
|
|
|
if body.holes_planned is not None:
|
|
|
|
|
|
await conn.execute("UPDATE round SET holes_planned = $2 WHERE id = $1", round_id, body.holes_planned)
|
|
|
|
|
|
if body.start_hole is not None:
|
|
|
|
|
|
await conn.execute("UPDATE round SET start_hole = $2 WHERE id = $1", round_id, body.start_hole)
|
2026-07-25 06:29:09 +02:00
|
|
|
|
if body.name is not None:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"UPDATE round SET name = $2 WHERE id = $1",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
body.name.strip() if body.name.strip() else None,
|
|
|
|
|
|
)
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
if body.play_format is not None:
|
|
|
|
|
|
await conn.execute("UPDATE round SET play_format = $2 WHERE id = $1", round_id, body.play_format)
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
if body.allowance_override is not None:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"UPDATE round SET allowance_override = $2::jsonb WHERE id = $1",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
json.dumps(body.allowance_override),
|
|
|
|
|
|
)
|
2026-07-24 11:21:39 +02:00
|
|
|
|
if body.started_at is not None:
|
|
|
|
|
|
await conn.execute("UPDATE round SET started_at = $2 WHERE id = $1", round_id, body.started_at)
|
|
|
|
|
|
if body.completed_at is not None:
|
|
|
|
|
|
await conn.execute("UPDATE round SET completed_at = $2 WHERE id = $1", round_id, body.completed_at)
|
2026-07-28 19:25:37 +02:00
|
|
|
|
if body.visibility_mode is not None:
|
|
|
|
|
|
await conn.execute("UPDATE round SET visibility_mode = $2 WHERE id = $1", round_id, body.visibility_mode)
|
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
|
|
|
|
if body.flight_group_id is not None:
|
|
|
|
|
|
await conn.execute("UPDATE round SET flight_group_id = $2 WHERE id = $1", round_id, body.flight_group_id)
|
2026-07-28 19:25:37 +02:00
|
|
|
|
if body.visible_categories is not None:
|
|
|
|
|
|
await conn.execute("DELETE FROM round_visible_category WHERE round_id = $1", round_id)
|
|
|
|
|
|
for category in sorted(set(body.visible_categories)):
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"INSERT INTO round_visible_category (round_id, category) VALUES ($1, $2)",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
category,
|
|
|
|
|
|
)
|
2026-07-24 11:21:39 +02:00
|
|
|
|
|
|
|
|
|
|
if resolved is not None and participants is not None:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
UPDATE round SET course_source = $2, teeoff_facility_slug = $3, teeoff_course_id = $4,
|
|
|
|
|
|
personal_course_id = $5, course_name_snapshot = $6, tee_name_snapshot = $7
|
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
body.course_source,
|
|
|
|
|
|
body.teeoff_facility_slug,
|
|
|
|
|
|
str(body.teeoff_course_id) if body.teeoff_course_id is not None else None,
|
|
|
|
|
|
body.personal_course_id,
|
|
|
|
|
|
resolved.course_name,
|
|
|
|
|
|
body.tee_name,
|
|
|
|
|
|
)
|
|
|
|
|
|
par_by_hole = {h[0]: h[1] for h in resolved.holes}
|
|
|
|
|
|
index_by_hole = {h[0]: h[2] for h in resolved.holes}
|
|
|
|
|
|
for p in participants:
|
|
|
|
|
|
course_rating, slope_rating, tee_par = resolved.rating_for(body.tee_name, p["gender"])
|
|
|
|
|
|
course_handicap_snapshot = None
|
|
|
|
|
|
if p["handicap_index_snapshot"] is not None:
|
|
|
|
|
|
course_handicap_snapshot = course_handicap(
|
|
|
|
|
|
p["handicap_index_snapshot"], slope_rating, course_rating, tee_par
|
|
|
|
|
|
)
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
2026-07-26 09:18:52 +02:00
|
|
|
|
UPDATE round_participant SET tee_name_snapshot = $2, course_rating_snapshot = $3,
|
|
|
|
|
|
slope_rating_snapshot = $4, tee_par_snapshot = $5, course_handicap_snapshot = $6
|
2026-07-24 11:21:39 +02:00
|
|
|
|
WHERE id = $1
|
|
|
|
|
|
""",
|
2026-07-26 09:18:52 +02:00
|
|
|
|
p["id"], body.tee_name, course_rating, slope_rating, tee_par, course_handicap_snapshot,
|
2026-07-24 11:21:39 +02:00
|
|
|
|
)
|
|
|
|
|
|
for hole_number in range(1, 19):
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
UPDATE round_hole SET par = $3, stroke_index = $4
|
|
|
|
|
|
WHERE round_participant_id = $1 AND hole_number = $2
|
|
|
|
|
|
""",
|
|
|
|
|
|
p["id"], hole_number, par_by_hole[hole_number], index_by_hole[hole_number],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
# HCP-prosent/Match-HCP-endring (migrasjon 051) -- regn playing_
|
|
|
|
|
|
# handicap på nytt for allerede tildelte sider, samme mønster som
|
|
|
|
|
|
# tournaments.py sin _recompute_session_matches ved allowance-
|
|
|
|
|
|
# bytte. Kun meningsfullt for to-sidede formater (round_side
|
|
|
|
|
|
# finnes ikke for andre -- harmløst no-op der uansett).
|
|
|
|
|
|
if body.allowance_override is not None:
|
|
|
|
|
|
effective_play_format = body.play_format if body.play_format is not None else round_row["play_format"]
|
|
|
|
|
|
if effective_play_format in _TWO_SIDED_FORMATS:
|
|
|
|
|
|
await _recompute_side_handicaps(conn, round_id, effective_play_format)
|
|
|
|
|
|
|
2026-07-26 08:49:37 +02:00
|
|
|
|
result = await _load_round_out(conn, round_id, user.user_id)
|
|
|
|
|
|
|
|
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
return result
|
2026-07-24 11:21:39 +02:00
|
|
|
|
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
@router.delete("/rounds/{round_id}", status_code=204)
|
|
|
|
|
|
async def delete_round(round_id: str, user: CurrentUser = Depends(get_current_user)) -> None:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_owned_round_or_404(conn, round_id, user.user_id)
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
# Fanges FØR sletting -- kaskaden fjerner round_participant-radene,
|
|
|
|
|
|
# så de tellende brukerne må hentes ut nå for å kunne regnes på nytt.
|
|
|
|
|
|
affected_users = await conn.fetch(
|
|
|
|
|
|
"SELECT user_id::text AS user_id FROM round_participant "
|
|
|
|
|
|
"WHERE round_id = $1 AND user_id IS NOT NULL AND counts_for_handicap",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
await conn.execute("DELETE FROM round WHERE id = $1", round_id)
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
for row in affected_users:
|
|
|
|
|
|
await _recompute_computed_handicap_index(conn, row["user_id"])
|
2026-07-26 08:49:37 +02:00
|
|
|
|
# Varsler evt. tilkoblede medspillere om at runden er borte -- deres
|
|
|
|
|
|
# neste refetch vil da naturlig treffe 404 og vise en tydelig feil.
|
|
|
|
|
|
await broadcast_round_update(round_id)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
2026-07-26 08:22:41 +02:00
|
|
|
|
# Medspillere (flighten) -- enten en EKTE TeeCup-bruker (funnet via det
|
|
|
|
|
|
# delte tiered personsøket, /people/search, ADR-036) eller en gjest uten
|
|
|
|
|
|
# konto (fritekstnavn). ADR-036 fase 3-utvidelsen (2026-07-26) -- inntil da
|
|
|
|
|
|
# var alle deltakere utenom eieren gjester.
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-07-26 08:22:41 +02:00
|
|
|
|
class ParticipantCreate(BaseModel):
|
2026-08-03 06:24:57 +02:00
|
|
|
|
# Nøyaktig én av user_id/guest_first_name.
|
2026-07-26 08:22:41 +02:00
|
|
|
|
user_id: str | None = None
|
2026-08-03 06:24:57 +02:00
|
|
|
|
# guest_last_name er bevisst valgfritt (en gjest kan legges til med kun
|
|
|
|
|
|
# fornavn) -- guest_first_name er den eneste PÅKREVDE gjeste-identiteten.
|
|
|
|
|
|
guest_first_name: str | None = Field(default=None, min_length=1, max_length=60)
|
|
|
|
|
|
guest_last_name: str | None = Field(default=None, min_length=1, max_length=60)
|
|
|
|
|
|
# Kun brukt for gjest-varianten -- for user_id hentes kjønn/HCP fra
|
2026-07-26 08:22:41 +02:00
|
|
|
|
# personens egen profil (samme kilde som eierens egen deltaker-rad).
|
|
|
|
|
|
gender: Literal["m", "f", "x"] | None = None
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
handicap_index: float | None = Field(default=None, ge=-10, le=54)
|
2026-07-26 09:42:06 +02:00
|
|
|
|
# Valgfritt kontaktfelt for en gjest (2026-07-26) -- avvist hvis oppgitt
|
|
|
|
|
|
# sammen med user_id (en lenket bruker har allerede sin egen e-post).
|
|
|
|
|
|
guest_email: EmailStr | None = None
|
2026-07-26 09:18:52 +02:00
|
|
|
|
# Utslagssted for DENNE deltakeren (2026-07-26) -- valgfritt, faller
|
|
|
|
|
|
# tilbake til rundens eget utslag (samme som før) hvis utelatt. Kan
|
|
|
|
|
|
# avvike fra de andre deltakerne, f.eks. et kjønnsdelt utslag.
|
|
|
|
|
|
tee_name: str | None = None
|
2026-07-24 06:27:35 +02:00
|
|
|
|
stat_level: StatLevel = "strokes_only"
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
# ADR-038 Beslutning C/D -- kun meningsfullt for user_id-varianten (en
|
|
|
|
|
|
# gjest har ingen computed_handicap_index å beskytte). Frontend
|
|
|
|
|
|
# forhåndsutfyller True når rundens play_format='match'.
|
|
|
|
|
|
exclude_from_handicap: bool = False
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# ADR-039 Beslutning A -- valgfritt, kun gyldig når rundens play_format
|
|
|
|
|
|
# bruker sider (match/fourball/foursome/greensome/scramble_*). Kan også
|
|
|
|
|
|
# settes/endres senere via PATCH .../participants/{id}.
|
|
|
|
|
|
round_side_id: str | None = None
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/rounds/{round_id}/participants", response_model=RoundParticipantOut, status_code=201)
|
2026-07-26 08:22:41 +02:00
|
|
|
|
async def add_participant(
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
round_id: str,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
body: ParticipantCreate,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> RoundParticipantOut:
|
2026-08-03 06:24:57 +02:00
|
|
|
|
if (body.user_id is not None) == (body.guest_first_name is not None):
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Oppgi enten user_id (funnet via søk) eller guest_first_name, ikke begge/ingen.")
|
2026-07-26 09:42:06 +02:00
|
|
|
|
if body.user_id is not None and body.guest_email is not None:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "guest_email gir ikke mening sammen med user_id -- en lenket bruker har allerede sin egen konto-e-post.")
|
2026-07-26 08:22:41 +02:00
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
round_row = await conn.fetchrow(
|
2026-07-26 08:22:41 +02:00
|
|
|
|
"SELECT owner_user_id::text AS owner_user_id, course_source, teeoff_facility_slug, "
|
Update Todos
Les add_participant, complete_round, friend_categorization-skjema for design
Design + skriv backend for tre rundevarsler (medspiller lagt til, venn starter synlig runde, tilkoblet runde fullført)
Scratch-verifiser alle tre varseltypene
Oppdater CLAUDE.md status + rull ut mot ekte systemer (med bekreftelse)
Live. Rundevarsler er nå koblet på, ren backend-endring (varslingssenteret hadde allerede round/result-typene klare, bare ingenting skrev dem):
Medspiller lagt til — varsel med lenke til /my-rounds/{id} (full tilgang)
En venn starter en synlig runde — varsel til aksepterte venner som faktisk kan se den (offentlig, eller riktig kategori), lenke til /watch/{id}
Tilkoblet runde fullført — medspillere og tredjeparts-venner varsles, aldri personen som selv fullførte
28/28 nye sjekker + full regresjon av tre eksisterende testsuiter (191 sjekker) grønt. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
2026-07-28 19:54:05 +02:00
|
|
|
|
"teeoff_course_id, personal_course_id, tee_name_snapshot, play_format, completed_at, "
|
|
|
|
|
|
"name, course_name_snapshot "
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
"FROM round WHERE id = $1",
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
2026-07-26 08:22:41 +02:00
|
|
|
|
if round_row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Runden finnes ikke.")
|
|
|
|
|
|
if round_row["owner_user_id"] != user.user_id:
|
|
|
|
|
|
raise app_error(403, "NOT_AUTHORIZED", "Du eier ikke denne runden.")
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
if body.round_side_id is not None:
|
|
|
|
|
|
if round_row["play_format"] not in _TWO_SIDED_FORMATS:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Denne spilleformen bruker ikke sider.")
|
|
|
|
|
|
side_exists = await conn.fetchval(
|
|
|
|
|
|
"SELECT 1 FROM round_side WHERE id = $1 AND round_id = $2", body.round_side_id, round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if not side_exists:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Siden finnes ikke på denne runden.")
|
|
|
|
|
|
await _check_side_capacity(conn, body.round_side_id, round_row["play_format"])
|
|
|
|
|
|
|
|
|
|
|
|
resolved = await _resolve_round_course(round_row)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
2026-07-26 08:22:41 +02:00
|
|
|
|
if body.user_id is not None:
|
|
|
|
|
|
if body.user_id == round_row["owner_user_id"]:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Du er allerede med i runden som eier.")
|
|
|
|
|
|
target = await conn.fetchrow(
|
|
|
|
|
|
"SELECT gender, handicap_index::float AS handicap_index FROM app_user WHERE id = $1",
|
|
|
|
|
|
body.user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if target is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Fant ikke brukeren.")
|
|
|
|
|
|
if target["gender"] is None:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED",
|
|
|
|
|
|
"Spilleren du valgte har ikke fullført profilen sin (mangler kjønn) og kan ikke legges til ennå.",
|
|
|
|
|
|
)
|
|
|
|
|
|
gender = target["gender"]
|
|
|
|
|
|
handicap_index = target["handicap_index"]
|
|
|
|
|
|
guest_name = None
|
2026-08-03 06:24:57 +02:00
|
|
|
|
guest_first_name = None
|
|
|
|
|
|
guest_last_name = None
|
2026-07-26 08:22:41 +02:00
|
|
|
|
linked_user_id = body.user_id
|
|
|
|
|
|
else:
|
|
|
|
|
|
gender = body.gender
|
|
|
|
|
|
handicap_index = body.handicap_index
|
2026-08-03 06:24:57 +02:00
|
|
|
|
guest_first_name = body.guest_first_name.strip()
|
|
|
|
|
|
guest_last_name = body.guest_last_name.strip() if body.guest_last_name else None
|
|
|
|
|
|
guest_name = f"{guest_first_name} {guest_last_name}".strip() if guest_last_name else guest_first_name
|
2026-07-26 08:22:41 +02:00
|
|
|
|
linked_user_id = None
|
|
|
|
|
|
if gender is None:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "gender er påkrevd for en gjest uten konto.")
|
|
|
|
|
|
|
2026-07-26 09:18:52 +02:00
|
|
|
|
tee_name = body.tee_name or round_row["tee_name_snapshot"]
|
|
|
|
|
|
if resolved.rating_for(tee_name, gender) is None and handicap_index is not None:
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED",
|
2026-07-26 09:18:52 +02:00
|
|
|
|
"Valgt utslag har ingen rating for dette kjønnet -- HCP-sporing er ikke mulig for denne deltakeren.",
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async with conn.transaction(), translate_db_errors():
|
|
|
|
|
|
participant_id = await _create_participant(
|
2026-07-26 09:18:52 +02:00
|
|
|
|
conn, round_id, resolved, tee_name,
|
2026-08-03 06:24:57 +02:00
|
|
|
|
user_id=linked_user_id, guest_name=guest_name,
|
|
|
|
|
|
guest_first_name=guest_first_name, guest_last_name=guest_last_name, is_owner=False,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
gender=gender, handicap_index=handicap_index,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
stat_level=body.stat_level,
|
2026-07-26 09:42:06 +02:00
|
|
|
|
guest_email=body.guest_email,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
exclude_from_handicap=body.exclude_from_handicap,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
round_side_id=body.round_side_id,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
individual_round_hole=_ROUND_FORMAT_TO_ENGINE_FORMAT.get(
|
|
|
|
|
|
round_row["play_format"], round_row["play_format"]
|
|
|
|
|
|
)
|
|
|
|
|
|
not in SIDE_IS_UNIT,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
if body.round_side_id is not None:
|
|
|
|
|
|
await _recompute_side_handicaps(conn, round_id, round_row["play_format"])
|
Update Todos
Les add_participant, complete_round, friend_categorization-skjema for design
Design + skriv backend for tre rundevarsler (medspiller lagt til, venn starter synlig runde, tilkoblet runde fullført)
Scratch-verifiser alle tre varseltypene
Oppdater CLAUDE.md status + rull ut mot ekte systemer (med bekreftelse)
Live. Rundevarsler er nå koblet på, ren backend-endring (varslingssenteret hadde allerede round/result-typene klare, bare ingenting skrev dem):
Medspiller lagt til — varsel med lenke til /my-rounds/{id} (full tilgang)
En venn starter en synlig runde — varsel til aksepterte venner som faktisk kan se den (offentlig, eller riktig kategori), lenke til /watch/{id}
Tilkoblet runde fullført — medspillere og tredjeparts-venner varsles, aldri personen som selv fullførte
28/28 nye sjekker + full regresjon av tre eksisterende testsuiter (191 sjekker) grønt. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
2026-07-28 19:54:05 +02:00
|
|
|
|
|
2026-07-28 20:09:21 +02:00
|
|
|
|
# Rundevarsel (2026-07-28) -- bevisst ETTER at transaksjonen er
|
|
|
|
|
|
# begått, samme mønster som create_round/complete_round (en
|
|
|
|
|
|
# e-post-utsending, se create_notification, skal aldri skje mens
|
|
|
|
|
|
# en DB-transaksjon holder låser).
|
|
|
|
|
|
if linked_user_id is not None:
|
|
|
|
|
|
owner_name = await conn.fetchval("SELECT display_name FROM app_user WHERE id = $1", user.user_id)
|
|
|
|
|
|
round_label = (round_row["name"] or "").strip() or round_row["course_name_snapshot"]
|
|
|
|
|
|
await create_notification(
|
|
|
|
|
|
conn,
|
|
|
|
|
|
user_id=linked_user_id,
|
|
|
|
|
|
type="round",
|
|
|
|
|
|
message=f"{owner_name} la deg til som medspiller på en runde ({round_label}).",
|
|
|
|
|
|
link_path=f"/my-rounds/{round_id}",
|
|
|
|
|
|
)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
2026-08-03 06:24:57 +02:00
|
|
|
|
SELECT rp.id::text AS id, rp.user_id::text AS user_id, rp.guest_name,
|
|
|
|
|
|
rp.guest_first_name, rp.guest_last_name, rp.guest_email,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS display_name,
|
2026-07-26 09:18:52 +02:00
|
|
|
|
rp.is_owner, rp.gender, rp.tee_name_snapshot,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
rp.handicap_index_snapshot::float AS handicap_index_snapshot,
|
|
|
|
|
|
rp.course_handicap_snapshot, rp.counts_for_handicap,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
rp.score_differential::float AS score_differential, rp.stat_level,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
rp.exclude_from_handicap, rp.round_side_id::text AS round_side_id,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
rp.playing_handicap, rp.lineup_order
|
2026-07-26 08:22:41 +02:00
|
|
|
|
FROM round_participant rp
|
|
|
|
|
|
LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.id = $1
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
""",
|
|
|
|
|
|
participant_id,
|
|
|
|
|
|
)
|
2026-07-26 08:49:37 +02:00
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
return RoundParticipantOut(**dict(row))
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Sider (ADR-039 Beslutning A) -- nøyaktig to per runde, kun for formater
|
|
|
|
|
|
# som faktisk bruker sider. Håndhevet i app-laget (samme mønster som
|
|
|
|
|
|
# ADR-011s to-lags-grense for org-turneringer), ikke en DB-constraint.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class RoundSideCreate(BaseModel):
|
|
|
|
|
|
label: str | None = Field(default=None, max_length=100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/rounds/{round_id}/sides", response_model=RoundSideOut, status_code=201)
|
|
|
|
|
|
async def create_round_side(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
body: RoundSideCreate,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> RoundSideOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_owned_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT play_format, course_source, teeoff_facility_slug, teeoff_course_id, "
|
|
|
|
|
|
"personal_course_id, completed_at FROM round WHERE id = $1",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if round_row["play_format"] not in _TWO_SIDED_FORMATS:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Denne spilleformen bruker ikke sider.")
|
|
|
|
|
|
if round_row["completed_at"] is not None:
|
|
|
|
|
|
raise app_error(409, "ALREADY_COMPLETED", "Runden er allerede fullført.")
|
|
|
|
|
|
existing_count = await conn.fetchval("SELECT count(*) FROM round_side WHERE round_id = $1", round_id)
|
|
|
|
|
|
if existing_count >= 2:
|
|
|
|
|
|
raise app_error(409, "LIMIT_REACHED", "En runde kan maks ha to sider.")
|
|
|
|
|
|
|
|
|
|
|
|
async with conn.transaction():
|
|
|
|
|
|
side_row = await conn.fetchrow(
|
|
|
|
|
|
"INSERT INTO round_side (round_id, label) VALUES ($1, $2) RETURNING id::text AS id, label",
|
|
|
|
|
|
round_id, body.label,
|
|
|
|
|
|
)
|
|
|
|
|
|
# Delt-ball-formater (ADR-039 Beslutning C): siden får sine
|
|
|
|
|
|
# round_hole-rader med en gang -- rating trengs ikke her, kun
|
|
|
|
|
|
# par/stroke-index-snapshot, uavhengig av om siden har fått
|
|
|
|
|
|
# noen deltakere ennå.
|
|
|
|
|
|
engine_format = _ROUND_FORMAT_TO_ENGINE_FORMAT[round_row["play_format"]]
|
|
|
|
|
|
if engine_format in SIDE_IS_UNIT:
|
|
|
|
|
|
resolved = await _resolve_round_course(round_row)
|
|
|
|
|
|
for hole_number, par, stroke_index in resolved.holes:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"INSERT INTO round_hole (round_side_id, hole_number, par, stroke_index) VALUES ($1, $2, $3, $4)",
|
|
|
|
|
|
side_row["id"], hole_number, par, stroke_index,
|
|
|
|
|
|
)
|
|
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
return RoundSideOut(**dict(side_row))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/rounds/{round_id}/sides/{side_id}", status_code=204)
|
|
|
|
|
|
async def delete_round_side(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
side_id: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_owned_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
round_row = await conn.fetchrow("SELECT completed_at FROM round WHERE id = $1", round_id)
|
|
|
|
|
|
if round_row["completed_at"] is not None:
|
|
|
|
|
|
raise app_error(409, "ALREADY_COMPLETED", "Runden er allerede fullført.")
|
|
|
|
|
|
side_exists = await conn.fetchval(
|
|
|
|
|
|
"SELECT 1 FROM round_side WHERE id = $1 AND round_id = $2", side_id, round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if not side_exists:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Siden finnes ikke.")
|
|
|
|
|
|
# Kaskade fjerner sidens round_hole-rader; deltakere som var på
|
|
|
|
|
|
# denne siden faller tilbake til round_side_id = NULL (ON DELETE
|
|
|
|
|
|
# SET NULL) -- de forblir i runden, bare uten side-tilhørighet.
|
|
|
|
|
|
await conn.execute("DELETE FROM round_side WHERE id = $1", side_id)
|
|
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-26 09:18:52 +02:00
|
|
|
|
@router.get("/rounds/{round_id}/tee-options", response_model=list[TeeOption])
|
|
|
|
|
|
async def get_round_tee_options(round_id: str, user: CurrentUser = Depends(get_current_user)) -> list[TeeOption]:
|
|
|
|
|
|
"""Utslagene tilgjengelig på RUNDENS bane -- brukt av 'rediger spiller'-
|
|
|
|
|
|
panelet til å la eieren velge et annet utslag enn rundens default for en
|
|
|
|
|
|
enkelt deltaker (f.eks. et kjønnsdelt utslag). Tilgjengelig for eier
|
|
|
|
|
|
ELLER en lenket medspiller (samme lese-tilgang som resten av runden)."""
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT course_source, teeoff_facility_slug, teeoff_course_id, personal_course_id FROM round WHERE id = $1",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if round_row["course_source"] == "teeoff":
|
|
|
|
|
|
resolved = await _resolve_teeoff_course(round_row["teeoff_facility_slug"], int(round_row["teeoff_course_id"]))
|
|
|
|
|
|
else:
|
|
|
|
|
|
resolved = await _resolve_personal_course(round_row["personal_course_id"])
|
|
|
|
|
|
return resolved.tee_options()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-24 06:27:35 +02:00
|
|
|
|
class ParticipantUpdate(BaseModel):
|
2026-07-26 09:18:52 +02:00
|
|
|
|
# Vanlig exclude_unset-PATCH-semantikk, som resten av appen -- et
|
|
|
|
|
|
# utelatt felt endres ikke. `handicap_index` kan sendes eksplisitt som
|
|
|
|
|
|
# `null` for å fjerne HCP-sporing for denne deltakeren (samme mønster
|
|
|
|
|
|
# som profil-PATCH-en).
|
|
|
|
|
|
stat_level: StatLevel | None = None
|
|
|
|
|
|
# Utslagssted OG HCP er begge KUN et snapshot for DENNE runden (2026-07-26)
|
|
|
|
|
|
# -- endrer aldri spillerens faktiske profil, samme reproduserbarhets-
|
|
|
|
|
|
# prinsipp som resten av round_participant. Bevisst avvist etter at
|
|
|
|
|
|
# runden er fullført, samme presedens som bane-bytte i RoundUpdate
|
|
|
|
|
|
# (differensialen er da allerede beregnet fra det gamle grunnlaget).
|
|
|
|
|
|
tee_name: str | None = None
|
|
|
|
|
|
handicap_index: float | None = Field(default=None, ge=-10, le=54)
|
2026-07-26 09:42:06 +02:00
|
|
|
|
# KUN gyldig for en gjest (user_id IS NULL) -- gir ingen mening for en
|
|
|
|
|
|
# lenket bruker, som allerede har sin egen kontos navn/kjønn/e-post.
|
|
|
|
|
|
# `gender` er en del av rating-bunten under (påvirker hvilken utslags-
|
2026-08-03 06:24:57 +02:00
|
|
|
|
# rating som er gyldig), `guest_first_name`/`guest_last_name`/
|
|
|
|
|
|
# `guest_email` er rene metadata. `guest_name` (det VISTE fulle navnet)
|
|
|
|
|
|
# synkes automatisk av update_participant når ett av disse to endres --
|
|
|
|
|
|
# sendes aldri direkte av klienten.
|
|
|
|
|
|
guest_first_name: str | None = Field(default=None, min_length=1, max_length=60)
|
|
|
|
|
|
guest_last_name: str | None = Field(default=None, min_length=1, max_length=60)
|
2026-07-26 09:42:06 +02:00
|
|
|
|
gender: Literal["m", "f", "x"] | None = None
|
|
|
|
|
|
guest_email: EmailStr | None = None
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
# ADR-038 Beslutning C -- eneste feltet en IKKE-eier (en lenket
|
|
|
|
|
|
# medspiller som redigerer SIN EGEN rad) får sende her.
|
|
|
|
|
|
exclude_from_handicap: bool | None = None
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# ADR-039 Beslutning A -- (re)tildel/fjern en deltakers side. Sendes som
|
|
|
|
|
|
# `null` for å fjerne fra en side. Eier-only (se _OWNER_ONLY_
|
|
|
|
|
|
# PARTICIPANT_FIELDS under).
|
|
|
|
|
|
round_side_id: str | None = None
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
# Money Ball (2026-07-30) -- KUN meningsfullt for play_format=
|
|
|
|
|
|
# 'money_ball'. 0-3, unik per runde -- avgjør rotasjonsrekkefølgen
|
|
|
|
|
|
# ((posisjon-1) % 4) for hvem som er "money ball" på hvilket hull.
|
|
|
|
|
|
lineup_order: int | None = Field(default=None, ge=0, le=3)
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Felt en ikke-eier ALDRI kan sende her, uansett hvilken rad det gjelder --
|
|
|
|
|
|
# eier-only, se update_participant sin autorisasjonssjekk.
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
_OWNER_ONLY_PARTICIPANT_FIELDS = {
|
2026-08-03 06:24:57 +02:00
|
|
|
|
"stat_level", "tee_name", "handicap_index", "guest_first_name", "guest_last_name", "gender", "guest_email",
|
|
|
|
|
|
"round_side_id", "lineup_order",
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}
|
2026-07-24 06:27:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/rounds/{round_id}/participants/{participant_id}", response_model=RoundParticipantOut)
|
|
|
|
|
|
async def update_participant(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
participant_id: str,
|
|
|
|
|
|
body: ParticipantUpdate,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> RoundParticipantOut:
|
2026-07-26 09:18:52 +02:00
|
|
|
|
updates = body.model_dump(exclude_unset=True)
|
|
|
|
|
|
if not updates:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.")
|
|
|
|
|
|
|
2026-07-24 06:27:35 +02:00
|
|
|
|
async with plain_connection() as conn:
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
# Eier ELLER en lenket medspiller (som kun får røre SIN EGEN
|
|
|
|
|
|
# eksklusjons-flagg, se under) -- ikke strengt eier-only lenger.
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
2026-07-26 09:18:52 +02:00
|
|
|
|
current = await conn.fetchrow(
|
|
|
|
|
|
"""
|
2026-07-26 09:42:06 +02:00
|
|
|
|
SELECT rp.user_id::text AS user_id, rp.gender, rp.tee_name_snapshot,
|
2026-07-26 09:18:52 +02:00
|
|
|
|
rp.handicap_index_snapshot::float AS handicap_index_snapshot,
|
2026-08-03 06:24:57 +02:00
|
|
|
|
rp.counts_for_handicap, rp.guest_first_name, rp.guest_last_name,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
r.owner_user_id::text AS round_owner_user_id,
|
2026-07-26 09:18:52 +02:00
|
|
|
|
r.completed_at, r.course_source, r.teeoff_facility_slug,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
r.teeoff_course_id, r.personal_course_id, r.play_format
|
2026-07-26 09:18:52 +02:00
|
|
|
|
FROM round_participant rp
|
|
|
|
|
|
JOIN round r ON r.id = rp.round_id
|
|
|
|
|
|
WHERE rp.id = $1 AND rp.round_id = $2
|
|
|
|
|
|
""",
|
|
|
|
|
|
participant_id, round_id,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
)
|
2026-07-26 09:18:52 +02:00
|
|
|
|
if current is None:
|
2026-07-26 08:22:41 +02:00
|
|
|
|
raise app_error(404, "NOT_FOUND", "Deltakeren finnes ikke på denne runden.")
|
2026-07-26 09:18:52 +02:00
|
|
|
|
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
is_owner = current["round_owner_user_id"] == user.user_id
|
|
|
|
|
|
if not is_owner:
|
|
|
|
|
|
if _OWNER_ONLY_PARTICIPANT_FIELDS & updates.keys():
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
403, "NOT_AUTHORIZED",
|
|
|
|
|
|
"Du kan kun endre om DIN egen deltakelse skal telle mot ditt faktiske HCP her.",
|
|
|
|
|
|
)
|
|
|
|
|
|
if current["user_id"] != user.user_id:
|
|
|
|
|
|
raise app_error(403, "NOT_AUTHORIZED", "Du kan kun endre dette for din egen deltakelse.")
|
|
|
|
|
|
|
2026-08-03 06:24:57 +02:00
|
|
|
|
guest_only_fields = {"guest_first_name", "guest_last_name", "gender", "guest_email"}
|
2026-07-26 09:42:06 +02:00
|
|
|
|
if current["user_id"] is not None and guest_only_fields & updates.keys():
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED",
|
|
|
|
|
|
"Navn/kjønn/e-post kan ikke endres her for en spiller med egen konto -- kun for en gjest.",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
rating_changed = "tee_name" in updates or "handicap_index" in updates or "gender" in updates
|
2026-07-26 09:18:52 +02:00
|
|
|
|
if rating_changed and current["completed_at"] is not None:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
409, "ALREADY_COMPLETED",
|
2026-07-26 09:42:06 +02:00
|
|
|
|
"Runden er allerede fullført -- kan ikke endre utslag, HCP eller kjønn i etterkant.",
|
2026-07-26 09:18:52 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
set_clauses: list[str] = []
|
|
|
|
|
|
values: list[object] = []
|
|
|
|
|
|
|
|
|
|
|
|
if "stat_level" in updates:
|
|
|
|
|
|
values.append(updates["stat_level"])
|
|
|
|
|
|
set_clauses.append(f"stat_level = ${len(values)}")
|
2026-08-03 06:24:57 +02:00
|
|
|
|
if "guest_first_name" in updates or "guest_last_name" in updates:
|
|
|
|
|
|
# guest_name (det VISTE fulle navnet) synkes automatisk her --
|
|
|
|
|
|
# samme mønster som app_user.display_name synkes fra first_name/
|
|
|
|
|
|
# last_name (update_profile, routers/auth.py). Slår sammen med
|
|
|
|
|
|
# det som IKKE ble sendt i dette kallet, slik at en PATCH med
|
|
|
|
|
|
# kun ett av de to feltene ikke nullstiller det andre.
|
|
|
|
|
|
new_first = (
|
|
|
|
|
|
updates["guest_first_name"].strip() if "guest_first_name" in updates else current["guest_first_name"]
|
|
|
|
|
|
)
|
|
|
|
|
|
new_last_raw = updates["guest_last_name"] if "guest_last_name" in updates else current["guest_last_name"]
|
|
|
|
|
|
new_last = new_last_raw.strip() if new_last_raw else None
|
|
|
|
|
|
values.append(new_first)
|
|
|
|
|
|
set_clauses.append(f"guest_first_name = ${len(values)}")
|
|
|
|
|
|
values.append(new_last)
|
|
|
|
|
|
set_clauses.append(f"guest_last_name = ${len(values)}")
|
|
|
|
|
|
values.append(f"{new_first} {new_last}".strip() if new_last else new_first)
|
2026-07-26 09:42:06 +02:00
|
|
|
|
set_clauses.append(f"guest_name = ${len(values)}")
|
|
|
|
|
|
if "guest_email" in updates:
|
|
|
|
|
|
values.append(updates["guest_email"])
|
|
|
|
|
|
set_clauses.append(f"guest_email = ${len(values)}")
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
if "exclude_from_handicap" in updates:
|
|
|
|
|
|
values.append(updates["exclude_from_handicap"])
|
|
|
|
|
|
set_clauses.append(f"exclude_from_handicap = ${len(values)}")
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
if "round_side_id" in updates:
|
|
|
|
|
|
new_side_id = updates["round_side_id"]
|
|
|
|
|
|
if new_side_id is not None:
|
|
|
|
|
|
if current["play_format"] not in _TWO_SIDED_FORMATS:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Denne spilleformen bruker ikke sider.")
|
|
|
|
|
|
side_exists = await conn.fetchval(
|
|
|
|
|
|
"SELECT 1 FROM round_side WHERE id = $1 AND round_id = $2", new_side_id, round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if not side_exists:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Siden finnes ikke på denne runden.")
|
|
|
|
|
|
await _check_side_capacity(conn, new_side_id, current["play_format"], exclude_participant_id=participant_id)
|
|
|
|
|
|
values.append(new_side_id)
|
|
|
|
|
|
set_clauses.append(f"round_side_id = ${len(values)}")
|
2026-07-26 09:18:52 +02:00
|
|
|
|
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
if "lineup_order" in updates:
|
|
|
|
|
|
new_lineup_order = updates["lineup_order"]
|
|
|
|
|
|
if new_lineup_order is not None:
|
|
|
|
|
|
if current["play_format"] != "money_ball":
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Denne spilleformen bruker ikke lagrekkefølge.")
|
|
|
|
|
|
taken = await conn.fetchval(
|
|
|
|
|
|
"SELECT 1 FROM round_participant WHERE round_id = $1 AND lineup_order = $2 AND id != $3",
|
|
|
|
|
|
round_id, new_lineup_order, participant_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if taken:
|
|
|
|
|
|
raise app_error(409, "DUPLICATE", "En annen spiller har allerede denne plassen i rekkefølgen.")
|
|
|
|
|
|
values.append(new_lineup_order)
|
|
|
|
|
|
set_clauses.append(f"lineup_order = ${len(values)}")
|
|
|
|
|
|
|
2026-07-26 09:18:52 +02:00
|
|
|
|
if rating_changed:
|
|
|
|
|
|
if current["course_source"] == "teeoff":
|
|
|
|
|
|
resolved = await _resolve_teeoff_course(
|
|
|
|
|
|
current["teeoff_facility_slug"], int(current["teeoff_course_id"])
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
resolved = await _resolve_personal_course(current["personal_course_id"])
|
|
|
|
|
|
|
|
|
|
|
|
new_tee_name = updates.get("tee_name") or current["tee_name_snapshot"]
|
2026-07-26 09:42:06 +02:00
|
|
|
|
new_gender = updates.get("gender") or current["gender"]
|
2026-07-26 09:18:52 +02:00
|
|
|
|
new_handicap_index = (
|
|
|
|
|
|
updates["handicap_index"] if "handicap_index" in updates else current["handicap_index_snapshot"]
|
|
|
|
|
|
)
|
2026-07-26 09:42:06 +02:00
|
|
|
|
rating = resolved.rating_for(new_tee_name, new_gender)
|
2026-07-26 09:18:52 +02:00
|
|
|
|
if rating is None:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED",
|
2026-07-26 09:42:06 +02:00
|
|
|
|
"Valgt utslag har ingen rating for spillerens (nye) kjønn.",
|
2026-07-26 09:18:52 +02:00
|
|
|
|
)
|
|
|
|
|
|
course_rating, slope_rating, tee_par = rating
|
|
|
|
|
|
course_handicap_snapshot = (
|
|
|
|
|
|
course_handicap(new_handicap_index, slope_rating, course_rating, tee_par)
|
|
|
|
|
|
if new_handicap_index is not None else None
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
values.append(new_tee_name)
|
|
|
|
|
|
set_clauses.append(f"tee_name_snapshot = ${len(values)}")
|
|
|
|
|
|
values.append(new_handicap_index)
|
|
|
|
|
|
set_clauses.append(f"handicap_index_snapshot = ${len(values)}")
|
|
|
|
|
|
values.append(course_rating)
|
|
|
|
|
|
set_clauses.append(f"course_rating_snapshot = ${len(values)}")
|
|
|
|
|
|
values.append(slope_rating)
|
|
|
|
|
|
set_clauses.append(f"slope_rating_snapshot = ${len(values)}")
|
|
|
|
|
|
values.append(tee_par)
|
|
|
|
|
|
set_clauses.append(f"tee_par_snapshot = ${len(values)}")
|
|
|
|
|
|
values.append(course_handicap_snapshot)
|
|
|
|
|
|
set_clauses.append(f"course_handicap_snapshot = ${len(values)}")
|
2026-07-26 09:42:06 +02:00
|
|
|
|
if "gender" in updates:
|
|
|
|
|
|
values.append(new_gender)
|
|
|
|
|
|
set_clauses.append(f"gender = ${len(values)}")
|
2026-07-26 09:18:52 +02:00
|
|
|
|
|
|
|
|
|
|
values.append(participant_id)
|
|
|
|
|
|
updated_id = await conn.fetchval(
|
|
|
|
|
|
f"UPDATE round_participant SET {', '.join(set_clauses)} WHERE id = ${len(values)} RETURNING id",
|
|
|
|
|
|
*values,
|
|
|
|
|
|
)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
# Side-tildeling ELLER rating (HCP/utslag/kjønn) endret (ADR-039) --
|
|
|
|
|
|
# regn playing_handicap for begge sider på nytt FØR raden under
|
|
|
|
|
|
# leses, slik at responsen reflekterer den ferske verdien (billig
|
|
|
|
|
|
# no-op hvis fortsatt ikke komplett). Reelt funn (2026-07-30,
|
|
|
|
|
|
# scratch-verifisering av High-low-high): en deltaker som FØRST ble
|
|
|
|
|
|
# tildelt en side og SIDEN fikk sin HCP satt/endret (f.eks. en gjest
|
|
|
|
|
|
# opprettet uten kjent HCP) fikk ALDRI regnet playing_handicap på
|
|
|
|
|
|
# nytt, siden det opprinnelige triggeret kun så på round_side_id --
|
|
|
|
|
|
# laget lignende resultatet permanent "ikke klart" til siden ble
|
|
|
|
|
|
# rørt på nytt. rating_changed dekker nå samme sak.
|
|
|
|
|
|
if (
|
|
|
|
|
|
("round_side_id" in updates or rating_changed)
|
|
|
|
|
|
and current["play_format"] in _TWO_SIDED_FORMATS
|
|
|
|
|
|
):
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
await _recompute_side_handicaps(conn, round_id, current["play_format"])
|
|
|
|
|
|
|
2026-07-24 06:27:35 +02:00
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
2026-08-03 06:24:57 +02:00
|
|
|
|
SELECT rp.id::text AS id, rp.user_id::text AS user_id, rp.guest_name,
|
|
|
|
|
|
rp.guest_first_name, rp.guest_last_name, rp.guest_email,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS display_name,
|
2026-07-26 09:18:52 +02:00
|
|
|
|
rp.is_owner, rp.gender, rp.tee_name_snapshot,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
rp.handicap_index_snapshot::float AS handicap_index_snapshot,
|
|
|
|
|
|
rp.course_handicap_snapshot, rp.counts_for_handicap,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
rp.score_differential::float AS score_differential, rp.stat_level,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
rp.exclude_from_handicap, rp.round_side_id::text AS round_side_id,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
rp.playing_handicap, rp.lineup_order
|
2026-07-26 08:22:41 +02:00
|
|
|
|
FROM round_participant rp
|
|
|
|
|
|
LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.id = $1
|
2026-07-24 06:27:35 +02:00
|
|
|
|
""",
|
2026-07-26 08:22:41 +02:00
|
|
|
|
updated_id,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
)
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
|
|
|
|
|
|
# En eksklusjons-endring på en ALLEREDE FULLFØRT, tellende runde
|
|
|
|
|
|
# endrer umiddelbart hvilke differensialer faktisk HCP skal regnes
|
|
|
|
|
|
# fra -- regn på nytt med en gang, ikke vent til neste fullføring.
|
|
|
|
|
|
if (
|
|
|
|
|
|
"exclude_from_handicap" in updates
|
|
|
|
|
|
and current["user_id"] is not None
|
|
|
|
|
|
and current["completed_at"] is not None
|
|
|
|
|
|
):
|
|
|
|
|
|
await _recompute_computed_handicap_index(conn, current["user_id"])
|
2026-07-26 09:18:52 +02:00
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
return RoundParticipantOut(**dict(row))
|
2026-07-24 06:27:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
@router.delete("/rounds/{round_id}/participants/{participant_id}", status_code=204)
|
|
|
|
|
|
async def remove_guest_participant(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
participant_id: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_owned_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
row = await conn.fetchrow(
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
"SELECT is_owner, user_id::text AS user_id, counts_for_handicap FROM round_participant "
|
|
|
|
|
|
"WHERE id = $1 AND round_id = $2",
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
participant_id, round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Deltakeren finnes ikke.")
|
|
|
|
|
|
if row["is_owner"]:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Kan ikke fjerne runde-eieren.")
|
|
|
|
|
|
await conn.execute("DELETE FROM round_participant WHERE id = $1", participant_id)
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
|
|
|
|
|
|
# Fjerner en tellende differensial fra grunnlaget -- regn faktisk
|
|
|
|
|
|
# HCP på nytt for den fjernede spilleren (ikke eieren, som er igjen).
|
|
|
|
|
|
if row["user_id"] is not None and row["counts_for_handicap"]:
|
|
|
|
|
|
await _recompute_computed_handicap_index(conn, row["user_id"])
|
2026-07-26 08:49:37 +02:00
|
|
|
|
await broadcast_round_update(round_id)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Hull-for-hull-registrering
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
class RoundHoleOut(BaseModel):
|
|
|
|
|
|
hole_number: int
|
|
|
|
|
|
par: int
|
|
|
|
|
|
stroke_index: int
|
|
|
|
|
|
played: bool
|
|
|
|
|
|
score: int | None
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
# "Plukket opp" (migrasjon 038, Stableford) -- sann når spilleren
|
|
|
|
|
|
# plukket opp ballen fordi hullet uansett var klart 0 poeng. `score`
|
|
|
|
|
|
# er DA fortsatt satt (til Net Double Bogey, samme cap som enhver
|
|
|
|
|
|
# annen høy score allerede får for HCP-formål), kun for VISNING skal
|
|
|
|
|
|
# dette feltet vises ("Plukket opp") i stedet for det rå tallet.
|
|
|
|
|
|
picked_up: bool
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
putts: int | None
|
|
|
|
|
|
club_off_tee: str | None
|
|
|
|
|
|
tee_shot_result: str | None
|
|
|
|
|
|
approach_result: str | None
|
|
|
|
|
|
chip_count: int | None
|
|
|
|
|
|
bunker_shot_count: int | None
|
|
|
|
|
|
penalty_strokes: int | None
|
2026-07-24 06:27:35 +02:00
|
|
|
|
first_putt_distance_bucket: str | None
|
|
|
|
|
|
anyway_strokes: int | None
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
# Slag mottatt på dette hullet, utledet fra course_handicap_snapshot
|
|
|
|
|
|
# (samme allokeringsalgoritme som resten av appen) -- kun til visning
|
|
|
|
|
|
# av netto-score i en oversiktstabell, aldri lagret. None hvis
|
|
|
|
|
|
# deltakeren ikke har en beregnet course handicap (f.eks. gjest uten
|
|
|
|
|
|
# HCP).
|
|
|
|
|
|
strokes_received: int | None
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
async def _build_participant_holes(conn, round_id: str, participant_id: str) -> list[RoundHoleOut]:
|
|
|
|
|
|
"""Delt mellom den autentiserte GET og den offentlige/venn-gatede
|
|
|
|
|
|
/public/rounds-varianten (ADR-036 fase 2) -- selve dataen er identisk,
|
|
|
|
|
|
kun autorisasjonssjekken FØR dette kalles skiller de to."""
|
|
|
|
|
|
participant_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT course_handicap_snapshot FROM round_participant WHERE id = $1 AND round_id = $2",
|
|
|
|
|
|
participant_id, round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if participant_row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Deltakeren finnes ikke på denne runden.")
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
SELECT hole_number, par, stroke_index, played, score, picked_up, putts, club_off_tee,
|
2026-07-28 19:25:37 +02:00
|
|
|
|
tee_shot_result, approach_result, chip_count, bunker_shot_count,
|
|
|
|
|
|
penalty_strokes, first_putt_distance_bucket, anyway_strokes
|
|
|
|
|
|
FROM round_hole WHERE round_participant_id = $1 ORDER BY hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
participant_id,
|
|
|
|
|
|
)
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
|
|
|
|
|
|
strokes_received_by_hole: dict[int, int] | None = None
|
|
|
|
|
|
if participant_row["course_handicap_snapshot"] is not None:
|
|
|
|
|
|
allocation = allocate_strokes_by_index(
|
|
|
|
|
|
participant_row["course_handicap_snapshot"], [r["stroke_index"] for r in rows]
|
|
|
|
|
|
)
|
|
|
|
|
|
strokes_received_by_hole = {r["hole_number"]: a for r, a in zip(rows, allocation)}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
RoundHoleOut(
|
|
|
|
|
|
**dict(r),
|
|
|
|
|
|
strokes_received=strokes_received_by_hole[r["hole_number"]] if strokes_received_by_hole else None,
|
|
|
|
|
|
)
|
|
|
|
|
|
for r in rows
|
|
|
|
|
|
]
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
@router.get(
|
|
|
|
|
|
"/rounds/{round_id}/participants/{participant_id}/holes",
|
|
|
|
|
|
response_model=list[RoundHoleOut],
|
|
|
|
|
|
)
|
|
|
|
|
|
async def list_holes(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
participant_id: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> list[RoundHoleOut]:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
return await _build_participant_holes(conn, round_id, participant_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Delt-ball-scoring (ADR-039 Beslutning C) -- foursome/greensome/scramble
|
|
|
|
|
|
# lagrer ÉN score PER SIDE per hull, ikke per spiller. Ingen av de andre
|
|
|
|
|
|
# detalj-feltene (kølle/retning/chip/bunker/straffeslag/putt-lengde) gir
|
|
|
|
|
|
# entydig mening for en delt ball -- kun selve slagtallet.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class RoundSideHoleOut(BaseModel):
|
|
|
|
|
|
hole_number: int
|
|
|
|
|
|
par: int
|
|
|
|
|
|
stroke_index: int
|
|
|
|
|
|
played: bool
|
|
|
|
|
|
score: int | None
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
# Slag mottatt av SIDEN (kombinert playing_handicap, samme for begge
|
|
|
|
|
|
# partnere på siden) på dette hullet -- samme allokeringsalgoritme og
|
|
|
|
|
|
# samme "kun til visning av netto"-begrunnelse som RoundHoleOut sin
|
|
|
|
|
|
# strokes_received. None hvis siden ikke har en beregnet playing
|
|
|
|
|
|
# handicap ennå (f.eks. kun én av to partnere tildelt).
|
|
|
|
|
|
strokes_received: int | None = None
|
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
|
|
|
|
# Hvilken av sidens partnere sitt utslag ble valgt (migrasjon 036) --
|
|
|
|
|
|
# helt valgfritt, null = ikke registrert. Kun meningsfullt her (delt-
|
|
|
|
|
|
# ball-hull) -- individuell ball spiller alltid egen ball, spørsmålet
|
|
|
|
|
|
# gir ikke mening for RoundHoleOut.
|
|
|
|
|
|
selected_participant_id: str | None = None
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
async def _build_side_holes(conn, round_id: str, side_id: str) -> list[RoundSideHoleOut]:
|
|
|
|
|
|
"""Delt mellom den autentiserte GET og /public/rounds (ADR-036 fase 2)
|
|
|
|
|
|
-- se _build_participant_holes over for samme begrunnelse."""
|
|
|
|
|
|
side_exists = await conn.fetchval("SELECT 1 FROM round_side WHERE id = $1 AND round_id = $2", side_id, round_id)
|
|
|
|
|
|
if not side_exists:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Siden finnes ikke.")
|
|
|
|
|
|
rows = await conn.fetch(
|
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
|
|
|
|
"SELECT hole_number, par, stroke_index, played, score, selected_participant_id::text AS selected_participant_id "
|
|
|
|
|
|
"FROM round_hole WHERE round_side_id = $1 ORDER BY hole_number",
|
2026-07-28 19:25:37 +02:00
|
|
|
|
side_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
playing_handicap = await conn.fetchval(
|
|
|
|
|
|
"SELECT playing_handicap FROM round_participant WHERE round_side_id = $1 LIMIT 1", side_id
|
|
|
|
|
|
)
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
|
|
|
|
|
|
strokes_received_by_hole: dict[int, int] | None = None
|
|
|
|
|
|
if playing_handicap is not None:
|
|
|
|
|
|
allocation = allocate_strokes_by_index(playing_handicap, [r["stroke_index"] for r in rows])
|
|
|
|
|
|
strokes_received_by_hole = {r["hole_number"]: a for r, a in zip(rows, allocation)}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
RoundSideHoleOut(
|
|
|
|
|
|
**dict(r),
|
|
|
|
|
|
strokes_received=strokes_received_by_hole[r["hole_number"]] if strokes_received_by_hole else None,
|
|
|
|
|
|
)
|
|
|
|
|
|
for r in rows
|
|
|
|
|
|
]
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
@router.get("/rounds/{round_id}/sides/{side_id}/holes", response_model=list[RoundSideHoleOut])
|
|
|
|
|
|
async def list_side_holes(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
side_id: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> list[RoundSideHoleOut]:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
return await _build_side_holes(conn, round_id, side_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
class SideHoleUpdate(BaseModel):
|
|
|
|
|
|
played: bool = True
|
|
|
|
|
|
score: int | None = Field(default=None, ge=1, le=20)
|
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
|
|
|
|
# Hvem sitt utslag ble valgt (migrasjon 036) -- helt valgfritt, samme
|
|
|
|
|
|
# "full overwrite hver gang"-kontrakt som played/score over (klienten
|
|
|
|
|
|
# sender alltid gjeldende verdi, null = ikke registrert -- IKKE en
|
|
|
|
|
|
# delvis PATCH). Validert til å tilhøre nøyaktig DENNE siden under.
|
|
|
|
|
|
selected_participant_id: str | None = None
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/rounds/{round_id}/sides/{side_id}/holes/{hole_number}", response_model=RoundSideHoleOut)
|
|
|
|
|
|
async def update_side_hole(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
side_id: str,
|
|
|
|
|
|
hole_number: int,
|
|
|
|
|
|
body: SideHoleUpdate,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> RoundSideHoleOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
# Samme begrunnelse som update_hole -- en medspiller som fører
|
|
|
|
|
|
# score for flighten skal også kunne føre for en delt-ball-side.
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
side_exists = await conn.fetchval("SELECT 1 FROM round_side WHERE id = $1 AND round_id = $2", side_id, round_id)
|
|
|
|
|
|
if not side_exists:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Siden finnes ikke.")
|
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
|
|
|
|
if body.selected_participant_id is not None:
|
|
|
|
|
|
belongs = await conn.fetchval(
|
|
|
|
|
|
"SELECT 1 FROM round_participant WHERE id = $1 AND round_side_id = $2",
|
|
|
|
|
|
body.selected_participant_id, side_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not belongs:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED", "Valgt spiller hører ikke til denne siden."
|
|
|
|
|
|
)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
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
|
|
|
|
UPDATE round_hole SET played = $3, score = $4, selected_participant_id = $5
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
WHERE round_side_id = $1 AND hole_number = $2
|
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
|
|
|
|
RETURNING hole_number, par, stroke_index, played, score,
|
|
|
|
|
|
selected_participant_id::text AS selected_participant_id
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
""",
|
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
|
|
|
|
side_id, hole_number, body.played, body.score, body.selected_participant_id,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
)
|
|
|
|
|
|
if row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Hullet finnes ikke på denne siden.")
|
|
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
return RoundSideHoleOut(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
|
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Bingo Bango Bongo (2026-07-30) -- MANUELL per-hull-registrering (samme
|
|
|
|
|
|
# mønster som "valgt utslag" i scramble), ikke GPS/live-sporet. Egen liten
|
|
|
|
|
|
# tabell (round_bbb_hole) siden dette er en per-HULL-fakta, ikke per-
|
|
|
|
|
|
# spiller/side slik round_hole ellers er strukturert.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class BBBHoleUpdate(BaseModel):
|
|
|
|
|
|
bingo_participant_id: str | None = None
|
|
|
|
|
|
bango_participant_id: str | None = None
|
|
|
|
|
|
bongo_participant_id: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BBBHoleOut(BaseModel):
|
|
|
|
|
|
hole_number: int
|
|
|
|
|
|
bingo_participant_id: str | None
|
|
|
|
|
|
bango_participant_id: str | None
|
|
|
|
|
|
bongo_participant_id: str | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/rounds/{round_id}/bbb/{hole_number}", response_model=BBBHoleOut)
|
|
|
|
|
|
async def update_bbb_hole(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
hole_number: int,
|
|
|
|
|
|
body: BBBHoleUpdate,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> BBBHoleOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT play_format, holes_planned FROM round WHERE id = $1", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if round_row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Runden finnes ikke.")
|
|
|
|
|
|
if round_row["play_format"] != "bbb":
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Denne runden bruker ikke Bingo Bango Bongo.")
|
|
|
|
|
|
if not (1 <= hole_number <= round_row["holes_planned"]):
|
|
|
|
|
|
raise app_error(400, "OUT_OF_SCOPE", "Hullnummeret er utenfor rundens hullomfang.")
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
|
|
|
|
|
|
for pid in (body.bingo_participant_id, body.bango_participant_id, body.bongo_participant_id):
|
|
|
|
|
|
if pid is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
belongs = await conn.fetchval(
|
|
|
|
|
|
"SELECT 1 FROM round_participant WHERE id = $1 AND round_id = $2", pid, round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if not belongs:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Valgt spiller hører ikke til denne runden.")
|
|
|
|
|
|
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO round_bbb_hole
|
|
|
|
|
|
(round_id, hole_number, bingo_participant_id, bango_participant_id, bongo_participant_id)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
|
|
|
|
ON CONFLICT (round_id, hole_number) DO UPDATE SET
|
|
|
|
|
|
bingo_participant_id = $3, bango_participant_id = $4, bongo_participant_id = $5
|
|
|
|
|
|
RETURNING hole_number, bingo_participant_id::text AS bingo_participant_id,
|
|
|
|
|
|
bango_participant_id::text AS bango_participant_id,
|
|
|
|
|
|
bongo_participant_id::text AS bongo_participant_id
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
hole_number,
|
|
|
|
|
|
body.bingo_participant_id,
|
|
|
|
|
|
body.bango_participant_id,
|
|
|
|
|
|
body.bongo_participant_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
return BBBHoleOut(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
|
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
@router.get("/rounds/{round_id}/bbb", response_model=list[BBBHoleOut])
|
|
|
|
|
|
async def list_bbb_holes(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> list[BBBHoleOut]:
|
|
|
|
|
|
"""Rå bingo/bango/bongo-VALG per hull -- ikke poengene selv (de er
|
|
|
|
|
|
utledet, se _build_format_result sin bbb-gren). Trengs for at
|
|
|
|
|
|
frontend-widgeten skal kunne vise HVEM som allerede er valgt på et
|
|
|
|
|
|
hull ved gjenlasting, ikke bare la brukeren gjette på nytt hver gang."""
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT hole_number, bingo_participant_id::text AS bingo_participant_id,
|
|
|
|
|
|
bango_participant_id::text AS bango_participant_id,
|
|
|
|
|
|
bongo_participant_id::text AS bongo_participant_id
|
|
|
|
|
|
FROM round_bbb_hole WHERE round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return [BBBHoleOut(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Format-resultat (ADR-039) -- løpende matchstatus (match/fourball/foursome/
|
|
|
|
|
|
# greensome/scramble) eller skins-tavle, avledet ved LESING (aldri lagret),
|
|
|
|
|
|
# samme "cache aldri det som kan regnes ut fra rå data"-filosofi som resten
|
|
|
|
|
|
# av appen. Slagspill trenger ingen egen visning her -- eksisterende
|
|
|
|
|
|
# scorekort/leaderboard dekker det allerede.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
class FlagParticipantResult(BaseModel):
|
|
|
|
|
|
"""Flaggturnering -- se handicap_engine.py sin flag_result()."""
|
|
|
|
|
|
|
|
|
|
|
|
holes_completed: int
|
|
|
|
|
|
ran_out: bool
|
|
|
|
|
|
strokes_remaining: int
|
|
|
|
|
|
label: str
|
|
|
|
|
|
|
|
|
|
|
|
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
class FormatHoleEntry(BaseModel):
|
|
|
|
|
|
"""Én "enhet" sitt tall på ett hull -- en deltaker for individuell-ball-
|
|
|
|
|
|
formater (match/fourball/skins), eller en SIDE for delt-ball-formater
|
|
|
|
|
|
(foursome/greensome/scramble, ADR-039 Beslutning C: én score for hele
|
|
|
|
|
|
siden, ingen individuell deltaker-rad å vise i stedet)."""
|
|
|
|
|
|
|
|
|
|
|
|
id: str # participant_id, eller round_side_id for delt-ball
|
|
|
|
|
|
label: str
|
|
|
|
|
|
side: Literal["a", "b"] | None = None # kun to-sidede formater
|
|
|
|
|
|
gross: int | None = None
|
|
|
|
|
|
net: int | None = None # None hvis ingen course handicap er klart (eller skins_scoring="gross")
|
|
|
|
|
|
strokes_received: int | None = None
|
|
|
|
|
|
# Fourball: KUN den ene av de to partnernes netto teller for siden det
|
|
|
|
|
|
# hullet (R&A-regelen: laveste netto blant lagets spillere) -- markerer
|
|
|
|
|
|
# HVILKEN, slik at "hvorfor vant siden" er synlig uten egen forklarende
|
|
|
|
|
|
# tekst. Alltid sann for formater med kun én enhet per side (match,
|
|
|
|
|
|
# foursome/greensome/scramble sin ene side-rad).
|
|
|
|
|
|
counted: bool = True
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
# Københavner (2026-07-30) -- poeng denne deltakeren fikk PÅ DETTE
|
|
|
|
|
|
# hullet (0/1/2/3/4, summerer alltid til 6 blant de tre deltakerne).
|
|
|
|
|
|
points: int | None = None
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FormatHoleOut(BaseModel):
|
|
|
|
|
|
hole_number: int
|
|
|
|
|
|
par: int
|
|
|
|
|
|
stroke_index: int
|
|
|
|
|
|
entries: list[FormatHoleEntry]
|
|
|
|
|
|
result: str | None = None # to-sidede formater: "a"/"b"/"halved"
|
|
|
|
|
|
winner_ids: list[str] | None = None # skins: hvem vant (evt. delte) hullets skin(s)
|
|
|
|
|
|
carried: bool | None = None # skins: hullet var uavgjort og potten rullet videre
|
|
|
|
|
|
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
class RoundFormatResultOut(BaseModel):
|
|
|
|
|
|
play_format: str
|
|
|
|
|
|
ready: bool # False = handicap ikke klart for én eller begge sider ennå
|
|
|
|
|
|
# Match/fourball/foursome/greensome/scramble:
|
|
|
|
|
|
match_lead: int | None = None
|
|
|
|
|
|
match_holes_played: int | None = None
|
|
|
|
|
|
match_holes_remaining: int | None = None
|
|
|
|
|
|
match_is_closed: bool | None = None
|
|
|
|
|
|
match_is_dormie: bool | None = None
|
|
|
|
|
|
match_status_text: str | None = None
|
|
|
|
|
|
hole_results: list[str] | None = None # "a"/"b"/"halved" per avgjort hull, i spillerekkefølge
|
|
|
|
|
|
# Skins:
|
|
|
|
|
|
skins_won: dict[str, float] | None = None
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
# Københavner: totale poeng per deltaker (summerer til 6 * antall
|
|
|
|
|
|
# fullførte hull blant de tre deltakerne).
|
|
|
|
|
|
copenhagen_points: dict[str, int] | None = None
|
|
|
|
|
|
# Bingo Bango Bongo: totale poeng per deltaker.
|
|
|
|
|
|
bbb_points: dict[str, int] | None = None
|
|
|
|
|
|
# Flaggturnering: resultat per deltaker (None hvis course handicap
|
|
|
|
|
|
# ikke er klart ennå for den deltakeren).
|
|
|
|
|
|
flag_results: dict[str, FlagParticipantResult] | None = None
|
|
|
|
|
|
# Shamble: lagets (= hele rundens) samlede hullscore-sum -- kun basert
|
|
|
|
|
|
# på hull der ALLE spillerne har registrert, samme "vent til alle er
|
|
|
|
|
|
# ferdig"-prinsipp som resten av formatene.
|
|
|
|
|
|
shamble_team_score: int | None = None
|
|
|
|
|
|
# Money Ball: lagets (= hele rundens) samlede hullscore-sum.
|
|
|
|
|
|
money_ball_team_score: int | None = None
|
|
|
|
|
|
# High-low-high: literal poengsum per side (IKKE match-play "up"/
|
|
|
|
|
|
# "down" -- se high_low_high_running_score). match_lead brukes likevel
|
|
|
|
|
|
# til fortegn/hvem-leder (positiv = side A), men disse to gir de
|
|
|
|
|
|
# FAKTISKE tallene til visning (f.eks. "2-1").
|
|
|
|
|
|
hlh_points_a: int | None = None
|
|
|
|
|
|
hlh_points_b: int | None = None
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
# Hull-for-hull-forløp (2026-07-28, "hvorfor ble resultatet som det ble" --
|
|
|
|
|
|
# brutto/netto/slag-mottatt per enhet, ikke bare et vinn/tap-merke).
|
|
|
|
|
|
# None når ready=False (to-sidede formater) -- ellers alltid satt,
|
|
|
|
|
|
# inkl. skins (som ikke har noe eget "ready"-konsept).
|
|
|
|
|
|
holes: list[FormatHoleOut] | None = None
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _round_hole_order(start_hole: int, holes_planned: int) -> list[int]:
|
|
|
|
|
|
"""Samme sirkulære spillerekkefølge som frontend allerede bruker
|
|
|
|
|
|
(round-detail.tsx) -- starter på start_hole, ruller rundt 1-18."""
|
|
|
|
|
|
return [((start_hole - 1 + i) % 18) + 1 for i in range(holes_planned)]
|
|
|
|
|
|
|
|
|
|
|
|
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
async def _build_high_low_high_result(conn, round_id: str, round_row) -> RoundFormatResultOut:
|
|
|
|
|
|
"""High-low-high (2026-07-30) -- passer IKKE inn i den generiske to-
|
|
|
|
|
|
siders-grenen under (som forutsetter ÉN sammenlignbar netto-verdi per
|
|
|
|
|
|
side per hull via side_net()) -- trenger BEGGE spilleres individuelle
|
|
|
|
|
|
Stableford-poeng per hull for selve høy/lav-rangeringen. Egen,
|
|
|
|
|
|
selvstendig sti, samme mønster som Shamble/Money Ball sine dedikerte
|
|
|
|
|
|
grener. Beregnes ved LESING, ingen cache-kolonne (som Nassau)."""
|
|
|
|
|
|
relative = await _relative_strokes_for_round(conn, round_id, "high_low_high")
|
|
|
|
|
|
if not relative:
|
|
|
|
|
|
return RoundFormatResultOut(play_format="high_low_high", ready=False)
|
|
|
|
|
|
|
|
|
|
|
|
side_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id, label FROM round_side WHERE round_id = $1 ORDER BY id", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if len(side_rows) != 2:
|
|
|
|
|
|
return RoundFormatResultOut(play_format="high_low_high", ready=False)
|
|
|
|
|
|
participant_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS id, rp.round_side_id::text AS side_id,
|
|
|
|
|
|
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS label
|
|
|
|
|
|
FROM round_participant rp LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.round_id = $1 AND rp.round_side_id IS NOT NULL
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
by_side: dict[str, list[str]] = {}
|
|
|
|
|
|
for p in participant_rows:
|
|
|
|
|
|
by_side.setdefault(p["side_id"], []).append(p["id"])
|
|
|
|
|
|
if any(len(by_side.get(s["id"], [])) != 2 for s in side_rows):
|
|
|
|
|
|
return RoundFormatResultOut(play_format="high_low_high", ready=False)
|
|
|
|
|
|
|
|
|
|
|
|
hole_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS participant_id, rh.hole_number, rh.par, rh.stroke_index, rh.played, rh.score
|
|
|
|
|
|
FROM round_hole rh JOIN round_participant rp ON rp.id = rh.round_participant_id
|
|
|
|
|
|
WHERE rp.round_id = $1 AND rp.round_side_id IS NOT NULL
|
|
|
|
|
|
ORDER BY rh.hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
all_18_si_by_hole: dict[int, int] = {}
|
|
|
|
|
|
par_by_hole: dict[int, int] = {}
|
|
|
|
|
|
gross_by_hole: dict[int, dict[str, int]] = {}
|
|
|
|
|
|
for r in hole_rows:
|
|
|
|
|
|
all_18_si_by_hole[r["hole_number"]] = r["stroke_index"]
|
|
|
|
|
|
par_by_hole[r["hole_number"]] = r["par"]
|
|
|
|
|
|
if r["played"] and r["score"] is not None:
|
|
|
|
|
|
gross_by_hole.setdefault(r["hole_number"], {})[r["participant_id"]] = r["score"]
|
|
|
|
|
|
if len(all_18_si_by_hole) != 18:
|
|
|
|
|
|
return RoundFormatResultOut(play_format="high_low_high", ready=False)
|
|
|
|
|
|
all_18_si = [all_18_si_by_hole[h] for h in range(1, 19)]
|
|
|
|
|
|
|
|
|
|
|
|
strokes_per_hole = {
|
|
|
|
|
|
pid: dict(zip(range(1, 19), allocate_over_played_holes(total, all_18_si, list(range(1, 19)))))
|
|
|
|
|
|
for pid, total in relative.items()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
play_order = _round_hole_order(round_row["start_hole"], round_row["holes_planned"])
|
|
|
|
|
|
side_a_id, side_b_id = sorted(by_side.keys())
|
|
|
|
|
|
label_by_participant = {p["id"]: p["label"] for p in participant_rows}
|
|
|
|
|
|
|
|
|
|
|
|
hole_points: list[tuple[int, int]] = []
|
|
|
|
|
|
holes = []
|
|
|
|
|
|
for h in play_order:
|
|
|
|
|
|
scores_this_hole = gross_by_hole.get(h, {})
|
|
|
|
|
|
pids_a = by_side[side_a_id]
|
|
|
|
|
|
pids_b = by_side[side_b_id]
|
|
|
|
|
|
if any(pid not in scores_this_hole for pid in pids_a + pids_b):
|
|
|
|
|
|
break
|
|
|
|
|
|
points_a_players = tuple(
|
|
|
|
|
|
stableford_points_for_hole(par_by_hole[h], scores_this_hole[pid], strokes_per_hole[pid][h])
|
|
|
|
|
|
for pid in pids_a
|
|
|
|
|
|
)
|
|
|
|
|
|
points_b_players = tuple(
|
|
|
|
|
|
stableford_points_for_hole(par_by_hole[h], scores_this_hole[pid], strokes_per_hole[pid][h])
|
|
|
|
|
|
for pid in pids_b
|
|
|
|
|
|
)
|
|
|
|
|
|
pa, pb = high_low_high_points_for_hole(points_a_players, points_b_players)
|
|
|
|
|
|
hole_points.append((pa, pb))
|
|
|
|
|
|
entries = [
|
|
|
|
|
|
FormatHoleEntry(
|
|
|
|
|
|
id=pid,
|
|
|
|
|
|
label=label_by_participant.get(pid, "Ukjent"),
|
|
|
|
|
|
side="a" if pid in pids_a else "b",
|
|
|
|
|
|
gross=scores_this_hole[pid],
|
|
|
|
|
|
strokes_received=strokes_per_hole[pid][h],
|
|
|
|
|
|
points=pts,
|
|
|
|
|
|
)
|
|
|
|
|
|
for pid, pts in zip(
|
|
|
|
|
|
pids_a + pids_b, points_a_players + points_b_players
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
holes.append(
|
|
|
|
|
|
FormatHoleOut(
|
|
|
|
|
|
hole_number=h, par=par_by_hole[h], stroke_index=all_18_si_by_hole[h], entries=entries,
|
|
|
|
|
|
result=("a" if pa > pb else "b" if pb > pa else "halved"),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
total_a, total_b = high_low_high_running_score(hole_points)
|
|
|
|
|
|
return RoundFormatResultOut(
|
|
|
|
|
|
play_format="high_low_high",
|
|
|
|
|
|
ready=True,
|
|
|
|
|
|
match_lead=total_a - total_b,
|
|
|
|
|
|
match_holes_played=len(hole_points),
|
|
|
|
|
|
hlh_points_a=total_a,
|
|
|
|
|
|
hlh_points_b=total_b,
|
|
|
|
|
|
holes=holes,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
async def _build_format_result(conn, round_id: str) -> RoundFormatResultOut:
|
|
|
|
|
|
"""Delt mellom den autentiserte GET og den offentlige/venn-gatede
|
|
|
|
|
|
/public/rounds-varianten (ADR-036 fase 2) -- selve dataen er identisk,
|
|
|
|
|
|
kun autorisasjonssjekken FØR dette kalles skiller de to."""
|
|
|
|
|
|
round_row = await conn.fetchrow(
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
"SELECT play_format, skins_scoring, skins_tie_handling, start_hole, holes_planned, "
|
|
|
|
|
|
"bbb_sweep_bonus_enabled, shamble_best_n FROM round WHERE id = $1",
|
2026-07-28 19:25:37 +02:00
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
play_format = round_row["play_format"]
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
if play_format == "high_low_high":
|
|
|
|
|
|
return await _build_high_low_high_result(conn, round_id, round_row)
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
if play_format in _TWO_SIDED_FORMATS:
|
|
|
|
|
|
relative = await _relative_strokes_for_round(conn, round_id, play_format)
|
|
|
|
|
|
if not relative:
|
|
|
|
|
|
return RoundFormatResultOut(play_format=play_format, ready=False)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
engine_format = _ROUND_FORMAT_TO_ENGINE_FORMAT[play_format]
|
|
|
|
|
|
play_order = _round_hole_order(round_row["start_hole"], round_row["holes_planned"])
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
if engine_format in SIDE_IS_UNIT:
|
|
|
|
|
|
# Delt ball -- én rad per SIDE per hull. "Enheten" ER siden,
|
|
|
|
|
|
# så label = side.label (fallback "Side A"/"Side B" avgjøres
|
|
|
|
|
|
# under, samme sortert-på-id-konvensjon som frontend bruker
|
|
|
|
|
|
# for match_status_text sin bokstavelige "(A)"/"(B)").
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rh.round_side_id::text AS unit, rh.hole_number, rh.par, rh.stroke_index, rh.played, rh.score
|
|
|
|
|
|
FROM round_hole rh
|
|
|
|
|
|
JOIN round_side rs ON rs.id = rh.round_side_id
|
|
|
|
|
|
WHERE rs.round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
)
|
2026-07-28 19:25:37 +02:00
|
|
|
|
side_label_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id, label FROM round_side WHERE round_id = $1", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
unit_label = {r["id"]: r["label"] for r in side_label_rows}
|
|
|
|
|
|
unit_to_side: dict[str, str] | None = None
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Individuell ball -- nøkkelen ("unit") er DELTAKEREN selv
|
|
|
|
|
|
# (samme som _relative_strokes_for_round sin nøkkel for
|
|
|
|
|
|
# disse formatene). Henter navn+side-tilhørighet i samme
|
|
|
|
|
|
# spørring (én rundtur, ikke to).
|
|
|
|
|
|
rows = await conn.fetch(
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
"""
|
2026-07-28 19:25:37 +02:00
|
|
|
|
SELECT rp.id::text AS unit, rh.hole_number, rh.par, rh.stroke_index, rh.played, rh.score
|
|
|
|
|
|
FROM round_hole rh
|
|
|
|
|
|
JOIN round_participant rp ON rp.id = rh.round_participant_id
|
|
|
|
|
|
WHERE rp.round_id = $1 AND rp.round_side_id IS NOT NULL
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
""",
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
2026-07-28 19:25:37 +02:00
|
|
|
|
participant_label_rows = await conn.fetch(
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
"""
|
2026-07-28 19:25:37 +02:00
|
|
|
|
SELECT rp.id::text AS id, rp.round_side_id::text AS side_id,
|
|
|
|
|
|
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS label
|
|
|
|
|
|
FROM round_participant rp LEFT JOIN app_user au ON au.id = rp.user_id
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
2026-07-28 19:25:37 +02:00
|
|
|
|
unit_label = {r["id"]: r["label"] for r in participant_label_rows}
|
|
|
|
|
|
unit_to_side = {r["id"]: r["side_id"] for r in participant_label_rows}
|
|
|
|
|
|
|
|
|
|
|
|
by_unit_hole: dict[tuple[str, int], list] = {}
|
|
|
|
|
|
stroke_index_by_hole: dict[int, int] = {}
|
|
|
|
|
|
par_by_hole: dict[int, int] = {}
|
|
|
|
|
|
for r in rows:
|
|
|
|
|
|
if r["played"] and r["score"] is not None:
|
|
|
|
|
|
by_unit_hole.setdefault((r["unit"], r["hole_number"]), []).append(r["score"])
|
|
|
|
|
|
stroke_index_by_hole[r["hole_number"]] = r["stroke_index"]
|
|
|
|
|
|
par_by_hole[r["hole_number"]] = r["par"]
|
|
|
|
|
|
all_18_si = [stroke_index_by_hole[h] for h in range(1, 19)] if len(stroke_index_by_hole) == 18 else None
|
|
|
|
|
|
|
|
|
|
|
|
if engine_format in SIDE_IS_UNIT:
|
|
|
|
|
|
side_ids = sorted(relative.keys())
|
|
|
|
|
|
else:
|
|
|
|
|
|
side_ids = sorted({s for s in unit_to_side.values() if s is not None})
|
|
|
|
|
|
|
|
|
|
|
|
if len(side_ids) != 2 or all_18_si is None:
|
|
|
|
|
|
return RoundFormatResultOut(play_format=play_format, ready=False)
|
|
|
|
|
|
|
|
|
|
|
|
strokes_per_hole = {
|
|
|
|
|
|
unit: dict(zip(range(1, 19), allocate_over_played_holes(v, all_18_si, list(range(1, 19)))))
|
|
|
|
|
|
for unit, v in relative.items()
|
|
|
|
|
|
}
|
|
|
|
|
|
side_a_id, side_b_id = side_ids
|
|
|
|
|
|
side_label_of: dict[str, Literal["a", "b"]] = {side_a_id: "a", side_b_id: "b"}
|
|
|
|
|
|
expected_players = _SIDE_PLAYER_COUNT[play_format]
|
|
|
|
|
|
|
|
|
|
|
|
def units_for_side(side_id: str) -> list[str]:
|
|
|
|
|
|
if engine_format in SIDE_IS_UNIT:
|
|
|
|
|
|
return [side_id]
|
|
|
|
|
|
return [uid for uid, sid in unit_to_side.items() if sid == side_id]
|
|
|
|
|
|
|
|
|
|
|
|
# Delt ball -- siden ER enheten (nøyaktig én per side), uansett
|
|
|
|
|
|
# hvor mange SPILLERE formatet krever. "expected_players" (2 for
|
|
|
|
|
|
# foursome/greensome, 2 eller 4 for scramble) er PLAYER-antallet,
|
|
|
|
|
|
# ikke UNIT-antallet -- bruk kun for individuell ball, der hver
|
|
|
|
|
|
# spiller er sin egen enhet.
|
|
|
|
|
|
required_units = 1 if engine_format in SIDE_IS_UNIT else expected_players
|
|
|
|
|
|
|
|
|
|
|
|
def side_net(hole_number: int, side_id: str) -> int | None:
|
|
|
|
|
|
nets = []
|
|
|
|
|
|
for unit_id in units_for_side(side_id):
|
|
|
|
|
|
scores = by_unit_hole.get((unit_id, hole_number), [])
|
|
|
|
|
|
if len(scores) != 1:
|
|
|
|
|
|
return None
|
|
|
|
|
|
per_hole = strokes_per_hole.get(unit_id)
|
|
|
|
|
|
if per_hole is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
nets.append(scores[0] - per_hole[hole_number])
|
|
|
|
|
|
if len(nets) != required_units:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return min(nets)
|
|
|
|
|
|
|
|
|
|
|
|
hole_results: list[HoleResult] = []
|
|
|
|
|
|
# Hull-for-hull-forløp (2026-07-28) -- samme sannhetskilde/
|
|
|
|
|
|
# trunkering som hole_results: kun hull som er FERDIG avgjort
|
|
|
|
|
|
# (begge sider komplette) tas med, i samme rekkefølge. Et
|
|
|
|
|
|
# ufullstendig hull (noen har registrert, andre ikke) vises
|
|
|
|
|
|
# ikke her ennå -- samme "vent til begge er ferdig"-prinsipp
|
|
|
|
|
|
# som resten av match-motoren.
|
|
|
|
|
|
holes: list[FormatHoleOut] = []
|
|
|
|
|
|
for hole_number in play_order:
|
|
|
|
|
|
net_a = side_net(hole_number, side_a_id)
|
|
|
|
|
|
net_b = side_net(hole_number, side_b_id)
|
|
|
|
|
|
if net_a is None or net_b is None:
|
|
|
|
|
|
break
|
|
|
|
|
|
if net_a < net_b:
|
|
|
|
|
|
hole_results.append(HoleResult.SIDE_A)
|
|
|
|
|
|
elif net_b < net_a:
|
|
|
|
|
|
hole_results.append(HoleResult.SIDE_B)
|
|
|
|
|
|
else:
|
|
|
|
|
|
hole_results.append(HoleResult.HALVED)
|
|
|
|
|
|
|
|
|
|
|
|
entries: list[FormatHoleEntry] = []
|
|
|
|
|
|
for side_id, side_net_value in ((side_a_id, net_a), (side_b_id, net_b)):
|
|
|
|
|
|
for unit_id in units_for_side(side_id):
|
|
|
|
|
|
gross = by_unit_hole[(unit_id, hole_number)][0]
|
|
|
|
|
|
strokes = strokes_per_hole[unit_id][hole_number]
|
|
|
|
|
|
net = gross - strokes
|
|
|
|
|
|
entries.append(
|
|
|
|
|
|
FormatHoleEntry(
|
|
|
|
|
|
id=unit_id,
|
|
|
|
|
|
label=unit_label.get(unit_id, "Ukjent"),
|
|
|
|
|
|
side=side_label_of[side_id],
|
|
|
|
|
|
gross=gross,
|
|
|
|
|
|
net=net,
|
|
|
|
|
|
strokes_received=strokes,
|
|
|
|
|
|
# Fourball: kun laveste av de to partnernes
|
|
|
|
|
|
# netto teller for siden -- markerer HVILKEN,
|
|
|
|
|
|
# slik at "hvorfor vant siden" er synlig uten
|
|
|
|
|
|
# egen forklarende tekst (R&A-regelen, samme
|
|
|
|
|
|
# side_net()-funksjon over).
|
|
|
|
|
|
counted=(net == side_net_value),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
holes.append(
|
|
|
|
|
|
FormatHoleOut(
|
|
|
|
|
|
hole_number=hole_number,
|
|
|
|
|
|
par=par_by_hole[hole_number],
|
|
|
|
|
|
stroke_index=stroke_index_by_hole[hole_number],
|
|
|
|
|
|
entries=entries,
|
|
|
|
|
|
result=_RESULT_LABEL[hole_results[-1]],
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
state = compute_match_state(hole_results, total_holes=round_row["holes_planned"])
|
|
|
|
|
|
return RoundFormatResultOut(
|
|
|
|
|
|
play_format=play_format,
|
|
|
|
|
|
ready=True,
|
|
|
|
|
|
match_lead=state.lead,
|
|
|
|
|
|
match_holes_played=state.holes_played,
|
|
|
|
|
|
match_holes_remaining=state.holes_remaining,
|
|
|
|
|
|
match_is_closed=state.is_closed,
|
|
|
|
|
|
match_is_dormie=state.is_dormie,
|
|
|
|
|
|
match_status_text=state.describe(),
|
|
|
|
|
|
hole_results=[_RESULT_LABEL[r] for r in hole_results],
|
|
|
|
|
|
holes=holes,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if play_format == "skins":
|
|
|
|
|
|
participant_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS id, rp.course_handicap_snapshot,
|
|
|
|
|
|
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS label
|
|
|
|
|
|
FROM round_participant rp LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
label_by_participant = {p["id"]: p["label"] for p in participant_rows}
|
|
|
|
|
|
hole_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS participant_id, rh.hole_number, rh.par, rh.stroke_index, rh.played, rh.score
|
|
|
|
|
|
FROM round_hole rh JOIN round_participant rp ON rp.id = rh.round_participant_id
|
|
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
ORDER BY rh.hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
strokes_by_participant: dict[str, dict[int, int]] = {}
|
|
|
|
|
|
if round_row["skins_scoring"] == "net":
|
|
|
|
|
|
by_participant_holes: dict[str, list] = {}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
for r in hole_rows:
|
2026-07-28 19:25:37 +02:00
|
|
|
|
by_participant_holes.setdefault(r["participant_id"], []).append(r)
|
|
|
|
|
|
ch_by_participant = {p["id"]: p["course_handicap_snapshot"] for p in participant_rows}
|
|
|
|
|
|
for pid, holes_for_participant in by_participant_holes.items():
|
|
|
|
|
|
ch = ch_by_participant.get(pid)
|
|
|
|
|
|
if ch is None:
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
continue
|
2026-07-28 19:25:37 +02:00
|
|
|
|
allocation = allocate_strokes_by_index(ch, [h["stroke_index"] for h in holes_for_participant])
|
|
|
|
|
|
strokes_by_participant[pid] = {h["hole_number"]: a for h, a in zip(holes_for_participant, allocation)}
|
|
|
|
|
|
|
|
|
|
|
|
is_net_mode = round_row["skins_scoring"] == "net"
|
|
|
|
|
|
gross_by_hole_number: dict[int, dict[str, int]] = {}
|
|
|
|
|
|
par_by_hole: dict[int, int] = {}
|
|
|
|
|
|
si_by_hole: dict[int, int] = {}
|
|
|
|
|
|
scores_by_hole_number: dict[int, list[tuple[str, int]]] = {}
|
|
|
|
|
|
for r in hole_rows:
|
|
|
|
|
|
par_by_hole[r["hole_number"]] = r["par"]
|
|
|
|
|
|
si_by_hole[r["hole_number"]] = r["stroke_index"]
|
|
|
|
|
|
if not r["played"] or r["score"] is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
gross_by_hole_number.setdefault(r["hole_number"], {})[r["participant_id"]] = r["score"]
|
|
|
|
|
|
if is_net_mode:
|
|
|
|
|
|
per_hole = strokes_by_participant.get(r["participant_id"])
|
|
|
|
|
|
if per_hole is None:
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
continue
|
2026-07-28 19:25:37 +02:00
|
|
|
|
value = r["score"] - per_hole[r["hole_number"]]
|
|
|
|
|
|
else:
|
|
|
|
|
|
value = r["score"]
|
|
|
|
|
|
scores_by_hole_number.setdefault(r["hole_number"], []).append((r["participant_id"], value))
|
|
|
|
|
|
|
|
|
|
|
|
play_order = _round_hole_order(round_row["start_hole"], round_row["holes_planned"])
|
|
|
|
|
|
scores_by_hole = [scores_by_hole_number.get(h, []) for h in play_order]
|
|
|
|
|
|
skins_won, hole_log = compute_skins_detail(scores_by_hole, tie_handling=round_row["skins_tie_handling"])
|
|
|
|
|
|
|
|
|
|
|
|
# Hull-for-hull-forløp -- kun hull der minst én deltaker faktisk
|
|
|
|
|
|
# har registrert en score tas med (et helt tomt hull har
|
|
|
|
|
|
# ingenting å vise frem).
|
|
|
|
|
|
holes = []
|
|
|
|
|
|
for hole_number, log_entry in zip(play_order, hole_log):
|
|
|
|
|
|
if not log_entry.values:
|
|
|
|
|
|
continue
|
|
|
|
|
|
gross_this_hole = gross_by_hole_number.get(hole_number, {})
|
|
|
|
|
|
entries = [
|
|
|
|
|
|
FormatHoleEntry(
|
|
|
|
|
|
id=pid,
|
|
|
|
|
|
label=label_by_participant.get(pid, "Ukjent"),
|
|
|
|
|
|
gross=gross_this_hole.get(pid),
|
|
|
|
|
|
net=value if is_net_mode else None,
|
|
|
|
|
|
strokes_received=strokes_by_participant.get(pid, {}).get(hole_number) if is_net_mode else None,
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
)
|
2026-07-28 19:25:37 +02:00
|
|
|
|
for pid, value in log_entry.values.items()
|
|
|
|
|
|
]
|
|
|
|
|
|
holes.append(
|
|
|
|
|
|
FormatHoleOut(
|
|
|
|
|
|
hole_number=hole_number,
|
|
|
|
|
|
par=par_by_hole[hole_number],
|
|
|
|
|
|
stroke_index=si_by_hole[hole_number],
|
|
|
|
|
|
entries=entries,
|
|
|
|
|
|
winner_ids=list(log_entry.awarded.keys()),
|
|
|
|
|
|
carried=log_entry.carried,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return RoundFormatResultOut(play_format=play_format, ready=True, skins_won=skins_won, holes=holes)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
if play_format == "copenhagen":
|
|
|
|
|
|
# Individuell ball, INGEN sider (flatt felt av 3, ADR-011s
|
|
|
|
|
|
# to-lags-modell passer ikke) -- alltid NETTO (egen, individuell
|
|
|
|
|
|
# course handicap-fordeling, ikke match-play-relativ som match/
|
|
|
|
|
|
# fourball -- Københavner har ingen "side" å være relativ TIL).
|
|
|
|
|
|
participant_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS id, rp.course_handicap_snapshot,
|
|
|
|
|
|
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS label
|
|
|
|
|
|
FROM round_participant rp LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
label_by_participant = {p["id"]: p["label"] for p in participant_rows}
|
|
|
|
|
|
hole_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS participant_id, rh.hole_number, rh.par, rh.stroke_index, rh.played, rh.score
|
|
|
|
|
|
FROM round_hole rh JOIN round_participant rp ON rp.id = rh.round_participant_id
|
|
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
ORDER BY rh.hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
by_participant_holes: dict[str, list] = {}
|
|
|
|
|
|
for r in hole_rows:
|
|
|
|
|
|
by_participant_holes.setdefault(r["participant_id"], []).append(r)
|
|
|
|
|
|
strokes_by_participant: dict[str, dict[int, int]] = {}
|
|
|
|
|
|
for p in participant_rows:
|
|
|
|
|
|
ch = p["course_handicap_snapshot"]
|
|
|
|
|
|
holes_for_participant = by_participant_holes.get(p["id"], [])
|
|
|
|
|
|
if ch is None or not holes_for_participant:
|
|
|
|
|
|
continue
|
|
|
|
|
|
allocation = allocate_strokes_by_index(ch, [h["stroke_index"] for h in holes_for_participant])
|
|
|
|
|
|
strokes_by_participant[p["id"]] = {
|
|
|
|
|
|
h["hole_number"]: a for h, a in zip(holes_for_participant, allocation)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
gross_by_hole: dict[int, dict[str, int]] = {}
|
|
|
|
|
|
par_by_hole: dict[int, int] = {}
|
|
|
|
|
|
si_by_hole: dict[int, int] = {}
|
|
|
|
|
|
net_by_hole: dict[int, dict[str, int]] = {}
|
|
|
|
|
|
for r in hole_rows:
|
|
|
|
|
|
par_by_hole[r["hole_number"]] = r["par"]
|
|
|
|
|
|
si_by_hole[r["hole_number"]] = r["stroke_index"]
|
|
|
|
|
|
if not r["played"] or r["score"] is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
gross_by_hole.setdefault(r["hole_number"], {})[r["participant_id"]] = r["score"]
|
|
|
|
|
|
per_hole = strokes_by_participant.get(r["participant_id"])
|
|
|
|
|
|
if per_hole is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
net_by_hole.setdefault(r["hole_number"], {})[r["participant_id"]] = r["score"] - per_hole[r["hole_number"]]
|
|
|
|
|
|
|
|
|
|
|
|
play_order = _round_hole_order(round_row["start_hole"], round_row["holes_planned"])
|
|
|
|
|
|
scores_by_hole = [
|
|
|
|
|
|
[(pid, net) for pid, net in net_by_hole.get(h, {}).items()] for h in play_order
|
|
|
|
|
|
]
|
|
|
|
|
|
copenhagen_points, hole_log = compute_copenhagen_detail(scores_by_hole, higher_is_better=False)
|
|
|
|
|
|
|
|
|
|
|
|
holes = []
|
|
|
|
|
|
for hole_number, log_entry in zip(play_order, hole_log):
|
|
|
|
|
|
if not log_entry.values:
|
|
|
|
|
|
continue
|
|
|
|
|
|
gross_this_hole = gross_by_hole.get(hole_number, {})
|
|
|
|
|
|
entries = [
|
|
|
|
|
|
FormatHoleEntry(
|
|
|
|
|
|
id=pid,
|
|
|
|
|
|
label=label_by_participant.get(pid, "Ukjent"),
|
|
|
|
|
|
gross=gross_this_hole.get(pid),
|
|
|
|
|
|
net=net_value,
|
|
|
|
|
|
strokes_received=strokes_by_participant.get(pid, {}).get(hole_number),
|
|
|
|
|
|
points=log_entry.points.get(pid),
|
|
|
|
|
|
)
|
|
|
|
|
|
for pid, net_value in log_entry.values.items()
|
|
|
|
|
|
]
|
|
|
|
|
|
holes.append(
|
|
|
|
|
|
FormatHoleOut(
|
|
|
|
|
|
hole_number=hole_number,
|
|
|
|
|
|
par=par_by_hole[hole_number],
|
|
|
|
|
|
stroke_index=si_by_hole[hole_number],
|
|
|
|
|
|
entries=entries,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return RoundFormatResultOut(
|
|
|
|
|
|
play_format=play_format, ready=True, copenhagen_points=copenhagen_points, holes=holes
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if play_format == "bbb":
|
|
|
|
|
|
participant_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS id, COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS label
|
|
|
|
|
|
FROM round_participant rp LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
label_by_participant = {p["id"]: p["label"] for p in participant_rows}
|
|
|
|
|
|
# Individuell-ball round_hole-rader finnes (samme som stroke/skins/
|
|
|
|
|
|
# copenhagen), men brukes ikke til poeng her -- kun til par/stroke-
|
|
|
|
|
|
# index-referanse for hull-visningen (identisk for alle deltakere).
|
|
|
|
|
|
par_si_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT DISTINCT hole_number, par, stroke_index FROM round_hole
|
|
|
|
|
|
WHERE round_participant_id IN (SELECT id FROM round_participant WHERE round_id = $1)
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
par_by_hole = {r["hole_number"]: r["par"] for r in par_si_rows}
|
|
|
|
|
|
si_by_hole = {r["hole_number"]: r["stroke_index"] for r in par_si_rows}
|
|
|
|
|
|
bbb_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT hole_number, bingo_participant_id::text AS bingo_participant_id,
|
|
|
|
|
|
bango_participant_id::text AS bango_participant_id,
|
|
|
|
|
|
bongo_participant_id::text AS bongo_participant_id
|
|
|
|
|
|
FROM round_bbb_hole WHERE round_id = $1 ORDER BY hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
sweep_bonus_enabled = round_row["bbb_sweep_bonus_enabled"]
|
|
|
|
|
|
hole_log = [
|
|
|
|
|
|
(r["bingo_participant_id"], r["bango_participant_id"], r["bongo_participant_id"]) for r in bbb_rows
|
|
|
|
|
|
]
|
|
|
|
|
|
bbb_points = compute_bbb(hole_log, sweep_bonus_enabled)
|
|
|
|
|
|
|
|
|
|
|
|
holes = []
|
|
|
|
|
|
for r, log_entry in zip(bbb_rows, hole_log):
|
|
|
|
|
|
bingo_id, bango_id, bongo_id = log_entry
|
|
|
|
|
|
if bingo_id is None and bango_id is None and bongo_id is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
hole_points = bbb_points_for_hole(bingo_id, bango_id, bongo_id, sweep_bonus_enabled)
|
|
|
|
|
|
entries = [
|
|
|
|
|
|
FormatHoleEntry(id=pid, label=label_by_participant.get(pid, "Ukjent"), points=p)
|
|
|
|
|
|
for pid, p in hole_points.items()
|
|
|
|
|
|
]
|
|
|
|
|
|
holes.append(
|
|
|
|
|
|
FormatHoleOut(
|
|
|
|
|
|
hole_number=r["hole_number"],
|
|
|
|
|
|
par=par_by_hole.get(r["hole_number"], 0),
|
|
|
|
|
|
stroke_index=si_by_hole.get(r["hole_number"], 0),
|
|
|
|
|
|
entries=entries,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return RoundFormatResultOut(play_format=play_format, ready=True, bbb_points=bbb_points, holes=holes)
|
|
|
|
|
|
|
|
|
|
|
|
if play_format == "flag":
|
|
|
|
|
|
# Individuell ball, INGEN sider. Budsjett = sum(par for hele
|
|
|
|
|
|
# runden) + course_handicap_snapshot (samme "100% individuell
|
|
|
|
|
|
# allowance"-prinsipp som Slagspill individuelt i EGA-tabellen,
|
|
|
|
|
|
# ADR-005) -- IKKE playing_handicap (den kolonnen er kun for
|
|
|
|
|
|
# to-siders match-play-relative formater, se ADR-039 Beslutning B).
|
|
|
|
|
|
participant_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS id, rp.course_handicap_snapshot,
|
|
|
|
|
|
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS label
|
|
|
|
|
|
FROM round_participant rp LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
play_order = _round_hole_order(round_row["start_hole"], round_row["holes_planned"])
|
|
|
|
|
|
flag_results: dict[str, FlagParticipantResult] = {}
|
|
|
|
|
|
for p in participant_rows:
|
|
|
|
|
|
if p["course_handicap_snapshot"] is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
hole_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT hole_number, par, played, score FROM round_hole WHERE round_participant_id = $1",
|
|
|
|
|
|
p["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
by_hole = {r["hole_number"]: r for r in hole_rows}
|
|
|
|
|
|
total_par = sum(by_hole[h]["par"] for h in play_order if h in by_hole)
|
|
|
|
|
|
# SAMMENHENGENDE prefiks fra start_hole -- samme prinsipp som
|
|
|
|
|
|
# match-motorens hull-for-hull-forløp (aldri hull lenger ute i
|
|
|
|
|
|
# sekvensen selv om de tilfeldigvis er registrert).
|
|
|
|
|
|
gross_prefix = []
|
|
|
|
|
|
for h in play_order:
|
|
|
|
|
|
row = by_hole.get(h)
|
|
|
|
|
|
if row is None or not row["played"] or row["score"] is None:
|
|
|
|
|
|
break
|
|
|
|
|
|
gross_prefix.append(row["score"])
|
|
|
|
|
|
budget = total_par + p["course_handicap_snapshot"]
|
|
|
|
|
|
fr = flag_result(gross_prefix, budget)
|
|
|
|
|
|
flag_results[p["id"]] = FlagParticipantResult(
|
|
|
|
|
|
holes_completed=fr.holes_completed,
|
|
|
|
|
|
ran_out=fr.ran_out,
|
|
|
|
|
|
strokes_remaining=fr.strokes_remaining,
|
|
|
|
|
|
label=p["label"],
|
|
|
|
|
|
)
|
|
|
|
|
|
return RoundFormatResultOut(play_format=play_format, ready=True, flag_results=flag_results)
|
|
|
|
|
|
|
|
|
|
|
|
if play_format == "shamble":
|
|
|
|
|
|
# Individuell ball, INGEN sider -- laget ER hele rundens deltaker-
|
|
|
|
|
|
# sett (bekreftet av bruker). Rå brutto (ikke netto -- kilden
|
|
|
|
|
|
# nevner ingen handicap-justering for selve hullscoren, kun for
|
|
|
|
|
|
# HVEM som spiller på laget sammen, se ADR-039-lignende allowance
|
|
|
|
|
|
# et fremtidig steg kunne legge til).
|
|
|
|
|
|
participant_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS id, COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS label
|
|
|
|
|
|
FROM round_participant rp LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
label_by_participant = {p["id"]: p["label"] for p in participant_rows}
|
|
|
|
|
|
best_n = round_row["shamble_best_n"]
|
|
|
|
|
|
hole_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS participant_id, rh.hole_number, rh.par, rh.stroke_index, rh.played, rh.score
|
|
|
|
|
|
FROM round_hole rh JOIN round_participant rp ON rp.id = rh.round_participant_id
|
|
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
ORDER BY rh.hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
by_hole: dict[int, dict[str, int]] = {}
|
|
|
|
|
|
par_by_hole: dict[int, int] = {}
|
|
|
|
|
|
si_by_hole: dict[int, int] = {}
|
|
|
|
|
|
for r in hole_rows:
|
|
|
|
|
|
par_by_hole[r["hole_number"]] = r["par"]
|
|
|
|
|
|
si_by_hole[r["hole_number"]] = r["stroke_index"]
|
|
|
|
|
|
if r["played"] and r["score"] is not None:
|
|
|
|
|
|
by_hole.setdefault(r["hole_number"], {})[r["participant_id"]] = r["score"]
|
|
|
|
|
|
|
|
|
|
|
|
play_order = _round_hole_order(round_row["start_hole"], round_row["holes_planned"])
|
|
|
|
|
|
expected_players = len(participant_rows)
|
|
|
|
|
|
team_total = 0
|
|
|
|
|
|
any_hole_ready = False
|
|
|
|
|
|
holes = []
|
|
|
|
|
|
for h in play_order:
|
|
|
|
|
|
scores_by_pid = by_hole.get(h, {})
|
|
|
|
|
|
if len(scores_by_pid) != expected_players or best_n is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
hole_score = shamble_hole_score(list(scores_by_pid.values()), best_n)
|
|
|
|
|
|
team_total += hole_score
|
|
|
|
|
|
any_hole_ready = True
|
|
|
|
|
|
counted_pids = {pid for pid, _ in sorted(scores_by_pid.items(), key=lambda kv: kv[1])[:best_n]}
|
|
|
|
|
|
entries = [
|
|
|
|
|
|
FormatHoleEntry(
|
|
|
|
|
|
id=pid, label=label_by_participant.get(pid, "Ukjent"), gross=score, counted=pid in counted_pids
|
|
|
|
|
|
)
|
|
|
|
|
|
for pid, score in scores_by_pid.items()
|
|
|
|
|
|
]
|
|
|
|
|
|
holes.append(FormatHoleOut(hole_number=h, par=par_by_hole[h], stroke_index=si_by_hole[h], entries=entries))
|
|
|
|
|
|
return RoundFormatResultOut(
|
|
|
|
|
|
play_format=play_format,
|
|
|
|
|
|
ready=True,
|
|
|
|
|
|
shamble_team_score=team_total if any_hole_ready else None,
|
|
|
|
|
|
holes=holes,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if play_format == "money_ball":
|
|
|
|
|
|
# Ett lag = HELE rundens deltakersett (som Shamble), FAST 4.
|
|
|
|
|
|
# Rotasjonen er basert på POSISJON i spillerekkefølgen (1. spilte
|
|
|
|
|
|
# hull = rotasjonsindeks 0), IKKE rått hullnummer -- riktig også
|
|
|
|
|
|
# for en runde som starter et annet sted enn hull 1.
|
|
|
|
|
|
participant_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS id, rp.lineup_order,
|
|
|
|
|
|
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS label
|
|
|
|
|
|
FROM round_participant rp LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
label_by_participant = {p["id"]: p["label"] for p in participant_rows}
|
|
|
|
|
|
ordered_pids = [p["id"] for p in sorted(participant_rows, key=lambda p: p["lineup_order"] or 0)]
|
|
|
|
|
|
if len(participant_rows) != 4 or any(p["lineup_order"] is None for p in participant_rows):
|
|
|
|
|
|
return RoundFormatResultOut(play_format=play_format, ready=False)
|
|
|
|
|
|
|
|
|
|
|
|
hole_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS participant_id, rh.hole_number, rh.par, rh.stroke_index, rh.played, rh.score
|
|
|
|
|
|
FROM round_hole rh JOIN round_participant rp ON rp.id = rh.round_participant_id
|
|
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
ORDER BY rh.hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
by_hole: dict[int, dict[str, int]] = {}
|
|
|
|
|
|
par_by_hole: dict[int, int] = {}
|
|
|
|
|
|
si_by_hole: dict[int, int] = {}
|
|
|
|
|
|
for r in hole_rows:
|
|
|
|
|
|
par_by_hole[r["hole_number"]] = r["par"]
|
|
|
|
|
|
si_by_hole[r["hole_number"]] = r["stroke_index"]
|
|
|
|
|
|
if r["played"] and r["score"] is not None:
|
|
|
|
|
|
by_hole.setdefault(r["hole_number"], {})[r["participant_id"]] = r["score"]
|
|
|
|
|
|
|
|
|
|
|
|
play_order = _round_hole_order(round_row["start_hole"], round_row["holes_planned"])
|
|
|
|
|
|
team_total = 0
|
|
|
|
|
|
any_hole_ready = False
|
|
|
|
|
|
holes = []
|
|
|
|
|
|
for position, h in enumerate(play_order, start=1):
|
|
|
|
|
|
scores_by_pid = by_hole.get(h, {})
|
|
|
|
|
|
if len(scores_by_pid) != 4:
|
|
|
|
|
|
continue
|
|
|
|
|
|
scores_in_order = [scores_by_pid[pid] for pid in ordered_pids]
|
|
|
|
|
|
hole_score = money_ball_hole_score(scores_in_order, position)
|
|
|
|
|
|
team_total += hole_score
|
|
|
|
|
|
any_hole_ready = True
|
|
|
|
|
|
mb_pid = ordered_pids[(position - 1) % 4]
|
|
|
|
|
|
others_scores = [s for pid, s in scores_by_pid.items() if pid != mb_pid]
|
|
|
|
|
|
best_other = min(others_scores) if others_scores else None
|
|
|
|
|
|
entries = [
|
|
|
|
|
|
FormatHoleEntry(
|
|
|
|
|
|
id=pid,
|
|
|
|
|
|
label=label_by_participant.get(pid, "Ukjent"),
|
|
|
|
|
|
gross=score,
|
|
|
|
|
|
counted=(pid == mb_pid) or (score == best_other),
|
|
|
|
|
|
)
|
|
|
|
|
|
for pid, score in scores_by_pid.items()
|
|
|
|
|
|
]
|
|
|
|
|
|
holes.append(FormatHoleOut(hole_number=h, par=par_by_hole[h], stroke_index=si_by_hole[h], entries=entries))
|
|
|
|
|
|
return RoundFormatResultOut(
|
|
|
|
|
|
play_format=play_format,
|
|
|
|
|
|
ready=True,
|
|
|
|
|
|
money_ball_team_score=team_total if any_hole_ready else None,
|
|
|
|
|
|
holes=holes,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
return RoundFormatResultOut(play_format=play_format, ready=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Nassau Match Play (2026-07-30) -- INGEN ny motor, INGEN lagring. Nassau er
|
|
|
|
|
|
# bare TRE parallelle vinduer (hull 1-9, 10-18, 1-18) av EKSISTERENDE
|
|
|
|
|
|
# hull-for-hull-data, hver kjørt gjennom den allerede eksisterende
|
|
|
|
|
|
# compute_match_state uendret. Krever nøyaktig 18 hull (holes_planned==18)
|
|
|
|
|
|
# -- windows 1-9/10-18 gir ikke mening for en 9-hulls runde.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Egen, selvstendig hull-resultat-bygger (ikke en refaktor av
|
|
|
|
|
|
# _build_format_result over) -- gjenbruker _relative_strokes_for_round
|
|
|
|
|
|
# (den kritiske, allerede testede handicap-delen), men bygger selve
|
|
|
|
|
|
# hole_results-listen på nytt her, isolert fra den komplekse, produksjons-
|
|
|
|
|
|
# kritiske _build_format_result-funksjonen for å unngå risiko ved å røre den.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
async def _two_sided_hole_results(conn, round_id: str, round_row) -> list[HoleResult] | None:
|
|
|
|
|
|
play_format = round_row["play_format"]
|
|
|
|
|
|
relative = await _relative_strokes_for_round(conn, round_id, play_format)
|
|
|
|
|
|
if not relative:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
engine_format = _ROUND_FORMAT_TO_ENGINE_FORMAT[play_format]
|
|
|
|
|
|
play_order = _round_hole_order(round_row["start_hole"], round_row["holes_planned"])
|
|
|
|
|
|
|
|
|
|
|
|
if engine_format in SIDE_IS_UNIT:
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rh.round_side_id::text AS unit, rh.hole_number, rh.stroke_index, rh.played, rh.score
|
|
|
|
|
|
FROM round_hole rh JOIN round_side rs ON rs.id = rh.round_side_id
|
|
|
|
|
|
WHERE rs.round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
unit_to_side = None
|
|
|
|
|
|
else:
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS unit, rh.hole_number, rh.stroke_index, rh.played, rh.score,
|
|
|
|
|
|
rp.round_side_id::text AS side_id
|
|
|
|
|
|
FROM round_hole rh JOIN round_participant rp ON rp.id = rh.round_participant_id
|
|
|
|
|
|
WHERE rp.round_id = $1 AND rp.round_side_id IS NOT NULL
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
unit_to_side = {r["unit"]: r["side_id"] for r in rows}
|
|
|
|
|
|
|
|
|
|
|
|
by_unit_hole: dict[tuple[str, int], list] = {}
|
|
|
|
|
|
si_by_hole: dict[int, int] = {}
|
|
|
|
|
|
for r in rows:
|
|
|
|
|
|
si_by_hole[r["hole_number"]] = r["stroke_index"]
|
|
|
|
|
|
if r["played"] and r["score"] is not None:
|
|
|
|
|
|
by_unit_hole.setdefault((r["unit"], r["hole_number"]), []).append(r["score"])
|
|
|
|
|
|
|
|
|
|
|
|
if len(si_by_hole) != 18:
|
|
|
|
|
|
return None
|
|
|
|
|
|
all_18_si = [si_by_hole[h] for h in range(1, 19)]
|
|
|
|
|
|
|
|
|
|
|
|
strokes_per_hole = {
|
|
|
|
|
|
unit: dict(zip(range(1, 19), allocate_over_played_holes(v, all_18_si, list(range(1, 19)))))
|
|
|
|
|
|
for unit, v in relative.items()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if engine_format in SIDE_IS_UNIT:
|
|
|
|
|
|
side_ids = sorted(relative.keys())
|
|
|
|
|
|
else:
|
|
|
|
|
|
side_ids = sorted({s for s in unit_to_side.values() if s is not None})
|
|
|
|
|
|
if len(side_ids) != 2:
|
|
|
|
|
|
return None
|
|
|
|
|
|
side_a_id, side_b_id = side_ids
|
|
|
|
|
|
|
|
|
|
|
|
def units_for_side(side_id: str) -> list[str]:
|
|
|
|
|
|
if engine_format in SIDE_IS_UNIT:
|
|
|
|
|
|
return [side_id]
|
|
|
|
|
|
return [uid for uid, sid in unit_to_side.items() if sid == side_id]
|
|
|
|
|
|
|
|
|
|
|
|
def side_net(hole_number: int, side_id: str) -> int | None:
|
|
|
|
|
|
# units_for_side returner ALLTID minst én enhet for en gyldig side --
|
|
|
|
|
|
# løkken returnerer None tidlig hvis NOEN av dem mangler score, så
|
|
|
|
|
|
# `nets` er enten fullt populert (alle enheter klare) eller vi har
|
|
|
|
|
|
# allerede returnert None. Samme "vent til alle er ferdig"-prinsipp
|
|
|
|
|
|
# som _build_format_result sin egen side_net.
|
|
|
|
|
|
nets = []
|
|
|
|
|
|
for unit_id in units_for_side(side_id):
|
|
|
|
|
|
scores = by_unit_hole.get((unit_id, hole_number), [])
|
|
|
|
|
|
if len(scores) != 1:
|
|
|
|
|
|
return None
|
|
|
|
|
|
per_hole = strokes_per_hole.get(unit_id)
|
|
|
|
|
|
if per_hole is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
nets.append(scores[0] - per_hole[hole_number])
|
|
|
|
|
|
if not nets:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return min(nets)
|
|
|
|
|
|
|
|
|
|
|
|
hole_results: list[HoleResult] = []
|
|
|
|
|
|
for hole_number in play_order:
|
|
|
|
|
|
net_a = side_net(hole_number, side_a_id)
|
|
|
|
|
|
net_b = side_net(hole_number, side_b_id)
|
|
|
|
|
|
if net_a is None or net_b is None:
|
|
|
|
|
|
break
|
|
|
|
|
|
if net_a < net_b:
|
|
|
|
|
|
hole_results.append(HoleResult.SIDE_A)
|
|
|
|
|
|
elif net_b < net_a:
|
|
|
|
|
|
hole_results.append(HoleResult.SIDE_B)
|
|
|
|
|
|
else:
|
|
|
|
|
|
hole_results.append(HoleResult.HALVED)
|
|
|
|
|
|
return hole_results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NassauWindowOut(BaseModel):
|
|
|
|
|
|
lead: int
|
|
|
|
|
|
holes_played: int
|
|
|
|
|
|
holes_remaining: int
|
|
|
|
|
|
is_closed: bool
|
|
|
|
|
|
is_dormie: bool
|
|
|
|
|
|
status_text: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoundNassauResultOut(BaseModel):
|
|
|
|
|
|
ready: bool
|
|
|
|
|
|
front9: NassauWindowOut | None = None
|
|
|
|
|
|
back9: NassauWindowOut | None = None
|
|
|
|
|
|
overall: NassauWindowOut | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _nassau_window(hole_results: list[HoleResult], total_holes: int) -> NassauWindowOut:
|
|
|
|
|
|
state = compute_match_state(hole_results, total_holes=total_holes)
|
|
|
|
|
|
return NassauWindowOut(
|
|
|
|
|
|
lead=state.lead,
|
|
|
|
|
|
holes_played=state.holes_played,
|
|
|
|
|
|
holes_remaining=state.holes_remaining,
|
|
|
|
|
|
is_closed=state.is_closed,
|
|
|
|
|
|
is_dormie=state.is_dormie,
|
|
|
|
|
|
status_text=state.describe(),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/rounds/{round_id}/format-result/nassau", response_model=RoundNassauResultOut)
|
|
|
|
|
|
async def get_round_nassau_result(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> RoundNassauResultOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT play_format, start_hole, holes_planned FROM round WHERE id = $1", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if round_row["play_format"] not in _TWO_SIDED_FORMATS:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Nassau krever et to-siders spilleformat.")
|
|
|
|
|
|
if round_row["holes_planned"] != 18:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Nassau krever 18 hull.")
|
|
|
|
|
|
hole_results = await _two_sided_hole_results(conn, round_id, round_row)
|
|
|
|
|
|
if hole_results is None:
|
|
|
|
|
|
return RoundNassauResultOut(ready=False)
|
|
|
|
|
|
return RoundNassauResultOut(
|
|
|
|
|
|
ready=True,
|
|
|
|
|
|
front9=_nassau_window(hole_results[0:9], total_holes=9),
|
|
|
|
|
|
back9=_nassau_window(hole_results[9:18], total_holes=9),
|
|
|
|
|
|
overall=_nassau_window(hole_results[0:18], total_holes=18),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
@router.get("/rounds/{round_id}/format-result", response_model=RoundFormatResultOut)
|
|
|
|
|
|
async def get_round_format_result(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> RoundFormatResultOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
return await _build_format_result(conn, round_id)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-26 15:48:07 +02:00
|
|
|
|
class LeaderboardHoleOut(BaseModel):
|
|
|
|
|
|
hole_number: int
|
|
|
|
|
|
par: int
|
|
|
|
|
|
stroke_index: int
|
|
|
|
|
|
played: bool
|
|
|
|
|
|
score: int | None
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
# "Plukket opp" (migrasjon 038) -- se RoundHoleOut for full begrunnelse.
|
|
|
|
|
|
picked_up: bool
|
2026-07-26 15:48:07 +02:00
|
|
|
|
# None når deltakeren ikke har noen beregnet course handicap (2026-07-26,
|
|
|
|
|
|
# samme betingelse som net_score_to_par/total_points under).
|
|
|
|
|
|
strokes_received: int | None
|
|
|
|
|
|
|
|
|
|
|
|
|
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 LeaderboardEntryOut(BaseModel):
|
|
|
|
|
|
participant_id: str
|
|
|
|
|
|
display_name: str
|
|
|
|
|
|
is_owner: bool
|
|
|
|
|
|
holes_played: int
|
|
|
|
|
|
total_score: int | None
|
|
|
|
|
|
score_to_par: int | None
|
|
|
|
|
|
# None når deltakeren ikke har noen beregnet course handicap (f.eks.
|
|
|
|
|
|
# gjest uten registrert HCP) -- samme begrensning som strokes_received
|
|
|
|
|
|
# i RoundHoleOut/list_holes, samme algoritme (allocate_strokes_by_index).
|
|
|
|
|
|
net_score_to_par: int | None
|
2026-07-26 15:48:07 +02:00
|
|
|
|
# Stableford-poengsum (2026-07-26, etterspurt av bruker) -- samme
|
|
|
|
|
|
# betingelse/algoritme som net_score_to_par (krever course handicap),
|
|
|
|
|
|
# summert over spilte hull med Math.max(0, par - netto + 2) per hull.
|
|
|
|
|
|
total_points: int | None
|
|
|
|
|
|
# Hullscorer (2026-07-26, etterspurt av bruker) -- samme rader som
|
|
|
|
|
|
# allerede ble hentet for å beregne total_score/net_score_to_par over,
|
|
|
|
|
|
# bare også eksponert rått slik at leaderboardet kan vise dem per hull.
|
|
|
|
|
|
holes: list[LeaderboardHoleOut]
|
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 LeaderboardOut(BaseModel):
|
|
|
|
|
|
holes_planned: int
|
|
|
|
|
|
completed: bool
|
|
|
|
|
|
entries: list[LeaderboardEntryOut]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
async def _build_leaderboard(conn, round_id: str) -> LeaderboardOut:
|
|
|
|
|
|
"""Delt mellom den autentiserte GET og den offentlige/venn-gatede
|
|
|
|
|
|
/public/rounds-varianten (ADR-036 fase 2)."""
|
|
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT holes_planned, completed_at FROM round WHERE id = $1", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
participant_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS id, rp.is_owner, rp.guest_name, rp.course_handicap_snapshot,
|
|
|
|
|
|
au.display_name AS linked_display_name
|
|
|
|
|
|
FROM round_participant rp
|
|
|
|
|
|
LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.round_id = $1
|
|
|
|
|
|
ORDER BY rp.is_owner DESC, rp.created_at
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
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 19:25:37 +02:00
|
|
|
|
entries: list[LeaderboardEntryOut] = []
|
|
|
|
|
|
for p in participant_rows:
|
|
|
|
|
|
hole_rows = await conn.fetch(
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
"SELECT hole_number, par, stroke_index, played, score, picked_up FROM round_hole WHERE round_participant_id = $1 ORDER BY hole_number",
|
2026-07-28 19:25:37 +02:00
|
|
|
|
p["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
played_holes = [h for h in hole_rows if h["played"] and h["score"] is not None]
|
|
|
|
|
|
holes_played = len(played_holes)
|
|
|
|
|
|
total_score = sum(h["score"] for h in played_holes) if holes_played else None
|
|
|
|
|
|
total_par = sum(h["par"] for h in played_holes) if holes_played else None
|
|
|
|
|
|
score_to_par = total_score - total_par if total_score is not None else None
|
|
|
|
|
|
|
|
|
|
|
|
# Beregnes uavhengig av om noen hull faktisk er spilt ennå --
|
|
|
|
|
|
# brukt BÅDE for net_score_to_par/total_points under OG for
|
|
|
|
|
|
# strokes_received per hull i den rå hull-listen som returneres.
|
|
|
|
|
|
strokes_received_by_hole: dict[int, int] = {}
|
|
|
|
|
|
if p["course_handicap_snapshot"] is not None:
|
|
|
|
|
|
allocation = allocate_strokes_by_index(
|
|
|
|
|
|
p["course_handicap_snapshot"], [h["stroke_index"] for h in hole_rows]
|
|
|
|
|
|
)
|
|
|
|
|
|
strokes_received_by_hole = {h["hole_number"]: a for h, a in zip(hole_rows, allocation)}
|
|
|
|
|
|
|
|
|
|
|
|
net_score_to_par = None
|
|
|
|
|
|
total_points = None
|
|
|
|
|
|
if total_score is not None and strokes_received_by_hole:
|
|
|
|
|
|
strokes_received_total = sum(strokes_received_by_hole[h["hole_number"]] for h in played_holes)
|
|
|
|
|
|
net_score_to_par = score_to_par - strokes_received_total
|
|
|
|
|
|
# Samme stableford-formel som round-scorecard.tsx sin
|
|
|
|
|
|
# stablefordPoints(): 2 poeng for netto par, +/-1 per slag,
|
|
|
|
|
|
# gulvet på 0.
|
|
|
|
|
|
total_points = sum(
|
|
|
|
|
|
max(0, h["par"] - (h["score"] - strokes_received_by_hole[h["hole_number"]]) + 2)
|
|
|
|
|
|
for h in played_holes
|
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 19:25:37 +02:00
|
|
|
|
# Aldri "Deg" her -- leaderboardet kan nå ses av flere (eier
|
|
|
|
|
|
# OG lenkede medspillere, ADR-036 fase 3-utvidelsen), så et
|
|
|
|
|
|
# viewer-relativt "Deg" ville vært feil for de andre som ser på.
|
|
|
|
|
|
display_name = p["guest_name"] or p["linked_display_name"] or "Medspiller"
|
|
|
|
|
|
entries.append(
|
|
|
|
|
|
LeaderboardEntryOut(
|
|
|
|
|
|
participant_id=p["id"],
|
|
|
|
|
|
display_name=display_name,
|
|
|
|
|
|
is_owner=p["is_owner"],
|
|
|
|
|
|
holes_played=holes_played,
|
|
|
|
|
|
total_score=total_score,
|
|
|
|
|
|
score_to_par=score_to_par,
|
|
|
|
|
|
net_score_to_par=net_score_to_par,
|
|
|
|
|
|
total_points=total_points,
|
|
|
|
|
|
holes=[
|
|
|
|
|
|
LeaderboardHoleOut(
|
|
|
|
|
|
hole_number=h["hole_number"],
|
|
|
|
|
|
par=h["par"],
|
|
|
|
|
|
stroke_index=h["stroke_index"],
|
|
|
|
|
|
played=h["played"],
|
|
|
|
|
|
score=h["score"],
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
picked_up=h["picked_up"],
|
2026-07-28 19:25:37 +02:00
|
|
|
|
strokes_received=strokes_received_by_hole.get(h["hole_number"]),
|
|
|
|
|
|
)
|
|
|
|
|
|
for h in hole_rows
|
|
|
|
|
|
],
|
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 19:25:37 +02:00
|
|
|
|
)
|
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 19:25:37 +02:00
|
|
|
|
entries.sort(key=lambda e: (e.score_to_par is None, e.score_to_par))
|
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
|
|
|
|
|
|
|
|
|
|
return LeaderboardOut(
|
|
|
|
|
|
holes_planned=round_row["holes_planned"],
|
|
|
|
|
|
completed=round_row["completed_at"] is not None,
|
|
|
|
|
|
entries=entries,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
@router.get("/rounds/{round_id}/leaderboard", response_model=LeaderboardOut)
|
|
|
|
|
|
async def get_leaderboard(round_id: str, user: CurrentUser = Depends(get_current_user)) -> LeaderboardOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
return await _build_leaderboard(conn, round_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Flere flighter i én "vanlig" runde (migrasjon 035) -- løs gruppering av
|
|
|
|
|
|
# separate `round`-rader, se moduldoc/FEATURE_BACKLOG.md. Bevisst egen,
|
|
|
|
|
|
# smal autorisasjon: kun eier-eller-deltaker (samme regel som resten av
|
|
|
|
|
|
# denne filens autentiserte lese-endepunkter) på DEN ANKER-runden anropet
|
|
|
|
|
|
# gjelder -- søsken-runder man selv ikke har tilgang til inkluderes ALDRI
|
|
|
|
|
|
# i svaret (verken i listen eller det slåtte leaderboardet), samme
|
|
|
|
|
|
# "trygg standard: se ingenting" som resten av rundevisibilitet.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FlightSummaryOut(BaseModel):
|
|
|
|
|
|
round_id: str
|
|
|
|
|
|
name: str | None
|
|
|
|
|
|
course_name_snapshot: str
|
|
|
|
|
|
played_at: date
|
|
|
|
|
|
started_at: str | None
|
|
|
|
|
|
completed_at: str | None
|
|
|
|
|
|
participant_count: int
|
|
|
|
|
|
is_anchor: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FlightGroupOut(BaseModel):
|
|
|
|
|
|
flight_group_id: str | None
|
|
|
|
|
|
flights: list[FlightSummaryOut]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _accessible_flight_rows(conn, round_id: str, user_id: str):
|
|
|
|
|
|
"""Anker-runden (tilgang allerede sjekket av kalleren) + ALLE søsken i
|
|
|
|
|
|
samme flight_group_id som brukeren selv eier eller er lenket deltaker
|
|
|
|
|
|
på -- rader brukeren ikke har tilgang til utelates helt, avsløres
|
|
|
|
|
|
aldri (verken navn eller at de finnes)."""
|
|
|
|
|
|
anchor = await conn.fetchrow(
|
|
|
|
|
|
"SELECT id::text AS id, flight_group_id::text AS flight_group_id FROM round WHERE id = $1",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
group_id = anchor["flight_group_id"]
|
|
|
|
|
|
if group_id is None:
|
|
|
|
|
|
sibling_ids = [round_id]
|
|
|
|
|
|
else:
|
|
|
|
|
|
sibling_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id FROM round WHERE flight_group_id = $1 ORDER BY created_at", group_id
|
|
|
|
|
|
)
|
|
|
|
|
|
sibling_ids = [r["id"] for r in sibling_rows]
|
|
|
|
|
|
rows = []
|
|
|
|
|
|
for sibling_id in sibling_ids:
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT r.id::text AS id, r.name, r.course_name_snapshot, r.played_at,
|
|
|
|
|
|
r.started_at, r.completed_at, r.owner_user_id::text AS owner_user_id,
|
|
|
|
|
|
EXISTS(SELECT 1 FROM round_participant WHERE round_id = r.id AND user_id = $2) AS is_linked_participant
|
|
|
|
|
|
FROM round r WHERE r.id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
sibling_id,
|
|
|
|
|
|
user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if row is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if row["owner_user_id"] != user_id and not row["is_linked_participant"]:
|
|
|
|
|
|
continue
|
|
|
|
|
|
rows.append(row)
|
|
|
|
|
|
return group_id, rows
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/rounds/{round_id}/flight-group", response_model=FlightGroupOut)
|
|
|
|
|
|
async def get_flight_group(round_id: str, user: CurrentUser = Depends(get_current_user)) -> FlightGroupOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
group_id, rows = await _accessible_flight_rows(conn, round_id, user.user_id)
|
|
|
|
|
|
flights = []
|
|
|
|
|
|
for row in rows:
|
|
|
|
|
|
participant_count = await conn.fetchval(
|
|
|
|
|
|
"SELECT COUNT(*) FROM round_participant WHERE round_id = $1", row["id"]
|
|
|
|
|
|
)
|
|
|
|
|
|
flights.append(
|
|
|
|
|
|
FlightSummaryOut(
|
|
|
|
|
|
round_id=row["id"],
|
|
|
|
|
|
name=row["name"],
|
|
|
|
|
|
course_name_snapshot=row["course_name_snapshot"],
|
|
|
|
|
|
played_at=row["played_at"],
|
|
|
|
|
|
started_at=row["started_at"].isoformat() if row["started_at"] else None,
|
|
|
|
|
|
completed_at=row["completed_at"].isoformat() if row["completed_at"] else None,
|
|
|
|
|
|
participant_count=participant_count,
|
|
|
|
|
|
is_anchor=row["id"] == round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return FlightGroupOut(flight_group_id=group_id, flights=flights)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FlightGroupLeaderboardEntryOut(LeaderboardEntryOut):
|
|
|
|
|
|
round_id: str
|
|
|
|
|
|
flight_label: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FlightGroupLeaderboardOut(BaseModel):
|
|
|
|
|
|
flight_group_id: str | None
|
|
|
|
|
|
flights: list[FlightSummaryOut]
|
|
|
|
|
|
entries: list[FlightGroupLeaderboardEntryOut]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/rounds/{round_id}/flight-group/leaderboard", response_model=FlightGroupLeaderboardOut)
|
|
|
|
|
|
async def get_flight_group_leaderboard(
|
|
|
|
|
|
round_id: str, user: CurrentUser = Depends(get_current_user)
|
|
|
|
|
|
) -> FlightGroupLeaderboardOut:
|
|
|
|
|
|
"""Slår sammen hver tilgjengelig søsken-flights EGET leaderboard
|
|
|
|
|
|
(_build_leaderboard, uendret formel) til ÉN rangert liste, tagget med
|
|
|
|
|
|
hvilken flight hver rad kom fra -- score_to_par er allerede normalisert
|
|
|
|
|
|
mot par og dermed sammenlignbart på tvers av ulike baner/hullantall."""
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
group_id, rows = await _accessible_flight_rows(conn, round_id, user.user_id)
|
|
|
|
|
|
flights: list[FlightSummaryOut] = []
|
|
|
|
|
|
entries: list[FlightGroupLeaderboardEntryOut] = []
|
|
|
|
|
|
for idx, row in enumerate(rows):
|
|
|
|
|
|
participant_count = await conn.fetchval(
|
|
|
|
|
|
"SELECT COUNT(*) FROM round_participant WHERE round_id = $1", row["id"]
|
|
|
|
|
|
)
|
|
|
|
|
|
label = row["name"] or f"Flight {idx + 1}"
|
|
|
|
|
|
flights.append(
|
|
|
|
|
|
FlightSummaryOut(
|
|
|
|
|
|
round_id=row["id"],
|
|
|
|
|
|
name=row["name"],
|
|
|
|
|
|
course_name_snapshot=row["course_name_snapshot"],
|
|
|
|
|
|
played_at=row["played_at"],
|
|
|
|
|
|
started_at=row["started_at"].isoformat() if row["started_at"] else None,
|
|
|
|
|
|
completed_at=row["completed_at"].isoformat() if row["completed_at"] else None,
|
|
|
|
|
|
participant_count=participant_count,
|
|
|
|
|
|
is_anchor=row["id"] == round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
sub_board = await _build_leaderboard(conn, row["id"])
|
|
|
|
|
|
for entry in sub_board.entries:
|
|
|
|
|
|
entries.append(
|
|
|
|
|
|
FlightGroupLeaderboardEntryOut(round_id=row["id"], flight_label=label, **entry.model_dump())
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
entries.sort(key=lambda e: (e.score_to_par is None, e.score_to_par))
|
|
|
|
|
|
return FlightGroupLeaderboardOut(flight_group_id=group_id, flights=flights, entries=entries)
|
|
|
|
|
|
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
class HoleUpdate(BaseModel):
|
|
|
|
|
|
played: bool = True
|
|
|
|
|
|
score: int | None = Field(default=None, ge=1, le=20)
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
# "Plukket opp" (migrasjon 038, Stableford) -- spilleren tar opp ballen
|
|
|
|
|
|
# fordi hullet uansett var klart 0 poeng. `score` sendt her IGNORERES
|
|
|
|
|
|
# når dette er sant -- serveren regner selv ut Net Double Bogey (samme
|
|
|
|
|
|
# cap `adjusted_gross_score` allerede bruker for enhver høy score) og
|
|
|
|
|
|
# skriver DEN, slik at eksisterende brutto/netto/HCP-pipeline fungerer
|
|
|
|
|
|
# helt uendret. Krever at deltakeren har en beregnet course handicap
|
|
|
|
|
|
# (samme forutsetning som strokes_received ellers).
|
|
|
|
|
|
picked_up: bool = False
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
putts: int | None = Field(default=None, ge=0, le=10)
|
|
|
|
|
|
club_off_tee: str | None = Field(default=None, max_length=50)
|
|
|
|
|
|
tee_shot_result: Literal["fairway", "left", "right"] | None = None
|
|
|
|
|
|
approach_result: Literal["hit", "long", "short", "left", "right"] | None = None
|
|
|
|
|
|
chip_count: int | None = Field(default=None, ge=0)
|
|
|
|
|
|
bunker_shot_count: int | None = Field(default=None, ge=0)
|
|
|
|
|
|
penalty_strokes: int | None = Field(default=None, ge=0)
|
2026-07-24 06:27:35 +02:00
|
|
|
|
first_putt_distance_bucket: Literal["<1m", "<2m", "<3m", "<5m", "<8m", "8m+"] | None = None
|
|
|
|
|
|
anyway_strokes: int | None = Field(default=None, ge=0)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
@router.patch(
|
|
|
|
|
|
"/rounds/{round_id}/participants/{participant_id}/holes/{hole_number}",
|
|
|
|
|
|
response_model=RoundHoleOut,
|
|
|
|
|
|
)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
async def update_hole(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
participant_id: str,
|
|
|
|
|
|
hole_number: int,
|
|
|
|
|
|
body: HoleUpdate,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
) -> RoundHoleOut:
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
async with plain_connection() as conn:
|
2026-07-26 08:22:41 +02:00
|
|
|
|
# ADR-036 fase 3-utvidelsen (2026-07-26): en lenket medspiller kan
|
|
|
|
|
|
# registrere score for HELE flighten (bekreftet regel), ikke bare
|
|
|
|
|
|
# sin egen rad -- derfor accessible-sjekk, ikke owner-only, og
|
|
|
|
|
|
# INGEN participant_id == viewer-sjekk under.
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
participant_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT course_handicap_snapshot FROM round_participant WHERE id = $1 AND round_id = $2",
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
participant_id, round_id,
|
|
|
|
|
|
)
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
if participant_row is None:
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
raise app_error(404, "NOT_FOUND", "Deltakeren finnes ikke på denne runden.")
|
|
|
|
|
|
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
# Hentes FØR selve UPDATE-en uansett (strokes_received-visning), og
|
|
|
|
|
|
# for "plukket opp" trengs par+strokes_received for AKKURAT dette
|
|
|
|
|
|
# hullet for å regne ut Net Double Bogey-scoren før vi skriver noe.
|
|
|
|
|
|
strokes_received = None
|
|
|
|
|
|
this_hole_par = None
|
|
|
|
|
|
all_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT hole_number, par, stroke_index FROM round_hole WHERE round_participant_id = $1 ORDER BY hole_number",
|
|
|
|
|
|
participant_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
this_hole_row = next((r for r in all_rows if r["hole_number"] == hole_number), None)
|
|
|
|
|
|
if this_hole_row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Hullet finnes ikke på denne deltakeren.")
|
|
|
|
|
|
this_hole_par = this_hole_row["par"]
|
|
|
|
|
|
if participant_row["course_handicap_snapshot"] is not None:
|
|
|
|
|
|
allocation = allocate_strokes_by_index(
|
|
|
|
|
|
participant_row["course_handicap_snapshot"], [r["stroke_index"] for r in all_rows]
|
|
|
|
|
|
)
|
|
|
|
|
|
by_hole = {r["hole_number"]: a for r, a in zip(all_rows, allocation)}
|
|
|
|
|
|
strokes_received = by_hole[hole_number]
|
|
|
|
|
|
|
|
|
|
|
|
score = body.score
|
|
|
|
|
|
played = body.played
|
|
|
|
|
|
if body.picked_up:
|
|
|
|
|
|
if strokes_received is None:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED",
|
|
|
|
|
|
"Kan ikke registrere «plukket opp» før handicap er beregnet for denne deltakeren.",
|
|
|
|
|
|
)
|
|
|
|
|
|
played = True
|
|
|
|
|
|
score = max_hole_score_for_handicap(this_hole_par, strokes_received)
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
async with translate_db_errors():
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
UPDATE round_hole SET
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
played = $3, score = $4, picked_up = $5, putts = $6, club_off_tee = $7,
|
|
|
|
|
|
tee_shot_result = $8, approach_result = $9, chip_count = $10,
|
|
|
|
|
|
bunker_shot_count = $11, penalty_strokes = $12, first_putt_distance_bucket = $13,
|
|
|
|
|
|
anyway_strokes = $14
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
WHERE round_participant_id = $1 AND hole_number = $2
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
RETURNING hole_number, par, stroke_index, played, score, picked_up, putts, club_off_tee,
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
tee_shot_result, approach_result, chip_count, bunker_shot_count,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
penalty_strokes, first_putt_distance_bucket, anyway_strokes
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
""",
|
|
|
|
|
|
participant_id, hole_number,
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
played, score, body.picked_up, body.putts, body.club_off_tee,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
body.tee_shot_result, body.approach_result, body.chip_count,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
body.bunker_shot_count, body.penalty_strokes, body.first_putt_distance_bucket,
|
|
|
|
|
|
body.anyway_strokes,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
)
|
|
|
|
|
|
if row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Hullet finnes ikke på denne deltakeren.")
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
|
2026-07-26 08:49:37 +02:00
|
|
|
|
await broadcast_round_update(round_id)
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
return RoundHoleOut(**dict(row), strokes_received=strokes_received)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Fullføring -- Adjusted Gross Score / Score Differential (ADR-033 Beslutning G)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/rounds/{round_id}/complete", response_model=RoundOut)
|
|
|
|
|
|
async def complete_round(round_id: str, user: CurrentUser = Depends(get_current_user)) -> RoundOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
2026-07-26 08:22:41 +02:00
|
|
|
|
# Samme begrunnelse som update_hole -- en medspiller som fører
|
|
|
|
|
|
# score for flighten skal også kunne markere runden ferdig.
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
Update Todos
Les add_participant, complete_round, friend_categorization-skjema for design
Design + skriv backend for tre rundevarsler (medspiller lagt til, venn starter synlig runde, tilkoblet runde fullført)
Scratch-verifiser alle tre varseltypene
Oppdater CLAUDE.md status + rull ut mot ekte systemer (med bekreftelse)
Live. Rundevarsler er nå koblet på, ren backend-endring (varslingssenteret hadde allerede round/result-typene klare, bare ingenting skrev dem):
Medspiller lagt til — varsel med lenke til /my-rounds/{id} (full tilgang)
En venn starter en synlig runde — varsel til aksepterte venner som faktisk kan se den (offentlig, eller riktig kategori), lenke til /watch/{id}
Tilkoblet runde fullført — medspillere og tredjeparts-venner varsles, aldri personen som selv fullførte
28/28 nye sjekker + full regresjon av tre eksisterende testsuiter (191 sjekker) grønt. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
2026-07-28 19:54:05 +02:00
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT holes_planned, play_format, owner_user_id::text AS owner_user_id, name, course_name_snapshot "
|
|
|
|
|
|
"FROM round WHERE id = $1",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
|
|
|
|
|
# 2026-07-28 oppfølging: match/skins/fourball/foursome/greensome/
|
|
|
|
|
|
# scramble krever flere spillere enn slagspill -- ingenting fanget
|
|
|
|
|
|
# dette opp før nå. Avvis tydelig FØR noe regnes ut, i stedet for å
|
|
|
|
|
|
# stille fullføre en runde som er strukturelt umulig for formatet.
|
|
|
|
|
|
setup_complete, setup_message = await _format_setup_status(conn, round_id, round_row["play_format"])
|
|
|
|
|
|
if not setup_complete:
|
|
|
|
|
|
raise app_error(409, "SETUP_INCOMPLETE", setup_message)
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
participants = await conn.fetch(
|
|
|
|
|
|
"""
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
SELECT id::text AS id, user_id::text AS user_id,
|
|
|
|
|
|
handicap_index_snapshot::float AS handicap_index_snapshot,
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
course_rating_snapshot::float AS course_rating_snapshot,
|
|
|
|
|
|
slope_rating_snapshot, tee_par_snapshot
|
|
|
|
|
|
FROM round_participant WHERE round_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for p in participants:
|
|
|
|
|
|
holes = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT hole_number, par, stroke_index, played, score
|
|
|
|
|
|
FROM round_hole WHERE round_participant_id = $1 ORDER BY hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
p["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
played_count = sum(1 for h in holes if h["played"])
|
|
|
|
|
|
counts = round_counts_for_handicap(played_count, round_row["holes_planned"])
|
|
|
|
|
|
differential = None
|
|
|
|
|
|
|
|
|
|
|
|
if counts and p["handicap_index_snapshot"] is not None and p["course_rating_snapshot"] is not None:
|
|
|
|
|
|
pars = [h["par"] for h in holes]
|
|
|
|
|
|
strokes_received = allocate_strokes_by_index(
|
|
|
|
|
|
_course_handicap_from_row(p),
|
|
|
|
|
|
[h["stroke_index"] for h in holes],
|
|
|
|
|
|
)
|
|
|
|
|
|
scores = [h["score"] if h["played"] else None for h in holes]
|
|
|
|
|
|
ags = adjusted_gross_score(scores, pars, strokes_received)
|
|
|
|
|
|
differential = score_differential(ags, p["course_rating_snapshot"], p["slope_rating_snapshot"])
|
|
|
|
|
|
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"UPDATE round_participant SET counts_for_handicap = $2, score_differential = $3 WHERE id = $1",
|
|
|
|
|
|
p["id"], counts, differential,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
await conn.execute("UPDATE round SET completed_at = now() WHERE id = $1", round_id)
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
|
|
|
|
|
|
# Faktisk HCP (ADR-038) -- alle differensialer for runden er nå
|
|
|
|
|
|
# skrevet, regn på nytt for hver deltaker med egen konto (gjester
|
|
|
|
|
|
# har ingen computed_handicap_index å oppdatere).
|
|
|
|
|
|
for p in participants:
|
|
|
|
|
|
if p["user_id"] is not None:
|
|
|
|
|
|
await _recompute_computed_handicap_index(conn, p["user_id"])
|
|
|
|
|
|
|
Update Todos
Les add_participant, complete_round, friend_categorization-skjema for design
Design + skriv backend for tre rundevarsler (medspiller lagt til, venn starter synlig runde, tilkoblet runde fullført)
Scratch-verifiser alle tre varseltypene
Oppdater CLAUDE.md status + rull ut mot ekte systemer (med bekreftelse)
Live. Rundevarsler er nå koblet på, ren backend-endring (varslingssenteret hadde allerede round/result-typene klare, bare ingenting skrev dem):
Medspiller lagt til — varsel med lenke til /my-rounds/{id} (full tilgang)
En venn starter en synlig runde — varsel til aksepterte venner som faktisk kan se den (offentlig, eller riktig kategori), lenke til /watch/{id}
Tilkoblet runde fullført — medspillere og tredjeparts-venner varsles, aldri personen som selv fullførte
28/28 nye sjekker + full regresjon av tre eksisterende testsuiter (191 sjekker) grønt. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
2026-07-28 19:54:05 +02:00
|
|
|
|
# Rundevarsler (2026-07-28 oppfølging av ADR-036 fase 2) -- to
|
|
|
|
|
|
# atskilte mottakergrupper med ulik lenke, siden de har ulik
|
|
|
|
|
|
# tilgang: lenkede medspillere ser den fulle autentiserte siden,
|
|
|
|
|
|
# tredjeparts-venner kun tredjeparts-/spectator-visningen.
|
|
|
|
|
|
actor_name = await conn.fetchval("SELECT display_name FROM app_user WHERE id = $1", user.user_id)
|
|
|
|
|
|
round_label = (round_row["name"] or "").strip() or round_row["course_name_snapshot"]
|
|
|
|
|
|
message = f"{actor_name} fullførte runden ({round_label})."
|
|
|
|
|
|
|
|
|
|
|
|
linked_ids = {p["user_id"] for p in participants if p["user_id"] is not None}
|
|
|
|
|
|
linked_ids.discard(user.user_id)
|
|
|
|
|
|
for uid in linked_ids:
|
|
|
|
|
|
await create_notification(
|
|
|
|
|
|
conn, user_id=uid, type="result", message=message, link_path=f"/my-rounds/{round_id}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watcher_ids = set(await _friends_who_can_see_round(conn, round_id, round_row["owner_user_id"]))
|
|
|
|
|
|
watcher_ids -= linked_ids
|
|
|
|
|
|
watcher_ids.discard(user.user_id)
|
|
|
|
|
|
for uid in watcher_ids:
|
|
|
|
|
|
await create_notification(
|
|
|
|
|
|
conn, user_id=uid, type="result", message=message, link_path=f"/watch/{round_id}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-03 06:24:57 +02:00
|
|
|
|
await _send_guest_round_summaries(conn, round_id, round_label)
|
|
|
|
|
|
|
2026-07-26 08:49:37 +02:00
|
|
|
|
result = await _load_round_out(conn, round_id, user.user_id)
|
|
|
|
|
|
|
|
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
return result
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 06:24:57 +02:00
|
|
|
|
async def _send_guest_round_summaries(conn, round_id: str, round_label: str) -> None:
|
|
|
|
|
|
"""Rundeoppsummering til midlertidige spillere med registrert e-post
|
|
|
|
|
|
(2026-08-03) -- ETT ekte magic-link-innloggingstoken per gjest, samme
|
|
|
|
|
|
mønster som tournaments.py sin send_scorecard_invitations. Graderer seg
|
|
|
|
|
|
naturlig etter individuell-ball (egne slag/putter) vs. delt-ball
|
|
|
|
|
|
(kun sidens felles slag, ADR-039 Beslutning C) -- ingen egen
|
|
|
|
|
|
format-sjekk trengs, feltene som ikke finnes er rett og slett fraværende
|
|
|
|
|
|
i kildedataene."""
|
|
|
|
|
|
guest_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT id::text AS id, guest_email, guest_first_name, round_side_id::text AS round_side_id, stat_level
|
|
|
|
|
|
FROM round_participant
|
|
|
|
|
|
WHERE round_id = $1 AND guest_email IS NOT NULL
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
for g in guest_rows:
|
|
|
|
|
|
stat_lines: list[tuple[str, str]] = []
|
|
|
|
|
|
if g["round_side_id"] is not None:
|
|
|
|
|
|
side_holes = await _build_side_holes(conn, round_id, g["round_side_id"])
|
|
|
|
|
|
summary_holes = [
|
|
|
|
|
|
RoundSummaryHole(
|
|
|
|
|
|
hole_number=h.hole_number,
|
|
|
|
|
|
par=h.par,
|
|
|
|
|
|
score=h.score if h.played else None,
|
|
|
|
|
|
net=h.score - h.strokes_received
|
|
|
|
|
|
if h.played and h.score is not None and h.strokes_received is not None
|
|
|
|
|
|
else None,
|
|
|
|
|
|
)
|
|
|
|
|
|
for h in side_holes
|
|
|
|
|
|
]
|
|
|
|
|
|
else:
|
|
|
|
|
|
p_holes = await _build_participant_holes(conn, round_id, g["id"])
|
|
|
|
|
|
summary_holes = [
|
|
|
|
|
|
RoundSummaryHole(
|
|
|
|
|
|
hole_number=h.hole_number,
|
|
|
|
|
|
par=h.par,
|
|
|
|
|
|
score=h.score if h.played else None,
|
|
|
|
|
|
net=h.score - h.strokes_received
|
|
|
|
|
|
if h.played and h.score is not None and h.strokes_received is not None
|
|
|
|
|
|
else None,
|
|
|
|
|
|
)
|
|
|
|
|
|
for h in p_holes
|
|
|
|
|
|
]
|
|
|
|
|
|
if g["stat_level"] != "strokes_only":
|
|
|
|
|
|
total_putts = sum(h.putts for h in p_holes if h.played and h.putts is not None)
|
|
|
|
|
|
if total_putts:
|
|
|
|
|
|
stat_lines.append(("Putter totalt", str(total_putts)))
|
|
|
|
|
|
|
|
|
|
|
|
played_holes = [h for h in summary_holes if h.score is not None]
|
|
|
|
|
|
if played_holes:
|
|
|
|
|
|
total_score = sum(h.score for h in played_holes)
|
|
|
|
|
|
to_par = total_score - sum(h.par for h in played_holes)
|
|
|
|
|
|
lead_lines = [("Slag totalt", str(total_score)), ("Til par", _signed(to_par))]
|
|
|
|
|
|
net_holes = [h for h in played_holes if h.net is not None]
|
|
|
|
|
|
if net_holes:
|
|
|
|
|
|
net_to_par = sum(h.net for h in net_holes) - sum(h.par for h in net_holes)
|
|
|
|
|
|
lead_lines.append(("Netto til par", _signed(net_to_par)))
|
|
|
|
|
|
stat_lines = lead_lines + stat_lines
|
|
|
|
|
|
|
|
|
|
|
|
email = g["guest_email"].strip().lower()
|
|
|
|
|
|
raw_token = secrets.token_urlsafe(32)
|
|
|
|
|
|
expires_at = datetime.now(timezone.utc) + timedelta(minutes=settings.MAGIC_LINK_MAX_AGE_MINUTES)
|
|
|
|
|
|
# Samme "ugyldiggjør eldre uforbrukte lenker"-mønster som
|
|
|
|
|
|
# request_magic_link/send_scorecard_invitations.
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"UPDATE magic_link_token SET consumed_at = now() WHERE email = $1 AND consumed_at IS NULL", email
|
|
|
|
|
|
)
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"INSERT INTO magic_link_token (email, token_hash, expires_at, locale) VALUES ($1, $2, $3, 'nb')",
|
|
|
|
|
|
email, _hash_secret(raw_token), expires_at,
|
|
|
|
|
|
)
|
|
|
|
|
|
if settings.SMTP_CONFIGURED:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await send_round_summary_email(
|
|
|
|
|
|
email, g["guest_first_name"] or "Gjest", round_label, summary_holes, stat_lines, raw_token,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
# Se app/email.py sitt mønster -- en driftsfeil i selve
|
|
|
|
|
|
# utsendingen skal aldri hindre fullføringen av runden.
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
elif settings.DEV_LOG_MAGIC_LINKS:
|
|
|
|
|
|
print(f"[DEV] Magic link for {email} (nb): {raw_token}", flush=True)
|
|
|
|
|
|
print(f"[DEV] Rundeoppsummering til {email}: {round_label}, {len(played_holes)} hull spilt", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _signed(value: int) -> str:
|
|
|
|
|
|
if value == 0:
|
|
|
|
|
|
return "E"
|
|
|
|
|
|
return f"+{value}" if value > 0 else str(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
def _course_handicap_from_row(p) -> int:
|
|
|
|
|
|
return course_handicap(
|
|
|
|
|
|
p["handicap_index_snapshot"], p["slope_rating_snapshot"], p["course_rating_snapshot"], p["tee_par_snapshot"]
|
|
|
|
|
|
)
|
2026-07-26 08:49:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Sanntid (2026-07-26, oppfølging av ADR-027) -- "noe endret seg, hent på
|
|
|
|
|
|
# nytt"-signal for round-detail.tsx/round-stats.tsx/round-scorecard.tsx.
|
2026-07-28 19:25:37 +02:00
|
|
|
|
# Denne PRIVATE varianten krever alltid en ekte, autorisert sesjon (eier
|
|
|
|
|
|
# ELLER lenket medspiller) -- en anonym/tredjeparts-venn kan ikke bruke
|
|
|
|
|
|
# denne, se `/ws/public/rounds/{round_id}/live` under (ADR-036 fase 2,
|
|
|
|
|
|
# 2026-07-28) for den offentlige/venn-gatede varianten.
|
2026-07-26 08:49:37 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
@router.websocket("/ws/rounds/{round_id}/live")
|
|
|
|
|
|
async def round_live_ws(websocket: WebSocket, round_id: str) -> None:
|
|
|
|
|
|
user = await get_current_user_from_websocket(websocket)
|
|
|
|
|
|
if user is None:
|
|
|
|
|
|
await websocket.close(code=4401)
|
|
|
|
|
|
return
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
except HTTPException as exc:
|
|
|
|
|
|
await websocket.close(code=4404 if exc.status_code == 404 else 4403)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
await websocket.accept()
|
|
|
|
|
|
sockets = live_sockets_for_round(round_id)
|
|
|
|
|
|
sockets.add(websocket)
|
|
|
|
|
|
try:
|
|
|
|
|
|
while True:
|
|
|
|
|
|
await websocket.receive_text()
|
|
|
|
|
|
except WebSocketDisconnect:
|
|
|
|
|
|
pass
|
|
|
|
|
|
finally:
|
|
|
|
|
|
sockets.discard(websocket)
|
2026-07-28 19:25:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Tredjeparts/offentlig lesevisning (ADR-036 fase 2, 2026-07-28) -- en venn
|
|
|
|
|
|
# (i en synlig kategori) eller alle (visibility_mode='public') kan følge en
|
|
|
|
|
|
# runde live UTEN å være eier/deltaker. Samme "egen /public-prefiks,
|
|
|
|
|
|
# get_current_user_optional"-mønster som turneringers offentlige side
|
|
|
|
|
|
# (ADR-018/026/027) -- egne, leaner response-modeller (aldri guest_email,
|
|
|
|
|
|
# som er PII, og aldri my_*/setup_*, som kun gir mening for eier/deltaker),
|
|
|
|
|
|
# men gjenbruker de SAMME underliggende dataleserne
|
|
|
|
|
|
# (_build_participant_holes/_build_side_holes/_build_leaderboard/
|
|
|
|
|
|
# _build_format_result) som de autentiserte endepunktene over, for å unngå
|
|
|
|
|
|
# to implementasjoner av samme forretningslogikk.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class PublicRoundParticipantOut(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
display_name: str
|
|
|
|
|
|
is_owner: bool
|
|
|
|
|
|
gender: str
|
|
|
|
|
|
tee_name_snapshot: str
|
|
|
|
|
|
handicap_index_snapshot: float | None
|
|
|
|
|
|
course_handicap_snapshot: int | None
|
|
|
|
|
|
round_side_id: str | None
|
|
|
|
|
|
playing_handicap: int | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicRoundOut(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str | None
|
|
|
|
|
|
course_name_snapshot: str
|
|
|
|
|
|
tee_name_snapshot: str
|
|
|
|
|
|
played_at: date
|
|
|
|
|
|
start_hole: int
|
|
|
|
|
|
holes_planned: int
|
|
|
|
|
|
play_format: str
|
|
|
|
|
|
completed_at: str | None
|
|
|
|
|
|
owner_display_name: str
|
|
|
|
|
|
participants: list[PublicRoundParticipantOut]
|
|
|
|
|
|
sides: list[RoundSideOut]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _build_public_round_out(conn, round_id: str) -> PublicRoundOut:
|
|
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT id::text AS id, name, course_name_snapshot, tee_name_snapshot,
|
|
|
|
|
|
played_at, start_hole, holes_planned, play_format, completed_at,
|
|
|
|
|
|
owner_user_id::text AS owner_user_id
|
|
|
|
|
|
FROM round WHERE id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
owner_display_name = await conn.fetchval(
|
|
|
|
|
|
"SELECT display_name FROM app_user WHERE id = $1", round_row["owner_user_id"]
|
|
|
|
|
|
)
|
|
|
|
|
|
participant_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rp.id::text AS id,
|
|
|
|
|
|
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS display_name,
|
|
|
|
|
|
rp.is_owner, rp.gender, rp.tee_name_snapshot,
|
|
|
|
|
|
rp.handicap_index_snapshot::float AS handicap_index_snapshot,
|
|
|
|
|
|
rp.course_handicap_snapshot, rp.round_side_id::text AS round_side_id,
|
|
|
|
|
|
rp.playing_handicap
|
|
|
|
|
|
FROM round_participant rp
|
|
|
|
|
|
LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.round_id = $1 ORDER BY rp.is_owner DESC, rp.created_at
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
side_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id, label FROM round_side WHERE round_id = $1 ORDER BY id", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
return PublicRoundOut(
|
|
|
|
|
|
id=round_row["id"],
|
|
|
|
|
|
name=round_row["name"],
|
|
|
|
|
|
course_name_snapshot=round_row["course_name_snapshot"],
|
|
|
|
|
|
tee_name_snapshot=round_row["tee_name_snapshot"],
|
|
|
|
|
|
played_at=round_row["played_at"],
|
|
|
|
|
|
start_hole=round_row["start_hole"],
|
|
|
|
|
|
holes_planned=round_row["holes_planned"],
|
|
|
|
|
|
play_format=round_row["play_format"],
|
|
|
|
|
|
completed_at=round_row["completed_at"].isoformat() if round_row["completed_at"] else None,
|
|
|
|
|
|
owner_display_name=owner_display_name or "Ukjent",
|
|
|
|
|
|
participants=[PublicRoundParticipantOut(**dict(r)) for r in participant_rows],
|
|
|
|
|
|
sides=[RoundSideOut(**dict(r)) for r in side_rows],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/public/rounds/{round_id}", response_model=PublicRoundOut)
|
|
|
|
|
|
async def get_public_round(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
|
) -> PublicRoundOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_viewable_round_or_404(conn, round_id, user.user_id if user else None)
|
|
|
|
|
|
return await _build_public_round_out(conn, round_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
|
"/public/rounds/{round_id}/participants/{participant_id}/holes",
|
|
|
|
|
|
response_model=list[RoundHoleOut],
|
|
|
|
|
|
)
|
|
|
|
|
|
async def get_public_participant_holes(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
participant_id: str,
|
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
|
) -> list[RoundHoleOut]:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_viewable_round_or_404(conn, round_id, user.user_id if user else None)
|
|
|
|
|
|
return await _build_participant_holes(conn, round_id, participant_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/public/rounds/{round_id}/sides/{side_id}/holes", response_model=list[RoundSideHoleOut])
|
|
|
|
|
|
async def get_public_side_holes(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
side_id: str,
|
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
|
) -> list[RoundSideHoleOut]:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_viewable_round_or_404(conn, round_id, user.user_id if user else None)
|
|
|
|
|
|
return await _build_side_holes(conn, round_id, side_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/public/rounds/{round_id}/leaderboard", response_model=LeaderboardOut)
|
|
|
|
|
|
async def get_public_leaderboard(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
|
) -> LeaderboardOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_viewable_round_or_404(conn, round_id, user.user_id if user else None)
|
|
|
|
|
|
return await _build_leaderboard(conn, round_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/public/rounds/{round_id}/format-result", response_model=RoundFormatResultOut)
|
|
|
|
|
|
async def get_public_format_result(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
|
) -> RoundFormatResultOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_viewable_round_or_404(conn, round_id, user.user_id if user else None)
|
|
|
|
|
|
return await _build_format_result(conn, round_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.websocket("/ws/public/rounds/{round_id}/live")
|
|
|
|
|
|
async def public_round_live_ws(websocket: WebSocket, round_id: str) -> None:
|
|
|
|
|
|
user = await get_current_user_from_websocket(websocket)
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await _get_viewable_round_or_404(conn, round_id, user.user_id if user else None)
|
|
|
|
|
|
except HTTPException as exc:
|
|
|
|
|
|
await websocket.close(code=4404 if exc.status_code == 404 else 4403)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
await websocket.accept()
|
|
|
|
|
|
sockets = live_sockets_for_round(round_id)
|
|
|
|
|
|
sockets.add(websocket)
|
|
|
|
|
|
try:
|
|
|
|
|
|
while True:
|
|
|
|
|
|
await websocket.receive_text()
|
|
|
|
|
|
except WebSocketDisconnect:
|
|
|
|
|
|
pass
|
|
|
|
|
|
finally:
|
|
|
|
|
|
sockets.discard(websocket)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicPersonRoundOut(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str | None
|
|
|
|
|
|
course_name_snapshot: str
|
|
|
|
|
|
played_at: date
|
|
|
|
|
|
completed_at: str | None
|
|
|
|
|
|
play_format: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/public/people/{user_id}/rounds", response_model=list[PublicPersonRoundOut])
|
|
|
|
|
|
async def list_public_person_rounds(
|
|
|
|
|
|
user_id: str,
|
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
|
) -> list[PublicPersonRoundOut]:
|
|
|
|
|
|
"""Brukes av vennprofil-siden (ADR-036 fase 2) -- lister EIERENS runder
|
|
|
|
|
|
som SPØRREREN har lov til å se (public alltid, friends kun med riktig
|
|
|
|
|
|
kategori-match, private aldri) -- egen lenket medspiller-runde vises
|
|
|
|
|
|
ikke her (det er "Mine runder", en annen sak). Pågående
|
|
|
|
|
|
(completed_at IS NULL) alltid øverst, deretter nyeste fullførte."""
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
person_exists = await conn.fetchval("SELECT 1 FROM app_user WHERE id = $1", user_id)
|
|
|
|
|
|
if person_exists is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Fant ikke brukeren.")
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT id::text AS id, name, course_name_snapshot, played_at, completed_at, play_format
|
|
|
|
|
|
FROM round WHERE owner_user_id = $1
|
|
|
|
|
|
ORDER BY (completed_at IS NULL) DESC, played_at DESC, created_at DESC
|
|
|
|
|
|
""",
|
|
|
|
|
|
user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
viewer_id = user.user_id if user else None
|
|
|
|
|
|
visible = [r for r in rows if await _can_view_round(conn, r["id"], viewer_id)]
|
|
|
|
|
|
return [
|
|
|
|
|
|
PublicPersonRoundOut(
|
|
|
|
|
|
id=r["id"],
|
|
|
|
|
|
name=r["name"],
|
|
|
|
|
|
course_name_snapshot=r["course_name_snapshot"],
|
|
|
|
|
|
played_at=r["played_at"],
|
|
|
|
|
|
completed_at=r["completed_at"].isoformat() if r["completed_at"] else None,
|
|
|
|
|
|
play_format=r["play_format"],
|
|
|
|
|
|
)
|
|
|
|
|
|
for r in visible
|
|
|
|
|
|
]
|