From 2e9c97c943dfdee455b9f95d697329f56a18276b Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sun, 19 Jul 2026 21:51:05 +0200 Subject: [PATCH] Update Todos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Legge til status i TournamentUpdate + enum-cast i update_tournament Scratch-verifisere status-oppdatering (enum-cast, PATCH-semantikk, validering) Bygge frontend-UI for å sette turnering-status Ekte typesjekket frontend-build Oppdatere .md-filer + deploy etter bekreftelse --- CLAUDE.md | 37 ++++++++++++++ FEATURE_BACKLOG.md | 22 +++++++-- app/routers/tournaments.py | 10 +++- frontend/components/tournament-detail.tsx | 29 ++++++++++- .../components/tournament-status-badge.tsx | 49 +++++++++++++++++++ 5 files changed, 139 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 70bc20f..aeeef51 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1443,6 +1443,43 @@ Ferdig og verifisert: begge containere boot-et rent, `/health` og `/dashboard` → 200, `teeoff.no` upåvirket. +- **Turnering-status via API, SCRATCH-VERIFISERT (2026-07-19):** brukeren + valgte dette som neste steg etter walkover/konsesjon-runden (fikk velge + mellom denne lille opprydningen og å starte Kommunikasjon-runden). `status` + (draft/active/completed/archived) har ligget i skjemaet siden migrasjon + 001, men INGEN endepunkt kunne endre det — kun `INSERT`-defaulten `'draft'` + fra `create_tournament`. + **Bygget:** lagt til i `TournamentUpdate` (`app/routers/tournaments.py`), + settes via det eksisterende generiske `PATCH /orgs/{id}/tournaments/{id}` + (`exclude_unset`-mønsteret, ekte PATCH-semantikk uendret). + **Reelt funn UNDER scratch-testing, ikke antatt riktig på forhånd:** + `status` er -- ulikt `visibility` (som er ren `text`+`CHECK`) -- en EKTE + Postgres ENUM-type (`tournament_status`). Den generiske + `set_clauses`-byggeren (`f"{key} = ${i}"`) hadde derfor trengt et + eksplisitt cast for at asyncpg sin ukjent-typede parameter skulle løses + riktig mot en enum-kolonne -- lagt til en spesialsjekk (`::tournament_status` + kun for `status`-nøkkelen) FØR jeg antok mekanismen "bare fungerer" fordi + den gjør det for de andre feltene. + Bevisst INGEN tilstandsmaskin/overgangsregler bygget (kan f.eks. gå fra + `completed` tilbake til `draft` fritt) -- samme tillitsnivå som resten av + appen, ikke etterspurt. + **Frontend:** `tournament-status-badge.tsx` fikk en ny redigerbar + `TournamentStatusPicker` (dropdown over de fire verdiene, optimistisk + UI-oppdatering med rollback ved feil), koblet inn i `tournament-detail.tsx` + sin header ved siden av invitasjonskode-chipen. Den eksisterende + skrivebeskyttede `TournamentStatusBadge` (dashbordets kortliste) urørt. + **Verifisert i scratch:** ny turnering får riktig default `draft`, PATCH + til `active` bekreftet enum-castet faktisk løser problemet, PATCH med + status+et annet felt samtidig fungerer, PATCH UTEN status-felt lar + verdien stå urørt (regresjon på eksisterende PATCH-semantikk), ugyldig + status-verdi avvist med 422 (Pydantic-mønster), `GET`-listen viser samme + verdi etterpå. `test_isolation.sql` 12/12 uendret (ingen skjemaendring). + Ekte typesjekket produksjonsbuild kjørt og bekreftet. + **Rullet ut live 2026-07-19**, bruker bekreftet eksplisitt: ingen + migrasjon, `docker compose up -d --build teecup_api teecup_frontend`, + begge containere boot-et rent, `/health` og `/dashboard` → 200, + `teeoff.no` upåvirket. + Neste steg: 1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet. 2. Kommunikasjon (chat/feed) — ikke startet. diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 0b2a882..31f7fd4 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -349,9 +349,7 @@ talt hull avvist, GET scorecard fortsatt leselig, ikke-avgjorte matcher upåvirket. **Bevisst IKKE bygget:** ingen manuell «avslutt match før den er avgjort»-handling (det grenser mot walkover under, fortsatt utsatt). - Turnering har et `status`-felt (draft/active/completed/archived) men - INGEN endepunkt endrer det ennå — organisator kan ikke markere en turnering - ferdig via API-et i dag (eget, senere punkt). + Turnering-status kan nå settes via API — se eget punkt lenger ned. - **Walkover/konsesjon — ✅ BYGGET OG LIVE 2026-07-19 (ADR-024).** Løste det opprinnelige hullet: en side som aldri stiller nok spillere kan nå avsluttes ved at den TAPENDE siden (kaptein, eller org-admin) erklærer @@ -364,6 +362,20 @@ forblir urørt i scorekortet. Se ARCHITECTURE_DECISIONS.md ADR-024. Frontend: `session-scorecard.tsx` (gi opp én match) og `tournament-detail.tsx` (gi opp resten av turneringen, per lag). +- **Turnering-status via API — ✅ BYGGET OG LIVE 2026-07-19.** `status` lagt + til i `TournamentUpdate` (`app/routers/tournaments.py`), settes via det + eksisterende `PATCH /orgs/{id}/tournaments/{id}` (ekte PATCH-semantikk + uendret — et utelatt `status`-felt lar verdien stå urørt). `status` er, + ulikt de andre PATCH-bare feltene, en EKTE Postgres ENUM-type + (`tournament_status`, migrasjon 001) — den generiske SQL-byggeren i + `update_tournament` fikk et eksplisitt `::tournament_status`-cast for + akkurat dette feltet, funnet og løst FØR noe ble antatt riktig (verifisert + i scratch at castet faktisk trengs). Ingen egen tilstandsmaskin/ + overgangsregler — enhver org-medlem med skrivetilgang kan sette hvilken + som helst av de fire verdiene, samme tillitsnivå som resten av appen. + Frontend: `tournament-status-badge.tsx` fikk en ny redigerbar + `TournamentStatusPicker` (dropdown, optimistisk oppdatering med + rollback ved feil), koblet inn i `tournament-detail.tsx` sin header. - **Avgjort 2026-07-16:** - **Match-lås ved avgjørelse: ✅ bygget** — se eget punkt over. Automatisk (ikke en handling noen utfører), så «hvem får låse» ble aldri et @@ -371,8 +383,8 @@ - **Fortsatt åpent:** (a) ~~skal score-føring begrenses til faktiske matchdeltakere~~ ✅ avgjort/bygget 2026-07-19 (ADR-023, se over). (b) skal korrigering kreve motpartens godkjenning, eller er upsert-modellen god nok - for v1? (c) skal turnering-status (draft/active/completed/archived) kunne - settes via API? + for v1? (c) ~~skal turnering-status kunne settes via API~~ ✅ avgjort/ + bygget 2026-07-19 (se over). ### Blind draw (skjult lagoppstilling) - **Status:** ✅ skjema (migrasjon 003, `lineup_lock`) + API bygget og verifisert diff --git a/app/routers/tournaments.py b/app/routers/tournaments.py index df4fde7..901b8ee 100644 --- a/app/routers/tournaments.py +++ b/app/routers/tournaments.py @@ -116,6 +116,7 @@ class TournamentUpdate(BaseModel): """Alle felt valgfrie -- kun de som faktisk sendes med blir endret (PATCH-semantikk via Pydantic sin exclude_unset, se update_tournament).""" + status: str | None = Field(default=None, pattern="^(draft|active|completed|archived)$") visibility: str | None = Field(default=None, pattern="^(public|org|participants)$") description: str | None = None registration_deadline: datetime | None = None @@ -142,7 +143,14 @@ async def update_tournament( # Trygt mot injeksjon: nøklene kommer KUN fra TournamentUpdate sine egne # feltnavn (en fast, kjent hviteliste), aldri fra en rå streng brukeren # sender inn. - set_clauses = [f"{key} = ${i}" for i, key in enumerate(updates, start=1)] + # `status` er -- ulikt de andre feltene her -- en EKTE Postgres ENUM-type + # (tournament_status, migrasjon 001), ikke text+CHECK. asyncpg sender + # parameteren som ukjent/text; et eksplisitt cast trengs for at + # tildelingen skal løses riktig. + set_clauses = [ + f"{key} = ${i}::tournament_status" if key == "status" else f"{key} = ${i}" + for i, key in enumerate(updates, start=1) + ] values = list(updates.values()) values.append(tournament_id) diff --git a/frontend/components/tournament-detail.tsx b/frontend/components/tournament-detail.tsx index ed2ea13..ee0fd1c 100644 --- a/frontend/components/tournament-detail.tsx +++ b/frontend/components/tournament-detail.tsx @@ -26,6 +26,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import { TournamentStatusPicker, type TournamentStatus } from "@/components/tournament-status-badge" import { cn } from "@/lib/utils" // --- Types (matcher API-kontrakten i app/routers/tournaments.py/players.py) - @@ -76,6 +77,7 @@ export function TournamentDetail({ const [teams, setTeams] = useState<[Team | null, Team | null]>([null, null]) const [pool, setPool] = useState([]) const [joinCode, setJoinCode] = useState(null) + const [status, setStatus] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -98,9 +100,13 @@ export function TournamentDetail({ // join_code (ADR-020), så vi finner raden herfra i stedet for å // legge til et nytt endepunkt kun for dette. if (tournamentsRes.ok) { - const tournamentsData: { id: string; join_code: string }[] = await tournamentsRes.json() + const tournamentsData: { id: string; join_code: string; status: TournamentStatus }[] = + await tournamentsRes.json() const mine = tournamentsData.find((t) => t.id === tournamentId) - if (mine) setJoinCode(mine.join_code) + if (mine) { + setJoinCode(mine.join_code) + setStatus(mine.status) + } } const withRosters = await Promise.all( @@ -229,6 +235,24 @@ export function TournamentDetail({ } } + async function updateStatus(newStatus: TournamentStatus) { + setError(null) + const previous = status + setStatus(newStatus) // optimistisk -- rulles tilbake under ved feil + try { + const res = await fetch(`/orgs/${organizationId}/tournaments/${tournamentId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ status: newStatus }), + }) + if (!res.ok) throw new Error(`update status: ${res.status}`) + } catch { + setStatus(previous) + setError("Klarte ikke å endre turnering-status. Prøv igjen.") + } + } + // Walkover/konsesjon på turnering-nivå (ADR-024): gir opp ALLE // ikke-avgjorte matcher laget har i turneringen, i én operasjon. Kun // kaptein for laget som gir seg (eller org-admin) får lov -- backend @@ -283,6 +307,7 @@ export function TournamentDetail({ {tournamentName} + {status && } {joinCode && } diff --git a/frontend/components/tournament-status-badge.tsx b/frontend/components/tournament-status-badge.tsx index e2130f1..1f4aa05 100644 --- a/frontend/components/tournament-status-badge.tsx +++ b/frontend/components/tournament-status-badge.tsx @@ -1,3 +1,10 @@ +import { ChevronDown } from "lucide-react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" import { cn } from "@/lib/utils" export type TournamentStatus = "draft" | "active" | "completed" | "archived" @@ -42,3 +49,45 @@ export function TournamentStatusBadge({ status }: { status: TournamentStatus }) ) } + +// Redigerbar variant -- lar organisator sette status via PATCH +// /orgs/{id}/tournaments/{id} (ingen egen tilstandsmaskin/overgangsregler, +// samme tillitsnivå som resten av appen). Brukt i tournament-detail.tsx sin +// header; den skrivebeskyttede TournamentStatusBadge over brukes fortsatt i +// dashbordets read-only kortliste. +export function TournamentStatusPicker({ + status, + onChange, +}: { + status: TournamentStatus + onChange: (status: TournamentStatus) => void +}) { + const config = STATUS_CONFIG[status] + return ( + + + + + {(Object.keys(STATUS_CONFIG) as TournamentStatus[]).map((s) => ( + onChange(s)} + className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold" + > + + ))} + + + ) +}