Begge implementert, browserverifisert og live:

Lenke scorekort → statistikk lagt til nederst på scorekortsiden — symmetrisk med den eksisterende lenken tilbake.
Prikker for mottatte slag i stedet for "−N"-tekst, i live-registreringen (begge grid-typer) — verifisert med en HCP 28-spiller som viste nøyaktig 2 prikker på de rette hullene.
Selve spørsmålet om å slå alt sammen til én visning er bevisst latt åpent til dere har fått testet med ekte brukere — notert tydelig i FEATURE_BACKLOG.md slik at det ikke går i glemmeboken.
This commit is contained in:
Erol Haagenrud 2026-07-30 09:36:52 +02:00
parent bcb98a8625
commit facefd9951
6 changed files with 480 additions and 55 deletions

116
CLAUDE.md
View file

@ -5739,6 +5739,122 @@ Ferdig og verifisert:
bekreftet nåbare over ekte https med riktig `content-type`, `teeoff.no`
upåvirket.
- **Individuelle turneringer (ADR-037): scoring-autorisasjon strammet inn,
BYGGET, SCRATCH-VERIFISERT OG LIVE (2026-07-30), samme dag som frontend-
runden:** direkte oppfølging av det noterte hullet fra byggerunden
tidligere samme dag — ethvert org-medlem kunne skrive score for HVEM SOM
HELST i en individuell turnering, ikke bare sin egen.
Ny `user_is_own_tournament_participant` i `app/team_authz.py` — samme
mønster som `user_is_match_participant` (ADR-023): krever at brukeren ER
spilleren bak `tournament_participant`-raden (via `player.user_id`),
eller er org-eier/admin. Brukt av `individual_tournaments.py` sin
`update_hole` (eneste endepunkt strammet inn). Runde-/deltaker-OPPSETT
(opprett/slett runde, legg til/fjern turnering-/rundedeltaker) forblir
bevisst på vanlig org-medlemsnivå — matcher presedensen fra `session`-
opprettelse og `team_roster`-tilføyelse i tournaments.py, som heller
aldri har vært captain-/admin-gatet.
**Scratch-verifisert grundig** (isolert `teecup_app_scratch`-rolle +
isolert scratch-MinIO + engangs API-container, alle 40 migrasjoner kjørt
friskt): full regresjon av den eksisterende 52-punkts testsuiten (uendret
grønn — org-eier brukes gjennomgående der, rammes ikke av innstrammingen),
pluss 13 nye målrettede sjekker: et fremmed org-medlem (uten kobling til
noen av deltakerne) NEKTES å score for både en annen deltaker OG en
tredje deltaker (403 `NOT_TOURNAMENT_PARTICIPANT`), en spiller KOBLET til
sin egen `tournament_participant` (via `player.email` + ADR-017s
kontokobling ved innlogging) FÅR score seg selv men NEKTES å score for en
annen, org-eier beholder uendret admin-fallback for begge, og lesing
(`GET .../holes`) er bekreftet uendret tilgjengelig for et vanlig
org-medlem (kun skriving er strammet inn, ikke lesing).
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ingen
migrasjon (ren Python-logikk), `docker compose up -d --build
teecup_api`. Containeren boot-et rent (`Application startup complete`),
`/health`/`/dashboard` → 200, `teeoff.no` upåvirket.
- **To notater fra brukeren, BEGGE BYGGET, GRUNDIG BROWSERVERIFISERT OG
LIVE (2026-07-30), samme dag som ADR-037-autorisasjonsfiksen:**
1. **GIR-auto-inferens for "Innspill: Traff":** ny `useEffect` i
`ScoringWizard` (`round-detail.tsx`) -- idet "flere detaljer"-steget
nås, settes `stat.approach = "hit"` automatisk når
`stat.strokes - stat.putts <= hole.par - 2` (samme formel som den
allerede eksisterende GIR-STATISTIKK-inferensen i `round-stats.tsx`
sin `isGir`, nå også koblet til selve REGISTRERINGEN) -- men KUN når
`stat.approach` fortsatt er `null` (rører aldri et allerede satt
manuelt ELLER tidligere auto-satt valg).
2. **Auto-prompt "Fullfør runde":** ny `allHolesEnteredForEveryone`-
beregning + `AllHolesEnteredBanner`-komponent i `RoundDetail` -- viser
en tydelig CTA øverst på Score-fanen ("Alle hull er ført — Klar til å
fullføre runden?") så snart ALLE spillere (eller BEGGE sider for
delt-ball-formater) har `played=true` på alle hull i `holeOrder`,
gatet på `round.setup_complete`. Kaller samme `finishRound()` som den
eksisterende, tidligere passive knappen.
**Browserverifisert grundig i en isolert scratch-nettleserøkt** (fersk
`teecup_scratch`-database + isolert scratch-MinIO + engangs API-
container + en isolert `next dev`-frontend-container med KOPIERT, ikke
bind-mountet, kildekode -- bind-mount ga gjentatte Turbopack-panics i
denne økten, løst ved å `tar`-kopiere kildetreet inn i en isolert
container-filsystem i stedet): GIR-auto-inferens bekreftet BÅDE positivt
(birdie+1-putt -> "Traff" auto-merket umiddelbart, uten klikk, bekreftet
visuelt OG direkte i databasen) og negativt (bogey+1-putt -> "Traff"
korrekt IKKE forhåndsmerket). Auto-prompt-banneret bekreftet å dukke opp
automatisk idet siste hull ble fylt for begge spillerne i en 18-hulls
2-spiller-runde, og "Fullfør runden"-knappen i banneret bekreftet å
fullføre runden korrekt (håndterte den native `confirm()`-dialogen via
Chrome DevTools -- HCP-differensialer beregnet og vist etterpå). Ingen
konsollfeil i noen av rundene.
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ingen
migrasjon (ren frontend), `docker compose up -d --build
teecup_frontend` (gjenskapte også `teecup_api` som vanlig bivirkning,
ingen backend-kode rørt). Begge containere boot-et rent, `/health`/
`/dashboard` → 200, `teeoff.no` upåvirket.
- **Symbolforklaringen fjernet fra scorekortet + to design-notater fanget
opp (2026-07-30), samme dag:** brukeren viste et skjermbilde av
`round-scorecard.tsx` med "Under par (sirkel)/Over par (firkant)/Fylt
symbol..."-forklaringen sirklet inn og ba om at den fjernes — `Legend`-
komponenten (kun ett bruksted) fjernet fullstendig. Samtidig reist:
(1) spilleren bør kunne se historikk/statistikk for NØYAKTIG hullet som
spilles/er spilt (ikke bygget — krever en ny, ikke-triviell "aggreger på
tvers av runder, filtrert til banenavn+hullnummer"-spørring, notert i
FEATURE_BACKLOG.md); (2) manglende lenke scorekort→statistikk, og et
åpent spørsmål om scorekort/statistikk/live-registrering burde slås
sammen til én visning, med mottatte slag vist som prikker. Svarte
direkte (ikke bygget): behold scorekort og statistikk som to separate
sider (bevisst skilt 2026-07-25 nettopp for å unngå én lang/rotete
side) — legg heller til den manglende lenken; behold også selve
live-registrerings-gridet uendret (bygget 2026-07-27 som en AKTIV
data-entry-flate, ikke en lesevisning — et vertikalt front9/back9-delt
format ville svekket registreringsergonomikken). "Prikker for mottatte
slag" vurdert som en god, uavhengig senere polish-oppgave. Full
begrunnelse i FEATURE_BACKLOG.md.
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ekte
typesjekket produksjonsbuild kompilerte rent, `docker compose up -d
--build teecup_frontend` (gjenskapte også `teecup_api` som vanlig
bivirkning). Begge containere boot-et rent, `/health` → 200,
scorekort-siden bekreftet 200, `teeoff.no` upåvirket.
- **De to avgrensede scorekort-oppfølgerne fra samme dag BYGGET,
BROWSERVERIFISERT OG LIVE (2026-07-30):** (1) ny "Se full
rundestatistikk"-lenke nederst på `round-scorecard.tsx` (symmetrisk med
den eksisterende motsatte lenken på statistikksiden); (2) mottatte
slag-hintet (vist FØR et hull er fylt ut) endret fra "N"-tekst til
prikker (`StrokeDots`, `round-detail.tsx`, i BÅDE `ScorecardGrid` og
`SideScorecardGrid`) — selve tallet bevart i `aria-label` for
tilgjengelighet. Selve spørsmålet om å slå sammen scorekort/statistikk/
live-registrering til én visning er BEVISST IKKE avgjort — bruker
ønsker en fremtidig brukertest først.
**Browserverifisert grundig i en isolert scratch-nettleserøkt** (samme
mønster som resten av uken): en HCP 28-spiller (course handicap 30)
bekreftet å vise nøyaktig to prikker på hull 1-12 i live-
registreringsgridet (riktig ut fra 30-18=12 ekstra slag), den nye
lenken bekreftet klikkbar og navigerte korrekt til `/stats`. Ekte
typesjekket produksjonsbuild kompilerte rent. Ingen konsollfeil (kun en
godartet, urelatert WebSocket-advarsel fra rask sidenavigasjon).
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ingen
migrasjon, `docker compose up -d --build teecup_frontend` (gjenskapte
også `teecup_api` som vanlig bivirkning). Begge containere boot-et
rent, `/health`/`/dashboard`/scorekort-siden → 200, `teeoff.no`
upåvirket.
Neste steg:
0a. **Spillerliste-redesign — nå FAKTISK nettleser-bekreftet
(2026-07-27, full 22-skjerms gjennomgang):** rendrer korrekt, ingen

