From facefd99516740a709a9ddc313a11b7aee2b3af3 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Thu, 30 Jul 2026 09:36:52 +0200 Subject: [PATCH] Begge implementert, browserverifisert og live: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CLAUDE.md | 116 ++++++++++++++ FEATURE_BACKLOG.md | 200 +++++++++++++++++++++++- app/routers/individual_tournaments.py | 37 ++++- app/team_authz.py | 34 ++++ frontend/components/round-detail.tsx | 107 +++++++++++-- frontend/components/round-scorecard.tsx | 41 ++--- 6 files changed, 480 insertions(+), 55 deletions(-) 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 ( +