Update Todos
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
This commit is contained in:
parent
dd54a78ce3
commit
2e9c97c943
5 changed files with 139 additions and 8 deletions
37
CLAUDE.md
37
CLAUDE.md
|
|
@ -1443,6 +1443,43 @@ Ferdig og verifisert:
|
||||||
begge containere boot-et rent, `/health` og `/dashboard` → 200,
|
begge containere boot-et rent, `/health` og `/dashboard` → 200,
|
||||||
`teeoff.no` upåvirket.
|
`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:
|
Neste steg:
|
||||||
1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
|
1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
|
||||||
2. Kommunikasjon (chat/feed) — ikke startet.
|
2. Kommunikasjon (chat/feed) — ikke startet.
|
||||||
|
|
|
||||||
|
|
@ -349,9 +349,7 @@
|
||||||
talt hull avvist, GET scorecard fortsatt leselig, ikke-avgjorte matcher
|
talt hull avvist, GET scorecard fortsatt leselig, ikke-avgjorte matcher
|
||||||
upåvirket. **Bevisst IKKE bygget:** ingen manuell «avslutt match før den
|
upåvirket. **Bevisst IKKE bygget:** ingen manuell «avslutt match før den
|
||||||
er avgjort»-handling (det grenser mot walkover under, fortsatt utsatt).
|
er avgjort»-handling (det grenser mot walkover under, fortsatt utsatt).
|
||||||
Turnering har et `status`-felt (draft/active/completed/archived) men
|
Turnering-status kan nå settes via API — se eget punkt lenger ned.
|
||||||
INGEN endepunkt endrer det ennå — organisator kan ikke markere en turnering
|
|
||||||
ferdig via API-et i dag (eget, senere punkt).
|
|
||||||
- **Walkover/konsesjon — ✅ BYGGET OG LIVE 2026-07-19 (ADR-024).** Løste det
|
- **Walkover/konsesjon — ✅ BYGGET OG LIVE 2026-07-19 (ADR-024).** Løste det
|
||||||
opprinnelige hullet: en side som aldri stiller nok spillere kan nå
|
opprinnelige hullet: en side som aldri stiller nok spillere kan nå
|
||||||
avsluttes ved at den TAPENDE siden (kaptein, eller org-admin) erklærer
|
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.
|
forblir urørt i scorekortet. Se ARCHITECTURE_DECISIONS.md ADR-024.
|
||||||
Frontend: `session-scorecard.tsx` (gi opp én match) og
|
Frontend: `session-scorecard.tsx` (gi opp én match) og
|
||||||
`tournament-detail.tsx` (gi opp resten av turneringen, per lag).
|
`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:**
|
- **Avgjort 2026-07-16:**
|
||||||
- **Match-lås ved avgjørelse: ✅ bygget** — se eget punkt over. Automatisk
|
- **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
|
(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
|
- **Fortsatt åpent:** (a) ~~skal score-føring begrenses til faktiske
|
||||||
matchdeltakere~~ ✅ avgjort/bygget 2026-07-19 (ADR-023, se over). (b) skal
|
matchdeltakere~~ ✅ avgjort/bygget 2026-07-19 (ADR-023, se over). (b) skal
|
||||||
korrigering kreve motpartens godkjenning, eller er upsert-modellen god nok
|
korrigering kreve motpartens godkjenning, eller er upsert-modellen god nok
|
||||||
for v1? (c) skal turnering-status (draft/active/completed/archived) kunne
|
for v1? (c) ~~skal turnering-status kunne settes via API~~ ✅ avgjort/
|
||||||
settes via API?
|
bygget 2026-07-19 (se over).
|
||||||
|
|
||||||
### Blind draw (skjult lagoppstilling)
|
### Blind draw (skjult lagoppstilling)
|
||||||
- **Status:** ✅ skjema (migrasjon 003, `lineup_lock`) + API bygget og verifisert
|
- **Status:** ✅ skjema (migrasjon 003, `lineup_lock`) + API bygget og verifisert
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ class TournamentUpdate(BaseModel):
|
||||||
"""Alle felt valgfrie -- kun de som faktisk sendes med blir endret
|
"""Alle felt valgfrie -- kun de som faktisk sendes med blir endret
|
||||||
(PATCH-semantikk via Pydantic sin exclude_unset, se update_tournament)."""
|
(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)$")
|
visibility: str | None = Field(default=None, pattern="^(public|org|participants)$")
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
registration_deadline: datetime | 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
|
# Trygt mot injeksjon: nøklene kommer KUN fra TournamentUpdate sine egne
|
||||||
# feltnavn (en fast, kjent hviteliste), aldri fra en rå streng brukeren
|
# feltnavn (en fast, kjent hviteliste), aldri fra en rå streng brukeren
|
||||||
# sender inn.
|
# 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 = list(updates.values())
|
||||||
values.append(tournament_id)
|
values.append(tournament_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import { TournamentStatusPicker, type TournamentStatus } from "@/components/tournament-status-badge"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
// --- Types (matcher API-kontrakten i app/routers/tournaments.py/players.py) -
|
// --- 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 [teams, setTeams] = useState<[Team | null, Team | null]>([null, null])
|
||||||
const [pool, setPool] = useState<ApiPlayer[]>([])
|
const [pool, setPool] = useState<ApiPlayer[]>([])
|
||||||
const [joinCode, setJoinCode] = useState<string | null>(null)
|
const [joinCode, setJoinCode] = useState<string | null>(null)
|
||||||
|
const [status, setStatus] = useState<TournamentStatus | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
|
@ -98,9 +100,13 @@ export function TournamentDetail({
|
||||||
// join_code (ADR-020), så vi finner raden herfra i stedet for å
|
// join_code (ADR-020), så vi finner raden herfra i stedet for å
|
||||||
// legge til et nytt endepunkt kun for dette.
|
// legge til et nytt endepunkt kun for dette.
|
||||||
if (tournamentsRes.ok) {
|
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)
|
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(
|
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
|
// Walkover/konsesjon på turnering-nivå (ADR-024): gir opp ALLE
|
||||||
// ikke-avgjorte matcher laget har i turneringen, i én operasjon. Kun
|
// ikke-avgjorte matcher laget har i turneringen, i én operasjon. Kun
|
||||||
// kaptein for laget som gir seg (eller org-admin) får lov -- backend
|
// kaptein for laget som gir seg (eller org-admin) får lov -- backend
|
||||||
|
|
@ -283,6 +307,7 @@ export function TournamentDetail({
|
||||||
{tournamentName}
|
{tournamentName}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
{status && <TournamentStatusPicker status={status} onChange={updateStatus} />}
|
||||||
{joinCode && <JoinCodeChip code={joinCode} />}
|
{joinCode && <JoinCodeChip code={joinCode} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export type TournamentStatus = "draft" | "active" | "completed" | "archived"
|
export type TournamentStatus = "draft" | "active" | "completed" | "archived"
|
||||||
|
|
@ -42,3 +49,45 @@ export function TournamentStatusBadge({ status }: { status: TournamentStatus })
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
aria-label="Endre turnering-status"
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold transition-opacity hover:opacity-80",
|
||||||
|
config.badge,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn("size-1.5 rounded-full", config.dot)} aria-hidden="true" />
|
||||||
|
{config.label}
|
||||||
|
<ChevronDown aria-hidden="true" className="size-3" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="w-40 rounded-2xl p-1.5">
|
||||||
|
{(Object.keys(STATUS_CONFIG) as TournamentStatus[]).map((s) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={s}
|
||||||
|
onClick={() => onChange(s)}
|
||||||
|
className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold"
|
||||||
|
>
|
||||||
|
<span className={cn("size-1.5 rounded-full", STATUS_CONFIG[s].dot)} aria-hidden="true" />
|
||||||
|
{STATUS_CONFIG[s].label}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue