Fiks: eierens statistikknivå/utslag ble ikke lagret i Ny runde-veiviseren (ADR-073)

Eierens spillerobjekt i ny-runde-veiviseren hadde egne teeId/statLevel-felt,
kun synkronisert fra state.teeId/state.statLevel ÉN gang da banen først ble
valgt. For utslag var dette kosmetisk (submit() leser state.teeId direkte
for eieren), men for statistikknivå en reell datafeil: "Statistikk for deg
selv"-valget i steg 2 hadde ingen effekt på eierens faktiske innsending,
som alltid endte på strokes_only uansett hva som ble valgt.

Erstatter den punktvise engangs-synken med én kontinuerlig useEffect i
wizard-context.tsx. Bruker-rapportert regresjon (video vedlagt) -- se
ADR-073/CHANGELOG for full detalj og verifisering.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erol Haagenrud 2026-08-15 07:20:20 +02:00
parent bb087b4437
commit bf6955bff6
4 changed files with 167 additions and 7 deletions

View file

@ -7215,6 +7215,100 @@ bruker. Begge containere startet rent.
--- ---
## ADR-073: Regresjon i "Ny runde"-veiviseren -- eierens statistikknivå (og utslag ved endring) ble ikke lagret (2026-08-15)
Foranledning: bruker rapporterte at de "til stadighet må gå tilbake å
sette utslagssted og statistikktype på nytt før runden begynner", og at
"det må ha blitt borte i prosessen, for det fungerte tidligere" --
vedla en skjermopptak-video (`Temp-uploads/Video_2026-08-15_070115.mp4`,
lest via `ffmpeg` hentet på farten som npm-pakke `ffmpeg-static`, siden
ingen video-verktøy fantes fra før og `apt`/`pip` krevde sudo).
**Videoen viste presist to symptomer for RUNDE-EIEREN** (frittstående
runde, `ny-runde/`-veiviseren): (1) endres utslag i "Bane & tid"-
bekreftelsesskjermen ETTER at banen først ble valgt, viser steg 3 sitt
spillerkort fortsatt det GAMLE utslaget -- (2) "Statistikk for deg
selv"-valget i steg 2 ("Spilleform") hadde INGEN effekt på den faktiske
innsendingen -- runden ble alltid opprettet med `strokes_only` ("Kun
slag") uansett hva som ble valgt der, bekreftet ved å åpne "Rediger" på
eget spillerkort i steg 3 rett før innsending.
**Rotårsak (`wizard-context.tsx`/`step1-course-time.tsx`/`step2-
format.tsx`):** eierens spillerobjekt i `state.players[]` har SINE EGNE
`teeId`/`statLevel`-felt, atskilt fra de øverste `state.teeId`/`state.
statLevel`-feltene steg 1/steg 2 faktisk redigerer. Disse ble tidligere
KUN synkronisert i ett øyeblikk -- da banen først velges (`pickCourse()`
i `step1-course-time.tsx`, som satte begge samtidig). Endres utslag
IGJEN i bekreftelsesskjermen (`Fields()`, linje 746, `onChange={(e) =>
patch({ teeId: ... })}`), oppdateres kun det øverste feltet -- for
utslag var dette rent KOSMETISK (selve `submit()` i `wizard-context.tsx`
leser `state.teeId` direkte for eieren ved selve rundeopprettelsen, se
linje 307/311), men for statistikknivå var det en REELL datafeil:
`submit()` sin `stat_level`-verdi for eieren leses fra `owner?.
statLevel` (spillerobjektet, linje 317) -- IKKE fra `state.statLevel`.
`owner.statLevel` settes KUN til `"score"` hardkodet i `loadMe()`
(linje 217) når `/auth/me` laster, og synkroniseres ALDRI fra `state.
statLevel` når brukeren faktisk gjør et valg i steg 2 -- "Statistikk for
deg selv"-seksjonen påvirket dermed KUN default-verdien for NYE
medspillere lagt til senere (`defaultStat`-proppen til `AddPlayerPanel`,
uendret/fortsatt riktig), aldri eierens eget valg.
**Ikke en ny mangel -- en bekreftet regresjon:** kommentaren over
`pickCourse()` sin manuelle engangs-synk siterte eksplisitt at den
"matcher eksakt" den opprinnelige (nå oppdelte) `new-round.tsx` sin
`chooseCourse()`-logikk for UTSLAG -- selve engang-synk-mønsteret er
altså gammelt og forsettlig replikert ved en tidligere refaktorering til
dagens `ny-runde/`-mappestruktur. Ingenting tilsvarende fantes noensinne
for STATISTIKKNIVÅ i denne koden -- det er uklart om en eldre
enkeltfil-versjon av veiviseren hadde `owner.statLevel` lest direkte fra
`state.statLevel` uten et eget spillerfelt (brukerens "det fungerte
tidligere" tyder på det), men konklusjonen er uansett den samme: i
DAGENS kode har "Statistikk for deg selv" vært virkningsløs for eieren
siden splittingen til separate steg-komponenter, sannsynligvis
usett fordi standardverdien (`strokes_only`) er nettopp det de fleste
runder uansett bruker.
**Fiks:** i stedet for punktvise manuelle synkroniseringer, en ENKELT
`useEffect` i `WizardProvider` (`wizard-context.tsx`) som holder
`players[owner].teeId`/`statLevel` KONTINUERLIG i synk med `state.teeId`/
`state.statLevel`, uansett hvor mange ganger disse endres eller fra
hvilket steg. Kjører kun en faktisk `patch()` når verdiene faktisk
avviker (unngår unødvendige re-renders/uendelig løkke). Den nå
overflødige manuelle engangs-synken i `pickCourse()` fjernet -- ett
sted for denne logikken fremfor to som lett driver fra hverandre igjen.
Endrer brukeren stat-nivået sitt EKSPLISITT i steg 3 sitt eget
spillerkort (i stedet for steg 2), respekteres det inntil de ev. går
TILBAKE til steg 2 og endrer det der igjen (samme presedens-rekkefølge
som "siste redigerte felt vinner", ingen ny brukerforvirring innført).
**Verifisert:** `tsc --noEmit` rent + 45/45 vitest (ingen eksisterende
test dekket denne regresjonen -- INGEN ny automatisert test lagt til
heller, siden `ny-runde/`-veiviseren ikke har noen eksisterende
test-infrastruktur i dette repoet; browserverifisering ansett
tilstrekkelig for en ren frontend-tilstandssynk-fiks). Egen
scratch-database + scratch `teecup_api`-container (port 18007,
live-montert kode) + lokal `next dev` (port 13007), MED en egen bane
opprettet med to utslag (for å kunne reprodusere "bytt utslag etter
førstevalg") -- gjenskapte videoens eksakte scenario (banevalg → bytt
utslag fra standard "32" til "50" i bekreftelsesskjermen → "All
statistikk" i steg 2 → steg 3 sitt spillerkort viste nå KORREKT "Utslag:
50" og "Alt" (uten regresjonens stale "32"/"Kun slag") → fullført runde
→ bekreftet direkte i databasen at BÅDE `round.tee_name_snapshot='50'`
OG `round_participant.stat_level='full'` faktisk ble lagret → åpnet
scoreregistrering for hull 1, bekreftet at veiviseren auto-hopper videre
til Putter-steget (kun mulig for `strokes_and_putts`/`full`, aldri for
`strokes_only`) -- beviser fiksen virker HELE VEIEN til faktisk lagret
data, ikke bare i visningen. Lys+mørk bekreftet for selve
"Ny runde"-veiviseren (steg 1-3). Scratch-stacken fullstendig revet ned
-- ekte `teecup_db`/`teecup_api`/`teecup_frontend` urørt.
**Ingen migrasjon** -- ren frontend-tilstandssynk-fiks.
**IKKE rullet ut ennå** -- venter på eksplisitt bekreftelse fra bruker
før `teecup_frontend` bygges/startes på nytt.
---
## Utviklingsplan (rekkefølge) ## Utviklingsplan (rekkefølge)
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003) 1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)

View file

@ -11485,3 +11485,47 @@ Neste steg:
**Rullet ut 2026-08-15** -- ingen migrasjon, `docker compose build **Rullet ut 2026-08-15** -- ingen migrasjon, `docker compose build
teecup_api teecup_frontend && up -d` etter eksplisitt bekreftelse fra teecup_api teecup_frontend && up -d` etter eksplisitt bekreftelse fra
bruker. Begge containere startet rent. bruker. Begge containere startet rent.
89. **Fiks: eierens statistikknivå (og utslag ved endring) ble ikke
lagret i "Ny runde"-veiviseren (ADR-073) — 2026-08-15.** Bruker
rapporterte at de "til stadighet må" sette utslag/statistikktype på
nytt, og at "det må ha blitt borte i prosessen, for det fungerte
tidligere" -- vedla en skjermopptak-video som viste to symptomer.
Rotårsak: eierens spillerobjekt i veiviserens `state.players[]` har
EGNE `teeId`/`statLevel`-felt, atskilt fra `state.teeId`/`state.
statLevel` (de faktiske feltene steg 1/steg 2 redigerer) -- kun
synkronisert ÉN gang, da banen først velges. For utslag var
konsekvensen rent kosmetisk (steg 3 viste et gammelt utslag, men
selve innsendingen leste `state.teeId` direkte for eieren) -- for
statistikknivå var det en REELL datafeil: `submit()` leser eierens
`stat_level` fra spillerobjektet (hardkodet `"score"` fra `/auth/me`-
lastingen), ALDRI fra `state.statLevel` -- "Statistikk for deg
selv"-valget i steg 2 var dermed virkningsløst for eieren, kun
default for NYE medspillere lagt til senere.
Fiks: én samlet `useEffect` i `wizard-context.tsx` som holder
eierens spillerobjekt kontinuerlig i synk med `state.teeId`/`state.
statLevel`, uansett hvor mange ganger de endres eller fra hvilket
steg -- erstatter den tidligere punktvise engangs-synken i
`pickCourse()` (fjernet, nå overflødig).
**Verifisert:** `tsc --noEmit` rent + 45/45 vitest (ingen eksisterende
testinfrastruktur for `ny-runde/`-veiviseren i dette repoet -- ingen
ny automatisert test lagt til, browserverifisering ansett
tilstrekkelig for en ren tilstandssynk-fiks). Scratch-database +
scratch `teecup_api` (port 18007, live-montert kode) + lokal `next
dev` (port 13007) med en egen bane med to utslag: gjenskapte
videoens eksakte scenario (bytt utslag etter førstevalg, velg "All
statistikk") -- steg 3 viste nå korrekt utslag+nivå, OG bekreftet
direkte i databasen at både `round.tee_name_snapshot` og
`round_participant.stat_level='full'` faktisk ble lagret (ikke bare
riktig i visningen) -- scoreregistreringen auto-hoppet deretter
videre til Putter-steget, kun mulig for `full`. Lys+mørk bekreftet.
Scratch-stacken fullstendig revet ned -- ekte `teecup_db`/
`teecup_api`/`teecup_frontend` urørt.
**Ingen migrasjon** -- ren frontend-tilstandssynk-fiks.
**IKKE rullet ut ennå** -- venter på eksplisitt bekreftelse fra
bruker før `teecup_frontend` bygges/startes på nytt.

View file

@ -28,19 +28,16 @@ export function Step1CourseTime() {
} }
const compatible = state.ownGender ? course.tees.filter((t) => t.genders.includes(state.ownGender!)) : course.tees const compatible = state.ownGender ? course.tees.filter((t) => t.genders.includes(state.ownGender!)) : course.tees
const tee = compatible[0] const tee = compatible[0]
// Eierens EGET spillerkort (steg 3) sitt teeId-felt synkroniseres nå
// løpende fra state.teeId av en egen effekt i wizard-context.tsx
// (fanger også senere endringer i "Bane & tid"-bekreftelsen, ikke bare
// dette førstevalget) -- ingen manuell synk nødvendig her lenger.
patch({ patch({
course, course,
courseMeta: meta, courseMeta: meta,
courseSource: meta.source === "teeoff" ? "official" : "own", courseSource: meta.source === "teeoff" ? "official" : "own",
teeId: tee?.id, teeId: tee?.id,
s1Sub: "fields", s1Sub: "fields",
// Eierens EGET spillerkort (steg 3) har sitt eget teeId-felt, atskilt
// fra state.teeId over -- uten denne synkroniseringen viser kortet
// "Ikke valgt" selv om et utslag faktisk er valgt (kun kosmetisk, selve
// innsendingen leser state.teeId direkte for eieren -- men samme
// synkronisering som den opprinnelige new-round.tsx sin chooseCourse()
// gjorde, så oppførselen matcher eksakt).
players: state.players.map((p) => (p.kind === "owner" ? { ...p, teeId: tee?.id } : p)),
}) })
} }

