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 systematisk `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 (
-
-
- )}
- >
+ {/* 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 (
-