diff --git a/CLAUDE.md b/CLAUDE.md
index 63cedb3..6915419 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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
diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md
index 6aaa4c9..3947ac6 100644
--- a/FEATURE_BACKLOG.md
+++ b/FEATURE_BACKLOG.md
@@ -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
diff --git a/app/routers/individual_tournaments.py b/app/routers/individual_tournaments.py
index 0052310..449e28d 100644
--- a/app/routers/individual_tournaments.py
+++ b/app/routers/individual_tournaments.py
@@ -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 nå 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 på
+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 PÅ 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"],
diff --git a/app/team_authz.py b/app/team_authz.py
index 4835406..3aca9a0 100644
--- a/app/team_authz.py
+++ b/app/team_authz.py
@@ -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 på 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
diff --git a/frontend/components/round-detail.tsx b/frontend/components/round-detail.tsx
index 88e2207..6d632b1 100644
--- a/frontend/components/round-detail.tsx
+++ b/frontend/components/round-detail.tsx
@@ -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. */}
+ {/* 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 && }
+
{/* Completed summary banner */}
{completed && (
{
+ 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 (
+
+
+
+
+
+
+ Alle hull er ført
+
Klar til å fullføre runden?
+
+
+
+
+ )
+}
+
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 && {net}
) : (
// 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 && (
-
- −{apiHole.strokes_received}
-
- )
+ apiHole.strokes_received > 0 &&
)}
@@ -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 (
+
+ {Array.from({ length: count }, (_, i) => (
+
+ ))}
+
+ )
+}
+
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 && (
-
- −{apiHole.strokes_received}
-
+
)}
diff --git a/frontend/components/round-scorecard.tsx b/frontend/components/round-scorecard.tsx
index 6d0551e..46e8a78 100644
--- a/frontend/components/round-scorecard.tsx
+++ b/frontend/components/round-scorecard.tsx
@@ -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 (
-
-
-
- 3
-
- Under par (sirkel)
-
-
-
- 5
-
- Over par (firkant)
-
- Fylt symbol = 2 slag eller mer
-
{front.length > 0 && (
@@ -1106,6 +1078,17 @@ export function RoundScorecard({ roundId }: { roundId: string }) {
)}
{isSkins && }
+
+ {/* Symmetrisk med round-stats.tsx sin "Se scorekort"-lenke tilbake
+ hit (2026-07-30, etterspurt av bruker -- manglet fra scorekortet
+ og andre veien). */}
+
+ Se full rundestatistikk
+
+