View file

@ -733,11 +733,16 @@ deltakere avvist 409, turnering-deltaker fortsatt referert av en
rundedeltaker avvist 400 RESTRICT-FK). `test_isolation.sql` 12/12
uendret (additiv migrasjon).
**Autorisasjon** er fortsatt bredt org-medlemskap for ALT inkl. selve
scoreregistreringen -- en senere innstramming (analogt ADR-023s
kaptein-only for lagturneringer) er en naturlig, men separat oppfølger.
De fem konkrete formatene (Københavner m.fl.) og Order of Merit fortsatt
ikke designet.
**Autorisasjon — ✅ SCORING STRAMMET INN, SCRATCH-VERIFISERT OG LIVE
2026-07-30** (samme dag, egen del-runde etter frontend-utrullingen): ny
`user_is_own_tournament_participant` (`app/team_authz.py`, samme mønster
som ADR-023s `user_is_match_participant`) brukt av `update_hole` — kun
deltakeren selv eller org-admin kan nå skrive score for en gitt
rundedeltaker. Runde-/deltaker-oppsett forblir bevisst på org-medlemsnivå
(samme presedens som `session`/`team_roster` i tournaments.py). 13 nye
scratch-sjekker + full 52-punkts regresjon, se CLAUDE.md-status
2026-07-30 for full detalj. De fem konkrete formatene (Københavner m.fl.)
og Order of Merit fortsatt ikke designet.
### Oppdatering 2026-07-30, samme dag: frontend HÅNDKODET og LIVE
@ -1559,6 +1564,96 @@ i CLAUDE.md sin statuslogg (2026-07-27) — ikke duplisert her.
---
## Symbolforklaringen på scorekortet fjernet — ✅ BYGGET OG LIVE 2026-07-30
Brukeren pekte direkte på "Under par (sirkel) / Over par (firkant) / Fylt
symbol = 2 slag eller mer"-forklaringen på `round-scorecard.tsx` og ba om
at den fjernes. `Legend`-komponenten (kun brukt ett sted) fjernet
fullstendig, ikke bare skjult. Den andre, urelaterte inline-teksten på
samme fil ("Fylt sirkel = vunnet hull ...", brukt av match-scorekortet)
er en annen streng, ikke rørt. Ekte typesjekket produksjonsbuild kompilerte
rent. Rullet ut (kun `teecup_frontend`), ingen migrasjon, `teeoff.no`
upåvirket.
## Notat 2026-07-30: per-hull historikk/statistikk for spilleren — 📋 NOTERT, IKKE bygget
Reist av brukeren: spilleren bør kunne se all historikk/statistikk for
NØYAKTIG det hullet vedkommende skal spille (eller har spilt) — f.eks. "du
har i snitt brukt X slag/Y putter på hull 7 på denne banen, GIR Z% av
gangene" — ikke bare hva som skjedde DENNE runden.
**Ikke trivielt, verdt å notere hvorfor:** dagens data er scoped PER
RUNDE (`round_hole`), ingen eksisterende spørring aggregerer "alle mine
tidligere runder på DENNE banen, filtrert til DETTE hullnummeret". Banen
identifiseres i dag kun ved navn-snapshot (`round.course_name_snapshot`,
se `PlayedCourses`/`course-rounds.tsx` sin eksisterende gruppering på
akkurat dette navnet) — samme identifikator kan trolig gjenbrukes til en
ny spørring: `GET /rounds/holes/{course_name}/{hole_number}/history` (eller
tilsvarende), som slår sammen `round_hole`-rader på tvers av alle
fullførte runder brukeren eier/har spilt på den banen. Naturlig plassering
i UI-et: en liten utvidbar seksjon i `ScoringWizard` sitt "strokes"-steg
(`round-detail.tsx`), og/eller i `round-scorecard.tsx`/`round-stats.tsx`
sine hull-visninger. Ingen design/ADR skrevet ennå — kun fanget opp her.
## Notat 2026-07-30: lenke til rundestatistikk fra scorekortet, og hvorvidt scorekort/statistikk/live-registrering bør slås sammen til én visning — lenke + prikker ✅ BYGGET, BROWSERVERIFISERT OG LIVE; sammenslåing bevisst UTSATT til brukertesting
Brukeren observerte at `round-scorecard.tsx` (det tradisjonelle,
front9/back9-delte scorekortet) mangler en lenke videre til
`/my-rounds/{id}/stats` (som selv HAR en lenke tilbake til scorekortet,
`CompletedBanner`) — og spurte om det heller burde vært ÉN samlet visning
i stedet for to, og i så fall om selve LIVE-registrerings-gridet
(`ScorecardGrid` i `round-detail.tsx`, bygget som spec-dokumentets §1
2026-07-27) burde erstattes av det samme front9/back9-delte formatet, med
mottatte slag vist som prikker i stedet for tekst.
**Svart direkte i chatten (ikke bygget), gjengitt her for historikken:**
anbefalte å IKKE slå sammen scorekort og statistikk til én visning — de
ble bevisst SKILT UT i to dedikerte sider 2026-07-25 (se eget punkt lenger
opp i denne filen: "Rundeliste + scorekort-redesign", "Den gamle vertikale
'Scorekort'-tabellen i round-stats.tsx FJERNET (erstattet med en lenke til
den nye siden)") — nøyaktig for å unngå at én side blir for lang/rotete.
Riktig, minimal fiks er heller den manglende lenken SCOREKORT → STATISTIKK
(symmetrisk med den allerede eksisterende STATISTIKK → SCOREKORT-lenken).
Anbefalte OGSÅ å IKKE erstatte selve live-registrerings-gridet med det
front9/back9-vertikalt-delte formatet — gridet ble nettopp bygget 2026-07-27
for å ligne et ekte scorekort (Ut/Inn/Sum, alle 18 hull som kolonner,
`§Golfscore-språket`), men er samtidig en AKTIV data-registreringsflate
(tapp en celle for å åpne veiviseren) — et vertikalt delt to-blokk-format
(designet for LESING) ville krevd smalere celler/mer scrolling og dermed
svekket selve registreringsergonomikken uten reell gevinst. "Prikker for
mottatte slag i stedet for tekst" ble vurdert som en god idé UAVHENGIG av
selve sammenslåings-spørsmålet — kan bygges som en liten, isolert
polish-oppgave på eksisterende `N`-tekst-hint, i BEGGE grid-typer.
**De to avgrensede oppfølgerne bygget samme dag, sammenslåings-spørsmålet
bevisst utsatt** (brukeren: "Vi får senere gjøre en test med faktiske
brukere for å beslutte endelig om alt skal samles på en side"):
1. Ny "Se full rundestatistikk"-lenke nederst på `round-scorecard.tsx`
(symmetrisk med den eksisterende "Se scorekort"-lenken i motsatt
retning, `round-stats.tsx`).
2. `N`-teksthintet for mottatte slag (FØR et hull er fylt ut) erstattet
med prikker (`StrokeDots`, ny liten komponent i `round-detail.tsx`) i
BÅDE `ScorecardGrid` (individuell-ball) og `SideScorecardGrid`
(delt-ball) — selve informasjonen bevart i knappens `aria-label`
("...mottar N slag"), siden prikkene alene er dekorative
(§tilgjengelighet: aldri kun et visuelt symbol for noe som betyr noe).
**Browserverifisert grundig i en isolert scratch-nettleserøkt** (samme
mønster som resten av uken -- fersk `teecup_scratch`-database + isolert
scratch-MinIO + engangs API-container + en isolert `next dev`-container
med kopiert kildekode): en HCP 28-spiller (course handicap 30, seedet via
API) bekreftet å vise nøyaktig to prikker på hull 1-12 (stroke index
1-12, riktig ut fra 30-18=12 ekstra slag) i live-registrerings-gridet;
den nye lenken på scorekortet bekreftet klikkbar og navigerte korrekt til
`/stats`-siden. Ekte typesjekket produksjonsbuild kompilerte rent. Ingen
konsollfeil (kun en godartet WebSocket-advarsel fra rask sidenavigasjon,
urelatert).
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ingen
migrasjon, `docker compose up -d --build teecup_frontend` (gjenskapte
også `teecup_api` som vanlig bivirkning). Begge containere boot-et rent,
`/health`/`/dashboard`/scorekort-siden → 200, `teeoff.no` upåvirket.
**Selve sammenslåings-spørsmålet (scorekort+statistikk+live-registrering
til én visning) er bevisst IKKE avgjort** — venter på en fremtidig
brukertest før noen endelig beslutning tas.
## En tredje (informasjons-)farge til designet — ✅ BYGGET OG LIVE 2026-07-28
Brukeren spurte om det ville vært en idé å introdusere én (eller kanskje
@ -3508,6 +3603,101 @@ over ekte https, `teeoff.no` upåvirket.
---
## Notat 2026-07-30: GIR burde utlede "innspill traff green" automatisk, ikke kreve manuell merking — ✅ BYGGET, BROWSERVERIFISERT OG LIVE
Reist av brukeren: får man birdie med kun én putt, MÅ innspillet ha truffet
greenen (par 2 slag brukt før putting = GIR per definisjon) — da bør
appen ikke kreve at spilleren i tillegg manuelt krysser av "Innspill: Traff"
i `DirectionCross`-feltet i `ScoringWizard` sitt "flere detaljer"-steg
(`round-detail.tsx`, `stat.approach`).
**Presisering, funnet ved kodegjennomgang:** appen har ALLEREDE en generell
GIR-inferens fra slag+putt (`score putts ≤ par 2`, `round-stats.tsx` sin
`isGir`/`round-detail.tsx` sin `showGir`) — brukt til selve GIR-STATISTIKKEN
(greentreff-donut, GIR-avhengige snitt). Det som IKKE er koblet sammen er
den separate, manuelt utfylte `approach_result`-verdien ("Innspill: Traff/
Kort/Langt/Venstre/Høyre") — brukt til RETNINGS-statistikken (hvilken vei
man bommer greenen). Disse to feltene lever i dag adskilt: den utledede
GIR-verdien styrer ALDRI hva som vises/kreves i `DirectionCross`-feltet.
**Naturlig fiks (ikke bygget):** når `score putts ≤ par 2` er sann for
et hull (dvs. GIR er logisk garantert), bør `stat.approach` auto-settes til
`"hit"` i stedet for å kreve et eget klikk — analogt de andre auto-hopp-
mekanismene i samme wizard (`autoAdvanceFieldFor`). Motsatt gjelder IKKE:
GIR kan være usann uten at man vet retningen (f.eks. en birdie med to
putter er ikke GIR-garantert, og en scrambling-birdie fra utenfor green gir
identisk slag/putt-mønster som en ekte GIR — kun i RETNING vet vi ingenting
automatisk der). Trigger-tidspunktet er litt kinkig: putt-tallet kommer
FØR "flere detaljer"-steget i wizard-rekkefølgen (`strokes → putts →
puttDistance → details`), så inferensen kan skje idet man ANKOMMER
detalj-steget (begge tall er da kjent) — ikke midt i et tidligere steg.
**Bygget nøyaktig som beskrevet over, samme dag:** ny `useEffect` i
`ScoringWizard` (`round-detail.tsx`) som trigges idet "details"-steget
nås — setter `stat.approach = "hit"` KUN når `stat.approach` fortsatt er
`null` (rører aldri et allerede satt, manuelt ELLER tidligere auto-satt
valg) og `stat.strokes - stat.putts <= hole.par - 2`.
**Browserverifisert grundig i en isolert scratch-nettleserøkt** (fersk
`teecup_scratch`-database + isolert scratch-MinIO + engangs API-container
+ en isolert `next dev`-frontend-container med KOPIERT, ikke bind-mountet,
kildekode -- bind-mount ga gjentatte Turbopack-panics i denne økten):
positiv sjekk (birdie+1-putt på et par 4-hull, hull 17) bekreftet
"Innspill: Traff" auto-merket UMIDDELBART ved ankomst til detalj-steget,
UTEN noe klikk -- bekreftet BÅDE visuelt (skjermbilde) og direkte i
databasen (`approach_result='hit'`). Negativ sjekk (bogey+1-putt, hull 2,
satt via direkte API-kall FØR wizard åpnet) bekreftet "Traff" korrekt
IKKE forhåndsmerket ved ankomst til samme steg. Ingen konsollfeil.
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ingen
migrasjon, `docker compose up -d --build teecup_frontend` (gjenskapte
også `teecup_api` som vanlig bivirkning). Begge containere boot-et rent,
`/health`/`/dashboard` → 200, `teeoff.no` upåvirket.
## Notat 2026-07-30: auto-prompt "Fullfør runde" når alle hull er ført for alle spillere — ✅ BYGGET, BROWSERVERIFISERT OG LIVE
Reist av brukeren rett etter scoring-autorisasjonsfiksen for ADR-037.
Gjelder "single-runder" i CLAUDE.md sin etablerte betydning — frittstående
runder (`/my-rounds/{id}`, ADR-033), ikke org-scopede individuelle
turneringer.
**Nå:** `round-detail.tsx` sin "Fullfør runde"-knapp finnes allerede (i den
tonet-ned Rediger/Fullfør/Slett-raden øverst, se 2026-07-25s UX-fiks), men
er en helt PASSIV knapp — ingen kode sjekker i dag om alle hull faktisk er
ferdig utfylt for alle deltakere, og ingenting fremhever/popper opp knappen
når det skjer. Brukeren må selv huske å scrolle opp og trykke, selv lenge
etter siste hull er registrert.
**Naturlig fiks (ikke bygget):** en klientside-sjekk (samme type
`isEntryComplete()`-logikk som allerede finnes for wizard-fremdrift, se
`advanceToNextPlayerOrHole()`-mønsteret fra 2026-07-26) som kjører etter
HVER hull-innsending — er `stat_level`-relevante felt fylt ut for SISTE
hull for ALLE deltakere (ikke bare aktiv spiller), vis en tydelig CTA/
banner ("Alle hull er ført — Fullfør runden?") i stedet for kun den passive
knappen i den tonede raden øverst. For delt-ball-formater (foursome/
greensome/scramble) gjelder samme sjekk på SIDE-nivå (`round_side`), ikke
per spiller. Ingen backend-endring nødvendig — dette er ren
frontend-tilstand avledet fra data som allerede lastes.
**Bygget nøyaktig som beskrevet over, samme dag:** ny
`allHolesEnteredForEveryone`-beregning i `RoundDetail` — individuelle
formater: ALLE spillere har `played=true` på ALLE hull i `holeOrder`;
delt-ball-formater: BEGGE sider har `played=true` på alle hull. Gatet på
`round.setup_complete` (samme gate som selve knappen) og `!completed`. Ny
`AllHolesEnteredBanner`-komponent (samme visuelle språk som
`CompletedBanner`) vist øverst på Score-fanen når betingelsen er sann, med
en direkte "Fullfør runden"-CTA (kaller samme `finishRound()` som den
eksisterende knappen, inkl. samme `confirm()`-bekreftelse).
**Browserverifisert grundig i samme isolerte scratch-økt:** fylte de to
siste hullene (owner hull 17+18 via wizard, gjest hull 18 via API) for en
18-hulls 2-spiller-runde, reloadet siden — banneret dukket opp automatisk
med korrekt tekst ("Alle hull er ført — Klar til å fullføre runden?").
Klikket "Fullfør runden" (håndterte den native `confirm()`-dialogen via
Chrome DevTools) — runden ble korrekt fullført (HCP-differensialer
beregnet og vist, banneret erstattet av det eksisterende
"Runde fullført"-banneret). Ingen konsollfeil gjennom hele flyten.
**Rullet ut live 2026-07-30**, samme utrulling som GIR-fiksen over (én
felles `docker compose up -d --build teecup_frontend`), bruker bekreftet
eksplisitt.
## Bevisst endret fra opprinnelige (Gemini-)råd
- 🔀 **Banedata:** API mot teeoff (ADR-004), IKKE direkte delt database. Direkte

View file

@ -13,11 +13,17 @@ SQL summerer bare det cachede ved lesing"-mønster som `match.status_text`/
`points_side_a/b`. Sammenlagt over flere runder summeres VED LESING i
leaderboardet, ikke i en egen tredje cache-tabell (ADR-037 Beslutning C).
Autorisasjon i denne runden: org-medlemskap (`get_authorized_org`) for ALT,
inkl. selve score-registreringen -- samme brede tillitsnivå lagturneringer
hadde FØR ADR-023 (kaptein-only) ble bygget som en egen, senere runde. En
tilsvarende innstramming for individuelle turneringer er en naturlig
oppfølger, ikke del av denne grunnstrukturen.
Autorisasjon (strammet inn 2026-07-30, se `user_is_own_tournament_
participant` i `app/team_authz.py`): selve SCORE-REGISTRERINGEN
(`update_hole`) krever at brukeren ER deltakeren (via `player.user_id`)
ELLER org-eier/admin -- samme mønster som `user_is_match_participant`
strammet inn lagturneringer i ADR-023. Runde-/deltaker-OPPSETT (opprett/
slett runde, legg til/fjern turnering-/rundedeltaker) forblir bevisst
vanlig org-medlemsnivå (`get_authorized_org`) -- samme presedens som
`session`-opprettelse og `team_roster`-tilføyelse i tournaments.py, som
heller aldri har vært captain-/admin-gatet. Kun VALGET AV HVEM SOM SPILLER
og selve SCORINGEN er gatet strengere noe sted i appen, aldri det generelle
oppsettet.
Bevisst UTENFOR omfang (se ADR-037): Københavner/High-low-high/Robbins/
Try all/Flaggturnering (krever paring TVERS av spillere per hull, ikke
@ -39,9 +45,10 @@ from handicap_engine import (
stroke_play_net_total,
)
from ..auth import get_authorized_org
from ..auth import CurrentUser, get_authorized_org, get_current_user
from ..db import org_connection
from ..errors import app_error, translate_db_errors
from ..team_authz import user_is_own_tournament_participant
from .scoring import played_hole_numbers
router = APIRouter()
@ -572,6 +579,7 @@ async def update_hole(
hole_number: int,
body: HoleUpdate,
organization_id: str = Depends(get_authorized_org),
user: CurrentUser = Depends(get_current_user),
) -> RoundHoleOut:
async with org_connection(organization_id) as conn, translate_db_errors():
round_row = await conn.fetchrow(
@ -583,14 +591,27 @@ async def update_hole(
if hole_number not in played_hole_numbers(round_row["hole_config"]):
raise app_error(400, "OUT_OF_SCOPE", "Hullnummeret er utenfor rundens hullomfang.")
rp = await conn.fetchval(
"SELECT id FROM tournament_round_participant WHERE id = $1 AND tournament_round_id = $2",
rp = await conn.fetchrow(
"""
SELECT trp.id, trp.tournament_participant_id::text AS tournament_participant_id
FROM tournament_round_participant trp
WHERE trp.id = $1 AND trp.tournament_round_id = $2
""",
round_participant_id,
round_id,
)
if rp is None:
raise app_error(404, "NOT_FOUND", "Rundedeltakeren finnes ikke.")
if not await user_is_own_tournament_participant(
conn, organization_id, rp["tournament_participant_id"], user.user_id
):
raise app_error(
403,
"NOT_TOURNAMENT_PARTICIPANT",
"Du kan kun registrere score for deg selv (eller være organisasjonsadministrator).",
)
hole = await conn.fetchrow(
"SELECT par, stroke_index FROM hole WHERE course_id = $1 AND hole_number = $2",
round_row["course_id"],

View file

@ -189,6 +189,40 @@ async def user_is_tournament_participant(
)
async def user_is_own_tournament_participant(
conn: Connection, organization_id: str, tournament_participant_id: str, user_id: str
) -> bool:
"""Individuelle turneringer (ADR-037): er brukeren SELV spilleren bak
denne `tournament_participant`-raden (via `player.user_id`)?
Mirror av `user_is_match_participant`, men uten `team_side` -- en
individuell turnering har ingen lag å begrense siden til. Brukt av
`individual_tournaments.py` sin `update_hole` (2026-07-30): før denne
kunne ETHVERT org-medlem skrive score for EN HVILKEN SOM HELST
deltaker, ikke bare sin egen -- samme klasse hull `user_is_match_
participant` lukket for lagturneringer i ADR-023. Kun scoring er
strammet inn her; runde-/deltaker-oppsett (opprett runde, legg til/
fjern turnering-/rundedeltaker) forblir bevisst org-medlemsnivå,
samme presedens som `session`-opprettelse og `team_roster`-tilføyelse
i tournaments.py -- ingen av dem er captain-/admin-gatet heller, kun
selve VALGET AV HVEM SOM SPILLER EN GITT MATCH/RUNDE og selve
scoreregistreringen er det."""
is_self = await conn.fetchval(
"""
SELECT EXISTS (
SELECT 1 FROM tournament_participant tp
JOIN player p ON p.id = tp.player_id
WHERE tp.id = $1 AND p.user_id = $2
)
""",
tournament_participant_id,
user_id,
)
if is_self:
return True
return await is_org_admin(conn, organization_id, user_id)
async def user_is_rostered_on_team(conn: Connection, team_id: str, user_id: str) -> bool:
"""Kun for lag-chat (ADR-025 Beslutning C) -- BEVISST INGEN org-admin-
fallback, ulikt de to funksjonene over. «Det hemmelige rommet» er ekte

View file

@ -848,6 +848,29 @@ export function RoundDetail({ roundId }: { roundId: string }) {
return null
})()
// Auto-prompt "Fullfør runde" (2026-07-30): er ALLE hull ført for ALLE
// deltakere (individuelle formater) eller for BEGGE sider (delt-ball) --
// vis en tydelig CTA på Score-fanen i stedet for å stole på at brukeren
// selv husker den tonet-ned knappen i "Spillere og runde"-fanen. Samme
// `setup_complete`-gate som selve knappen (unngår å reklamere for en
// handling som uansett ville vært deaktivert).
const allHolesEnteredForEveryone =
!completed &&
round !== null &&
round.setup_complete &&
holeOrder.length > 0 &&
(isSharedBall
? round.sides.length >= 2 &&
round.sides.every((s) => {
const list = holesBySide[s.id]
return list !== undefined && holeOrder.every((n) => list.find((h) => h.hole_number === n)?.played === true)
})
: players.length > 0 &&
players.every((p) => {
const list = holesByParticipant[p.id]
return list !== undefined && holeOrder.every((n) => list.find((h) => h.hole_number === n)?.played === true)
}))
// Begge feltene (score OG selected_participant_id) sendes ALLTID sammen
// (samme "full overwrite hvert kall"-kontrakt som played/score alltid har
// hatt) -- ellers ville en score-oppdatering stille nullstilt en allerede
@ -1290,6 +1313,11 @@ export function RoundDetail({ roundId }: { roundId: string }) {
Samme komponent, ikke duplisert logikk. */}
<FormatResultPanel round={round} players={players} result={formatResult} />
{/* Auto-prompt "Fullfør runde" (2026-07-30) -- kun synlig når alle
hull faktisk er ført for alle, forsvinner selv idet runden er
fullført (completed flipper til true). */}
{allHolesEnteredForEveryone && <AllHolesEnteredBanner completing={completing} onComplete={finishRound} />}
{/* Completed summary banner */}
{completed && (
<CompletedBanner
@ -1555,6 +1583,26 @@ function ScoringWizard({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stat.strokes, stat.putts, stat.firstPuttBucket])
// GIR-auto-inferens (2026-07-30): score - putts <= par - 2 GARANTERER
// greentreff i regulering (samme formel som round-stats.tsx sin isGir/
// round-detail.tsx sin showGir, som allerede styrer GIR-STATISTIKKEN) --
// da er det unødvendig å i tillegg kreve et manuelt "Innspill: Traff"-
// valg i DirectionCross-feltet under. Kun en FORHÅNDSUTFYLLING: rører
// ALDRI et allerede satt (manuelt ELLER tidligere auto-satt) approach-
// valg, og gjelder aldri motsatt vei (GIR usann sier ingenting om
// retning -- en scrambling-birdie gir identisk slag/putt-mønster som en
// ekte GIR). Trigges når "details"-steget nås, siden slag+putt begge må
// være kjent (kommer tidligere i wizard-rekkefølgen).
useEffect(() => {
if (step !== "details") return
if (stat.approach !== null) return
if (stat.strokes === null || stat.putts === null) return
if (stat.strokes - stat.putts <= hole.par - 2) {
onUpdate({ approach: "hit" })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [step, stat.strokes, stat.putts, stat.approach, hole.par])
function back() {
if (isFirstStep) {
onClose()
@ -1796,6 +1844,30 @@ function ScoringWizard({
// --- Completed banner ------------------------------------------------------
function AllHolesEnteredBanner({ completing, onComplete }: { completing: boolean; onComplete: () => void }) {
return (
<div className="mb-6 flex flex-col gap-3 rounded-3xl border border-primary/30 bg-primary/10 p-5 sm:flex-row sm:items-center sm:justify-between sm:p-6">
<div className="flex items-center gap-3">
<div className="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-sm">
<Check aria-hidden="true" className="size-6" />
</div>
<div className="flex flex-col">
<span className="text-xs font-semibold uppercase tracking-wide text-primary">Alle hull er ført</span>
<h2 className="text-lg font-extrabold tracking-tight text-foreground">Klar til å fullføre runden?</h2>
</div>
</div>
<button
type="button"
disabled={completing}
onClick={onComplete}
className="flex min-h-12 items-center justify-center gap-1.5 rounded-2xl bg-primary px-5 text-base font-bold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 disabled:opacity-60 sm:shrink-0"
>
{completing ? "Fullfører…" : "Fullfør runden"}
</button>
</div>
)
}
function CompletedBanner({
players,
duration,
@ -4233,7 +4305,7 @@ function ScorecardGrid({
aria-label={
hasScore
? `${player.name}, hull ${n}, ${apiHole?.score} slag${diff !== null && meta ? `, ${golfTermForScore(apiHole!.score as number, meta.par) ?? ""}` : ""}${won ? ", vant hullet" : ""}`
: `Registrer ${player.name} sin score på hull ${n}`
: `Registrer ${player.name} sin score på hull ${n}${apiHole?.strokes_received ? `, mottar ${apiHole.strokes_received} slag` : ""}`
}
className="flex min-h-12 w-full flex-col items-center justify-center gap-0.5 py-1 transition-colors hover:bg-accent/40 disabled:opacity-60"
>
@ -4255,15 +4327,11 @@ function ScorecardGrid({
net !== null && <span className="text-[10px] font-semibold tabular-nums text-muted-foreground">{net}</span>
) : (
// Mottatte slag vist FØR hullet er fylt ut (2026-07-29,
// etterspurt av bruker) -- samme verdi som ville blitt
// trukket fra ved utfylling, kun når spilleren faktisk
// etterspurt av bruker) -- kun når spilleren faktisk
// mottar minst ett slag her (ingen støy på de andre).
// Prikker i stedet for "N"-tekst (2026-07-30).
apiHole?.strokes_received != null &&
apiHole.strokes_received > 0 && (
<span className="text-[10px] font-semibold tabular-nums text-info">
{apiHole.strokes_received}
</span>
)
apiHole.strokes_received > 0 && <StrokeDots count={apiHole.strokes_received} />
)}
</button>
</td>
@ -4330,6 +4398,21 @@ function ScorecardGrid({
// prosjektets etablerte "lokale komponentmønstre per fil"-konvensjon, se
// DESIGN_SYSTEM.md). Sirkel under par, "firkant" (`rounded-[4px]`) over
// par, ren tekst på par, fylt ved 2+ slag fra par.
// Mottatte slag FØR hullet er fylt ut, vist som prikker i stedet for tekst
// (2026-07-30, etterspurt av bruker som en kompakt erstatning for "N") --
// ren visuell hint, den faktiske informasjonen ligger i knappens eget
// aria-label (§tilgjengelighet: aldri kun et visuelt symbol uten tekst for
// noe som faktisk betyr noe).
function StrokeDots({ count }: { count: number }) {
return (
<span className="flex items-center justify-center gap-0.5" aria-hidden="true">
{Array.from({ length: count }, (_, i) => (
<span key={i} className="size-1.5 rounded-full bg-info" />
))}
</span>
)
}
function ScorecardCell({
value,
diff,
@ -4624,7 +4707,7 @@ function SideScorecardGrid({
aria-label={
hasScore
? `${side.label ?? "Side"}, hull ${n}, ${apiHole?.score} slag${won ? ", vant hullet" : ""}`
: `Registrer score for hull ${n}`
: `Registrer score for hull ${n}${apiHole?.strokes_received ? `, mottar ${apiHole.strokes_received} slag` : ""}`
}
className="flex min-h-12 w-full flex-col items-center justify-center gap-0.5 py-1 transition-colors hover:bg-accent/40 disabled:opacity-60"
>
@ -4640,11 +4723,9 @@ function SideScorecardGrid({
)}
{/* Mottatte slag vist FØR hullet er fylt ut (2026-07-29) --
samme kombinerte side-verdi som ville blitt trukket fra
ved utfylling. */}
ved utfylling. Prikker i stedet for "N"-tekst (2026-07-30). */}
{!hasScore && apiHole?.strokes_received != null && apiHole.strokes_received > 0 && (
<span className="text-[10px] font-semibold tabular-nums text-info">
{apiHole.strokes_received}
</span>
<StrokeDots count={apiHole.strokes_received} />
)}
</button>
</td>

View file

@ -30,7 +30,7 @@
import { useEffect, useState } from "react"
import Link from "next/link"
import { ArrowLeft } from "lucide-react"
import { ArrowLeft, ChevronRight } from "lucide-react"
import { cn } from "@/lib/utils"
// --- API-typer ---------------------------------------------------------------
@ -317,31 +317,6 @@ function ScoreBlock({
)
}
// --- Legend ----------------------------------------------------------------
function Legend() {
return (
<div
className="flex flex-wrap items-center gap-x-4 gap-y-2 rounded-2xl border border-border bg-card px-4 py-3"
aria-label="Symbolforklaring"
>
<span className="flex items-center gap-2">
<span className="flex size-6 items-center justify-center rounded-full border-2 border-primary bg-primary/10 text-xs font-extrabold text-primary">
3
</span>
<span className="text-sm font-semibold text-foreground">Under par (sirkel)</span>
</span>
<span className="flex items-center gap-2">
<span className="flex size-6 items-center justify-center rounded-[4px] border-2 border-brand-orange bg-brand-orange/10 text-xs font-extrabold text-brand-orange">
5
</span>
<span className="text-sm font-semibold text-foreground">Over par (firkant)</span>
</span>
<span className="text-sm font-medium text-muted-foreground">Fylt symbol = 2 slag eller mer</span>
</div>
)
}
// --- Combined total --------------------------------------------------------
function TotalTile({ label, value, tone }: { label: string; value: string; tone?: "orange" | "green" }) {
@ -1077,9 +1052,6 @@ export function RoundScorecard({ roundId }: { roundId: string }) {
</div>
)}
{/* Legend */}
<Legend />
{/* Scorecard blocks */}
<div className="flex flex-col gap-3 rounded-3xl border border-border bg-card p-3 shadow-md shadow-black/8 sm:p-4">
{front.length > 0 && (
@ -1106,6 +1078,17 @@ export function RoundScorecard({ roundId }: { roundId: string }) {
<MatchScorecardGrid round={round} roundId={roundId} viewerId={viewerId} refreshKey={refreshKey} />
)}
{isSkins && <SkinsProgressTable roundId={roundId} refreshKey={refreshKey} />}
{/* Symmetrisk med round-stats.tsx sin "Se scorekort"-lenke tilbake
hit (2026-07-30, etterspurt av bruker -- manglet fra scorekortet
og andre veien). */}
<Link
href={`/my-rounds/${roundId}/stats`}
className="flex min-h-14 items-center justify-center gap-1.5 rounded-2xl border border-primary/40 bg-card px-5 text-base font-bold text-foreground shadow-sm transition-colors hover:bg-accent/50"
>
Se full rundestatistikk
<ChevronRight aria-hidden="true" className="size-5" />
</Link>
</main>
</div>
)