View file

@ -228,6 +228,31 @@ export function WizardProvider({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
// Eierens EGET teeId/statLevel-felt i players[] (lest av steg 3 sitt
// spillerkort, OG -- for statLevel -- av submit() sin faktiske innsending,
// se linje ~317) er egne, atskilte felt fra state.teeId/state.statLevel
// (steg 1/steg 2 sine felt). De ble tidligere kun synkronisert ÉN gang,
// idet banen først velges (pickCourse i step1-course-time.tsx) -- endrer
// brukeren utslag i "Bane & tid"-bekreftelsen ETTERPÅ, eller statistikknivå
// i "Spilleform", flyter de to fra hverandre. For utslag var dette rent
// kosmetisk (steg 3 viste et gammelt utslag, men submit() leser
// state.teeId direkte for eieren) -- for statistikknivå var det en REELL
// datafeil: "Statistikk for deg selv"-valget i steg 2 hadde INGEN effekt
// på selve innsendingen for eieren (kun på default for NYE medspillere via
// defaultStat-proppen), siden owner.statLevel forble hardkodet "score" fra
// loadMe() over. Bruker-rapportert regresjon 2026-08-15 (video vedlagt) --
// holder nå de to i kontinuerlig synk fremfor kun ved første valg.
useEffect(() => {
setState((s) => {
const owner = s.players.find((p) => p.kind === "owner")
if (!owner || (owner.teeId === s.teeId && owner.statLevel === s.statLevel)) return s
return {
...s,
players: s.players.map((p) => (p.kind === "owner" ? { ...p, teeId: s.teeId, statLevel: s.statLevel } : p)),
}
})
}, [state.teeId, state.statLevel])
const hasTeamStep = showsTeamStep(state.format) const hasTeamStep = showsTeamStep(state.format)
const visibleSteps = useMemo(() => (hasTeamStep ? [1, 2, 3, 4, 5] : [1, 2, 3, 5]), [hasTeamStep]) const visibleSteps = useMemo(() => (hasTeamStep ? [1, 2, 3, 4, 5] : [1, 2, 3, 5]), [hasTeamStep])