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
|
2026-08-07 22:02:56 +02:00
|
|
|
|
import urllib.parse
|
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-08-04 11:34:28 +02:00
|
|
|
|
import asyncpg
|
2026-08-07 22:02:56 +02:00
|
|
|
|
import httpx
|
2026-08-12 21:39:13 +02:00
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Path, 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
|
|
|
|
|
2026-08-12 21:39:13 +02:00
|
|
|
|
from .. import golfapi_cache, golfapi_client, storage, 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,
|
2026-08-05 21:37:32 +02:00
|
|
|
|
PerPlayerPercentage,
|
|
|
|
|
|
TeamAverage,
|
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,
|
2026-08-05 21:37:32 +02:00
|
|
|
|
net_stroke_play_margin,
|
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
|
|
|
|
shamble_hole_score,
|
|
|
|
|
|
stableford_points_for_hole,
|
2026-08-05 21:37:32 +02:00
|
|
|
|
stroke_play_net_total,
|
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"]
|
2026-08-10 07:00:11 +02:00
|
|
|
|
course_rating: float = Field(ge=50, le=90)
|
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
|
|
|
|
slope_rating: int = Field(ge=55, le=155)
|
2026-08-10 07:00:11 +02:00
|
|
|
|
# Banens TOTALE par (18 hull), brukt i WHS-beregningen -- ikke per-hull-
|
|
|
|
|
|
# par (det er PersonalCourseHoleIn.par, ge=3/le=6, en helt annen ting).
|
|
|
|
|
|
par: int = Field(ge=54, le=90)
|
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 PersonalCourseTeeIn(BaseModel):
|
2026-08-10 07:00:11 +02:00
|
|
|
|
name: str = Field(min_length=1, max_length=100)
|
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
|
|
|
|
ratings: list[PersonalCourseTeeRatingIn] = Field(min_length=1, max_length=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PersonalCourseCreate(BaseModel):
|
2026-08-10 07:00:11 +02:00
|
|
|
|
name: str = Field(min_length=1, max_length=100)
|
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
|
|
|
|
holes: list[PersonalCourseHoleIn] = Field(min_length=18, max_length=18)
|
|
|
|
|
|
tees: list[PersonalCourseTeeIn] = Field(min_length=1)
|
2026-08-04 11:34:28 +02:00
|
|
|
|
# Satt når banen opprettes med en annen offentlig custom-bane som
|
|
|
|
|
|
# utgangspunkt/mal (turneringsmodul + single-runde-modul, 2026-08-04) --
|
|
|
|
|
|
# ren attribusjon/proveniens, INGEN vedvarende kobling (samme "engangs-
|
|
|
|
|
|
# kopi"-filosofi som offisiell TeeOff-import, ADR-019).
|
|
|
|
|
|
forked_from_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 PersonalCourseOut(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str
|
2026-08-04 11:34:28 +02:00
|
|
|
|
is_mine: bool
|
|
|
|
|
|
created_by_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
|
|
|
|
|
|
|
|
|
|
|
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]
|
2026-08-04 11:34:28 +02:00
|
|
|
|
# Full hull-/utslagdata -- kun brukt til å forhåndsutfylle et redigerbart
|
|
|
|
|
|
# baneoppsett-skjema når denne banen brukes som MAL (2026-08-04). `tees`
|
|
|
|
|
|
# over (kun navn+kjønn) er uendret -- eksisterende kallsteder som kun
|
|
|
|
|
|
# velger EN allerede ferdig bane til en runde bruker fortsatt den formen.
|
|
|
|
|
|
holes: list[PersonalCourseHoleIn]
|
|
|
|
|
|
full_tees: list[PersonalCourseTeeIn]
|
|
|
|
|
|
is_mine: bool
|
|
|
|
|
|
created_by_display_name: str
|
|
|
|
|
|
forked_from_id: str | 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
|
|
|
|
|
|
|
|
|
|
|
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 = "",
|
2026-08-04 11:34:28 +02:00
|
|
|
|
# "Mine baner"-administrasjon (account-settings.tsx, 2026-08-04) --
|
|
|
|
|
|
# begrenser til EGNE baner uten å være underlagt den vanlige LIMIT 20-
|
|
|
|
|
|
# navnesøket sitt treffbilde. Ikke kombinert med `q` -- eier-visningen
|
|
|
|
|
|
# trenger ikke tekstsøk, kun en fullstendig liste over egne baner.
|
|
|
|
|
|
mine: 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
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> list[PersonalCourseOut]:
|
|
|
|
|
|
async with plain_connection() as conn:
|
2026-08-04 11:34:28 +02:00
|
|
|
|
if mine:
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT pc.id::text AS id, pc.name, true AS is_mine, au.display_name AS created_by_display_name
|
|
|
|
|
|
FROM personal_course pc
|
|
|
|
|
|
JOIN app_user au ON au.id = pc.created_by_user_id
|
|
|
|
|
|
WHERE pc.created_by_user_id = $1
|
|
|
|
|
|
ORDER BY pc.name
|
|
|
|
|
|
""",
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT pc.id::text AS id, pc.name,
|
|
|
|
|
|
(pc.created_by_user_id::text = $2) AS is_mine,
|
|
|
|
|
|
au.display_name AS created_by_display_name
|
|
|
|
|
|
FROM personal_course pc
|
|
|
|
|
|
JOIN app_user au ON au.id = pc.created_by_user_id
|
|
|
|
|
|
WHERE pc.name ILIKE $1
|
|
|
|
|
|
ORDER BY pc.name LIMIT 20
|
|
|
|
|
|
""",
|
|
|
|
|
|
f"%{q.strip()}%",
|
|
|
|
|
|
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
|
|
|
|
return [PersonalCourseOut(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 21:39:13 +02:00
|
|
|
|
# --- Internasjonal banedata fra GolfAPI.io (ADR-064) -------------------------
|
|
|
|
|
|
#
|
|
|
|
|
|
# Speiler international-search/-import i courses.py (org-scopet variant),
|
|
|
|
|
|
# men skriver inn i den globale personal_course-katalogen i stedet for
|
|
|
|
|
|
# course/hole/tee. Samme cache-chokepoint (golfapi_cache), samme "fail
|
|
|
|
|
|
# loudly"-prinsipp. round.course_source trenger INGEN endring -- en
|
|
|
|
|
|
# GolfAPI-importert bane blir en helt vanlig personal_course-rad
|
|
|
|
|
|
# (course_source='custom'), kun med external_golfapi_course_id satt for
|
|
|
|
|
|
# å (a) hindre dupliserte importer, (b) la rangefinder-oppslaget finne
|
|
|
|
|
|
# koordinatene senere.
|
2026-08-13 07:39:29 +02:00
|
|
|
|
#
|
|
|
|
|
|
# MERK plasseringen: MÅ stå FØR GET /personal-courses/{personal_course_id}
|
|
|
|
|
|
# under -- FastAPI matcher ruter i registreringsrekkefølge, og en generisk
|
|
|
|
|
|
# {personal_course_id}-parameter ville ellers grådig fanget opp
|
|
|
|
|
|
# "international-search" som en (ugyldig) UUID FØR denne mer spesifikke
|
|
|
|
|
|
# ruten noensinne nås. Fanget som en ekte 500 under nettleserverifisering
|
|
|
|
|
|
# 2026-08-13 -- se CHANGELOG.md.
|
2026-08-12 21:39:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InternationalCourseOption(BaseModel):
|
|
|
|
|
|
course_id: str
|
|
|
|
|
|
course_name: str
|
|
|
|
|
|
num_holes: int
|
|
|
|
|
|
has_gps: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InternationalClub(BaseModel):
|
|
|
|
|
|
club_id: str
|
|
|
|
|
|
club_name: str
|
|
|
|
|
|
city: str | None = None
|
|
|
|
|
|
country: str | None = None
|
|
|
|
|
|
courses: list[InternationalCourseOption]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InternationalCourseImport(BaseModel):
|
|
|
|
|
|
golfapi_course_id: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/personal-courses/international-search", response_model=list[InternationalClub])
|
|
|
|
|
|
async def search_international_personal_courses(
|
|
|
|
|
|
q: str = Query(default=""),
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> list[InternationalClub]:
|
|
|
|
|
|
if not q.strip():
|
|
|
|
|
|
return []
|
|
|
|
|
|
try:
|
|
|
|
|
|
clubs = await golfapi_client.search_clubs(name=q)
|
|
|
|
|
|
except golfapi_client.GolfApiNotConfiguredError:
|
|
|
|
|
|
raise app_error(400, "NOT_CONFIGURED", "GolfAPI er ikke konfigurert ennå.")
|
|
|
|
|
|
except golfapi_client.GolfApiUnavailableError:
|
|
|
|
|
|
raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baner fra GolfAPI akkurat nå.")
|
|
|
|
|
|
return [
|
|
|
|
|
|
InternationalClub(
|
|
|
|
|
|
club_id=c["clubID"],
|
|
|
|
|
|
club_name=c["clubName"],
|
|
|
|
|
|
city=c.get("city"),
|
|
|
|
|
|
country=c.get("country"),
|
|
|
|
|
|
courses=[
|
|
|
|
|
|
InternationalCourseOption(
|
|
|
|
|
|
course_id=course["courseID"],
|
|
|
|
|
|
course_name=course.get("courseName") or "Bane",
|
|
|
|
|
|
num_holes=int(course["numHoles"]),
|
|
|
|
|
|
has_gps=bool(int(course.get("hasGPS") or 0)),
|
|
|
|
|
|
)
|
|
|
|
|
|
for course in c.get("courses", [])
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
for c in clubs
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/personal-courses/international-import", response_model=PersonalCourseOut, status_code=201)
|
|
|
|
|
|
async def import_international_personal_course(
|
|
|
|
|
|
body: InternationalCourseImport,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> PersonalCourseOut:
|
|
|
|
|
|
if not settings.GOLFAPI_TOKEN:
|
|
|
|
|
|
raise app_error(400, "NOT_CONFIGURED", "GolfAPI er ikke konfigurert ennå.")
|
|
|
|
|
|
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
# Idempotent, samme begrunnelse som TeeOff-importen: gi tilbake den
|
|
|
|
|
|
# EKSISTERENDE (globale -- kan være importert av en ANNEN bruker)
|
|
|
|
|
|
# personal_course-raden fremfor å avvise med 409.
|
|
|
|
|
|
existing = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT pc.id::text AS id, pc.name, (pc.created_by_user_id::text = $2) AS is_mine, au.display_name AS created_by_display_name
|
|
|
|
|
|
FROM personal_course pc JOIN app_user au ON au.id = pc.created_by_user_id
|
|
|
|
|
|
WHERE pc.external_golfapi_course_id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
body.golfapi_course_id,
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if existing is not None:
|
|
|
|
|
|
return PersonalCourseOut(**dict(existing))
|
|
|
|
|
|
|
|
|
|
|
|
async with plain_connection() as conn, translate_db_errors():
|
|
|
|
|
|
try:
|
|
|
|
|
|
await golfapi_cache.get_or_fetch_golfapi_course(conn, body.golfapi_course_id)
|
|
|
|
|
|
except golfapi_client.GolfApiNotFoundError:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Banen finnes ikke hos GolfAPI.")
|
|
|
|
|
|
except golfapi_client.GolfApiUnavailableError:
|
|
|
|
|
|
raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baneinfo fra GolfAPI akkurat nå.")
|
|
|
|
|
|
|
|
|
|
|
|
gc = await conn.fetchrow(
|
|
|
|
|
|
"SELECT club_name, course_name FROM golfapi_course WHERE course_id = $1", body.golfapi_course_id
|
|
|
|
|
|
)
|
|
|
|
|
|
holes = await conn.fetch(
|
|
|
|
|
|
"SELECT hole_number, par_men, index_men FROM golfapi_course_hole WHERE golfapi_course_id = $1 ORDER BY hole_number",
|
|
|
|
|
|
body.golfapi_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
tees = await conn.fetch(
|
|
|
|
|
|
"SELECT name, course_rating_men, slope_men, course_rating_women, slope_women FROM golfapi_course_tee WHERE golfapi_course_id = $1",
|
|
|
|
|
|
body.golfapi_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if len(holes) != 18:
|
|
|
|
|
|
raise app_error(400, "EXTERNAL_DATA_INCOMPLETE", "Banen har ikke 18 registrerte hull hos GolfAPI.")
|
|
|
|
|
|
if not tees:
|
|
|
|
|
|
raise app_error(400, "EXTERNAL_DATA_INCOMPLETE", "Banen har ingen registrerte utslag hos GolfAPI.")
|
2026-08-13 21:24:25 +02:00
|
|
|
|
if not any(
|
|
|
|
|
|
(t["course_rating_men"] is not None and t["slope_men"] is not None)
|
|
|
|
|
|
or (t["course_rating_women"] is not None and t["slope_women"] is not None)
|
|
|
|
|
|
for t in tees
|
|
|
|
|
|
):
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "EXTERNAL_DATA_INCOMPLETE", "Ingen av utslagene har en brukbar rating/slope hos GolfAPI."
|
|
|
|
|
|
)
|
2026-08-12 21:39:13 +02:00
|
|
|
|
|
|
|
|
|
|
par_total = sum(h["par_men"] for h in holes)
|
|
|
|
|
|
course_name = f"{gc['club_name']} – {gc['course_name']}" if gc["course_name"] else gc["club_name"]
|
|
|
|
|
|
own_name = await conn.fetchval("SELECT display_name FROM app_user WHERE id = $1", user.user_id)
|
|
|
|
|
|
|
|
|
|
|
|
async with conn.transaction():
|
|
|
|
|
|
course_row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO personal_course (name, created_by_user_id, external_golfapi_course_id)
|
|
|
|
|
|
VALUES ($1, $2, $3) RETURNING id::text AS id, name
|
|
|
|
|
|
""",
|
|
|
|
|
|
course_name,
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
body.golfapi_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
for h in 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_men"], h["index_men"],
|
|
|
|
|
|
)
|
|
|
|
|
|
for t in tees:
|
2026-08-13 21:24:25 +02:00
|
|
|
|
# Reelt funn (Nesbyen, 2026-08-13): se tilsvarende kommentar i
|
|
|
|
|
|
# courses.py sin international-import -- begge kjønn sjekkes
|
|
|
|
|
|
# individuelt, utslag uten NOEN brukbar rating hoppes over.
|
|
|
|
|
|
ratings: list[tuple[str, float, int, int]] = []
|
|
|
|
|
|
if t["course_rating_men"] is not None and t["slope_men"] is not None:
|
|
|
|
|
|
ratings.append(("m", float(t["course_rating_men"]), int(t["slope_men"]), par_total))
|
|
|
|
|
|
if t["course_rating_women"] is not None and t["slope_women"] is not None:
|
|
|
|
|
|
ratings.append(("f", float(t["course_rating_women"]), int(t["slope_women"]), par_total))
|
|
|
|
|
|
if not ratings:
|
|
|
|
|
|
continue
|
2026-08-12 21:39:13 +02:00
|
|
|
|
tee_row = await conn.fetchrow(
|
|
|
|
|
|
"INSERT INTO personal_course_tee (personal_course_id, name) VALUES ($1, $2) RETURNING id",
|
|
|
|
|
|
course_row["id"], t["name"],
|
|
|
|
|
|
)
|
|
|
|
|
|
for gender, course_rating, slope_rating, par in 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"], gender, course_rating, slope_rating, par,
|
|
|
|
|
|
)
|
|
|
|
|
|
return PersonalCourseOut(id=course_row["id"], name=course_row["name"], is_mine=True, created_by_display_name=own_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-13 07:39:29 +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 pc.id::text AS id, pc.name, pc.forked_from_id::text AS forked_from_id,
|
|
|
|
|
|
(pc.created_by_user_id::text = $2) AS is_mine,
|
|
|
|
|
|
au.display_name AS created_by_display_name
|
|
|
|
|
|
FROM personal_course pc
|
|
|
|
|
|
JOIN app_user au ON au.id = pc.created_by_user_id
|
|
|
|
|
|
WHERE pc.id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
personal_course_id,
|
|
|
|
|
|
user.user_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, 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
|
|
|
|
|
|
ORDER BY t.name
|
|
|
|
|
|
""",
|
|
|
|
|
|
personal_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
hole_rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT hole_number, par, stroke_index FROM personal_course_hole
|
|
|
|
|
|
WHERE personal_course_id = $1 ORDER BY hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
personal_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
tees: dict[str, list[str]] = {}
|
|
|
|
|
|
full_tees: dict[str, list[PersonalCourseTeeRatingIn]] = {}
|
|
|
|
|
|
for r in tee_rows:
|
|
|
|
|
|
tees.setdefault(r["tee_name"], []).append(r["gender"])
|
|
|
|
|
|
full_tees.setdefault(r["tee_name"], []).append(
|
|
|
|
|
|
PersonalCourseTeeRatingIn(
|
|
|
|
|
|
gender=r["gender"], course_rating=r["course_rating"], slope_rating=r["slope_rating"], par=r["par"]
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return PersonalCourseDetail(
|
|
|
|
|
|
id=course_row["id"],
|
|
|
|
|
|
name=course_row["name"],
|
|
|
|
|
|
tees=[TeeOption(name=name, genders=genders) for name, genders in tees.items()],
|
|
|
|
|
|
holes=[PersonalCourseHoleIn(**dict(h)) for h in hole_rows],
|
|
|
|
|
|
full_tees=[PersonalCourseTeeIn(name=name, ratings=ratings) for name, ratings in full_tees.items()],
|
|
|
|
|
|
is_mine=course_row["is_mine"],
|
|
|
|
|
|
created_by_display_name=course_row["created_by_display_name"],
|
|
|
|
|
|
forked_from_id=course_row["forked_from_id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _replace_personal_course_contents(
|
|
|
|
|
|
conn, personal_course_id: str, name: str, holes: list[PersonalCourseHoleIn], tees: list[PersonalCourseTeeIn]
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""Sletter og setter inn hull/utslag på nytt -- trygt siden datamengden er
|
|
|
|
|
|
liten og fast (18 hull, få utslag), samme prinsipp som selve
|
|
|
|
|
|
opprettelsen. Brukt av BÅDE "rediger min egen bane i sted" og
|
|
|
|
|
|
"fork -- opprett en ny rad med disse verdiene"."""
|
|
|
|
|
|
await conn.execute("UPDATE personal_course SET name = $2 WHERE id = $1", personal_course_id, name.strip())
|
|
|
|
|
|
await conn.execute("DELETE FROM personal_course_hole WHERE personal_course_id = $1", personal_course_id)
|
|
|
|
|
|
# personal_course_tee_rating kaskade-slettes med personal_course_tee.
|
|
|
|
|
|
await conn.execute("DELETE FROM personal_course_tee WHERE personal_course_id = $1", personal_course_id)
|
|
|
|
|
|
for h in holes:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"INSERT INTO personal_course_hole (personal_course_id, hole_number, par, stroke_index) VALUES ($1, $2, $3, $4)",
|
|
|
|
|
|
personal_course_id,
|
|
|
|
|
|
h.hole_number,
|
|
|
|
|
|
h.par,
|
|
|
|
|
|
h.stroke_index,
|
|
|
|
|
|
)
|
|
|
|
|
|
for t in tees:
|
|
|
|
|
|
tee_row = await conn.fetchrow(
|
|
|
|
|
|
"INSERT INTO personal_course_tee (personal_course_id, name) VALUES ($1, $2) RETURNING id",
|
|
|
|
|
|
personal_course_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,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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():
|
|
|
|
|
|
own_name = await conn.fetchval("SELECT display_name FROM app_user WHERE id = $1", user.user_id)
|
|
|
|
|
|
async with conn.transaction():
|
|
|
|
|
|
course_row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO personal_course (name, created_by_user_id, forked_from_id)
|
|
|
|
|
|
VALUES ($1, $2, $3) RETURNING id::text AS id, name
|
|
|
|
|
|
""",
|
|
|
|
|
|
body.name.strip(),
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
body.forked_from_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(id=course_row["id"], name=course_row["name"], is_mine=True, created_by_display_name=own_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-04 11:34:28 +02:00
|
|
|
|
@router.patch("/personal-courses/{personal_course_id}", response_model=PersonalCourseOut)
|
|
|
|
|
|
async def update_personal_course(
|
|
|
|
|
|
personal_course_id: str,
|
|
|
|
|
|
body: PersonalCourseCreate,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> PersonalCourseOut:
|
|
|
|
|
|
"""Eier: oppdaterer banen i sted. IKKE eier: forker automatisk -- en NY
|
|
|
|
|
|
rad opprettes, eid av innlogget bruker, med `forked_from_id` satt til
|
|
|
|
|
|
originalen -- originalen selv røres ikke. Ett endepunkt dekker begge
|
|
|
|
|
|
"rediger min egen"/"lag min redigerbare kopi av andres"-casene, se
|
|
|
|
|
|
plan 2026-08-04 -- unngår at frontend selv må forgrene på eierskap."""
|
|
|
|
|
|
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():
|
|
|
|
|
|
owner_id = await conn.fetchval(
|
|
|
|
|
|
"SELECT created_by_user_id::text FROM personal_course WHERE id = $1", personal_course_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if owner_id is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Den egendefinerte banen finnes ikke.")
|
|
|
|
|
|
own_name = await conn.fetchval("SELECT display_name FROM app_user WHERE id = $1", user.user_id)
|
|
|
|
|
|
|
|
|
|
|
|
if owner_id == user.user_id:
|
|
|
|
|
|
async with conn.transaction():
|
|
|
|
|
|
await _replace_personal_course_contents(conn, personal_course_id, body.name, body.holes, body.tees)
|
|
|
|
|
|
return PersonalCourseOut(
|
|
|
|
|
|
id=personal_course_id, name=body.name.strip(), is_mine=True, created_by_display_name=own_name
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async with conn.transaction():
|
|
|
|
|
|
new_row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO personal_course (name, created_by_user_id, forked_from_id)
|
|
|
|
|
|
VALUES ($1, $2, $3) RETURNING id::text AS id
|
|
|
|
|
|
""",
|
|
|
|
|
|
body.name.strip(),
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
personal_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
await _replace_personal_course_contents(conn, new_row["id"], body.name, body.holes, body.tees)
|
|
|
|
|
|
return PersonalCourseOut(id=new_row["id"], name=body.name.strip(), is_mine=True, created_by_display_name=own_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/personal-courses/{personal_course_id}/duplicate", response_model=PersonalCourseOut, status_code=201)
|
|
|
|
|
|
async def duplicate_personal_course(
|
|
|
|
|
|
personal_course_id: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> PersonalCourseOut:
|
|
|
|
|
|
"""Eksplisitt "lag en kopi"-handling, uavhengig av eierskap på kilden --
|
|
|
|
|
|
i tillegg til (ikke erstatning for) fork-ved-rediger over. Dekker f.eks.
|
|
|
|
|
|
å lage en variant av DIN EGEN bane uten å endre originalen, noe
|
|
|
|
|
|
fork-ved-rediger alene ikke gjør (den trigges kun på ANDRES baner)."""
|
|
|
|
|
|
async with plain_connection() as conn, translate_db_errors():
|
|
|
|
|
|
source = await conn.fetchrow("SELECT name FROM personal_course WHERE id = $1", personal_course_id)
|
|
|
|
|
|
if source is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Den egendefinerte banen finnes ikke.")
|
|
|
|
|
|
own_name = await conn.fetchval("SELECT display_name FROM app_user WHERE id = $1", user.user_id)
|
|
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
tees_by_name: dict[str, list[PersonalCourseTeeRatingIn]] = {}
|
|
|
|
|
|
for r in tee_rows:
|
|
|
|
|
|
tees_by_name.setdefault(r["tee_name"], []).append(
|
|
|
|
|
|
PersonalCourseTeeRatingIn(
|
|
|
|
|
|
gender=r["gender"], course_rating=r["course_rating"], slope_rating=r["slope_rating"], par=r["par"]
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
new_name = f"{source['name']} (kopi)"
|
|
|
|
|
|
|
|
|
|
|
|
async with conn.transaction():
|
|
|
|
|
|
new_row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO personal_course (name, created_by_user_id, forked_from_id)
|
|
|
|
|
|
VALUES ($1, $2, $3) RETURNING id::text AS id
|
|
|
|
|
|
""",
|
|
|
|
|
|
new_name,
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
personal_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
for h in hole_rows:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"INSERT INTO personal_course_hole (personal_course_id, hole_number, par, stroke_index) VALUES ($1, $2, $3, $4)",
|
|
|
|
|
|
new_row["id"],
|
|
|
|
|
|
h["hole_number"],
|
|
|
|
|
|
h["par"],
|
|
|
|
|
|
h["stroke_index"],
|
|
|
|
|
|
)
|
|
|
|
|
|
for tee_name, ratings in tees_by_name.items():
|
|
|
|
|
|
tee_row = await conn.fetchrow(
|
|
|
|
|
|
"INSERT INTO personal_course_tee (personal_course_id, name) VALUES ($1, $2) RETURNING id",
|
|
|
|
|
|
new_row["id"],
|
|
|
|
|
|
tee_name,
|
|
|
|
|
|
)
|
|
|
|
|
|
for r in 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(id=new_row["id"], name=new_name, is_mine=True, created_by_display_name=own_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/personal-courses/{personal_course_id}", status_code=204)
|
|
|
|
|
|
async def delete_personal_course(
|
|
|
|
|
|
personal_course_id: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
owner_id = await conn.fetchval(
|
|
|
|
|
|
"SELECT created_by_user_id::text FROM personal_course WHERE id = $1", personal_course_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if owner_id is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Den egendefinerte banen finnes ikke.")
|
|
|
|
|
|
if owner_id != user.user_id:
|
|
|
|
|
|
raise app_error(403, "FORBIDDEN", "Du kan bare slette dine egne baner.")
|
|
|
|
|
|
try:
|
|
|
|
|
|
await conn.execute("DELETE FROM personal_course WHERE id = $1", personal_course_id)
|
|
|
|
|
|
except asyncpg.ForeignKeyViolationError:
|
|
|
|
|
|
# round.personal_course_id har ingen ON DELETE-klausul (default
|
|
|
|
|
|
# NO ACTION, 020_personal_rounds.sql) -- en bane som er brukt i
|
|
|
|
|
|
# minst én runde er derfor allerede trygt beskyttet mot sletting
|
|
|
|
|
|
# på databasenivå. IKKE `translate_db_errors()` her -- den ville
|
|
|
|
|
|
# gitt "INVALID_REFERENCE" (feil retning, ment for INSERT/UPDATE
|
|
|
|
|
|
# som peker på noe som ikke finnes), ikke denne "i bruk"-casen.
|
|
|
|
|
|
raise app_error(409, "IN_USE", "Denne banen er i bruk i én eller flere runder og kan ikke slettes.")
|
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
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 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]
|
2026-08-04 11:34:28 +02:00
|
|
|
|
# Fullt hull-/utslagoppsett -- KUN populert når teeoff har komplette data
|
|
|
|
|
|
# (alle 18 hull med par+hcp-indeks, minst én tee med rating). Brukt til å
|
|
|
|
|
|
# forhåndsutfylle et redigerbart baneoppsett-skjema når spilleren velger
|
|
|
|
|
|
# "bruk som mal" i stedet for direkte spill på banen (2026-08-04, samme
|
|
|
|
|
|
# mønster som org-siden sin `official-import` allerede parser -- her
|
|
|
|
|
|
# returneres dataene i stedet for å skrives, siden frittstående runder
|
|
|
|
|
|
# aldri persisterer teeoff-baner, ADR-033 Beslutning C). Tom liste = ikke
|
|
|
|
|
|
# komplett nok til å brukes som mal ennå.
|
|
|
|
|
|
holes: list[PersonalCourseHoleIn]
|
|
|
|
|
|
full_tees: list[PersonalCourseTeeIn]
|
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 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", []):
|
2026-08-04 11:34:28 +02:00
|
|
|
|
raw_holes = c.get("holes") or []
|
|
|
|
|
|
if len(raw_holes) != 18:
|
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
|
|
|
|
continue
|
|
|
|
|
|
tees: list[TeeOption] = []
|
2026-08-04 11:34:28 +02:00
|
|
|
|
full_tees: list[PersonalCourseTeeIn] = []
|
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
|
|
|
|
for t in c.get("tees") or []:
|
|
|
|
|
|
genders: list[str] = []
|
2026-08-04 11:34:28 +02:00
|
|
|
|
ratings: list[PersonalCourseTeeRatingIn] = []
|
|
|
|
|
|
par_total = sum(h["par"] for h in raw_holes if h.get("par") is not 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
|
|
|
|
if t.get("cr_men") is not None and t.get("slope_men") is not None:
|
|
|
|
|
|
genders.append("m")
|
2026-08-04 11:34:28 +02:00
|
|
|
|
ratings.append(
|
|
|
|
|
|
PersonalCourseTeeRatingIn(
|
|
|
|
|
|
gender="m", course_rating=float(t["cr_men"]), slope_rating=int(t["slope_men"]), par=par_total
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
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
|
|
|
|
if t.get("cr_women") is not None and t.get("slope_women") is not None:
|
|
|
|
|
|
genders.append("f")
|
2026-08-04 11:34:28 +02:00
|
|
|
|
ratings.append(
|
|
|
|
|
|
PersonalCourseTeeRatingIn(
|
|
|
|
|
|
gender="f", course_rating=float(t["cr_women"]), slope_rating=int(t["slope_women"]), par=par_total
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
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
|
|
|
|
if genders:
|
|
|
|
|
|
tees.append(TeeOption(name=t.get("name") or "Tee", genders=genders))
|
2026-08-04 11:34:28 +02:00
|
|
|
|
full_tees.append(PersonalCourseTeeIn(name=t.get("name") or "Tee", ratings=ratings))
|
|
|
|
|
|
|
|
|
|
|
|
# Malbruk krever komplette data (samme fullstendighetskrav som
|
|
|
|
|
|
# org-siden sin ADR-019-import) -- ellers tomme lister, kursets
|
|
|
|
|
|
# øvrige felt (navn/tees for direktevalg) er fortsatt fylt ut.
|
|
|
|
|
|
holes_complete = all(h.get("par") is not None and h.get("hcp_index") is not None for h in raw_holes)
|
|
|
|
|
|
template_holes = (
|
|
|
|
|
|
[
|
|
|
|
|
|
PersonalCourseHoleIn(hole_number=h["hole_number"], par=h["par"], stroke_index=h["hcp_index"])
|
|
|
|
|
|
for h in raw_holes
|
|
|
|
|
|
]
|
|
|
|
|
|
if holes_complete and full_tees
|
|
|
|
|
|
else []
|
|
|
|
|
|
)
|
|
|
|
|
|
|
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
|
|
|
|
courses.append(
|
|
|
|
|
|
OfficialCourseOption(
|
|
|
|
|
|
teeoff_course_id=c["id"],
|
|
|
|
|
|
name=c["name"],
|
|
|
|
|
|
is_main_course=bool(c.get("is_main_course")),
|
|
|
|
|
|
tees=tees,
|
2026-08-04 11:34:28 +02:00
|
|
|
|
holes=template_holes,
|
|
|
|
|
|
full_tees=full_tees if template_holes else [],
|
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
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
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"])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-05 21:37:32 +02:00
|
|
|
|
async def _recompute_scramble_solo_handicaps(conn, round_id: str) -> None:
|
|
|
|
|
|
"""Beregn og lagre playing_handicap for BEGGE sider i scramble_solo
|
|
|
|
|
|
(migrasjon 056) -- egen søsterfunksjon til _recompute_side_handicaps,
|
|
|
|
|
|
som IKKE gjenbrukes: den forutsetter ÉN felles strategi (via parse_
|
|
|
|
|
|
allowance_config/allowance_override-JSON) og ETT fast forventet
|
|
|
|
|
|
spillerantall for HELE formatet -- ingen av delene stemmer her.
|
|
|
|
|
|
|
|
|
|
|
|
Lagsiden (variabel størrelse, N>=2): TeamAverage, ALDRI konfigurerbar.
|
|
|
|
|
|
Individualsiden (nøyaktig 1 spiller): PerPlayerPercentage, prosenten
|
|
|
|
|
|
lest direkte fra round.scramble_solo_individual_pct (ikke allowance_
|
|
|
|
|
|
override -- se RoundCreate/RoundUpdate).
|
|
|
|
|
|
"""
|
|
|
|
|
|
pct = await conn.fetchval("SELECT scramble_solo_individual_pct FROM round WHERE id = $1", round_id)
|
|
|
|
|
|
sides = await conn.fetch("SELECT id::text AS id, side_role 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 not participants or any(p["course_handicap_snapshot"] is None for p in participants):
|
|
|
|
|
|
continue # ikke komplett ennå, eller mangler HCP-sporing for noen
|
|
|
|
|
|
|
|
|
|
|
|
if side["side_role"] == "team":
|
|
|
|
|
|
if len(participants) < 2:
|
|
|
|
|
|
continue # TeamAverage krever minst to -- ikke komplett ennå
|
|
|
|
|
|
course_handicaps = [float(p["course_handicap_snapshot"]) for p in participants]
|
|
|
|
|
|
playing = TeamAverage().playing_handicap(course_handicaps)
|
|
|
|
|
|
for p in participants:
|
|
|
|
|
|
await conn.execute("UPDATE round_participant SET playing_handicap = $1 WHERE id = $2", playing, p["id"])
|
|
|
|
|
|
elif side["side_role"] == "individual":
|
|
|
|
|
|
if len(participants) != 1:
|
|
|
|
|
|
continue # nøyaktig én forventet -- ikke komplett ennå
|
|
|
|
|
|
p = participants[0]
|
|
|
|
|
|
playing = PerPlayerPercentage(pct / 100).playing_handicap([float(p["course_handicap_snapshot"])])
|
|
|
|
|
|
await conn.execute("UPDATE round_participant SET playing_handicap = $1 WHERE id = $2", playing, p["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
|
|
|
|
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."""
|
|
|
|
|
|
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",
|
|
|
|
|
|
)
|
2026-08-05 21:37:32 +02:00
|
|
|
|
if play_format in _ASYMMETRIC_SIDE_FORMATS:
|
|
|
|
|
|
# scramble_solo (2026-08-05): kapasitet avhenger av HVILKEN rolle
|
|
|
|
|
|
# siden har, ikke et fast tall for hele formatet -- laget (variabel
|
|
|
|
|
|
# størrelse, N>=2) har ALDRI en øvre grense, individualsiden har
|
|
|
|
|
|
# ALLTID nøyaktig plass til én.
|
|
|
|
|
|
side_role = await conn.fetchval("SELECT side_role FROM round_side WHERE id = $1", side_id)
|
|
|
|
|
|
if side_role == "team":
|
|
|
|
|
|
return
|
|
|
|
|
|
if current_count >= 1:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
409, "SIDE_FULL",
|
|
|
|
|
|
"Individualspiller-siden har allerede sin ene spiller.",
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
expected = _SIDE_PLAYER_COUNT[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
|
|
|
|
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
|
|
|
|
|
|
|
2026-08-05 21:37:32 +02:00
|
|
|
|
if play_format in _SCRAMBLE_SOLO_FORMATS:
|
|
|
|
|
|
sides = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id, label, side_role FROM round_side WHERE round_id = $1 ORDER BY id", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if len(sides) != 2 or {s["side_role"] for s in sides} != {"team", "individual"}:
|
|
|
|
|
|
return False, "Denne spilleformen krever nøyaktig to sider -- ett lag og én individuell motstander."
|
|
|
|
|
|
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 side["side_role"] == "team" and count < 2:
|
|
|
|
|
|
label = side["label"] or "Laget"
|
|
|
|
|
|
return False, f"{label} har {count} spiller(e), men trenger minst 2."
|
|
|
|
|
|
if side["side_role"] == "individual" and count != 1:
|
|
|
|
|
|
label = side["label"] or "Individualspiller-siden"
|
|
|
|
|
|
return False, f"{label} har {count} spiller(e), men trenger nøyaktig 1."
|
|
|
|
|
|
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-08-05 21:37:32 +02:00
|
|
|
|
# Scramble mot enkeltspiller -- to varianter, BEGGE bevisst IKKE i
|
|
|
|
|
|
# _TWO_SIDED_FORMATS: to-siders-settet over forutsetter SAMME struktur på
|
|
|
|
|
|
# begge sider (én engine_format, én _SIDE_PLAYER_COUNT). Disse formatene
|
|
|
|
|
|
# er asymmetriske (variabel lagstørrelse vs. nøyaktig én individualspiller)
|
|
|
|
|
|
# -- egne gate-steder (se _check_side_capacity/create_round_side/
|
|
|
|
|
|
# add_participant/update_participant), egne _build_scramble_solo_*_result-
|
|
|
|
|
|
# funksjoner. Lag-/individual-håndteringen (kapasitet, side_role,
|
|
|
|
|
|
# individual_round_hole, playing_handicap-beregning via
|
|
|
|
|
|
# _recompute_scramble_solo_handicaps) er IDENTISK for begge variantene --
|
|
|
|
|
|
# kun selve resultat-sammenligningen skiller dem (scramble_solo: netto
|
|
|
|
|
|
# totalsum via net_stroke_play_margin, migrasjon 056. scramble_solo_match:
|
|
|
|
|
|
# hull-for-hull via compute_match_state, migrasjon 057) -- derfor étt
|
|
|
|
|
|
# delt sett brukt gjennomgående i stedet for to.
|
|
|
|
|
|
_SCRAMBLE_SOLO_FORMATS = {"scramble_solo", "scramble_solo_match"}
|
|
|
|
|
|
_ASYMMETRIC_SIDE_FORMATS = _SCRAMBLE_SOLO_FORMATS
|
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",
|
2026-08-05 21:37:32 +02:00
|
|
|
|
"chapman", "copenhagen", "bbb", "flag", "shamble", "money_ball", "high_low_high", "scramble_solo",
|
|
|
|
|
|
"scramble_solo_match",
|
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-08-05 21:37:32 +02:00
|
|
|
|
# KUN meningsfullt når play_format='scramble_solo' (migrasjon 056,
|
|
|
|
|
|
# 2026-08-05) -- individualspillerens allowance-%, arrangør-satt,
|
|
|
|
|
|
# standard 100. Lagsiden har INGEN tilsvarende konfig (alltid rent
|
|
|
|
|
|
# snitt via TeamAverage, se _recompute_scramble_solo_handicaps) --
|
|
|
|
|
|
# bevisst IKKE den generiske allowance_override-JSON-en over, som
|
|
|
|
|
|
# forutsetter ÉN delt strategi for hele runden.
|
|
|
|
|
|
scramble_solo_individual_pct: int = Field(default=100, ge=0, le=100)
|
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
|
2026-08-13 21:24:25 +02:00
|
|
|
|
# Per-deltaker avslutning (migrasjon 066) -- UAVHENGIG av round.completed_at
|
|
|
|
|
|
# (som forblir manuell/rundeomfattende). Sann etter PATCH {completed: true}
|
|
|
|
|
|
# (se update_participant) ELLER etter complete_round sin fallback for
|
|
|
|
|
|
# deltakere som aldri ble individuelt avsluttet. Brukes til å stoppe
|
|
|
|
|
|
# "mangler score"-nagging for DENNE spilleren uten å låse resten av flighten.
|
|
|
|
|
|
completed: 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoundSideOut(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
label: str | None
|
2026-08-05 21:37:32 +02:00
|
|
|
|
# KUN satt for scramble_solo (migrasjon 056) -- None for alle andre
|
|
|
|
|
|
# formater, som ikke skiller sidene fra hverandre strukturelt.
|
|
|
|
|
|
side_role: Literal["team", "individual"] | 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 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-08-05 21:37:32 +02:00
|
|
|
|
# KUN meningsfullt for play_format='scramble_solo' (migrasjon 056).
|
|
|
|
|
|
scramble_solo_individual_pct: int
|
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,
|
2026-08-05 21:37:32 +02:00
|
|
|
|
bbb_sweep_bonus_enabled, shamble_best_n, scramble_solo_individual_pct,
|
|
|
|
|
|
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,
|
2026-08-13 21:24:25 +02:00
|
|
|
|
rp.playing_handicap, rp.lineup_order, (rp.completed_at IS NOT NULL) AS completed
|
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(
|
2026-08-05 21:37:32 +02:00
|
|
|
|
"SELECT id::text AS id, label, side_role FROM round_side WHERE round_id = $1 ORDER BY id", 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 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-08-05 21:37:32 +02:00
|
|
|
|
scramble_solo_individual_pct=round_row["scramble_solo_individual_pct"],
|
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
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-05 21:37:32 +02:00
|
|
|
|
async def _individual_round_hole_needed(conn, play_format: str, round_side_id: str | None) -> bool:
|
|
|
|
|
|
"""Trenger denne deltakeren egne individuelle round_hole-rader, eller
|
|
|
|
|
|
leser/skriver den via en delt side-rad i stedet? For de fleste to-sidede
|
|
|
|
|
|
formater er dette EN fast egenskap ved formatet (SIDE_IS_UNIT). For
|
|
|
|
|
|
scramble_solo-familien (migrasjon 056/057) avhenger det av HVILKEN
|
|
|
|
|
|
side -- laget deler ALLTID siden sine round_hole-rader (samme som
|
|
|
|
|
|
foursome/scramble), individualsiden trenger ALLTID egne (samme som
|
|
|
|
|
|
singles)."""
|
|
|
|
|
|
if play_format in _SCRAMBLE_SOLO_FORMATS:
|
|
|
|
|
|
if round_side_id is None:
|
|
|
|
|
|
return True # ukjent side ennå -- samme trygge standard som resten av formatet
|
|
|
|
|
|
side_role = await conn.fetchval("SELECT side_role FROM round_side WHERE id = $1", round_side_id)
|
|
|
|
|
|
return side_role != "team"
|
|
|
|
|
|
return _ROUND_FORMAT_TO_ENGINE_FORMAT.get(play_format, 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
|
|
|
|
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,
|
2026-08-05 21:37:32 +02:00
|
|
|
|
bbb_sweep_bonus_enabled, shamble_best_n, scramble_solo_individual_pct, allowance_override)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21::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,
|
2026-08-05 21:37:32 +02:00
|
|
|
|
body.scramble_solo_individual_pct,
|
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,
|
2026-08-05 21:37:32 +02:00
|
|
|
|
individual_round_hole=await _individual_round_hole_needed(conn, body.play_format, 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-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-08-10 18:34:31 +02:00
|
|
|
|
# 2026-08-10: bruker ba eksplisitt om å fjerne e-post
|
|
|
|
|
|
# for akkurat denne hendelsen -- in-app-varselet og
|
|
|
|
|
|
# push beholdes uendret, se create_notification.
|
|
|
|
|
|
allow_email=False,
|
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-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
|
|
|
|
|
|
|
|
|
|
|
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats
Tre brukertilbakemeldinger etter gjennomgang av statistikksidene:
1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer
en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått).
2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP,
beregnet fra faktiske runder) i stedet for det manuelt satte tallet i
profilen -- historikk-sparklinen filtreres til samme kilde (source=
"computed"), så den ikke lenger blander manuelle og beregnede punkter.
3. /my-rounds/stats:
- Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig
selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst,
seende brukere så bare et ikon og et tall uten kontekst.
- Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet
andel, Innspill er en retningsbeskrivelse -- samme prinsipp som
round-stats.tsx sin eksisterende splitt). Utslag flyttet først.
Redning og "Til par: med vs. uten" flyttet ned under Annet.
- Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde,
eldste til venstre) -- tidligere bevisst utsatt pga. for lite
rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend:
RoundStatsSummary fikk et nytt round_series-felt (kronologisk
snitt-til-par per runde i valgt vindu).
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl.
det nye round_series-feltet via en scratch-DB-kjøring). Full scratch-
stack med seks syntetiske runder (varierende dato/resultat) browser-
verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for
manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige
delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db
urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
|
|
|
|
class RoundStatsSeriesPoint(BaseModel):
|
|
|
|
|
|
played_at: date
|
|
|
|
|
|
score_to_par: float
|
Statistikk: lineær regresjon i stedet for glidende snitt, trend på alle variabler
Brukeren påpekte (med rette) at det glidende 3-runde-snittet i "Utvikling"
bare gjentok stolpediagrammet med én runde forsinkelse -- for få punkter
til at det smoothet noe reelt. Foreslo tre alternativer via
AskUserQuestion (lineær regresjon / eksponentielt glidende snitt /
kumulativt løpende snitt); brukeren valgte lineær regresjon.
Lineær regresjon (minste kvadraters metode) gir en rett linje --
stigningstallet leses direkte som "endring per runde" og vises nå i
bildeteksten (f.eks. "+7 pp per runde"). Visuelt en helt annen form enn
stolpene (rett strek vs. hakkete stolper), som løser selve klagen.
Bruker ba samtidig om trend på ALLE variabler, ikke bare snitt til par.
Lagt til stolpe+trend for: Fairwaytreff, Greentreff (GIR), Putt/18,
Én-putt, Chip/runde, Bunkerslag/runde, Straffeslag/runde, Anywayslag/
runde -- hver plassert i det eksisterende kortet for akkurat den
metrikken. Scrambling/sand save er unntaket (brukervalg via
AskUserQuestion): for få kvalifiserte hull per runde (0-2) til at et
per-runde-tall er meningsfullt, vises i stedet som én kumulativ linje
("raten din til og med hver runde"), ingen stolper.
Backend: RoundStatsSummary sitt gamle round_series-felt (kun snitt til
par) erstattet av et nytt round_series på RoundStatsWindowSummary-nivå,
med per-runde-tall for alle variabler pluss kumulativ scrambling/sand
save. Fanget og rettet en enhets-bug selv under scratch-verifisering:
scrambling/sand save ble først bygget som 0-1-brøker (kopiert fra en
boolsk 1.0/0.0-sjekk) i stedet for samme 0-100-skala som resten av
API-et -- rettet før commit.
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester. Full
scratch-stack med åtte syntetiske runder (bevisst støyende fremgang, for
å faktisk se at regresjonslinjen skiller seg fra stolpene) browser-
verifisert lys+mørk: regresjonslinje bekreftet rett og visuelt distinkt
fra stolpene på alle variabler, riktig fortegn/farge på stigningstall,
ingen dobbel enhet i bildetekstene, kumulative linjer for scrambling/
sand save konvergerer som forventet. Ingen konsollfeil. teecup_db urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 11:17:00 +02:00
|
|
|
|
# Per-runde-tall for stolpe+trendlinje-grafene (bruker-instruks
|
|
|
|
|
|
# 2026-08-12: "trender på alle variablene"). Samme filtrering/formler
|
|
|
|
|
|
# som de tilsvarende pooled-tallene i RoundStatsSummary, bare regnet
|
|
|
|
|
|
# PER runde i stedet for poolet over alle runder i vinduet -- None der
|
|
|
|
|
|
# runden ikke har noe kvalifisert hull for akkurat den metrikken.
|
|
|
|
|
|
putts_per_18: float | None
|
|
|
|
|
|
fairway_hit_pct: float | None
|
|
|
|
|
|
gir_pct: float | None
|
|
|
|
|
|
one_putt_pct: float | None
|
|
|
|
|
|
chip_per_round: float | None
|
|
|
|
|
|
bunker_per_round: float | None
|
|
|
|
|
|
penalty_per_round: float | None
|
|
|
|
|
|
anyway_per_round: float | None
|
|
|
|
|
|
# Scrambling/sand save er KUMULATIVE (poolet over alle runder til og
|
|
|
|
|
|
# med denne), ikke per-runde -- en enkelt runde har ofte 0-2
|
|
|
|
|
|
# kvalifiserte hull (bom green / i bunker), for lite til et eget
|
|
|
|
|
|
# per-runde-tall (brukervalg 2026-08-12, se AskUserQuestion-svar).
|
|
|
|
|
|
scrambling_cumulative_pct: float | None
|
|
|
|
|
|
sand_save_cumulative_pct: float | None
|
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats
Tre brukertilbakemeldinger etter gjennomgang av statistikksidene:
1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer
en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått).
2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP,
beregnet fra faktiske runder) i stedet for det manuelt satte tallet i
profilen -- historikk-sparklinen filtreres til samme kilde (source=
"computed"), så den ikke lenger blander manuelle og beregnede punkter.
3. /my-rounds/stats:
- Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig
selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst,
seende brukere så bare et ikon og et tall uten kontekst.
- Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet
andel, Innspill er en retningsbeskrivelse -- samme prinsipp som
round-stats.tsx sin eksisterende splitt). Utslag flyttet først.
Redning og "Til par: med vs. uten" flyttet ned under Annet.
- Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde,
eldste til venstre) -- tidligere bevisst utsatt pga. for lite
rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend:
RoundStatsSummary fikk et nytt round_series-felt (kronologisk
snitt-til-par per runde i valgt vindu).
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl.
det nye round_series-feltet via en scratch-DB-kjøring). Full scratch-
stack med seks syntetiske runder (varierende dato/resultat) browser-
verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for
manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige
delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db
urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +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
|
Statistikk: lineær regresjon i stedet for glidende snitt, trend på alle variabler
Brukeren påpekte (med rette) at det glidende 3-runde-snittet i "Utvikling"
bare gjentok stolpediagrammet med én runde forsinkelse -- for få punkter
til at det smoothet noe reelt. Foreslo tre alternativer via
AskUserQuestion (lineær regresjon / eksponentielt glidende snitt /
kumulativt løpende snitt); brukeren valgte lineær regresjon.
Lineær regresjon (minste kvadraters metode) gir en rett linje --
stigningstallet leses direkte som "endring per runde" og vises nå i
bildeteksten (f.eks. "+7 pp per runde"). Visuelt en helt annen form enn
stolpene (rett strek vs. hakkete stolper), som løser selve klagen.
Bruker ba samtidig om trend på ALLE variabler, ikke bare snitt til par.
Lagt til stolpe+trend for: Fairwaytreff, Greentreff (GIR), Putt/18,
Én-putt, Chip/runde, Bunkerslag/runde, Straffeslag/runde, Anywayslag/
runde -- hver plassert i det eksisterende kortet for akkurat den
metrikken. Scrambling/sand save er unntaket (brukervalg via
AskUserQuestion): for få kvalifiserte hull per runde (0-2) til at et
per-runde-tall er meningsfullt, vises i stedet som én kumulativ linje
("raten din til og med hver runde"), ingen stolper.
Backend: RoundStatsSummary sitt gamle round_series-felt (kun snitt til
par) erstattet av et nytt round_series på RoundStatsWindowSummary-nivå,
med per-runde-tall for alle variabler pluss kumulativ scrambling/sand
save. Fanget og rettet en enhets-bug selv under scratch-verifisering:
scrambling/sand save ble først bygget som 0-1-brøker (kopiert fra en
boolsk 1.0/0.0-sjekk) i stedet for samme 0-100-skala som resten av
API-et -- rettet før commit.
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester. Full
scratch-stack med åtte syntetiske runder (bevisst støyende fremgang, for
å faktisk se at regresjonslinjen skiller seg fra stolpene) browser-
verifisert lys+mørk: regresjonslinje bekreftet rett og visuelt distinkt
fra stolpene på alle variabler, riktig fortegn/farge på stigningstall,
ingen dobbel enhet i bildetekstene, kumulative linjer for scrambling/
sand save konvergerer som forventet. Ingen konsollfeil. teecup_db urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 11:17:00 +02:00
|
|
|
|
# Kronologisk (eldste først) per-runde-serie for stolpe+trendlinje-
|
|
|
|
|
|
# grafene (brukerinstruks 2026-08-12). Kun for `current`-vinduet --
|
|
|
|
|
|
# grafene viser inneværende periodes utvikling, ikke en sammenligning
|
|
|
|
|
|
# mot forrige periode slik de andre tallene på siden gjør.
|
|
|
|
|
|
round_series: list[RoundStatsSeriesPoint]
|
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
|
|
|
|
|
Statistikk: lineær regresjon i stedet for glidende snitt, trend på alle variabler
Brukeren påpekte (med rette) at det glidende 3-runde-snittet i "Utvikling"
bare gjentok stolpediagrammet med én runde forsinkelse -- for få punkter
til at det smoothet noe reelt. Foreslo tre alternativer via
AskUserQuestion (lineær regresjon / eksponentielt glidende snitt /
kumulativt løpende snitt); brukeren valgte lineær regresjon.
Lineær regresjon (minste kvadraters metode) gir en rett linje --
stigningstallet leses direkte som "endring per runde" og vises nå i
bildeteksten (f.eks. "+7 pp per runde"). Visuelt en helt annen form enn
stolpene (rett strek vs. hakkete stolper), som løser selve klagen.
Bruker ba samtidig om trend på ALLE variabler, ikke bare snitt til par.
Lagt til stolpe+trend for: Fairwaytreff, Greentreff (GIR), Putt/18,
Én-putt, Chip/runde, Bunkerslag/runde, Straffeslag/runde, Anywayslag/
runde -- hver plassert i det eksisterende kortet for akkurat den
metrikken. Scrambling/sand save er unntaket (brukervalg via
AskUserQuestion): for få kvalifiserte hull per runde (0-2) til at et
per-runde-tall er meningsfullt, vises i stedet som én kumulativ linje
("raten din til og med hver runde"), ingen stolper.
Backend: RoundStatsSummary sitt gamle round_series-felt (kun snitt til
par) erstattet av et nytt round_series på RoundStatsWindowSummary-nivå,
med per-runde-tall for alle variabler pluss kumulativ scrambling/sand
save. Fanget og rettet en enhets-bug selv under scratch-verifisering:
scrambling/sand save ble først bygget som 0-1-brøker (kopiert fra en
boolsk 1.0/0.0-sjekk) i stedet for samme 0-100-skala som resten av
API-et -- rettet før commit.
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester. Full
scratch-stack med åtte syntetiske runder (bevisst støyende fremgang, for
å faktisk se at regresjonslinjen skiller seg fra stolpene) browser-
verifisert lys+mørk: regresjonslinje bekreftet rett og visuelt distinkt
fra stolpene på alle variabler, riktig fortegn/farge på stigningstall,
ingen dobbel enhet i bildetekstene, kumulative linjer for scrambling/
sand save konvergerer som forventet. Ingen konsollfeil. teecup_db urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 11:17:00 +02:00
|
|
|
|
def _build_round_series(
|
|
|
|
|
|
round_ids: set[str],
|
|
|
|
|
|
by_round: dict[str, list],
|
|
|
|
|
|
stat_level_by_round: dict[str, str],
|
|
|
|
|
|
played_at_by_round: dict[str, date],
|
|
|
|
|
|
) -> list[RoundStatsSeriesPoint]:
|
|
|
|
|
|
"""Kronologisk (eldste først) per-runde-serie for stolpe+trendlinje-
|
|
|
|
|
|
grafene. Samme filtre/formler som de pooled tallene i
|
|
|
|
|
|
`_summarize_rounds`, bare regnet PER runde -- unntatt scrambling/sand
|
|
|
|
|
|
save, som er bevisst KUMULATIVE (se `RoundStatsSeriesPoint`)."""
|
|
|
|
|
|
|
|
|
|
|
|
def avg(values: list[float]) -> float | None:
|
|
|
|
|
|
return sum(values) / len(values) if values else None
|
|
|
|
|
|
|
|
|
|
|
|
def is_gir(h) -> bool:
|
|
|
|
|
|
return h["score"] - h["putts"] <= h["par"] - 2
|
|
|
|
|
|
|
|
|
|
|
|
ordered = sorted(round_ids, key=lambda rid: played_at_by_round[rid])
|
|
|
|
|
|
points: list[RoundStatsSeriesPoint] = []
|
|
|
|
|
|
cum_scrambling: list[float] = []
|
|
|
|
|
|
cum_sand_save: list[float] = []
|
|
|
|
|
|
|
|
|
|
|
|
for rid in ordered:
|
|
|
|
|
|
holes = by_round.get(rid, [])
|
|
|
|
|
|
played = [h for h in holes if h["played"] and h["score"] is not None]
|
|
|
|
|
|
if not played:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
putts_per_18 = None
|
|
|
|
|
|
if stat_level_by_round.get(rid) != "strokes_only":
|
|
|
|
|
|
putts_per_18 = float(sum(h["putts"] if h["putts"] is not None else 2 for h in holes))
|
|
|
|
|
|
|
|
|
|
|
|
fairway_tracked = [h for h in 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])
|
|
|
|
|
|
|
|
|
|
|
|
gir_eligible = [h for h in 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])
|
|
|
|
|
|
|
|
|
|
|
|
def summed(field: str) -> float | None:
|
|
|
|
|
|
eligible = [h[field] for h in played if h[field] is not None]
|
|
|
|
|
|
return float(sum(eligible)) if eligible else None
|
|
|
|
|
|
|
|
|
|
|
|
# Utvider den kumulative poolen med DENNE rundens hull FØR prosenten
|
|
|
|
|
|
# regnes ut, slik at tallet ved runde N er "raten din til og med
|
|
|
|
|
|
# runde N" -- se RoundStatsSeriesPoint for hvorfor disse to alene
|
|
|
|
|
|
# er kumulative og ikke per-runde.
|
|
|
|
|
|
gir_missed_this_round = [h for h in gir_eligible if not is_gir(h)]
|
|
|
|
|
|
cum_scrambling.extend([100.0 if (h["score"] - h["par"]) <= 0 else 0.0 for h in gir_missed_this_round])
|
|
|
|
|
|
sand_this_round = [h for h in gir_missed_this_round if h["bunker_shot_count"] is not None and h["bunker_shot_count"] >= 1]
|
|
|
|
|
|
cum_sand_save.extend([100.0 if (h["score"] - h["par"]) <= 0 else 0.0 for h in sand_this_round])
|
|
|
|
|
|
|
|
|
|
|
|
points.append(
|
|
|
|
|
|
RoundStatsSeriesPoint(
|
|
|
|
|
|
played_at=played_at_by_round[rid],
|
|
|
|
|
|
score_to_par=float(sum(h["score"] - h["par"] for h in played)),
|
|
|
|
|
|
putts_per_18=putts_per_18,
|
|
|
|
|
|
fairway_hit_pct=fairway_hit_pct,
|
|
|
|
|
|
gir_pct=gir_pct,
|
|
|
|
|
|
one_putt_pct=one_putt_pct,
|
|
|
|
|
|
chip_per_round=summed("chip_count"),
|
|
|
|
|
|
bunker_per_round=summed("bunker_shot_count"),
|
|
|
|
|
|
penalty_per_round=summed("penalty_strokes"),
|
|
|
|
|
|
anyway_per_round=summed("anyway_strokes"),
|
|
|
|
|
|
scrambling_cumulative_pct=avg(cum_scrambling) if cum_scrambling else None,
|
|
|
|
|
|
sand_save_cumulative_pct=avg(cum_sand_save) if cum_sand_save else None,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return points
|
|
|
|
|
|
|
|
|
|
|
|
|
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats
Tre brukertilbakemeldinger etter gjennomgang av statistikksidene:
1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer
en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått).
2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP,
beregnet fra faktiske runder) i stedet for det manuelt satte tallet i
profilen -- historikk-sparklinen filtreres til samme kilde (source=
"computed"), så den ikke lenger blander manuelle og beregnede punkter.
3. /my-rounds/stats:
- Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig
selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst,
seende brukere så bare et ikon og et tall uten kontekst.
- Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet
andel, Innspill er en retningsbeskrivelse -- samme prinsipp som
round-stats.tsx sin eksisterende splitt). Utslag flyttet først.
Redning og "Til par: med vs. uten" flyttet ned under Annet.
- Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde,
eldste til venstre) -- tidligere bevisst utsatt pga. for lite
rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend:
RoundStatsSummary fikk et nytt round_series-felt (kronologisk
snitt-til-par per runde i valgt vindu).
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl.
det nye round_series-feltet via en scratch-DB-kjøring). Full scratch-
stack med seks syntetiske runder (varierende dato/resultat) browser-
verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for
manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige
delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db
urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
|
|
|
|
def _summarize_rounds(
|
|
|
|
|
|
round_ids: set[str],
|
|
|
|
|
|
by_round: dict[str, list],
|
|
|
|
|
|
stat_level_by_round: dict[str, str],
|
|
|
|
|
|
played_at_by_round: dict[str, date],
|
|
|
|
|
|
) -> 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
|
|
|
|
"""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:
|
Statistikk: lineær regresjon i stedet for glidende snitt, trend på alle variabler
Brukeren påpekte (med rette) at det glidende 3-runde-snittet i "Utvikling"
bare gjentok stolpediagrammet med én runde forsinkelse -- for få punkter
til at det smoothet noe reelt. Foreslo tre alternativer via
AskUserQuestion (lineær regresjon / eksponentielt glidende snitt /
kumulativt løpende snitt); brukeren valgte lineær regresjon.
Lineær regresjon (minste kvadraters metode) gir en rett linje --
stigningstallet leses direkte som "endring per runde" og vises nå i
bildeteksten (f.eks. "+7 pp per runde"). Visuelt en helt annen form enn
stolpene (rett strek vs. hakkete stolper), som løser selve klagen.
Bruker ba samtidig om trend på ALLE variabler, ikke bare snitt til par.
Lagt til stolpe+trend for: Fairwaytreff, Greentreff (GIR), Putt/18,
Én-putt, Chip/runde, Bunkerslag/runde, Straffeslag/runde, Anywayslag/
runde -- hver plassert i det eksisterende kortet for akkurat den
metrikken. Scrambling/sand save er unntaket (brukervalg via
AskUserQuestion): for få kvalifiserte hull per runde (0-2) til at et
per-runde-tall er meningsfullt, vises i stedet som én kumulativ linje
("raten din til og med hver runde"), ingen stolper.
Backend: RoundStatsSummary sitt gamle round_series-felt (kun snitt til
par) erstattet av et nytt round_series på RoundStatsWindowSummary-nivå,
med per-runde-tall for alle variabler pluss kumulativ scrambling/sand
save. Fanget og rettet en enhets-bug selv under scratch-verifisering:
scrambling/sand save ble først bygget som 0-1-brøker (kopiert fra en
boolsk 1.0/0.0-sjekk) i stedet for samme 0-100-skala som resten av
API-et -- rettet før commit.
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester. Full
scratch-stack med åtte syntetiske runder (bevisst støyende fremgang, for
å faktisk se at regresjonslinjen skiller seg fra stolpene) browser-
verifisert lys+mørk: regresjonslinje bekreftet rett og visuelt distinkt
fra stolpene på alle variabler, riktig fortegn/farge på stigningstall,
ingen dobbel enhet i bildetekstene, kumulative linjer for scrambling/
sand save konvergerer som forventet. Ingen konsollfeil. teecup_db urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 11:17:00 +02:00
|
|
|
|
to_par_values.append(float(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,
|
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats
Tre brukertilbakemeldinger etter gjennomgang av statistikksidene:
1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer
en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått).
2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP,
beregnet fra faktiske runder) i stedet for det manuelt satte tallet i
profilen -- historikk-sparklinen filtreres til samme kilde (source=
"computed"), så den ikke lenger blander manuelle og beregnede punkter.
3. /my-rounds/stats:
- Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig
selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst,
seende brukere så bare et ikon og et tall uten kontekst.
- Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet
andel, Innspill er en retningsbeskrivelse -- samme prinsipp som
round-stats.tsx sin eksisterende splitt). Utslag flyttet først.
Redning og "Til par: med vs. uten" flyttet ned under Annet.
- Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde,
eldste til venstre) -- tidligere bevisst utsatt pga. for lite
rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend:
RoundStatsSummary fikk et nytt round_series-felt (kronologisk
snitt-til-par per runde i valgt vindu).
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl.
det nye round_series-feltet via en scratch-DB-kjøring). Full scratch-
stack med seks syntetiske runder (varierende dato/resultat) browser-
verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for
manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige
delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db
urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
|
|
|
|
current=_summarize_rounds(current_ids, by_round, stat_level_by_round, played_at_by_round),
|
|
|
|
|
|
previous=_summarize_rounds(previous_ids, by_round, stat_level_by_round, played_at_by_round),
|
Statistikk: lineær regresjon i stedet for glidende snitt, trend på alle variabler
Brukeren påpekte (med rette) at det glidende 3-runde-snittet i "Utvikling"
bare gjentok stolpediagrammet med én runde forsinkelse -- for få punkter
til at det smoothet noe reelt. Foreslo tre alternativer via
AskUserQuestion (lineær regresjon / eksponentielt glidende snitt /
kumulativt løpende snitt); brukeren valgte lineær regresjon.
Lineær regresjon (minste kvadraters metode) gir en rett linje --
stigningstallet leses direkte som "endring per runde" og vises nå i
bildeteksten (f.eks. "+7 pp per runde"). Visuelt en helt annen form enn
stolpene (rett strek vs. hakkete stolper), som løser selve klagen.
Bruker ba samtidig om trend på ALLE variabler, ikke bare snitt til par.
Lagt til stolpe+trend for: Fairwaytreff, Greentreff (GIR), Putt/18,
Én-putt, Chip/runde, Bunkerslag/runde, Straffeslag/runde, Anywayslag/
runde -- hver plassert i det eksisterende kortet for akkurat den
metrikken. Scrambling/sand save er unntaket (brukervalg via
AskUserQuestion): for få kvalifiserte hull per runde (0-2) til at et
per-runde-tall er meningsfullt, vises i stedet som én kumulativ linje
("raten din til og med hver runde"), ingen stolper.
Backend: RoundStatsSummary sitt gamle round_series-felt (kun snitt til
par) erstattet av et nytt round_series på RoundStatsWindowSummary-nivå,
med per-runde-tall for alle variabler pluss kumulativ scrambling/sand
save. Fanget og rettet en enhets-bug selv under scratch-verifisering:
scrambling/sand save ble først bygget som 0-1-brøker (kopiert fra en
boolsk 1.0/0.0-sjekk) i stedet for samme 0-100-skala som resten av
API-et -- rettet før commit.
Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester. Full
scratch-stack med åtte syntetiske runder (bevisst støyende fremgang, for
å faktisk se at regresjonslinjen skiller seg fra stolpene) browser-
verifisert lys+mørk: regresjonslinje bekreftet rett og visuelt distinkt
fra stolpene på alle variabler, riktig fortegn/farge på stigningstall,
ingen dobbel enhet i bildetekstene, kumulative linjer for scrambling/
sand save konvergerer som forventet. Ingen konsollfeil. teecup_db urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 11:17:00 +02:00
|
|
|
|
round_series=_build_round_series(current_ids, by_round, stat_level_by_round, played_at_by_round),
|
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
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
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).
|
|
|
|
|
|
#
|
2026-08-08 12:37:09 +02:00
|
|
|
|
# Regel: MINST ÉN av vennens kategorier må være i rundens synlige sett
|
|
|
|
|
|
# (ADR-036 Beslutning E, 2026-08-08). En strengere "ALLE kategorier må
|
|
|
|
|
|
# være synlige"-variant gjaldt 2026-07-29--2026-08-08, men ga et reelt
|
|
|
|
|
|
# forvirrende utfall i praksis (bruker delte en runde med "Make"
|
|
|
|
|
|
# huket av, men vedkommendes ektefelle -- ALSO tagget "Storfamilie" --
|
|
|
|
|
|
# fikk ikke se runden fordi den kategorien ikke var huket av). Reversert
|
|
|
|
|
|
# til den opprinnelige ADR-036 Beslutning B-regelen (som alltid sa
|
|
|
|
|
|
# "minst én kategori", se punkt 5 der) etter brukerrapport. En venn med
|
|
|
|
|
|
# INGEN kategorier i det hele tatt vises fortsatt ALDRI.
|
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
|
2026-08-08 12:37:09 +02:00
|
|
|
|
AND category 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-08-08 12:37:09 +02:00
|
|
|
|
# Samme "minst én kategori er nok"-regel som _can_view_round (ADR-036
|
|
|
|
|
|
# Beslutning E).
|
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-08-08 12:37:09 +02:00
|
|
|
|
SELECT DISTINCT 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[])
|
2026-08-08 12:37:09 +02:00
|
|
|
|
AND fc.category IN (SELECT category FROM round_visible_category WHERE round_id = $3)
|
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
|
2026-08-08 12:37:09 +02:00
|
|
|
|
AND fc.category IN (
|
2026-08-01 21:15:07 +02:00
|
|
|
|
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",
|
2026-08-05 21:37:32 +02:00
|
|
|
|
"chapman", "copenhagen", "bbb", "flag", "shamble", "money_ball", "high_low_high", "scramble_solo",
|
|
|
|
|
|
"scramble_solo_match",
|
2026-08-03 14:32:48 +02:00
|
|
|
|
] | 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-08-05 21:37:32 +02:00
|
|
|
|
# KUN meningsfullt for play_format='scramble_solo' (migrasjon 056) --
|
|
|
|
|
|
# samme "None = ikke rør"-konvensjon som resten av denne modellen.
|
|
|
|
|
|
scramble_solo_individual_pct: int | None = Field(default=None, ge=0, le=100)
|
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-08-05 21:37:32 +02:00
|
|
|
|
if body.scramble_solo_individual_pct is not None:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"UPDATE round SET scramble_solo_individual_pct = $2 WHERE id = $1",
|
|
|
|
|
|
round_id,
|
|
|
|
|
|
body.scramble_solo_individual_pct,
|
|
|
|
|
|
)
|
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-08-05 21:37:32 +02:00
|
|
|
|
# scramble_solo-familien (migrasjon 056/057) bruker aldri
|
|
|
|
|
|
# allowance_override -- egen trigger på sin egen kolonne i
|
|
|
|
|
|
# stedet, samme "regn på nytt for allerede tildelte sider"-
|
|
|
|
|
|
# prinsipp som over.
|
|
|
|
|
|
if body.scramble_solo_individual_pct 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 _SCRAMBLE_SOLO_FORMATS:
|
|
|
|
|
|
await _recompute_scramble_solo_handicaps(conn, round_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
|
|
|
|
|
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:
|
2026-08-05 21:37:32 +02:00
|
|
|
|
if round_row["play_format"] not in (_TWO_SIDED_FORMATS | _ASYMMETRIC_SIDE_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
|
|
|
|
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,
|
2026-08-05 21:37:32 +02:00
|
|
|
|
individual_round_hole=await _individual_round_hole_needed(
|
|
|
|
|
|
conn, round_row["play_format"], body.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
|
|
|
|
)
|
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:
|
2026-08-05 21:37:32 +02:00
|
|
|
|
if round_row["play_format"] in _SCRAMBLE_SOLO_FORMATS:
|
|
|
|
|
|
await _recompute_scramble_solo_handicaps(conn, round_id)
|
|
|
|
|
|
else:
|
|
|
|
|
|
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,
|
2026-08-13 21:24:25 +02:00
|
|
|
|
rp.playing_handicap, rp.lineup_order, (rp.completed_at IS NOT NULL) AS completed
|
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)
|
2026-08-05 21:37:32 +02:00
|
|
|
|
# KUN meningsfullt (og påkrevd) når play_format='scramble_solo'
|
|
|
|
|
|
# (migrasjon 056) -- hvilken av de to strukturelt ulike sidene dette
|
|
|
|
|
|
# er. None for alle andre to-sidede formater, som ikke skiller sidene.
|
|
|
|
|
|
side_role: Literal["team", "individual"] | 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.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,
|
|
|
|
|
|
)
|
2026-08-05 21:37:32 +02:00
|
|
|
|
is_scramble_solo = round_row["play_format"] in _SCRAMBLE_SOLO_FORMATS
|
|
|
|
|
|
if round_row["play_format"] not in (_TWO_SIDED_FORMATS | _ASYMMETRIC_SIDE_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
|
|
|
|
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.")
|
2026-08-05 21:37:32 +02:00
|
|
|
|
if is_scramble_solo:
|
|
|
|
|
|
if body.side_role is None:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "side_role ('team'/'individual') er påkrevd for denne spilleformen.")
|
|
|
|
|
|
role_taken = await conn.fetchval(
|
|
|
|
|
|
"SELECT 1 FROM round_side WHERE round_id = $1 AND side_role = $2", round_id, body.side_role
|
|
|
|
|
|
)
|
|
|
|
|
|
if role_taken:
|
|
|
|
|
|
raise app_error(409, "DUPLICATE", f"Runden har allerede en side med rollen '{body.side_role}'.")
|
|
|
|
|
|
elif body.side_role is not None:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "side_role gir kun mening for scramble mot enkeltspiller-formatene.")
|
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 with conn.transaction():
|
|
|
|
|
|
side_row = await conn.fetchrow(
|
2026-08-05 21:37:32 +02:00
|
|
|
|
"INSERT INTO round_side (round_id, label, side_role) VALUES ($1, $2, $3) "
|
|
|
|
|
|
"RETURNING id::text AS id, label, side_role",
|
|
|
|
|
|
round_id, body.label, body.side_role,
|
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-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
|
2026-08-05 21:37:32 +02:00
|
|
|
|
# noen deltakere ennå. scramble_solo (migrasjon 056) er IKKE i
|
|
|
|
|
|
# _ROUND_FORMAT_TO_ENGINE_FORMAT (bevisst -- se _ASYMMETRIC_
|
|
|
|
|
|
# SIDE_FORMATS-kommentaren), så avgjør delt-ball KUN av
|
|
|
|
|
|
# side_role for dette formatet i stedet for et format-bredt
|
|
|
|
|
|
# engine_format-oppslag: kun lagsiden får delt-ball-rader.
|
|
|
|
|
|
wants_shared_hole = (
|
|
|
|
|
|
body.side_role == "team" if is_scramble_solo
|
|
|
|
|
|
else _ROUND_FORMAT_TO_ENGINE_FORMAT[round_row["play_format"]] in SIDE_IS_UNIT
|
|
|
|
|
|
)
|
|
|
|
|
|
if wants_shared_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
|
|
|
|
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-08-12 21:39:13 +02:00
|
|
|
|
class TargetPoint(BaseModel):
|
|
|
|
|
|
poi_type: str
|
|
|
|
|
|
location: str | None
|
|
|
|
|
|
side_fairway: str | None
|
|
|
|
|
|
latitude: float
|
|
|
|
|
|
longitude: float
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/rounds/{round_id}/holes/{hole_number}/target-points", response_model=list[TargetPoint])
|
|
|
|
|
|
async def get_round_hole_target_points(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
hole_number: int = Path(ge=1, le=18),
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> list[TargetPoint]:
|
|
|
|
|
|
"""Rangefinder-data for hullet (ADR-064) -- grønn front/midt/bak,
|
|
|
|
|
|
hindringer, tee-punkter fra GolfAPI. KUN for baner importert derfra
|
|
|
|
|
|
(`personal_course.external_golfapi_course_id` satt) -- returnerer tom
|
|
|
|
|
|
liste ellers (TeeOff-baner/manuelt opprettede baner har ingen
|
|
|
|
|
|
koordinatdata), rangefinder-UI-et skjules da bare, resten av
|
|
|
|
|
|
scorekortet upåvirket. Returnerer RÅ punkter, ALDRI en ferdigregnet
|
|
|
|
|
|
avstand -- den ville vært utdatert idet spilleren beveger seg. Klienten
|
|
|
|
|
|
Haversine-regner selv (frontend/lib/geo.ts, samme funksjon som
|
|
|
|
|
|
ADR-048) mot spillerens EGEN, ferske GPS-posisjon."""
|
|
|
|
|
|
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, personal_course_id FROM round WHERE id = $1", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if round_row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Runden finnes ikke.")
|
|
|
|
|
|
if round_row["course_source"] != "custom":
|
|
|
|
|
|
return []
|
|
|
|
|
|
golfapi_course_id = await conn.fetchval(
|
|
|
|
|
|
"SELECT external_golfapi_course_id FROM personal_course WHERE id = $1",
|
|
|
|
|
|
round_row["personal_course_id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
if golfapi_course_id is None:
|
|
|
|
|
|
return []
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT poi_type, location, side_fairway, latitude, longitude
|
|
|
|
|
|
FROM golfapi_course_coordinate
|
|
|
|
|
|
WHERE golfapi_course_id = $1 AND hole_number = $2
|
|
|
|
|
|
""",
|
|
|
|
|
|
golfapi_course_id,
|
|
|
|
|
|
hole_number,
|
|
|
|
|
|
)
|
|
|
|
|
|
return [TargetPoint(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-08-13 21:24:25 +02:00
|
|
|
|
# Per-deltaker avslutning (migrasjon 066) -- FLIGHT-STYRT, ikke
|
|
|
|
|
|
# eier-only/selv-only som resten av feltene her (se
|
|
|
|
|
|
# _FLIGHT_MANAGED_FIELDS/update_participant): en lenket medspiller kan
|
|
|
|
|
|
# avslutte EN HVILKEN SOM HELST deltaker, samme begrunnelse som at de
|
|
|
|
|
|
# allerede kan føre score for hele flighten (update_hole).
|
|
|
|
|
|
completed: bool | 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
2026-08-13 21:24:25 +02:00
|
|
|
|
# Motsatt av _OWNER_ONLY_PARTICIPANT_FIELDS over -- IKKE begrenset til
|
|
|
|
|
|
# eieren ELLER egen rad. Samme begrunnelse som update_hole (linje ~5375):
|
|
|
|
|
|
# en lenket medspiller registrerer allerede score for HELE flighten, så
|
|
|
|
|
|
# å avslutte en ANNEN deltaker er samme klasse handling, ikke en sensitiv
|
|
|
|
|
|
# profil-endring.
|
|
|
|
|
|
_FLIGHT_MANAGED_PARTICIPANT_FIELDS = {"completed"}
|
|
|
|
|
|
|
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,
|
2026-08-13 21:24:25 +02:00
|
|
|
|
rp.completed_at AS participant_completed_at,
|
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,
|
2026-08-13 21:24:25 +02:00
|
|
|
|
r.teeoff_course_id, r.personal_course_id, r.play_format, r.name AS round_name,
|
|
|
|
|
|
r.course_name_snapshot
|
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.",
|
|
|
|
|
|
)
|
2026-08-13 21:24:25 +02:00
|
|
|
|
non_flight_fields = updates.keys() - _FLIGHT_MANAGED_PARTICIPANT_FIELDS
|
|
|
|
|
|
if non_flight_fields and current["user_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
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-13 21:24:25 +02:00
|
|
|
|
# Per-deltaker avslutning (migrasjon 066) -- kan verken settes ELLER
|
|
|
|
|
|
# angres etter at RUNDEN som helhet er fullført (HCP-differensialer
|
|
|
|
|
|
# er da allerede beregnet og frosset for alle deltakere samlet).
|
|
|
|
|
|
newly_completed = False
|
|
|
|
|
|
if "completed" in updates:
|
|
|
|
|
|
if current["completed_at"] is not None:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
409, "ALREADY_COMPLETED",
|
|
|
|
|
|
"Runden er allerede fullført -- kan ikke avslutte/gjenåpne en enkeltdeltaker i etterkant.",
|
|
|
|
|
|
)
|
|
|
|
|
|
newly_completed = updates["completed"] and current["participant_completed_at"] is None
|
|
|
|
|
|
|
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:
|
2026-08-05 21:37:32 +02:00
|
|
|
|
if current["play_format"] not in (_TWO_SIDED_FORMATS | _ASYMMETRIC_SIDE_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
|
|
|
|
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.")
|
2026-08-05 21:37:32 +02:00
|
|
|
|
# scramble_solo (migrasjon 056), kjent v1-begrensning: et
|
|
|
|
|
|
# BYTTE av side_role (lag<->individuell) etter opprettelse
|
|
|
|
|
|
# oppdaterer IKKE round_hole-eierskapet retroaktivt (satt
|
|
|
|
|
|
# kun ved _create_participant-tidspunktet) -- flytting
|
|
|
|
|
|
# innad i SAMME rolle (f.eks. bytte hvilket lag) er
|
|
|
|
|
|
# uproblematisk. Ikke håndtert her, samme aksepterte-
|
|
|
|
|
|
# begrensning-mønster som Nassaus runaway-margin-kvirk.
|
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 _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-08-13 21:24:25 +02:00
|
|
|
|
if "completed" in updates:
|
|
|
|
|
|
values.append(datetime.now(timezone.utc) if updates["completed"] else None)
|
|
|
|
|
|
set_clauses.append(f"completed_at = ${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
|
|
|
|
|
2026-08-10 18:34:31 +02:00
|
|
|
|
# Reelt funn 2026-08-10 (bruker rapporterte at rundens eget utslag,
|
|
|
|
|
|
# vist i toppheaderen på tvers av Score/Scorekort/Leaderboard, ikke
|
|
|
|
|
|
# fulgte med etter et utslagsbytte): `round.tee_name_snapshot` settes
|
|
|
|
|
|
# KUN her (deltaker-PATCH) OG i update_round sin egen bane-bytte-gren
|
|
|
|
|
|
# -- de to var ikke holdt i synk. Speiler create_round/_create_participant
|
|
|
|
|
|
# sin oppførsel ved opprettelse, der eierens deltaker-rad og selve
|
|
|
|
|
|
# runden alltid får samme tee_name samtidig: når DENNE PATCH-en endrer
|
|
|
|
|
|
# utslaget for eierens EGEN deltaker-rad (ikke en medspillers/gjests),
|
|
|
|
|
|
# oppdateres round.tee_name_snapshot til samme verdi. Rører ikke
|
|
|
|
|
|
# runde-raden ved endring av en annen deltakers utslag -- ulike
|
|
|
|
|
|
# deltakere kan bevisst spille ulike utslag (se ParticipantCreate.tee_name),
|
|
|
|
|
|
# så kun eierens egen rad er en meningsfull "rundens utslag"-kilde.
|
|
|
|
|
|
if "tee_name" in updates and current["user_id"] == current["round_owner_user_id"]:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"UPDATE round SET tee_name_snapshot = $2 WHERE id = $1",
|
|
|
|
|
|
round_id, new_tee_name,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
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.
|
2026-08-05 21:37:32 +02:00
|
|
|
|
if ("round_side_id" in updates or rating_changed) and current["play_format"] in _SCRAMBLE_SOLO_FORMATS:
|
|
|
|
|
|
await _recompute_scramble_solo_handicaps(conn, round_id)
|
|
|
|
|
|
elif (
|
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
|
|
|
|
("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,
|
2026-08-13 21:24:25 +02:00
|
|
|
|
rp.playing_handicap, rp.lineup_order, (rp.completed_at IS NOT NULL) AS completed
|
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-08-13 21:24:25 +02:00
|
|
|
|
|
|
|
|
|
|
# Scorekort på e-post UMIDDELBART når DENNE spilleren blir ferdig
|
|
|
|
|
|
# (bekreftet med bruker 2026-08-13) -- ikke samlet ved rundens egen
|
|
|
|
|
|
# fullføring (complete_round har sin egen fallback for deltakere
|
|
|
|
|
|
# som ALDRI ble individuelt avsluttet, se der).
|
|
|
|
|
|
if newly_completed:
|
|
|
|
|
|
round_label = (current["round_name"] or "").strip() or current["course_name_snapshot"]
|
|
|
|
|
|
await _send_participant_round_summary(conn, round_id, participant_id, round_label)
|
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
|
2026-08-10 18:34:31 +02:00
|
|
|
|
# Optimistisk versjonssjekk (ADR-057, migrasjon 062) -- klienten sender
|
|
|
|
|
|
# denne tilbake som expected_version ved neste PATCH. Økes med 1 for
|
|
|
|
|
|
# hver skrivning, aldri direkte redigerbar.
|
|
|
|
|
|
version: int
|
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,
|
2026-08-10 18:34:31 +02:00
|
|
|
|
penalty_strokes, first_putt_distance_bucket, anyway_strokes, version
|
2026-07-28 19:25:37 +02:00
|
|
|
|
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
|
2026-08-10 18:34:31 +02:00
|
|
|
|
# Optimistisk versjonssjekk (ADR-057, migrasjon 062) -- se RoundHoleOut.
|
|
|
|
|
|
version: int
|
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(
|
2026-08-10 18:34:31 +02:00
|
|
|
|
"SELECT hole_number, par, stroke_index, played, score, "
|
|
|
|
|
|
"selected_participant_id::text AS selected_participant_id, version "
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
"FROM 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
|
2026-08-10 18:34:31 +02:00
|
|
|
|
# Optimistisk versjonssjekk (ADR-057). None = hopp over sjekken (samme
|
|
|
|
|
|
# bakoverkompatible mønster som resten av appens valgfrie felt) --
|
|
|
|
|
|
# klienten sender den alltid i praksis.
|
|
|
|
|
|
expected_version: int | 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(
|
|
|
|
|
|
"""
|
2026-08-10 18:34:31 +02:00
|
|
|
|
UPDATE round_hole SET played = $3, score = $4, selected_participant_id = $5,
|
|
|
|
|
|
version = version + 1
|
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
|
2026-08-10 18:34:31 +02:00
|
|
|
|
AND ($6::int IS NULL OR version = $6::int)
|
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,
|
2026-08-10 18:34:31 +02:00
|
|
|
|
selected_participant_id::text AS selected_participant_id, version
|
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,
|
2026-08-10 18:34:31 +02:00
|
|
|
|
body.expected_version,
|
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:
|
2026-08-10 18:34:31 +02:00
|
|
|
|
# Skiller "hullet finnes ikke" fra "noen andre skrev først" --
|
|
|
|
|
|
# UPDATE-en over kan returnere None av begge grunner.
|
|
|
|
|
|
exists = await conn.fetchval(
|
|
|
|
|
|
"SELECT 1 FROM round_hole WHERE round_side_id = $1 AND hole_number = $2",
|
|
|
|
|
|
side_id, hole_number,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not exists:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Hullet finnes ikke på denne siden.")
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
409, "STALE_VERSION",
|
|
|
|
|
|
"Noen andre har endret dette hullet i mellomtiden. Laster inn siste versjon.",
|
|
|
|
|
|
)
|
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 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:
|
2026-08-10 07:00:11 +02:00
|
|
|
|
# Autorisasjon FØR noe annet leses/valideres -- reell, om enn liten,
|
|
|
|
|
|
# informasjonslekkasje funnet under sikkerhetsgjennomgang 2026-08-09:
|
|
|
|
|
|
# med format-sjekken først kunne en bruker uten tilgang skille
|
|
|
|
|
|
# "runden finnes ikke" fra "runden finnes, feil format" for en
|
|
|
|
|
|
# fremmed runde, uten å faktisk oppnå skrivetilgang. Se
|
|
|
|
|
|
# ARCHITECTURE_DECISIONS.md-tillegg for detalj.
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_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
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT play_format, holes_planned FROM round WHERE id = $1", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
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.")
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-08-05 21:37:32 +02:00
|
|
|
|
# Scramble mot enkeltspiller (migrasjon 056, 2026-08-05): rene netto-
|
|
|
|
|
|
# totalsummer, INGEN match-play-tilstand -- side "a" er alltid LAGET,
|
|
|
|
|
|
# side "b" er alltid INDIVIDUALSPILLEREN (fast, ikke id-sortert som
|
|
|
|
|
|
# resten -- rollen er entydig, ikke vilkårlig). scramble_solo_margin
|
|
|
|
|
|
# er net_stroke_play_margin(team, individual): positiv = laget leder.
|
|
|
|
|
|
scramble_solo_team_net: int | None = None
|
|
|
|
|
|
scramble_solo_individual_net: int | None = None
|
|
|
|
|
|
scramble_solo_margin: 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-08-05 21:37:32 +02:00
|
|
|
|
async def _build_scramble_solo_result(conn, round_id: str, round_row) -> RoundFormatResultOut:
|
|
|
|
|
|
"""Scramble mot enkeltspiller, slagspill (migrasjon 056, 2026-08-05) --
|
|
|
|
|
|
passer IKKE inn i den generiske to-siders-grenen under (den forutsetter
|
|
|
|
|
|
ÉN engine_format/_SIDE_PLAYER_COUNT for HELE formatet og går via
|
|
|
|
|
|
compute_match_state) -- egen, selvstendig sti, samme mønster som High-
|
|
|
|
|
|
low-high/Shamble/Money Ball sine dedikerte grener. REN netto-totalsum-
|
|
|
|
|
|
sammenligning (net_stroke_play_margin), INGEN hull-for-hull-tilstand.
|
|
|
|
|
|
Beregnes ved LESING, ingen cache-kolonne (som Nassau/High-low-high).
|
|
|
|
|
|
"""
|
|
|
|
|
|
side_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id, label, side_role FROM round_side WHERE round_id = $1", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
team_side = next((s for s in side_rows if s["side_role"] == "team"), None)
|
|
|
|
|
|
individual_side = next((s for s in side_rows if s["side_role"] == "individual"), None)
|
|
|
|
|
|
if team_side is None or individual_side is None:
|
|
|
|
|
|
return RoundFormatResultOut(play_format="scramble_solo", ready=False)
|
|
|
|
|
|
|
|
|
|
|
|
team_participants = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id, playing_handicap, COALESCE(guest_name, "
|
|
|
|
|
|
"(SELECT display_name FROM app_user WHERE id = round_participant.user_id), 'Medspiller') AS label "
|
|
|
|
|
|
"FROM round_participant WHERE round_side_id = $1",
|
|
|
|
|
|
team_side["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
individual_participant = await conn.fetchrow(
|
|
|
|
|
|
"SELECT id::text AS id, playing_handicap, COALESCE(guest_name, "
|
|
|
|
|
|
"(SELECT display_name FROM app_user WHERE id = round_participant.user_id), 'Medspiller') AS label "
|
|
|
|
|
|
"FROM round_participant WHERE round_side_id = $1",
|
|
|
|
|
|
individual_side["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
if (
|
|
|
|
|
|
len(team_participants) < 2
|
|
|
|
|
|
or any(p["playing_handicap"] is None for p in team_participants)
|
|
|
|
|
|
or individual_participant is None
|
|
|
|
|
|
or individual_participant["playing_handicap"] is None
|
|
|
|
|
|
):
|
|
|
|
|
|
return RoundFormatResultOut(play_format="scramble_solo", ready=False)
|
|
|
|
|
|
|
|
|
|
|
|
team_playing_handicap = team_participants[0]["playing_handicap"] # samme verdi lagret på alle lagmedlemmer
|
|
|
|
|
|
individual_playing_handicap = individual_participant["playing_handicap"]
|
|
|
|
|
|
|
|
|
|
|
|
all_18_si_by_hole: dict[int, int] = {}
|
|
|
|
|
|
par_by_hole: dict[int, int] = {}
|
|
|
|
|
|
team_gross_by_hole: dict[int, int] = {}
|
|
|
|
|
|
individual_gross_by_hole: dict[int, int] = {}
|
|
|
|
|
|
|
|
|
|
|
|
team_hole_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT hole_number, par, stroke_index, played, score FROM round_hole WHERE round_side_id = $1",
|
|
|
|
|
|
team_side["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
individual_hole_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT hole_number, par, stroke_index, played, score FROM round_hole WHERE round_participant_id = $1",
|
|
|
|
|
|
individual_participant["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
for r in team_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:
|
|
|
|
|
|
team_gross_by_hole[r["hole_number"]] = r["score"]
|
|
|
|
|
|
for r in individual_hole_rows:
|
|
|
|
|
|
all_18_si_by_hole.setdefault(r["hole_number"], r["stroke_index"])
|
|
|
|
|
|
par_by_hole.setdefault(r["hole_number"], r["par"])
|
|
|
|
|
|
if r["played"] and r["score"] is not None:
|
|
|
|
|
|
individual_gross_by_hole[r["hole_number"]] = r["score"]
|
|
|
|
|
|
if len(all_18_si_by_hole) != 18:
|
|
|
|
|
|
return RoundFormatResultOut(play_format="scramble_solo", ready=False)
|
|
|
|
|
|
all_18_si = [all_18_si_by_hole[h] for h in range(1, 19)]
|
|
|
|
|
|
|
|
|
|
|
|
team_strokes_per_hole = dict(
|
|
|
|
|
|
zip(range(1, 19), allocate_over_played_holes(team_playing_handicap, all_18_si, list(range(1, 19))))
|
|
|
|
|
|
)
|
|
|
|
|
|
individual_strokes_per_hole = dict(
|
|
|
|
|
|
zip(range(1, 19), allocate_over_played_holes(individual_playing_handicap, all_18_si, list(range(1, 19))))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
play_order = _round_hole_order(round_row["start_hole"], round_row["holes_planned"])
|
|
|
|
|
|
team_label = team_side["label"] or "Laget"
|
|
|
|
|
|
individual_label = individual_side["label"] or individual_participant["label"]
|
|
|
|
|
|
|
|
|
|
|
|
team_gross_played: list[int] = []
|
|
|
|
|
|
team_received_played: list[int] = []
|
|
|
|
|
|
individual_gross_played: list[int] = []
|
|
|
|
|
|
individual_received_played: list[int] = []
|
|
|
|
|
|
holes: list[FormatHoleOut] = []
|
|
|
|
|
|
for h in play_order:
|
|
|
|
|
|
if h not in team_gross_by_hole or h not in individual_gross_by_hole:
|
|
|
|
|
|
break
|
|
|
|
|
|
team_gross = team_gross_by_hole[h]
|
|
|
|
|
|
individual_gross = individual_gross_by_hole[h]
|
|
|
|
|
|
team_received = team_strokes_per_hole[h]
|
|
|
|
|
|
individual_received = individual_strokes_per_hole[h]
|
|
|
|
|
|
team_gross_played.append(team_gross)
|
|
|
|
|
|
team_received_played.append(team_received)
|
|
|
|
|
|
individual_gross_played.append(individual_gross)
|
|
|
|
|
|
individual_received_played.append(individual_received)
|
|
|
|
|
|
team_net = team_gross - team_received
|
|
|
|
|
|
individual_net = individual_gross - individual_received
|
|
|
|
|
|
holes.append(
|
|
|
|
|
|
FormatHoleOut(
|
|
|
|
|
|
hole_number=h, par=par_by_hole[h], stroke_index=all_18_si_by_hole[h],
|
|
|
|
|
|
entries=[
|
|
|
|
|
|
FormatHoleEntry(
|
|
|
|
|
|
id=team_side["id"], label=team_label, side="a",
|
|
|
|
|
|
gross=team_gross, net=team_net, strokes_received=team_received,
|
|
|
|
|
|
),
|
|
|
|
|
|
FormatHoleEntry(
|
|
|
|
|
|
id=individual_participant["id"], label=individual_label, side="b",
|
|
|
|
|
|
gross=individual_gross, net=individual_net, strokes_received=individual_received,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
result=("a" if team_net < individual_net else "b" if individual_net < team_net else "halved"),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not holes:
|
|
|
|
|
|
return RoundFormatResultOut(play_format="scramble_solo", ready=True, holes=[])
|
|
|
|
|
|
|
|
|
|
|
|
team_net_total = stroke_play_net_total(team_gross_played, team_received_played)
|
|
|
|
|
|
individual_net_total = stroke_play_net_total(individual_gross_played, individual_received_played)
|
|
|
|
|
|
return RoundFormatResultOut(
|
|
|
|
|
|
play_format="scramble_solo",
|
|
|
|
|
|
ready=True,
|
|
|
|
|
|
scramble_solo_team_net=team_net_total,
|
|
|
|
|
|
scramble_solo_individual_net=individual_net_total,
|
|
|
|
|
|
scramble_solo_margin=net_stroke_play_margin(team_net_total, individual_net_total),
|
|
|
|
|
|
holes=holes,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _build_scramble_solo_match_result(conn, round_id: str, round_row) -> RoundFormatResultOut:
|
|
|
|
|
|
"""Scramble mot enkeltspiller, MATCHSPILL (migrasjon 057, 2026-08-05) --
|
|
|
|
|
|
samme asymmetriske lag-/individual-oppsett og samme blandede hull-
|
|
|
|
|
|
oppslag (round_side_id for laget, round_participant_id for
|
|
|
|
|
|
individualspilleren) som _build_scramble_solo_result over, men
|
|
|
|
|
|
sammenligningen er nå ekte matchspill: RELATIVE slag via
|
|
|
|
|
|
match_play_strokes([lag, individualspiller]) i stedet for hver sides
|
|
|
|
|
|
fulle allowance uavhengig (standard matchspill-konvensjon, samme
|
|
|
|
|
|
prinsipp som foursome/chapman/fourball allerede bruker) -- deretter
|
|
|
|
|
|
hull-for-hull HoleResult inn i compute_match_state, IKKE en netto
|
|
|
|
|
|
totalsum. Beregnes ved LESING, ingen cache-kolonne (som Nassau/
|
|
|
|
|
|
High-low-high/scramble_solo)."""
|
|
|
|
|
|
side_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id, label, side_role FROM round_side WHERE round_id = $1", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
team_side = next((s for s in side_rows if s["side_role"] == "team"), None)
|
|
|
|
|
|
individual_side = next((s for s in side_rows if s["side_role"] == "individual"), None)
|
|
|
|
|
|
if team_side is None or individual_side is None:
|
|
|
|
|
|
return RoundFormatResultOut(play_format="scramble_solo_match", ready=False)
|
|
|
|
|
|
|
|
|
|
|
|
team_participants = await conn.fetch(
|
|
|
|
|
|
"SELECT id::text AS id, playing_handicap, COALESCE(guest_name, "
|
|
|
|
|
|
"(SELECT display_name FROM app_user WHERE id = round_participant.user_id), 'Medspiller') AS label "
|
|
|
|
|
|
"FROM round_participant WHERE round_side_id = $1",
|
|
|
|
|
|
team_side["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
individual_participant = await conn.fetchrow(
|
|
|
|
|
|
"SELECT id::text AS id, playing_handicap, COALESCE(guest_name, "
|
|
|
|
|
|
"(SELECT display_name FROM app_user WHERE id = round_participant.user_id), 'Medspiller') AS label "
|
|
|
|
|
|
"FROM round_participant WHERE round_side_id = $1",
|
|
|
|
|
|
individual_side["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
if (
|
|
|
|
|
|
len(team_participants) < 2
|
|
|
|
|
|
or any(p["playing_handicap"] is None for p in team_participants)
|
|
|
|
|
|
or individual_participant is None
|
|
|
|
|
|
or individual_participant["playing_handicap"] is None
|
|
|
|
|
|
):
|
|
|
|
|
|
return RoundFormatResultOut(play_format="scramble_solo_match", ready=False)
|
|
|
|
|
|
|
|
|
|
|
|
team_playing_handicap = team_participants[0]["playing_handicap"] # samme verdi lagret på alle lagmedlemmer
|
|
|
|
|
|
individual_playing_handicap = individual_participant["playing_handicap"]
|
|
|
|
|
|
# Matchspill-konvensjon (R&A Appendix C, samme som resten av appens
|
|
|
|
|
|
# to-sidede formater): RELATIVE slag, ikke hver sides fulle absolutte
|
|
|
|
|
|
# allowance uavhengig -- dette er den ENESTE forskjellen fra
|
|
|
|
|
|
# _build_scramble_solo_result sin handicap-håndtering.
|
|
|
|
|
|
team_relative, individual_relative = match_play_strokes([team_playing_handicap, individual_playing_handicap])
|
|
|
|
|
|
|
|
|
|
|
|
# "a"/"b" avgjøres av SORTERT side-id, IKKE en fast lag=a/individuell=b-
|
|
|
|
|
|
# tildeling -- samme konvensjon som ALLE andre to-sidede formater i
|
|
|
|
|
|
# appen (generiske grenen under bruker sorted(relative.keys()), High-
|
|
|
|
|
|
# low-high bruker sorted(by_side.keys())). round.sides hentes ALLTID
|
|
|
|
|
|
# `ORDER BY id` frontend-siden (_load_round_out), så frontend sin
|
|
|
|
|
|
# TwoSidedBoard/buildSideTeams (som brukes UENDRET for dette formatet,
|
|
|
|
|
|
# se new-round.tsx/round-leaderboard.tsx) forutsetter at backend sin
|
|
|
|
|
|
# "a" alltid er round.sides[0] -- en fast lag=a-tildeling ville gitt
|
|
|
|
|
|
# FEIL ledelsesfarge når laget tilfeldigvis IKKE er den lavest-
|
|
|
|
|
|
# sorterte UUID-en.
|
|
|
|
|
|
team_is_a = team_side["id"] < individual_side["id"]
|
|
|
|
|
|
team_side_label_tag: Literal["a", "b"] = "a" if team_is_a else "b"
|
|
|
|
|
|
individual_side_label_tag: Literal["a", "b"] = "b" if team_is_a else "a"
|
|
|
|
|
|
|
|
|
|
|
|
all_18_si_by_hole: dict[int, int] = {}
|
|
|
|
|
|
par_by_hole: dict[int, int] = {}
|
|
|
|
|
|
team_gross_by_hole: dict[int, int] = {}
|
|
|
|
|
|
individual_gross_by_hole: dict[int, int] = {}
|
|
|
|
|
|
|
|
|
|
|
|
team_hole_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT hole_number, par, stroke_index, played, score FROM round_hole WHERE round_side_id = $1",
|
|
|
|
|
|
team_side["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
individual_hole_rows = await conn.fetch(
|
|
|
|
|
|
"SELECT hole_number, par, stroke_index, played, score FROM round_hole WHERE round_participant_id = $1",
|
|
|
|
|
|
individual_participant["id"],
|
|
|
|
|
|
)
|
|
|
|
|
|
for r in team_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:
|
|
|
|
|
|
team_gross_by_hole[r["hole_number"]] = r["score"]
|
|
|
|
|
|
for r in individual_hole_rows:
|
|
|
|
|
|
all_18_si_by_hole.setdefault(r["hole_number"], r["stroke_index"])
|
|
|
|
|
|
par_by_hole.setdefault(r["hole_number"], r["par"])
|
|
|
|
|
|
if r["played"] and r["score"] is not None:
|
|
|
|
|
|
individual_gross_by_hole[r["hole_number"]] = r["score"]
|
|
|
|
|
|
if len(all_18_si_by_hole) != 18:
|
|
|
|
|
|
return RoundFormatResultOut(play_format="scramble_solo_match", ready=False)
|
|
|
|
|
|
all_18_si = [all_18_si_by_hole[h] for h in range(1, 19)]
|
|
|
|
|
|
|
|
|
|
|
|
team_strokes_per_hole = dict(
|
|
|
|
|
|
zip(range(1, 19), allocate_over_played_holes(team_relative, all_18_si, list(range(1, 19))))
|
|
|
|
|
|
)
|
|
|
|
|
|
individual_strokes_per_hole = dict(
|
|
|
|
|
|
zip(range(1, 19), allocate_over_played_holes(individual_relative, all_18_si, list(range(1, 19))))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
play_order = _round_hole_order(round_row["start_hole"], round_row["holes_planned"])
|
|
|
|
|
|
team_label = team_side["label"] or "Laget"
|
|
|
|
|
|
individual_label = individual_side["label"] or individual_participant["label"]
|
|
|
|
|
|
|
|
|
|
|
|
hole_results: list[HoleResult] = []
|
|
|
|
|
|
holes: list[FormatHoleOut] = []
|
|
|
|
|
|
for h in play_order:
|
|
|
|
|
|
if h not in team_gross_by_hole or h not in individual_gross_by_hole:
|
|
|
|
|
|
break
|
|
|
|
|
|
team_gross = team_gross_by_hole[h]
|
|
|
|
|
|
individual_gross = individual_gross_by_hole[h]
|
|
|
|
|
|
team_received = team_strokes_per_hole[h]
|
|
|
|
|
|
individual_received = individual_strokes_per_hole[h]
|
|
|
|
|
|
team_net = team_gross - team_received
|
|
|
|
|
|
individual_net = individual_gross - individual_received
|
|
|
|
|
|
a_net, b_net = (team_net, individual_net) if team_is_a else (individual_net, team_net)
|
|
|
|
|
|
if a_net < b_net:
|
|
|
|
|
|
hole_results.append(HoleResult.SIDE_A)
|
|
|
|
|
|
elif b_net < a_net:
|
|
|
|
|
|
hole_results.append(HoleResult.SIDE_B)
|
|
|
|
|
|
else:
|
|
|
|
|
|
hole_results.append(HoleResult.HALVED)
|
|
|
|
|
|
holes.append(
|
|
|
|
|
|
FormatHoleOut(
|
|
|
|
|
|
hole_number=h, par=par_by_hole[h], stroke_index=all_18_si_by_hole[h],
|
|
|
|
|
|
entries=[
|
|
|
|
|
|
FormatHoleEntry(
|
|
|
|
|
|
id=team_side["id"], label=team_label, side=team_side_label_tag,
|
|
|
|
|
|
gross=team_gross, net=team_net, strokes_received=team_received,
|
|
|
|
|
|
),
|
|
|
|
|
|
FormatHoleEntry(
|
|
|
|
|
|
id=individual_participant["id"], label=individual_label, side=individual_side_label_tag,
|
|
|
|
|
|
gross=individual_gross, net=individual_net, strokes_received=individual_received,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
result=_RESULT_LABEL[hole_results[-1]],
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not holes:
|
|
|
|
|
|
return RoundFormatResultOut(play_format="scramble_solo_match", ready=True, holes=[])
|
|
|
|
|
|
|
|
|
|
|
|
state = compute_match_state(hole_results, total_holes=round_row["holes_planned"])
|
|
|
|
|
|
return RoundFormatResultOut(
|
|
|
|
|
|
play_format="scramble_solo_match",
|
|
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
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-08-05 21:37:32 +02:00
|
|
|
|
if play_format == "scramble_solo":
|
|
|
|
|
|
return await _build_scramble_solo_result(conn, round_id, round_row)
|
|
|
|
|
|
|
|
|
|
|
|
if play_format == "scramble_solo_match":
|
|
|
|
|
|
return await _build_scramble_solo_match_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)
|
2026-08-10 18:34:31 +02:00
|
|
|
|
# Optimistisk versjonssjekk (ADR-057). None = hopp over sjekken (samme
|
|
|
|
|
|
# bakoverkompatible mønster som resten av appens valgfrie felt) --
|
|
|
|
|
|
# klienten sender den alltid i praksis.
|
|
|
|
|
|
expected_version: int | 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
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-08-10 18:34:31 +02:00
|
|
|
|
score = max_hole_score_for_handicap(
|
|
|
|
|
|
this_hole_par, strokes_received, course_handicap=participant_row["course_handicap_snapshot"]
|
|
|
|
|
|
)
|
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
|
|
|
|
|
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,
|
2026-08-10 18:34:31 +02:00
|
|
|
|
anyway_strokes = $14, version = version + 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
|
|
|
|
WHERE round_participant_id = $1 AND hole_number = $2
|
2026-08-10 18:34:31 +02:00
|
|
|
|
AND ($15::int IS NULL OR version = $15::int)
|
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-08-10 18:34:31 +02:00
|
|
|
|
penalty_strokes, first_putt_distance_bucket, anyway_strokes, version
|
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,
|
2026-08-10 18:34:31 +02:00
|
|
|
|
body.anyway_strokes, body.expected_version,
|
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:
|
2026-08-10 18:34:31 +02:00
|
|
|
|
# this_hole_row over har allerede bekreftet at hullet FINNES --
|
|
|
|
|
|
# et None-resultat her betyr dermed alltid versjonskonflikt, ikke
|
|
|
|
|
|
# "ikke funnet" (ingen tvetydighet å skille, ulikt update_side_hole
|
|
|
|
|
|
# som ikke har en tilsvarende forhåndssjekk).
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
409, "STALE_VERSION",
|
|
|
|
|
|
"Noen andre har endret dette hullet i mellomtiden. Laster inn siste versjon.",
|
|
|
|
|
|
)
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-08-07 22:02:56 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Slag-for-slag GPS-avstandsmåling (ADR-048) -- helt frikoblet fra
|
|
|
|
|
|
# round_hole sine faste hull-aggregater over. round_shot kjenner kun
|
|
|
|
|
|
# round_hole_id, og arver dermed eierskap (deltaker ELLER side) transitivt
|
|
|
|
|
|
# via round_hole sin eksisterende round_participant_id/round_side_id-XOR
|
|
|
|
|
|
# (migrasjon 031) -- ingen egen XOR-sjekk trengs her, se 060_round_shot.sql.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class ShotIn(BaseModel):
|
|
|
|
|
|
club: str = Field(max_length=50)
|
|
|
|
|
|
distance_meters: float = Field(gt=0, lt=500)
|
|
|
|
|
|
start_method: Literal["gps", "map_tap"]
|
|
|
|
|
|
start_lat: float = Field(ge=-90, le=90)
|
|
|
|
|
|
start_lng: float = Field(ge=-180, le=180)
|
2026-08-08 21:38:10 +02:00
|
|
|
|
# end_method lagt til 2026-08-08 (ADR-048-tillegg): sluttpunktet var
|
|
|
|
|
|
# opprinnelig ALLTID GPS -- default holder eldre klienter fungerende
|
|
|
|
|
|
# uten å kreve feltet, men appen sender det alltid eksplisitt nå.
|
|
|
|
|
|
end_method: Literal["gps", "map_tap"] = "gps"
|
2026-08-07 22:02:56 +02:00
|
|
|
|
end_lat: float = Field(ge=-90, le=90)
|
|
|
|
|
|
end_lng: float = Field(ge=-180, le=180)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ShotOut(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
shot_number: int
|
|
|
|
|
|
club: str
|
|
|
|
|
|
distance_meters: float
|
|
|
|
|
|
start_method: str
|
|
|
|
|
|
start_lat: float
|
|
|
|
|
|
start_lng: float
|
2026-08-08 21:38:10 +02:00
|
|
|
|
end_method: str
|
2026-08-07 22:02:56 +02:00
|
|
|
|
end_lat: float
|
|
|
|
|
|
end_lng: float
|
|
|
|
|
|
shared_round_message_id: str | None
|
|
|
|
|
|
# Avledet fra shared_round_message_id -> round_message.image_key, ikke
|
|
|
|
|
|
# lagret på round_shot selv (samme "aldri lagre det som kan avledes"-
|
|
|
|
|
|
# prinsipp som storage.public_url() ellers brukes for).
|
|
|
|
|
|
shared_image_url: str | None
|
|
|
|
|
|
captured_at: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SHOT_SELECT = """
|
|
|
|
|
|
SELECT rs.id::text AS id, rs.shot_number, rs.club, rs.distance_meters, rs.start_method,
|
2026-08-08 21:38:10 +02:00
|
|
|
|
rs.start_lat, rs.start_lng, rs.end_method, rs.end_lat, rs.end_lng,
|
2026-08-07 22:02:56 +02:00
|
|
|
|
rs.shared_round_message_id::text AS shared_round_message_id,
|
|
|
|
|
|
rs.captured_at, rm.image_key AS shared_image_key
|
|
|
|
|
|
FROM round_shot rs
|
|
|
|
|
|
LEFT JOIN round_message rm ON rm.id = rs.shared_round_message_id
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _shot_out(row: asyncpg.Record) -> ShotOut:
|
|
|
|
|
|
return ShotOut(
|
|
|
|
|
|
id=row["id"],
|
|
|
|
|
|
shot_number=row["shot_number"],
|
|
|
|
|
|
club=row["club"],
|
|
|
|
|
|
distance_meters=float(row["distance_meters"]),
|
|
|
|
|
|
start_method=row["start_method"],
|
|
|
|
|
|
start_lat=row["start_lat"],
|
|
|
|
|
|
start_lng=row["start_lng"],
|
2026-08-08 21:38:10 +02:00
|
|
|
|
end_method=row["end_method"],
|
2026-08-07 22:02:56 +02:00
|
|
|
|
end_lat=row["end_lat"],
|
|
|
|
|
|
end_lng=row["end_lng"],
|
|
|
|
|
|
shared_round_message_id=row["shared_round_message_id"],
|
|
|
|
|
|
shared_image_url=storage.public_url(row["shared_image_key"]) if row["shared_image_key"] else None,
|
|
|
|
|
|
captured_at=row["captured_at"].isoformat(),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _resolve_round_message_author_name(conn, user_id: str) -> str:
|
|
|
|
|
|
"""Frosset ved skrivetidspunkt, samme prinsipp som
|
|
|
|
|
|
`message.author_display_name` -- her FULLT navn (forfatter-liste, ikke
|
|
|
|
|
|
direkte adressering, CLAUDE.md navneformat-regel). Ingen org-scopet
|
|
|
|
|
|
`player.display_name` å falle tilbake på her (runder er ikke
|
|
|
|
|
|
org-scopet) -- `app_user.display_name` er NOT NULL og alltid til
|
|
|
|
|
|
stede, så ingen ekstra fallback trengs.
|
|
|
|
|
|
|
|
|
|
|
|
Flyttet hit fra round_messages.py (som importerer den tilbake herfra)
|
|
|
|
|
|
slik at share_shot under kan gjenbruke den uten sirkulær import --
|
|
|
|
|
|
rounds.py var allerede den importerte, ikke-importerende parten."""
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT first_name, last_name, display_name FROM app_user WHERE id = $1", user_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if row["first_name"] and row["last_name"]:
|
|
|
|
|
|
return f"{row['first_name']} {row['last_name']}"
|
|
|
|
|
|
return row["display_name"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _resolve_participant_hole_id(conn, round_id: str, participant_id: str, hole_number: int) -> str:
|
|
|
|
|
|
participant_exists = await conn.fetchval(
|
|
|
|
|
|
"SELECT 1 FROM round_participant WHERE id = $1 AND round_id = $2", participant_id, round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if not participant_exists:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Deltakeren finnes ikke på denne runden.")
|
|
|
|
|
|
hole_id = await conn.fetchval(
|
|
|
|
|
|
"SELECT id::text FROM round_hole WHERE round_participant_id = $1 AND hole_number = $2",
|
|
|
|
|
|
participant_id, hole_number,
|
|
|
|
|
|
)
|
|
|
|
|
|
if hole_id is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Hullet finnes ikke på denne deltakeren.")
|
|
|
|
|
|
return hole_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _resolve_side_hole_id(conn, round_id: str, side_id: str, hole_number: int) -> str:
|
|
|
|
|
|
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.")
|
|
|
|
|
|
hole_id = await conn.fetchval(
|
|
|
|
|
|
"SELECT id::text FROM round_hole WHERE round_side_id = $1 AND hole_number = $2",
|
|
|
|
|
|
side_id, hole_number,
|
|
|
|
|
|
)
|
|
|
|
|
|
if hole_id is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Hullet finnes ikke på denne siden.")
|
|
|
|
|
|
return hole_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _list_shots_for_hole(conn, hole_id: str) -> list[ShotOut]:
|
|
|
|
|
|
rows = await conn.fetch(_SHOT_SELECT + " WHERE rs.round_hole_id = $1 ORDER BY rs.shot_number", hole_id)
|
|
|
|
|
|
return [_shot_out(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _insert_shot(conn, hole_id: str, user_id: str, body: ShotIn) -> ShotOut:
|
|
|
|
|
|
async with translate_db_errors():
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO round_shot (
|
|
|
|
|
|
round_hole_id, recorded_by_user_id, shot_number, club, distance_meters,
|
2026-08-08 21:38:10 +02:00
|
|
|
|
start_method, start_lat, start_lng, end_method, end_lat, end_lng
|
2026-08-07 22:02:56 +02:00
|
|
|
|
)
|
|
|
|
|
|
VALUES (
|
|
|
|
|
|
$1, $2,
|
|
|
|
|
|
COALESCE((SELECT MAX(shot_number) FROM round_shot WHERE round_hole_id = $1), 0) + 1,
|
2026-08-08 21:38:10 +02:00
|
|
|
|
$3, $4, $5, $6, $7, $8, $9, $10
|
2026-08-07 22:02:56 +02:00
|
|
|
|
)
|
|
|
|
|
|
RETURNING id::text AS id
|
|
|
|
|
|
""",
|
|
|
|
|
|
hole_id, user_id, body.club, body.distance_meters,
|
2026-08-08 21:38:10 +02:00
|
|
|
|
body.start_method, body.start_lat, body.start_lng,
|
|
|
|
|
|
body.end_method, body.end_lat, body.end_lng,
|
2026-08-07 22:02:56 +02:00
|
|
|
|
)
|
|
|
|
|
|
full_row = await conn.fetchrow(_SHOT_SELECT + " WHERE rs.id = $1", row["id"])
|
|
|
|
|
|
return _shot_out(full_row)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
|
"/rounds/{round_id}/participants/{participant_id}/holes/{hole_number}/shots",
|
|
|
|
|
|
response_model=list[ShotOut],
|
|
|
|
|
|
)
|
|
|
|
|
|
async def list_participant_shots(
|
|
|
|
|
|
round_id: str, participant_id: str, hole_number: int,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> list[ShotOut]:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
hole_id = await _resolve_participant_hole_id(conn, round_id, participant_id, hole_number)
|
|
|
|
|
|
return await _list_shots_for_hole(conn, hole_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
|
"/rounds/{round_id}/participants/{participant_id}/holes/{hole_number}/shots",
|
|
|
|
|
|
response_model=ShotOut,
|
|
|
|
|
|
status_code=201,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def create_participant_shot(
|
|
|
|
|
|
round_id: str, participant_id: str, hole_number: int, body: ShotIn,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> ShotOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
# Samme begrunnelse som update_hole -- en lenket medspiller kan
|
|
|
|
|
|
# måle slag for hele flighten, ikke bare sin egen rad.
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
hole_id = await _resolve_participant_hole_id(conn, round_id, participant_id, hole_number)
|
|
|
|
|
|
shot = await _insert_shot(conn, hole_id, user.user_id, body)
|
|
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
return shot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
|
"/rounds/{round_id}/sides/{side_id}/holes/{hole_number}/shots",
|
|
|
|
|
|
response_model=list[ShotOut],
|
|
|
|
|
|
)
|
|
|
|
|
|
async def list_side_shots(
|
|
|
|
|
|
round_id: str, side_id: str, hole_number: int,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> list[ShotOut]:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
hole_id = await _resolve_side_hole_id(conn, round_id, side_id, hole_number)
|
|
|
|
|
|
return await _list_shots_for_hole(conn, hole_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
|
"/rounds/{round_id}/sides/{side_id}/holes/{hole_number}/shots",
|
|
|
|
|
|
response_model=ShotOut,
|
|
|
|
|
|
status_code=201,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def create_side_shot(
|
|
|
|
|
|
round_id: str, side_id: str, hole_number: int, body: ShotIn,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> ShotOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
hole_id = await _resolve_side_hole_id(conn, round_id, side_id, hole_number)
|
|
|
|
|
|
shot = await _insert_shot(conn, hole_id, user.user_id, body)
|
|
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
return shot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/rounds/{round_id}/shots/{shot_id}", status_code=204)
|
|
|
|
|
|
async def delete_shot(
|
|
|
|
|
|
round_id: str, shot_id: str, user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
# Eierskap-agnostisk sletting -- round_shot kjenner kun round_hole_id,
|
|
|
|
|
|
# så tilhørighet til DENNE runden bekreftes via round_hole sin
|
|
|
|
|
|
# round_participant- ELLER round_side-kjede (forsvar mot id-gjetting
|
|
|
|
|
|
# på tvers av runder).
|
|
|
|
|
|
deleted = await conn.fetchval(
|
|
|
|
|
|
"""
|
|
|
|
|
|
DELETE FROM round_shot rs
|
|
|
|
|
|
USING round_hole rh
|
|
|
|
|
|
LEFT JOIN round_participant rp ON rp.id = rh.round_participant_id
|
|
|
|
|
|
LEFT JOIN round_side rsd ON rsd.id = rh.round_side_id
|
|
|
|
|
|
WHERE rs.round_hole_id = rh.id
|
|
|
|
|
|
AND rs.id = $1
|
|
|
|
|
|
AND COALESCE(rp.round_id, rsd.round_id) = $2
|
|
|
|
|
|
RETURNING rs.id
|
|
|
|
|
|
""",
|
|
|
|
|
|
shot_id, round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if deleted is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Slaget finnes ikke på denne runden.")
|
|
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _encode_polyline(points: list[tuple[float, float]]) -> str:
|
|
|
|
|
|
"""Google sin polyline-algoritme (5 desimaler) -- kun det appen faktisk
|
|
|
|
|
|
trenger, nøyaktig to punkter for et rett slag-overlay på Mapbox Static
|
|
|
|
|
|
Images API (path-overlay-parameteren krever encoded polyline, se
|
|
|
|
|
|
https://docs.mapbox.com/api/maps/static-images/)."""
|
|
|
|
|
|
result: list[str] = []
|
|
|
|
|
|
prev_lat = prev_lng = 0
|
|
|
|
|
|
for lat, lng in points:
|
|
|
|
|
|
lat_i = round(lat * 1e5)
|
|
|
|
|
|
lng_i = round(lng * 1e5)
|
|
|
|
|
|
for value, prev in ((lat_i, prev_lat), (lng_i, prev_lng)):
|
|
|
|
|
|
delta = value - prev
|
|
|
|
|
|
shifted = ~(delta << 1) if delta < 0 else (delta << 1)
|
|
|
|
|
|
chunk = ""
|
|
|
|
|
|
while shifted >= 0x20:
|
|
|
|
|
|
chunk += chr((0x20 | (shifted & 0x1F)) + 63)
|
|
|
|
|
|
shifted >>= 5
|
|
|
|
|
|
chunk += chr(shifted + 63)
|
|
|
|
|
|
result.append(chunk)
|
|
|
|
|
|
prev_lat, prev_lng = lat_i, lng_i
|
|
|
|
|
|
return "".join(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mapbox_static_snippet_url(start_lat: float, start_lng: float, end_lat: float, end_lng: float) -> str:
|
|
|
|
|
|
encoded = urllib.parse.quote(_encode_polyline([(start_lat, start_lng), (end_lat, end_lng)]), safe="")
|
|
|
|
|
|
overlay = (
|
|
|
|
|
|
f"pin-s-a+ff5a1f({start_lng},{start_lat}),"
|
|
|
|
|
|
f"pin-s-b+2f7a3f({end_lng},{end_lat}),"
|
|
|
|
|
|
f"path-4+ff5a1f-0.9({encoded})"
|
|
|
|
|
|
)
|
|
|
|
|
|
return (
|
|
|
|
|
|
f"https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12/static/{overlay}/auto/600x400@2x"
|
|
|
|
|
|
f"?padding=60&access_token={settings.MAPBOX_SECRET_TOKEN}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ShotShareIn(BaseModel):
|
|
|
|
|
|
# Brukerredigert, forhåndsutfylt i klienten med en auto-generert tekst
|
|
|
|
|
|
# ("⛳ {klubb}, {avstand} m – hull {n}") -- helt fritt herfra, samme
|
|
|
|
|
|
# "ingen lengdebegrensning utover dette" som round_messages.py.
|
|
|
|
|
|
body: str = Field(max_length=2000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/rounds/{round_id}/shots/{shot_id}/share", response_model=ShotOut)
|
|
|
|
|
|
async def share_shot(
|
|
|
|
|
|
round_id: str, shot_id: str, body: ShotShareIn, user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> ShotOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
shot_row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT rs.id::text AS id, rs.start_lat, rs.start_lng, rs.end_lat, rs.end_lng
|
|
|
|
|
|
FROM round_shot rs
|
|
|
|
|
|
JOIN round_hole rh ON rh.id = rs.round_hole_id
|
|
|
|
|
|
LEFT JOIN round_participant rp ON rp.id = rh.round_participant_id
|
|
|
|
|
|
LEFT JOIN round_side rsd ON rsd.id = rh.round_side_id
|
|
|
|
|
|
WHERE rs.id = $1 AND COALESCE(rp.round_id, rsd.round_id) = $2
|
|
|
|
|
|
""",
|
|
|
|
|
|
shot_id, round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if shot_row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Slaget finnes ikke på denne runden.")
|
|
|
|
|
|
|
|
|
|
|
|
author_display_name = await _resolve_round_message_author_name(conn, user.user_id)
|
|
|
|
|
|
|
|
|
|
|
|
image_key = None
|
|
|
|
|
|
if settings.MAPBOX_SECRET_TOKEN:
|
|
|
|
|
|
url = _mapbox_static_snippet_url(
|
|
|
|
|
|
shot_row["start_lat"], shot_row["start_lng"], shot_row["end_lat"], shot_row["end_lng"]
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=5.0)) as client:
|
|
|
|
|
|
resp = await client.get(url)
|
|
|
|
|
|
resp.raise_for_status()
|
|
|
|
|
|
image_key = await storage.upload_image("round_messages", round_id, resp.content)
|
|
|
|
|
|
except httpx.HTTPError:
|
|
|
|
|
|
# Deling degraderer grasiøst til ren tekst uten satellitt-
|
|
|
|
|
|
# utsnitt -- samme prinsipp som SMTP/push (config.py) --
|
|
|
|
|
|
# fremfor å blokkere hele delingen på et Mapbox-utfall.
|
|
|
|
|
|
image_key = None
|
|
|
|
|
|
|
|
|
|
|
|
async with translate_db_errors():
|
|
|
|
|
|
message_row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO round_message (round_id, author_user_id, author_display_name, body, image_key)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
|
|
|
|
RETURNING id::text AS id
|
|
|
|
|
|
""",
|
|
|
|
|
|
round_id, user.user_id, author_display_name, body.body, image_key,
|
|
|
|
|
|
)
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"UPDATE round_shot SET shared_round_message_id = $1 WHERE id = $2",
|
|
|
|
|
|
message_row["id"], shot_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
full_row = await conn.fetchrow(_SHOT_SELECT + " WHERE rs.id = $1", shot_id)
|
|
|
|
|
|
await broadcast_round_update(round_id)
|
|
|
|
|
|
return _shot_out(full_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
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 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,
|
2026-08-13 21:24:25 +02:00
|
|
|
|
slope_rating_snapshot, tee_par_snapshot, completed_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
|
|
|
|
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]
|
2026-08-10 18:34:31 +02:00
|
|
|
|
ags = adjusted_gross_score(
|
|
|
|
|
|
scores, pars, strokes_received, course_handicap=_course_handicap_from_row(p)
|
|
|
|
|
|
)
|
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
|
|
|
|
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
|
|
|
|
|
2026-08-13 21:24:25 +02:00
|
|
|
|
# Per-deltaker avslutning (migrasjon 066) -- fallback for deltakere
|
|
|
|
|
|
# som ALDRI ble individuelt avsluttet underveis (det vanlige
|
|
|
|
|
|
# tilfellet: alle spilte alle hull, ingen trykket "avslutt for meg"
|
|
|
|
|
|
# på egen hånd). Setter completed_at + sender scorekort-e-post NÅ
|
|
|
|
|
|
# for dem -- deltakere som ALLEREDE ble avsluttet+e-postet tidligere
|
|
|
|
|
|
# (completed_at allerede satt) røres ikke, ingen dobbel-sending.
|
|
|
|
|
|
round_label = (round_row["name"] or "").strip() or round_row["course_name_snapshot"]
|
|
|
|
|
|
for p in participants:
|
|
|
|
|
|
if p["completed_at"] is None:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"UPDATE round_participant SET completed_at = now() WHERE id = $1", p["id"]
|
|
|
|
|
|
)
|
|
|
|
|
|
await _send_participant_round_summary(conn, round_id, p["id"], round_label)
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
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-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-13 21:24:25 +02:00
|
|
|
|
async def _send_participant_round_summary(conn, round_id: str, participant_id: str, round_label: str) -> bool:
|
|
|
|
|
|
"""Scorekort på e-post til ÉN deltaker. Returnerer True hvis en adresse
|
|
|
|
|
|
fantes og sending faktisk ble forsøkt, False hvis deltakeren ikke har
|
|
|
|
|
|
noen registrert e-post (kalleren kan bruke dette til å gi tydelig
|
|
|
|
|
|
tilbakemelding ved en EKSPLISITT send-handling, se
|
|
|
|
|
|
send_participant_scorecard under -- de to andre kallerne, update_participant
|
|
|
|
|
|
og complete_round, ignorerer returverdien, samme "stille hvis ingen
|
|
|
|
|
|
adresse"-oppførsel som før 2026-08-13).
|
|
|
|
|
|
|
|
|
|
|
|
Tre kallere (migrasjon 066, 2026-08-13):
|
|
|
|
|
|
1. UMIDDELBART når DENNE deltakeren avsluttes (update_participant).
|
|
|
|
|
|
2. complete_round sin fallback for deltakere som aldri ble individuelt
|
|
|
|
|
|
avsluttet.
|
|
|
|
|
|
3. `POST .../participants/{id}/send-scorecard` -- eksplisitt, PÅ NYTT-
|
|
|
|
|
|
sending når som helst, ogsÅ for en runde som ble fullført for lenge
|
|
|
|
|
|
siden (bekreftet med bruker: må kunne gå inn i tidligere fullførte
|
|
|
|
|
|
runder og sende scorekort til enkeltspillere).
|
|
|
|
|
|
|
|
|
|
|
|
Dekker BÅDE gjester (guest_email) OG lenkede brukere (app_user.email) --
|
|
|
|
|
|
opprinnelig (2026-08-03) kun gjester. 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."""
|
|
|
|
|
|
g = await conn.fetchrow(
|
2026-08-03 06:24:57 +02:00
|
|
|
|
"""
|
2026-08-13 21:24:25 +02:00
|
|
|
|
SELECT rp.id::text AS id, rp.guest_email, rp.guest_first_name,
|
|
|
|
|
|
rp.round_side_id::text AS round_side_id, rp.stat_level,
|
|
|
|
|
|
rp.user_id::text AS user_id, au.email::text AS account_email, au.first_name AS account_first_name
|
|
|
|
|
|
FROM round_participant rp
|
|
|
|
|
|
LEFT JOIN app_user au ON au.id = rp.user_id
|
|
|
|
|
|
WHERE rp.id = $1 AND rp.round_id = $2
|
2026-08-03 06:24:57 +02:00
|
|
|
|
""",
|
2026-08-13 21:24:25 +02:00
|
|
|
|
participant_id, round_id,
|
2026-08-03 06:24:57 +02:00
|
|
|
|
)
|
2026-08-13 21:24:25 +02:00
|
|
|
|
if g is None:
|
|
|
|
|
|
return False
|
|
|
|
|
|
is_linked = g["user_id"] is not None
|
|
|
|
|
|
email = (g["account_email"] if is_linked else g["guest_email"]) or None
|
|
|
|
|
|
if email is None:
|
|
|
|
|
|
return False # ingen registrert adresse -- ingenting å sende (vanligste tilfellet)
|
|
|
|
|
|
first_name = (g["account_first_name"] if is_linked else g["guest_first_name"]) or ("Der" if is_linked else "Gjest")
|
|
|
|
|
|
|
|
|
|
|
|
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 = email.strip().lower()
|
|
|
|
|
|
raw_token: str | None = None
|
|
|
|
|
|
if not is_linked:
|
|
|
|
|
|
# Magic-link kun for gjester -- en lenket bruker har allerede en
|
|
|
|
|
|
# konto og logger inn som vanlig, se send_round_summary_email sin
|
|
|
|
|
|
# is_linked_account-gren.
|
2026-08-03 06:24:57 +02:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
2026-08-13 21:24:25 +02:00
|
|
|
|
if settings.SMTP_CONFIGURED:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await send_round_summary_email(
|
|
|
|
|
|
email, first_name, round_label, summary_holes, stat_lines, raw_token,
|
|
|
|
|
|
is_linked_account=is_linked, round_id=round_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
# Se app/email.py sitt mønster -- en driftsfeil i selve
|
|
|
|
|
|
# utsendingen skal aldri hindre fullføringen/avslutningen.
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
elif settings.DEV_LOG_MAGIC_LINKS:
|
|
|
|
|
|
if raw_token:
|
2026-08-03 06:24:57 +02:00
|
|
|
|
print(f"[DEV] Magic link for {email} (nb): {raw_token}", flush=True)
|
2026-08-13 21:24:25 +02:00
|
|
|
|
print(f"[DEV] Rundeoppsummering til {email}: {round_label}, {len(played_holes)} hull spilt", flush=True)
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/rounds/{round_id}/participants/{participant_id}/send-scorecard", status_code=204)
|
|
|
|
|
|
async def send_participant_scorecard(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
participant_id: str,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""Eksplisitt, på nytt-sending av scorekort-e-post -- NÅR SOM HELST,
|
|
|
|
|
|
også for en runde som ble fullført for lenge siden (bekreftet med
|
|
|
|
|
|
bruker 2026-08-13: må kunne gå inn i tidligere fullførte runder og
|
|
|
|
|
|
sende scorekort til enkeltspillere). Ingen ALREADY_COMPLETED-sperre
|
|
|
|
|
|
her (den gjelder kun selve completed-TOGGELEN, se update_participant)
|
|
|
|
|
|
-- denne handlingen endrer ingenting i databasen, kun en e-post ut.
|
|
|
|
|
|
Samme flight-styrte tilgang som update_participant sin `completed`-gren
|
|
|
|
|
|
(owner ELLER en hvilken som helst lenket medspiller kan sende for en
|
|
|
|
|
|
hvilken som helst deltaker, ikke bare seg selv)."""
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_accessible_round_or_404(conn, round_id, user.user_id)
|
|
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
|
"SELECT name, course_name_snapshot FROM round WHERE id = $1", round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if round_row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Runden finnes ikke.")
|
|
|
|
|
|
participant_exists = await conn.fetchval(
|
|
|
|
|
|
"SELECT 1 FROM round_participant WHERE id = $1 AND round_id = $2", participant_id, round_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if not participant_exists:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Deltakeren finnes ikke på denne runden.")
|
|
|
|
|
|
round_label = (round_row["name"] or "").strip() or round_row["course_name_snapshot"]
|
|
|
|
|
|
sent = await _send_participant_round_summary(conn, round_id, participant_id, round_label)
|
|
|
|
|
|
if not sent:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "NO_EMAIL_ADDRESS", "Denne spilleren har ingen registrert e-postadresse å sende til."
|
|
|
|
|
|
)
|
2026-08-03 06:24:57 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-08-07 22:02:56 +02:00
|
|
|
|
# Lagt til 2026-08-06 -- RoundMessages (ADR-044) sin "forfatter ELLER
|
|
|
|
|
|
# rundeeier kan slette"-sjekk trenger eierens app_user.id, ikke bare
|
|
|
|
|
|
# visningsnavnet. Ikke en større personvern-innrømmelse enn navnet
|
|
|
|
|
|
# selv (allerede eksponert over) -- en ugjennomsiktig UUID, sammenlignet
|
|
|
|
|
|
# mot innlogget bruker sin egen id, avslører ingenting nytt.
|
|
|
|
|
|
owner_user_id: str
|
2026-07-28 19:25:37 +02:00
|
|
|
|
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",
|
2026-08-07 22:02:56 +02:00
|
|
|
|
owner_user_id=round_row["owner_user_id"],
|
2026-07-28 19:25:37 +02:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-07 22:02:56 +02:00
|
|
|
|
async def _viewable_flight_rows(conn, round_id: str, viewer_user_id: str | None):
|
|
|
|
|
|
"""Offentlig/tredjeparts-variant av `_accessible_flight_rows` (samme
|
|
|
|
|
|
"avslør aldri en søsken-flight brukeren ikke har tilgang til"-prinsipp),
|
|
|
|
|
|
men bruker `_can_view_round` (eier/medspiller/`public`/riktig-
|
|
|
|
|
|
kategorisert `friends`-venn) i stedet for den strengere eier-eller-
|
|
|
|
|
|
medspiller-sjekken -- dette endepunktet betjener også anonyme/
|
|
|
|
|
|
tredjeparts lesere (ADR-036 fase 2), ikke bare eieren selv. Hver
|
|
|
|
|
|
søsken-flight sjekkes UAVHENGIG (kan ha forskjellig visibility_mode
|
|
|
|
|
|
enn anker-runden), samme prinsipp som resten av tilskuer-visningen."""
|
|
|
|
|
|
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:
|
|
|
|
|
|
if not await _can_view_round(conn, sibling_id, viewer_user_id):
|
|
|
|
|
|
continue
|
|
|
|
|
|
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
|
|
|
|
|
|
FROM round r WHERE r.id = $1
|
|
|
|
|
|
""",
|
|
|
|
|
|
sibling_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if row is not None:
|
|
|
|
|
|
rows.append(row)
|
|
|
|
|
|
return group_id, rows
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/public/rounds/{round_id}/flight-group", response_model=FlightGroupOut)
|
|
|
|
|
|
async def get_public_flight_group(
|
|
|
|
|
|
round_id: str,
|
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
|
) -> FlightGroupOut:
|
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
|
await _get_viewable_round_or_404(conn, round_id, user.user_id if user else None)
|
|
|
|
|
|
group_id, rows = await _viewable_flight_rows(conn, round_id, user.user_id if user else None)
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
@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
|
|
|
|
|
|
]
|