From ebd60da746277dbcf005c3182907b838cb4934f1 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Tue, 4 Aug 2026 19:48:17 +0200 Subject: [PATCH] =?UTF-8?q?Fiks:=20"Fullf=C3=B8r=20runde"=20i=20header-mod?= =?UTF-8?q?alen=20svelget=20feil=20stille=20og=20lukket=20seg=20uansett=20?= =?UTF-8?q?utfall?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modalen ("Administrer runde", tilgjengelig fra alle tre rundefanene) hadde en egen, uavhengig "Fullfør runde"-vei som aldri sjekket setup_complete og lukket dialogen umiddelbart uten å vente på fetch-svaret -- så en ufullstendig formatoppsett (f.eks. Københavner med for få spillere) så ut til å fullføres, men gjorde det aldri, uten noen feilmelding. Backend-valideringen var korrekt fra før; kun frontend fulgte aldri opp svaret. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 53 +++++++++++++++++++++++ frontend/components/round-header.tsx | 54 ++++++++++++++++++++---- frontend/components/round-page-shell.tsx | 16 +++++-- 3 files changed, 112 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e5c79f..64ff6c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7497,3 +7497,56 @@ Neste steg: boot-et rent, `/health` og `/dashboard` → 200 over https. Gjenstår fra opprinnelig plan (se plan-loggen 2026-08-04): Eclectic- aggregering, lag-OOM sin faktiske leaderboard-beregning. + +21. **Bugfiks: "Fullfør runde" i header-modalen ("Administrer runde") + svelget feil stille og lukket dialogen uansett utfall — 2026-08-04.** + Brukeren rapporterte (skjermbilder fra en ekte runde på + teecup.teeoff.no) at en Københavner-runde med kun 1 av påkrevde 3 + spillere så ut til å la seg "fullføre" via "Administrer"-knappens + modal (bekreftelsesdialogen dukket opp, ble klikket OK på), men + runden forble ufullført -- ingen feilmelding vist noe sted. + + **Rotårsak, `round-header.tsx`/`round-page-shell.tsx`:** rundens + sticky header (ADR-040 Beslutning C/D, brukt på alle tre + Score/Scorekort/Leaderboard-fanene) har sin EGEN, uavhengige + "Fullfør runde"-vei via "Administrer"-modalen -- parallell til, men + ikke delt med, `round-detail.tsx` sin allerede korrekte inline + "Spillere og runde"-fane (som riktig deaktiverer knappen og viser + `setup_message` når `setup_complete` er usann). Modal-veien hadde + ALDRI blitt koblet til `setup_complete`-gaten, OG knappens + `onClick` kalte `onClose()` rett etter å ha startet + `onFinishRound()` UTEN å vente på den asynkrone + bekreftelse+`fetch`-kjeden -- dialogen lukket seg altså alltid + umiddelbart, uavhengig av om `POST /rounds/{id}/complete` faktisk + lyktes (backend-en avviste korrekt med 409 `SETUP_INCOMPLETE` + + forklarende melding -- det var ren frontend som aldri fulgte opp + svaret). + + **Fiks:** `onFinishRound`-kontrakten endret fra `() => void` til + `() => Promise<{ok, message} | null>` (`null` = brukeren avbrøt + bekreftelsen, dialogen blir stående). Modalen venter nå på svaret + før den lukker seg -- ved `ok: false` vises `message` inline i + dialogen i stedet. Ny `finishDisabledReason`-prop deaktiverer + knappen og viser samme `setup_message` som den inline fanen, + fjerner selve muligheten til å trykke seg forbi en ufullstendig + formatoppsett via denne veien. Ingen backend-endring -- valideringen + fantes allerede og var korrekt, kun frontend fulgte den aldri opp. + + **Scratch-verifisert** (isolert `teecup_scratch`-DB, alle 55 + migrasjoner kjørt friskt, isolert `teecup_app_scratch`-rolle, + isolert scratch-MinIO, engangs API-/frontend-container bygget fra + de faktiske endrede filene, ekte nettleser via magic-link-innlogging): + reproduserte brukerens NØYAKTIGE scenario (Københavner-runde, 1 av 3 + spillere) -- bekreftet modalens "Fullfør runde" nå viser seg + deaktivert med "Københavner krever nøyaktig 3 spillere -- runden har + 1." synlig i dialogen (samme tekst som den inline fanen). La til to + gjestespillere (nå 3/3), bekreftet knappen ble aktiv, klikket + gjennom bekreftelsesdialogen, og bekreftet både at modalen lukket + seg OG at `completed_at` faktisk ble satt server-side. Full + typesjekket produksjonsbuild av frontend kjørt som del av samme + Docker-bygg. Scratch-miljøet ryddet opp fullstendig etterpå. + + **Rullet ut live 2026-08-04**, bruker bekreftet eksplisitt: `docker + compose up -d --build teecup_frontend` (ingen migrasjon involvert), + containeren boot-et rent, `/health` og `/dashboard` → 200 over + https. diff --git a/frontend/components/round-header.tsx b/frontend/components/round-header.tsx index fdc5cc5..e9b9b6f 100644 --- a/frontend/components/round-header.tsx +++ b/frontend/components/round-header.tsx @@ -52,8 +52,17 @@ type RoundHeaderProps = { tabHrefs?: Record /** Destination for the round-management screen ("Spillere og runde"). */ manageHref?: string - /** Mock admin actions. */ - onFinishRound?: () => void + /** Fullfør runde. Returnerer `null` hvis brukeren avbrøt bekreftelsen + * (dialogen skal da bare forbli åpen, ingen feilmelding), ellers + * `{ ok, message }` -- `ok: false` viser `message` i dialogen i stedet + * for å lukke den (2026-08-04 bugfiks: denne handlingen fulgte + * tidligere aldri opp om `fetch`-kallet faktisk lyktes -- dialogen + * lukket seg uansett utfall, uten feilmelding, se CHANGELOG.md). */ + onFinishRound?: () => Promise<{ ok: boolean; message?: string } | null> + /** Når satt: "Fullfør runde"-knappen deaktiveres og denne teksten vises + * i stedet (samme oppsett-ufullstendig-sjekk som round-detail.tsx sin + * egen, uavhengige "Spillere og runde"-fane allerede håndhever). */ + finishDisabledReason?: string | null onDeleteRound?: () => void className?: string } @@ -73,6 +82,7 @@ export function RoundHeader({ tabHrefs, manageHref = "#", onFinishRound, + finishDisabledReason, onDeleteRound, className, }: RoundHeaderProps) { @@ -194,6 +204,7 @@ export function RoundHeader({ manageHref={manageHref} onClose={() => setManageOpen(false)} onFinishRound={onFinishRound} + finishDisabledReason={finishDisabledReason} onDeleteRound={onDeleteRound} /> )} @@ -207,16 +218,34 @@ function ManageRoundDialog({ manageHref, onClose, onFinishRound, + finishDisabledReason, onDeleteRound, }: { manageHref: string onClose: () => void - onFinishRound?: () => void + onFinishRound?: () => Promise<{ ok: boolean; message?: string } | null> + finishDisabledReason?: string | null onDeleteRound?: () => void }) { const closeRef = useRef(null) const [confirmDelete, setConfirmDelete] = useState(false) const [mounted, setMounted] = useState(false) + const [finishing, setFinishing] = useState(false) + const [finishError, setFinishError] = useState(null) + + async function handleFinish() { + if (!onFinishRound || finishing) return + setFinishing(true) + setFinishError(null) + const result = await onFinishRound() + setFinishing(false) + if (result === null) return // brukeren avbrøt bekreftelsen -- la dialogen stå + if (result.ok) { + onClose() + } else { + setFinishError(result.message ?? "Klarte ikke å fullføre runden. Prøv igjen.") + } + } // Escape to close + focus the close button on open + lock scroll. useEffect(() => { @@ -293,15 +322,24 @@ function ManageRoundDialog({ {/* Administrative actions */}
+ {finishDisabledReason && ( +

+ {finishDisabledReason} +

+ )} + {finishError && ( +

+ {finishError} +

+ )} {confirmDelete ? (
diff --git a/frontend/components/round-page-shell.tsx b/frontend/components/round-page-shell.tsx index bc0fb52..1c3fcd1 100644 --- a/frontend/components/round-page-shell.tsx +++ b/frontend/components/round-page-shell.tsx @@ -22,6 +22,8 @@ type ApiRoundHeaderFields = { played_at: string started_at: string | null play_format: string + setup_complete: boolean + setup_message: string | null } const dateFormatter = new Intl.DateTimeFormat("no-NO", { day: "numeric", month: "long", year: "numeric" }) @@ -52,10 +54,15 @@ export function RoundPageShell({ } }, [roundId]) - async function onFinishRound() { - if (!confirm("Fullføre runden? Du kan fortsatt se den, men ikke lenger endre registrerte hull.")) return + async function onFinishRound(): Promise<{ ok: boolean; message?: string } | null> { + if (!confirm("Fullføre runden? Du kan fortsatt se den, men ikke lenger endre registrerte hull.")) return null const res = await fetch(`/rounds/${roundId}/complete`, { method: "POST", credentials: "include" }) - if (res.ok) router.refresh() + if (res.ok) { + router.refresh() + return { ok: true } + } + const body = await res.json().catch(() => null) + return { ok: false, message: body?.detail?.message ?? "Klarte ikke å fullføre runden. Prøv igjen." } } async function onDeleteRound() { @@ -84,6 +91,9 @@ export function RoundPageShell({ }} manageHref={`/my-rounds/${roundId}?tab=manage`} onFinishRound={onFinishRound} + finishDisabledReason={ + !round.setup_complete ? round.setup_message ?? "Oppsettet er ikke komplett ennå." : undefined + } onDeleteRound={onDeleteRound} /> )}