diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..de13aac --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": ["-y", "chrome-devtools-mcp@latest", "--headless", "--viewport", "390x844"] + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 36844d8..cf82264 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3576,6 +3576,103 @@ Ferdig og verifisert: også `teecup_api` som vanlig bivirkning, ingen backend-kode rørt). Begge containere boot-et rent, `/health`/`/dashboard` → 200, `teeoff.no` upåvirket. +- **Spec-dokumentets §1 (scorekort som fullt grid) BYGGET OG LIVE + (2026-07-27), samme dag rett etter compliance-passet:** brukeren + bekreftet eksplisitt "JA" på at grid-redesignet er en egen, bevisst + runde, egen informasjonsarkitektur enn ett-hull-om-gangen-listen + bygget dagen før. `round-detail.tsx` sin "Score"-fane fikk hull-strip + + ett-hull-panel ERSTATTET av en ny `ScorecardGrid`: spillere som rader + (sticky venstre navnekolonne, navn+HCP, samme tap-target åpner + veiviseren for gjeldende hull), hull som horisontalt scrollbare + kolonner, `Hcp`/`Par`-referanserader over spillerradene (samme + konvensjon som den allerede shippede `round-scorecard.tsx`), sticky + `Ut`/`Inn`/`Sum` til høyre (Ut/Inn gruppert på FYSISK hullnummer 1-9/ + 10-18, uavhengig av øktens starthull — riktig konvensjon uansett + spillerekkefølge; kun vist for 18-hulls runder, 9-hulls runder får + én samlet Sum). Ny `ScorecardCell` gjenbruker EKSAKT samme klassifisering + og Tailwind-klasser som `ScoreMark`/`classify` i `round-scorecard.tsx` + (lest direkte, ikke gjettet) — ulikt compliance-passets softere + `rounded-2xl`-knapp-variant (fortsatt riktig der, egen visuell kontekst), + siden dette er en LITEN tabellcelle som spec eksplisitt ber om å følge + språket "UBRYTELIG". + **Interaksjon:** tapp en score-celle ELLER spillerens navnecelle ELLER + en hull-kolonneoverskrift åpner `ScoringWizard` (uendret komponent fra + v2-runden) for akkurat den (spiller, hull)-kombinasjonen — kolonne- + overskrift alene (uten å tappe en celle) setter kun "gjeldende hull" + uten å åpne veiviseren, samme jobb som den fjernede `HoleNav` gjorde. + Beholdt en kompakt "Forrige/Neste hull"-knapperad under gridet for + rask sekvensiell registrering uten bred scrolling. + **Dødt kode fjernet i samme runde:** `HoleNav`, `holeIsPlayed`, + GIR-merket/`showGir` (ga ikke lenger mening i en multi-hull-visning), + og OGSÅ compliance-passets `scoreMarkClasses`-hjelpefunksjon (var kun + brukt av den nå fjernede ett-hull-listens store runde knapp). + **Verifisert:** ekte typesjekket produksjonsbuild (samme `Dockerfile` + som deployes) kompilerte rent, alle 24 ruter listet. I TILLEGG en + engangs full runner-image bygget og kjørt i en isolert container (ikke + bare `--target builder`) — `/my-rounds/[id]` for en ukjent runde-id + ga 200, ingen React-feilgrense/krasj-markup i responsen. **Samme + ærlige begrensning som ALT håndkodet arbeid denne uken, men STØRRE + konsekvens denne gangen siden dette er en vesentlig strukturell endring + (ny informasjonsarkitektur), ikke en liten fiks:** ingen ekte + nettleser-interaksjonstest (scrolling, tapping av celler/hull- + overskrifter/navnerad, faktisk visuelt resultat av sticky-kolonnene) + er utført — flagget eksplisitt til bruker FØR utrulling, bruker bør + selv klikke seg grundig gjennom før full tillit. + **Rullet ut live 2026-07-27**, bruker bekreftet eksplisitt: ingen + migrasjon, `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`/`/my-rounds` → 200, + `teeoff.no` upåvirket. +- **Reell produksjonsbug funnet OG fikset SAMME DAG via ekte nettleser- + testing (2026-07-27) — FØRSTE gang denne økten en Chrome DevTools MCP + har vært tilgjengelig.** Brukeren ba meg selv åpne gridet + (`localhost:3000/my-rounds/{id}`), logget meg inn (passord feilet først + — kontoen mangler passord/miljøet pekte annerledes; løst med en ekte + magic-link brukeren limte inn), og jeg tok et ekte skjermbilde av det + nettopp bygde scorekort-gridet på en mobil viewport (390×844) mot ekte + produksjonsdata (runden `c5db2e31-...`, 2 spillere, 4/18 hull spilt). + **Fant umiddelbart en alvorlig, reell rendering-bug** som ALDRI ble + fanget av typesjekking/container-boot-verifisering: `position: sticky` + på ``/`` inni en `` med `border-collapse` rendret + fullstendig ødelagt i Chrome — de sticky `Ut`/`Inn`/`Sum`-kolonnene + overlappet/utvisket hull-kolonnene bak seg (synlige sammenblandede + siffer, f.eks. "436"/"472" der Par-radens tall lå oppå hverandre). + Nøyaktig den typen feil den gjentatte "ingen ekte nettleser-test + utført"-forbeholdet advarte mot hele uken. + **Første fiks-forsøk (kun `border-collapse` → `border-separate + border-spacing-0`) løste IKKE problemet** — re-skjermbilde etter + redeploy viste samme overlapp. **Rot-årsaken var strukturell, ikke + syntaktisk:** å kombinere en sticky VENSTRE-kolonne MED sticky HØYRE- + kolonner i en tabell som er mye bredere enn viewporten er i seg selv + et ustabilt mønster — ved scroll-posisjon 0 blir de sticky høyre- + kolonnene umiddelbart trukket til synlig høyre kant, og alt som + "egentlig" befinner seg der i normal dokument-flyt (hull 3+) blir + liggende RETT BAK dem, delvis synlig gjennom `bg-muted/60`s + delvise gjennomsiktighet. **Fikset ved å fjerne sticky-posisjonering + fra `Ut`/`Inn`/`Sum`-kolonnene helt** (de scroller nå med resten av + hullene i normal flyt, samme velprøvde, veletablerte mønster som den + gjenværende sticky VENSTRE navnekolonnen, som fungerte korrekt hele + tiden) — droppet også `/60`-gjennomsiktigheten til fordel for en helt + opak `bg-muted`. + **Verifisert presist, ekte, etter fiksen** (ikke bare "ser bedre ut"): + nytt skjermbilde ved scroll-posisjon 0 viste alle tall rene og lesbare + (Hcp/Par-radene, begge spilleres scoringsceller, korrekt sirkel/firkant- + form for bogey/dobbel bogey/par), OG et script som scrollet gridet helt + til høyre (`scrollLeft = scrollWidth`) bekreftet `Ut`/`Inn`/`Sum` også + rene der (`Ut 36/Inn 36/Sum 72` på Par-raden — stemmer eksakt med en + 18-hulls par-72-bane), med navnekolonnen fortsatt korrekt pinnet til + venstre gjennom hele scrollingen. Tilgjengelighetstreet (`take_snapshot`) + bekreftet også at aria-labels med golftermer ("Bogey", "Dobbel bogey", + "Par") faktisk leses ut korrekt — `§Golfscore-språket`s "aldri farge + alene"-regel holder i praksis, ikke bare i teorien. + **Rullet ut live 2026-07-27**, samme dag: `docker compose up -d --build + teecup_frontend` kjørt to ganger (én for det mislykkede første forsøket, + én for den faktiske fiksen), `/health` 200 begge ganger, `teeoff.no` + upåvirket. + **Lærdom:** Chrome DevTools MCP-tilgangen endrer risikobildet for alt + fremtidig håndkodet frontend-arbeid denne økten — bruk den til å + FAKTISK se resultatet før noe rapporteres som ferdig, i stedet for kun + typesjekk+container-boot+`curl`-baserte proxyer for "det virker". Neste steg: 0a. **Spillerliste-redesign — LIVE, men ikke nettleser-testet diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 89b71ec..ce3d1cf 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -1333,10 +1333,26 @@ DAG — ✅ LIVE:** "Deg Deg"-badge-duplikat fjernet (`playerLabel()` viser nå alltid ekte navn), scoringslistens score-knapp gjort om til å følge `§Golfscore-språket` (gjenbruker `ScoreMark`s fargespråk fra `round-scorecard.tsx`), pluss et system­atisk `tabular-nums`/44px- -trykkgulv-avvik-audit fikset (13 steder). Spec-dokumentets §1 (scorekort -som fullt grid, egen skjerm) bevisst IKKE bygget — større strukturell -endring, egen fremtidig runde. Full detalj i CLAUDE.md sin statuslogg -(2026-07-27) — ikke duplisert her. +trykkgulv-avvik-audit fikset (13 steder). + +Spec-dokumentets §1 (scorekort som fullt grid) krevde et bevisst "JA" fra +brukeren først (egen informasjonsarkitektur enn ett-hull-om-gangen-listen) +— bekreftet SAMME dag, og BYGGET OG RULLET UT LIVE 2026-07-27 rett etter +compliance-passet: erstattet med en scrollbar `ScorecardGrid` (spillere +som rader, hull som kolonner, sticky navnekolonne + Ut/Inn/Sum), celler +følger `§Golfscore-språket` UBRYTELIG (gjenbrukt eksakt fra +`round-scorecard.tsx`). Tapp en celle/navnerad/hull-overskrift åpner +samme `ScoringWizard` som før, nå adressert direkte fra gridet. + +**Reell alvorlig rendering-bug funnet OG fikset SAMME DAG, rett etter +utrullingen, via ekte nettleser-testing** (første gang en Chrome DevTools +MCP var tilgjengelig denne økten) — `position: sticky` på tabellceller +kombinert med sticky venstre+høyre kolonner samtidig rendret fullstendig +ødelagt/overlappende i Chrome. Fikset ved å droppe sticky-posisjonering +på `Ut`/`Inn`/`Sum`-kolonnene (scroller nå med resten av hullene i normal +flyt) og beholde kun den velprøvde sticky venstre navnekolonnen. Verifisert +med ekte skjermbilder + scroll-simulering mot ekte produksjonsdata. Full +detalj i CLAUDE.md sin statuslogg (2026-07-27) — ikke duplisert her. --- diff --git a/frontend/components/round-detail.tsx b/frontend/components/round-detail.tsx index dc54e1d..e754aab 100644 --- a/frontend/components/round-detail.tsx +++ b/frontend/components/round-detail.tsx @@ -454,17 +454,6 @@ export function RoundDetail({ roundId }: { roundId: string }) { const holeOrder = round ? Array.from({ length: round.holes_planned }, (_, i) => ((round.start_hole - 1 + i) % 18) + 1) : [] - const orderedHoles = holeOrder.map((n) => holes.find((h) => h.holeNumber === n)).filter((h): h is Hole => h !== undefined) - - const currentApiHole = apiHoles?.find((h) => h.hole_number === activeHole) ?? null - const currentStat = currentApiHole ? apiHoleToStat(currentApiHole) : emptyStat() - - const showGir = - currentStat.played && - currentStat.strokes !== null && - currentStat.putts !== null && - hole !== null && - currentStat.strokes - currentStat.putts <= hole.par - 2 // Veiviserens mål (2026-07-26) -- HVEM sin score som registreres akkurat // nå, uavhengig av `activePlayerId` (som fortsatt kun styrer hvem sin @@ -490,10 +479,6 @@ export function RoundDetail({ roundId }: { roundId: string }) { })) } - function holeIsPlayed(holeNumber: number) { - return apiHoles?.find((h) => h.hole_number === holeNumber)?.played ?? false - } - function goPrev() { const i = holeOrder.indexOf(activeHole) setCurrentHole(holeOrder[(i - 1 + holeOrder.length) % holeOrder.length]) @@ -847,133 +832,51 @@ export function RoundDetail({ roundId }: { roundId: string }) { /> )} - {/* Hole navigation */} + {/* Scorekort-grid (§1 i teecup-scorekort-og-entry-spec.md, bygget + 2026-07-27 -- erstatter den forrige rundens ett-hull-om-gangen- + liste, bruker eksplisitt bekreftet "JA" på at grid-redesignet er + en egen, større runde). Spillere som rader (sticky venstre + navnekolonne), hull som horisontalt scrollbare kolonner -- + HELE runden synlig samtidig, ikke bare det aktive hullet. */} {holes.length === 0 ? (
) : ( - <> - + { + readOnly={readOnly} + onSelectHole={(n) => { setCurrentHole(n) scrollToHolePanel() }} + onOpenEntry={(playerId, holeNumber) => { + setCurrentHole(holeNumber) + setActivePlayerId(playerId) + setWizardPlayerId(playerId) + }} /> - {/* Current hole panel */} - {hole && ( -
- {/* Høyre kolonne reserverer plass ved siden av hull-headeren - til en fremtidig avstandsmåling-indikator (ikke bygget - ennå, men avklart 2026-07-25 at det kommer) -- GIR-merket - ligger under den reserverte plassen slik at et senere - avstand-chip her ikke krever noen layout-endring. */} -
-

- Hull {hole.holeNumber} · Par {hole.par} · Hcp {hole.index} - {/* Mottatte slag for AKTIV spiller på dette hullet (2026-07-26) - -- samme allerede hentede strokes_received som ScoreSoFar/ - leaderboardet bruker til netto, bare aldri vist FØR - scoring her. Golfvis fortegn (ekte minustegn), kun vist - når spilleren faktisk mottar minst ett slag her. */} - {currentApiHole?.strokes_received != null && currentApiHole.strokes_received > 0 && ( - <> · −{currentApiHole.strokes_received} - )} -

-
- {showGir && ( - - GIR - - )} -
-
- - {/* Registrering (2026-07-26, inspirert av en konkurrentapps - samlebåndsflyt): én kompakt rad per spiller med - akkumulert score-så-langt for RUNDEN alltid synlig, og en - trykkflate som åpner en fokusert, skjermovertagende - veiviser for AKKURAT denne spilleren og hullet -- i - stedet for ett langt skjema man skrollet gjennom for én - spiller om gangen, med manuell fanebytting mellom - spillere (rapportert av bruker 2026-07-26 som "ingen - forbedring", "visuelt like overveldende og rotete"). */} -
    - {players.map((player) => { - const playerHoles = holesByParticipant[player.id] - const playerApiHole = playerHoles?.find((h) => h.hole_number === activeHole) ?? null - const hasScore = Boolean(playerApiHole?.played) && playerApiHole?.score !== null - const running = computeRunningToPar(playerHoles) - // Golfscore-språket (2026-07-27, compliance-pass): sirkel - // under par / rundet firkant over par / nøytral sirkel på - // par -- ALDRI samme grønne fylte sirkel uansett resultat - // (bogey og dobbel bogey skal ikke se ut som birdie). - const diff = hasScore && hole ? (playerApiHole!.score as number) - hole.par : null - return ( -
  • - - - {player.name} - {player.isSelf && ( - - Deg - - )} - - - HCP {player.hcp !== null ? player.hcp.toFixed(1).replace(".", ",") : "ikke satt"} - {running && ( - <> - {" · "} - {formatToPar(running.toPar)} så langt ({running.holesPlayed} hull) - - )} - - - -
  • - ) - })} -
- -
- - -
-
- )} - + {/* Rask sekvensiell hull-navigasjon -- gridet viser allerede + alle hull som kolonner (tapp en kolonneoverskrift for å + velge), men disse to gir en rask vei videre uten å måtte + scrolle bredt for hvert eneste hull. */} +
+ + +
+ )} )} @@ -2630,50 +2533,265 @@ function ChangeCourseForm({ ) } -// --- Hole navigation ------------------------------------------------------- +// --- Scorekort-grid (§1 i teecup-scorekort-og-entry-spec.md, 2026-07-27) --- +// Spillere = rader (sticky venstre navnekolonne), hull = horisontalt +// scrollbare kolonner (sticky Ut/Inn/Sum til høyre) -- erstatter den +// forrige ett-hull-om-gangen-listen. Tapp en kolonneoverskrift for å velge +// "gjeldende" hull (kun navigasjon), tapp en spillers navnecelle eller en +// score-celle for å åpne `ScoringWizard` for AKKURAT den (spiller, hull)- +// kombinasjonen -- samme veiviser som før, bare adressert direkte fra +// gridet i stedet for via ett enkelt "aktivt hull"-panel. +// Par/stroke-indeks er samme fysiske hull for alle spillere -- hentes fra +// HVILKEN SOM HELST allerede lastet spillers hull-liste (ikke bare aktiv +// spiller), robust mot i hvilken rekkefølge deltakernes hull lastes inn. -function HoleNav({ - holes, +function ScorecardGrid({ + players, + holesByParticipant, + holeOrder, + holesPlanned, currentHole, - isPlayed, - onSelect, + readOnly, + onSelectHole, + onOpenEntry, }: { - holes: Hole[] + players: Player[] + holesByParticipant: Record + holeOrder: number[] + holesPlanned: number currentHole: number - isPlayed: (holeNumber: number) => boolean - onSelect: (holeNumber: number) => void + readOnly: boolean + onSelectHole: (holeNumber: number) => void + onOpenEntry: (playerId: string, holeNumber: number) => void }) { + const holeMeta: Record = {} + for (const list of Object.values(holesByParticipant)) { + for (const h of list) { + if (!(h.hole_number in holeMeta)) holeMeta[h.hole_number] = { par: h.par, index: h.stroke_index } + } + } + + // Ut(1-9)/Inn(10-18) er alltid gruppert på FYSISK hullnummer (tradisjonell + // scorekort-konvensjon), ikke på spillerekkefølgen -- riktig uansett + // hvilket starthull runden faktisk begynte på. Kun meningsfullt å splitte + // for en 18-hulls runde; en 9-hulls runde får kun én samlet Sum-kolonne. + const showSplit = holesPlanned === 18 + const front = holeOrder.filter((n) => n <= 9) + const back = holeOrder.filter((n) => n >= 10) + + function sumFor(playerId: string, numbers: number[]): { strokes: number; par: number } | null { + const list = holesByParticipant[playerId] + if (!list || numbers.length === 0) return null + const played = numbers + .map((n) => list.find((h) => h.hole_number === n)) + .filter((h): h is ApiHole => Boolean(h) && h!.played && h!.score !== null) + if (played.length === 0) return null + return { + strokes: played.reduce((sum, h) => sum + (h.score as number), 0), + par: played.reduce((sum, h) => sum + h.par, 0), + } + } + + const parRowTotal = holeOrder.reduce((sum, n) => sum + (holeMeta[n]?.par ?? 0), 0) + return ( -
+ + + {holeOrder.map((n) => ( + + ))} + {showSplit && } + {showSplit && } + + + + + + {holeOrder.map((n) => { + const active = n === currentHole + return ( + + ) + })} + {showSplit && ( + + )} + {showSplit && ( + + )} + + + + + {/* Hcp- og Par-referanserader (samme mønster som den allerede + shippede round-scorecard.tsx) -- rene informasjonsrader, ikke + trykkbare. */} + + + {holeOrder.map((n) => ( + + ))} + {showSplit && + + + {holeOrder.map((n) => ( + + ))} + {showSplit && ( + + )} + {showSplit && ( + + )} + + + + {players.map((player) => { + const list = holesByParticipant[player.id] + const frontSum = showSplit ? sumFor(player.id, front) : null + const backSum = showSplit ? sumFor(player.id, back) : null + const totalSum = sumFor(player.id, holeOrder) + return ( + + + {holeOrder.map((n) => { + const apiHole = list?.find((h) => h.hole_number === n) ?? null + const meta = holeMeta[n] + const hasScore = Boolean(apiHole?.played) && apiHole?.score !== null + const diff = hasScore && meta ? (apiHole!.score as number) - meta.par : null + const net = + hasScore && apiHole?.strokes_received != null ? (apiHole!.score as number) - apiHole!.strokes_received : null + return ( + + ) + })} + {showSplit && ( + + )} + {showSplit && ( + + )} + + + ) + })} + +
- {h.holeNumber} - - ) - })} - - + Spiller + + + + Ut + + Inn + + Sum +
+ Hcp + + {holeMeta[n]?.index ?? "–"} + } + {showSplit && } + +
+ Par + + {holeMeta[n]?.par ?? "–"} + + {front.reduce((sum, n) => sum + (holeMeta[n]?.par ?? 0), 0) || "–"} + + {back.reduce((sum, n) => sum + (holeMeta[n]?.par ?? 0), 0) || "–"} + + {parRowTotal || "–"} +
+ + + + + {frontSum ? frontSum.strokes : "–"} + + {backSum ? backSum.strokes : "–"} + + {totalSum ? ( + + {totalSum.strokes} + + {formatToPar(totalSum.strokes - totalSum.par)} + + + ) : ( + "–" + )} +
+ ) } +// Golfscore-språket for en LITEN tabellcelle -- samme eksakte klassifisering +// og Tailwind-klasser som `ScoreMark`/`classify` i round-scorecard.tsx +// (bekreftet ved å lese kildekoden der direkte, egen kopi her per +// 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. +function ScorecardCell({ value, diff }: { value: number | null; diff: number | null }) { + const shared = "flex size-7 items-center justify-center text-sm font-extrabold tabular-nums" + if (value === null) return + if (diff === null || diff === 0) return {value} + if (diff <= -2) return {value} + if (diff === -1) return {value} + if (diff === 1) { + return {value} + } + return {value} +} + // --- Statistikknivå-velger --------------------------------------------------- // "Hullet er spilt" fantes tidligere som egen avkrysning, men var reelt // overflødig -- score settes allerede automatisk til "spilt" idet et @@ -2697,26 +2815,6 @@ function golfTermForScore(n: number, par: number): string | null { return null } -// Golfscore-språket (§Golfscore-språket i DESIGN_SYSTEM.md, håndhevet -// eksplisitt av teecup-scorekort-og-entry-spec.md §0.3/§3): form encoder -// retning (sirkel = under par, "firkant" = over par) slik at betydningen -// overlever selv uten farge; fylt vs. border+10%-tint encoder omfang (fylt = -// 2 slag eller mer fra par). Samme klassifisering som `ScoreMark` i -// round-scorecard.tsx -- egen kopi her siden denne knappen (size-14, -// trykkbar) trenger andre grunnklasser enn scorekortets små tabell-merker, -// men SAMME fargespråk (2026-07-27, compliance-pass). Bevisst `rounded-2xl` -// for "over par" i stedet for scorekortets `rounded-[4px]` -- fortsatt -// tydelig IKKE en sirkel, men matcher denne skjermens øvrige store -// trykkflate-avrunding (NumberPicker/hull-panel bruker samme radius). -function scoreMarkClasses(diff: number | null): string { - if (diff === null) return "rounded-full border-dashed border-border text-muted-foreground hover:border-primary/60 hover:text-primary" - if (diff <= -2) return "rounded-full border-primary bg-primary text-primary-foreground" - if (diff === -1) return "rounded-full border-primary bg-primary/10 text-primary" - if (diff === 0) return "rounded-full border-border bg-card text-foreground" - if (diff === 1) return "rounded-2xl border-brand-orange bg-brand-orange/10 text-brand-orange" - return "rounded-2xl border-brand-orange bg-brand-orange text-brand-orange-foreground" -} - function NumberPicker({ label, value,