2026-07-16 09:16:22 +02:00
|
|
|
"""
|
|
|
|
|
Turneringsoppsett: turneringer, lag, roster, økter.
|
|
|
|
|
|
|
|
|
|
Autorisasjonsgrense (se plan): kun org-medlemskap (get_authorized_org) kreves
|
|
|
|
|
her — dette er organisator-arbeid. Å kreve at brukeren allerede er rostret på
|
|
|
|
|
et lag ville vært sirkulært for lag-/roster-opprettelse (ingen kan være
|
|
|
|
|
rostret på et lag som ikke finnes ennå).
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
import secrets
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
import traceback
|
|
|
|
|
from datetime import date, datetime, timedelta, timezone
|
2026-07-16 09:16:22 +02:00
|
|
|
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
import asyncpg
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
from fastapi import APIRouter, Depends, UploadFile
|
2026-07-16 09:16:22 +02:00
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
from handicap_engine import allocate_over_played_holes
|
|
|
|
|
|
2026-07-19 21:25:31 +02:00
|
|
|
from ..auth import CurrentUser, get_authorized_org, get_current_user
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
from ..blind_draw import locked_team_ids, own_team_ids
|
|
|
|
|
from ..config import settings
|
2026-07-16 09:16:22 +02:00
|
|
|
from ..db import org_connection
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
from ..email import send_session_result_email
|
2026-07-17 21:40:42 +02:00
|
|
|
from ..errors import app_error, translate_db_errors
|
2026-07-18 22:21:22 +02:00
|
|
|
from ..handicap import compute_and_store_side_handicaps, parse_allowance_config
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
from .auth import _hash_secret
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
from ..team_authz import is_org_member, user_is_team_captain, user_is_tournament_participant
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
from .scoring import apply_concession, played_hole_numbers, recompute_and_cache_match_state
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
from .. import storage
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
2026-07-17 21:40:42 +02:00
|
|
|
# Turneringer
|
2026-07-16 09:16:22 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class TournamentCreate(BaseModel):
|
|
|
|
|
name: str = Field(min_length=1, max_length=200)
|
2026-07-17 21:40:42 +02:00
|
|
|
start_date: date | None = None
|
|
|
|
|
end_date: date | None = None
|
Backend-laget (migrasjon 040 + motor + API) er ferdig bygget og grundig scratch-verifisert — 63/63 motor-tester, 52/52 API-sjekker, test_isolation.sql fortsatt 12/12. Dokumentasjonen er oppdatert (CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md).
Bevisst utenfor denne runden: frontend (ingen skjerm ennå — samme "motor → skjema → API → frontend"-rekkefølge som tidligere ADR-er), og en senere innstramming av autorisasjon (i dag bredt org-medlemskap for alt, inkl. scoring — analogt med at ADR-023s kaptein-only kom som egen, senere runde for lagturneringer).
Før jeg ruller ut mot ekte teecup_db, her er planen:
Kjør migrasjon 040_individual_tournaments.sql mot ekte teecup_db — rent additivt: to nye kolonner på tournament (format_type default 'team', scoring_method nullable) + fem nye tabeller (tournament_round, tournament_participant, tournament_round_participant, tournament_round_hole, tournament_round_score), full RLS. Ingen eksisterende rader røres.
Verifiser at kolonnene/tabellene ble opprettet riktig, og kjør test_isolation.sql mot ekte database (forventer fortsatt 12/12).
Redeploy kun teecup_api (docker compose up -d --build teecup_api) — ren backend-endring, ingen frontend-kode denne runden.
Verifiser at containeren booter rent, /health//dashboard fortsatt 200, den nye API-stien faktisk når FastAPI (f.eks. anonymt kall gir riktig 401, ikke en rå 404), og teeoff.no er upåvirket.
2026-07-30 07:12:05 +02:00
|
|
|
# ADR-037: 'team' (dagens Ryder Cup-modell, uendret default) eller
|
|
|
|
|
# 'individual' (flatt felt, egen tabellstruktur -- se
|
|
|
|
|
# 040_individual_tournaments.sql). scoring_method er kun meningsfullt
|
|
|
|
|
# for individual, håndhevet i app-laget når runder/score bygges på den.
|
|
|
|
|
format_type: str = Field(default="team", pattern="^(team|individual)$")
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
scoring_method: str | None = Field(
|
|
|
|
|
default=None, pattern="^(stroke_gross|stroke_net|stableford|copenhagen|bingo_bango_bongo|flag)$"
|
|
|
|
|
)
|
|
|
|
|
# KUN meningsfullt når scoring_method='bingo_bango_bongo' -- valgfri
|
|
|
|
|
# organisator-innstilling (samme som frittstående runders round.
|
|
|
|
|
# bbb_sweep_bonus_enabled), ikke fast påslått.
|
|
|
|
|
bbb_sweep_bonus_enabled: bool = False
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
_TOURNAMENT_COLUMNS = """
|
|
|
|
|
id::text, name, status::text, start_date, end_date,
|
|
|
|
|
visibility, description, registration_deadline, registration_capacity,
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
registration_overflow_policy, registration_requires_approval, hero_image_key,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
join_code, format_type, scoring_method, bbb_sweep_bonus_enabled
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
"""
|
|
|
|
|
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
# Uten 0/O/1/I -- lett å lese/skrive/si høyt (ADR-020 Beslutning A).
|
|
|
|
|
_JOIN_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_join_code() -> str:
|
|
|
|
|
return "".join(secrets.choice(_JOIN_CODE_ALPHABET) for _ in range(6))
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
class Tournament(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
name: str
|
|
|
|
|
status: str
|
2026-07-17 21:40:42 +02:00
|
|
|
start_date: date | None
|
|
|
|
|
end_date: date | None
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
visibility: str
|
|
|
|
|
description: str | None
|
|
|
|
|
registration_deadline: datetime | None
|
|
|
|
|
registration_capacity: int | None
|
|
|
|
|
registration_overflow_policy: str
|
|
|
|
|
registration_requires_approval: bool
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
hero_image_key: str | None
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
hero_image_url: str | None
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
join_code: str
|
Backend-laget (migrasjon 040 + motor + API) er ferdig bygget og grundig scratch-verifisert — 63/63 motor-tester, 52/52 API-sjekker, test_isolation.sql fortsatt 12/12. Dokumentasjonen er oppdatert (CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md).
Bevisst utenfor denne runden: frontend (ingen skjerm ennå — samme "motor → skjema → API → frontend"-rekkefølge som tidligere ADR-er), og en senere innstramming av autorisasjon (i dag bredt org-medlemskap for alt, inkl. scoring — analogt med at ADR-023s kaptein-only kom som egen, senere runde for lagturneringer).
Før jeg ruller ut mot ekte teecup_db, her er planen:
Kjør migrasjon 040_individual_tournaments.sql mot ekte teecup_db — rent additivt: to nye kolonner på tournament (format_type default 'team', scoring_method nullable) + fem nye tabeller (tournament_round, tournament_participant, tournament_round_participant, tournament_round_hole, tournament_round_score), full RLS. Ingen eksisterende rader røres.
Verifiser at kolonnene/tabellene ble opprettet riktig, og kjør test_isolation.sql mot ekte database (forventer fortsatt 12/12).
Redeploy kun teecup_api (docker compose up -d --build teecup_api) — ren backend-endring, ingen frontend-kode denne runden.
Verifiser at containeren booter rent, /health//dashboard fortsatt 200, den nye API-stien faktisk når FastAPI (f.eks. anonymt kall gir riktig 401, ikke en rå 404), og teeoff.no er upåvirket.
2026-07-30 07:12:05 +02:00
|
|
|
format_type: str
|
|
|
|
|
scoring_method: str | None
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
bbb_sweep_bonus_enabled: bool
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
def _tournament_from_row(row) -> Tournament:
|
|
|
|
|
"""Samme mønster som auth.py sin avatar_url: DB-kolonnen forblir
|
|
|
|
|
`hero_image_key` (den nøytrale, lagrings-uavhengige verdien), men
|
|
|
|
|
responsen får i tillegg en ferdig-konstruert URL -- organisator-
|
|
|
|
|
frontend skal aldri selv måtte kjenne til MinIO-bucket/base-URL."""
|
|
|
|
|
data = dict(row)
|
|
|
|
|
key = data["hero_image_key"]
|
|
|
|
|
return Tournament(**data, hero_image_url=storage.public_url(key) if key else None)
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
@router.get("/orgs/{organization_id}/tournaments", response_model=list[Tournament])
|
|
|
|
|
async def list_tournaments(
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> list[Tournament]:
|
2026-07-20 10:01:44 +02:00
|
|
|
# ADR-030: dashbord-kortet skal vise turneringens FAKTISKE datospenn.
|
|
|
|
|
# `tournament.start_date`/`end_date` er egne, eksplisitte felt (ADR-015)
|
|
|
|
|
# som ingen UI noensinne har hatt en vei til å SETTE -- alltid NULL i
|
|
|
|
|
# praksis. Eksplisitt satt verdi vinner hvis den noensinne blir satt
|
|
|
|
|
# (fremtidig PATCH-vei), ellers utledes spennet fra øktenes
|
|
|
|
|
# `scheduled_at` -- informasjonen finnes jo allerede der.
|
2026-07-16 09:16:22 +02:00
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
rows = await conn.fetch(
|
2026-07-20 10:01:44 +02:00
|
|
|
"""
|
|
|
|
|
SELECT t.id::text, t.name, t.status::text,
|
|
|
|
|
COALESCE(t.start_date, s.min_date) AS start_date,
|
|
|
|
|
COALESCE(t.end_date, s.max_date) AS end_date,
|
|
|
|
|
t.visibility, t.description, t.registration_deadline,
|
|
|
|
|
t.registration_capacity, t.registration_overflow_policy,
|
|
|
|
|
t.registration_requires_approval, t.hero_image_key,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
t.join_code, t.format_type, t.scoring_method, t.bbb_sweep_bonus_enabled
|
2026-07-20 10:01:44 +02:00
|
|
|
FROM tournament t
|
|
|
|
|
LEFT JOIN (
|
|
|
|
|
SELECT tournament_id,
|
|
|
|
|
min(scheduled_at)::date AS min_date,
|
|
|
|
|
max(scheduled_at)::date AS max_date
|
|
|
|
|
FROM session
|
|
|
|
|
WHERE scheduled_at IS NOT NULL
|
|
|
|
|
GROUP BY tournament_id
|
|
|
|
|
) s ON s.tournament_id = t.id
|
|
|
|
|
ORDER BY t.created_at DESC
|
|
|
|
|
"""
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
return [_tournament_from_row(r) for r in rows]
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/orgs/{organization_id}/tournaments", response_model=Tournament, status_code=201)
|
|
|
|
|
async def create_tournament(
|
|
|
|
|
body: TournamentCreate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> Tournament:
|
2026-07-17 21:40:42 +02:00
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
# join_code er globalt unikt (migrasjon 011) -- kollisjon er
|
|
|
|
|
# astronomisk usannsynlig (33^6 ≈ 1,3 mrd. kombinasjoner mot et
|
|
|
|
|
# lite antall turneringer totalt), men en liten retry-løkke koster
|
|
|
|
|
# ingenting og gjør oppførselen presis i stedet for å anta.
|
|
|
|
|
row = None
|
|
|
|
|
for _ in range(5):
|
|
|
|
|
try:
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
f"""
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
INSERT INTO tournament
|
|
|
|
|
(organization_id, name, start_date, end_date, join_code, format_type,
|
|
|
|
|
scoring_method, bbb_sweep_bonus_enabled)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
RETURNING {_TOURNAMENT_COLUMNS}
|
|
|
|
|
""",
|
|
|
|
|
organization_id,
|
|
|
|
|
body.name,
|
|
|
|
|
body.start_date,
|
|
|
|
|
body.end_date,
|
|
|
|
|
_generate_join_code(),
|
Backend-laget (migrasjon 040 + motor + API) er ferdig bygget og grundig scratch-verifisert — 63/63 motor-tester, 52/52 API-sjekker, test_isolation.sql fortsatt 12/12. Dokumentasjonen er oppdatert (CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md).
Bevisst utenfor denne runden: frontend (ingen skjerm ennå — samme "motor → skjema → API → frontend"-rekkefølge som tidligere ADR-er), og en senere innstramming av autorisasjon (i dag bredt org-medlemskap for alt, inkl. scoring — analogt med at ADR-023s kaptein-only kom som egen, senere runde for lagturneringer).
Før jeg ruller ut mot ekte teecup_db, her er planen:
Kjør migrasjon 040_individual_tournaments.sql mot ekte teecup_db — rent additivt: to nye kolonner på tournament (format_type default 'team', scoring_method nullable) + fem nye tabeller (tournament_round, tournament_participant, tournament_round_participant, tournament_round_hole, tournament_round_score), full RLS. Ingen eksisterende rader røres.
Verifiser at kolonnene/tabellene ble opprettet riktig, og kjør test_isolation.sql mot ekte database (forventer fortsatt 12/12).
Redeploy kun teecup_api (docker compose up -d --build teecup_api) — ren backend-endring, ingen frontend-kode denne runden.
Verifiser at containeren booter rent, /health//dashboard fortsatt 200, den nye API-stien faktisk når FastAPI (f.eks. anonymt kall gir riktig 401, ikke en rå 404), og teeoff.no er upåvirket.
2026-07-30 07:12:05 +02:00
|
|
|
body.format_type,
|
|
|
|
|
body.scoring_method,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
body.bbb_sweep_bonus_enabled,
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
except asyncpg.UniqueViolationError:
|
|
|
|
|
continue
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(500, "VALIDATION_FAILED", "Klarte ikke å generere en unik invitasjonskode. Prøv igjen.")
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
return _tournament_from_row(row)
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
class TournamentUpdate(BaseModel):
|
|
|
|
|
"""Alle felt valgfrie -- kun de som faktisk sendes med blir endret
|
|
|
|
|
(PATCH-semantikk via Pydantic sin exclude_unset, se update_tournament)."""
|
|
|
|
|
|
2026-07-19 21:51:05 +02:00
|
|
|
status: str | None = Field(default=None, pattern="^(draft|active|completed|archived)$")
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
visibility: str | None = Field(default=None, pattern="^(public|org|participants)$")
|
Backend-laget (migrasjon 040 + motor + API) er ferdig bygget og grundig scratch-verifisert — 63/63 motor-tester, 52/52 API-sjekker, test_isolation.sql fortsatt 12/12. Dokumentasjonen er oppdatert (CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md).
Bevisst utenfor denne runden: frontend (ingen skjerm ennå — samme "motor → skjema → API → frontend"-rekkefølge som tidligere ADR-er), og en senere innstramming av autorisasjon (i dag bredt org-medlemskap for alt, inkl. scoring — analogt med at ADR-023s kaptein-only kom som egen, senere runde for lagturneringer).
Før jeg ruller ut mot ekte teecup_db, her er planen:
Kjør migrasjon 040_individual_tournaments.sql mot ekte teecup_db — rent additivt: to nye kolonner på tournament (format_type default 'team', scoring_method nullable) + fem nye tabeller (tournament_round, tournament_participant, tournament_round_participant, tournament_round_hole, tournament_round_score), full RLS. Ingen eksisterende rader røres.
Verifiser at kolonnene/tabellene ble opprettet riktig, og kjør test_isolation.sql mot ekte database (forventer fortsatt 12/12).
Redeploy kun teecup_api (docker compose up -d --build teecup_api) — ren backend-endring, ingen frontend-kode denne runden.
Verifiser at containeren booter rent, /health//dashboard fortsatt 200, den nye API-stien faktisk når FastAPI (f.eks. anonymt kall gir riktig 401, ikke en rå 404), og teeoff.no er upåvirket.
2026-07-30 07:12:05 +02:00
|
|
|
format_type: str | None = Field(default=None, pattern="^(team|individual)$")
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
scoring_method: str | None = Field(
|
|
|
|
|
default=None, pattern="^(stroke_gross|stroke_net|stableford|copenhagen|bingo_bango_bongo|flag)$"
|
|
|
|
|
)
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
description: str | None = None
|
|
|
|
|
registration_deadline: datetime | None = None
|
|
|
|
|
registration_capacity: int | None = Field(default=None, gt=0)
|
|
|
|
|
registration_overflow_policy: str | None = Field(default=None, pattern="^(waitlist|closed)$")
|
|
|
|
|
registration_requires_approval: bool | None = None
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
bbb_sweep_bonus_enabled: bool | None = None
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
# hero_image_key er BEVISST IKKE med her -- den settes kun via
|
|
|
|
|
# upload_hero_image under, aldri som en fritekst-streng klienten sender,
|
|
|
|
|
# siden den nå alltid kommer fra en ekte, server-konvertert opplasting.
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/orgs/{organization_id}/tournaments/{tournament_id}", response_model=Tournament)
|
|
|
|
|
async def update_tournament(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
body: TournamentUpdate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> Tournament:
|
|
|
|
|
# exclude_unset: kun felt klienten FAKTISK sendte skal skrives -- et
|
|
|
|
|
# utelatt felt skal IKKE nullstilles (ekte PATCH-semantikk, ikke PUT).
|
|
|
|
|
updates = body.model_dump(exclude_unset=True)
|
|
|
|
|
if not updates:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.")
|
|
|
|
|
|
|
|
|
|
# Trygt mot injeksjon: nøklene kommer KUN fra TournamentUpdate sine egne
|
|
|
|
|
# feltnavn (en fast, kjent hviteliste), aldri fra en rå streng brukeren
|
|
|
|
|
# sender inn.
|
2026-07-19 21:51:05 +02:00
|
|
|
# `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)
|
|
|
|
|
]
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
values = list(updates.values())
|
|
|
|
|
values.append(tournament_id)
|
|
|
|
|
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
f"""
|
|
|
|
|
UPDATE tournament SET {', '.join(set_clauses)}
|
|
|
|
|
WHERE id = ${len(values)}
|
|
|
|
|
RETURNING {_TOURNAMENT_COLUMNS}
|
|
|
|
|
""",
|
|
|
|
|
*values,
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
return _tournament_from_row(row)
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
|
|
|
|
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
@router.post(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/hero-image",
|
|
|
|
|
response_model=Tournament,
|
|
|
|
|
)
|
|
|
|
|
async def upload_hero_image(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
file: UploadFile,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> Tournament:
|
|
|
|
|
"""Ekte multipart-opplasting (IKKE en presignert nettleser-URL): filen
|
|
|
|
|
går gjennom API-et, som konverterer den til AVIF før den lagres i MinIO
|
|
|
|
|
-- se app/storage.py sin moduldocstring for hvorfor."""
|
|
|
|
|
if file.content_type not in storage.ALLOWED_INPUT_CONTENT_TYPES:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Ustøttet bildeformat.")
|
|
|
|
|
|
|
|
|
|
raw = await file.read(storage.MAX_UPLOAD_BYTES + 1)
|
|
|
|
|
if len(raw) > storage.MAX_UPLOAD_BYTES:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Bildet er for stort (maks 8 MB).")
|
|
|
|
|
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
exists = await conn.fetchval("SELECT id FROM tournament WHERE id = $1", tournament_id)
|
|
|
|
|
if exists is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
key = await storage.upload_image("tournaments", tournament_id, raw)
|
|
|
|
|
except storage.InvalidImageError:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Filen er ikke et gyldig bilde.")
|
|
|
|
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
f"UPDATE tournament SET hero_image_key = $1 WHERE id = $2 RETURNING {_TOURNAMENT_COLUMNS}",
|
|
|
|
|
key,
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
return _tournament_from_row(row)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/hero-image",
|
|
|
|
|
response_model=Tournament,
|
|
|
|
|
)
|
|
|
|
|
async def delete_hero_image(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> Tournament:
|
|
|
|
|
"""Samme mønster som DELETE /auth/profile/avatar -- nullstiller kun
|
|
|
|
|
referansen, sletter ikke selve MinIO-objektet (samme aksepterte
|
|
|
|
|
forenkling som avatar-fjerning)."""
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
f"UPDATE tournament SET hero_image_key = NULL WHERE id = $1 RETURNING {_TOURNAMENT_COLUMNS}",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
return _tournament_from_row(row)
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
|
|
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Lag (ADR-011: maks to lag per turnering, håndhevet her — ikke i skjemaet —
|
|
|
|
|
# slik at match-modellen kan holdes generell for fremtidige turneringstyper)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class TeamCreate(BaseModel):
|
|
|
|
|
name: str = Field(min_length=1, max_length=200)
|
|
|
|
|
color: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Team(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
name: str
|
|
|
|
|
color: str | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/orgs/{organization_id}/tournaments/{tournament_id}/teams", response_model=list[Team])
|
|
|
|
|
async def list_teams(
|
|
|
|
|
tournament_id: str,
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
organization_id: str,
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
2026-07-16 09:16:22 +02:00
|
|
|
) -> list[Team]:
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
"""2026-07-21 (deltaker-tilgang-runden): org-medlem ELLER deltaker i
|
|
|
|
|
denne turneringen, samme mønster som scoring.py sin get_scorecard --
|
|
|
|
|
laste team-navn/-farge er en forutsetning for at scorekort-siden i
|
|
|
|
|
det hele tatt kan rendres for en deltaker uten org-medlemskap."""
|
2026-07-16 09:16:22 +02:00
|
|
|
async with org_connection(organization_id) as conn:
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
exists = await conn.fetchval("SELECT id FROM tournament WHERE id = $1", tournament_id)
|
|
|
|
|
if exists is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
if not await is_org_member(conn, organization_id, user.user_id) and not await user_is_tournament_participant(
|
|
|
|
|
conn, organization_id, tournament_id, user.user_id
|
|
|
|
|
):
|
|
|
|
|
raise app_error(403, "NOT_ORG_MEMBER", "Du har ikke tilgang til denne turneringen.")
|
2026-07-16 09:16:22 +02:00
|
|
|
rows = await conn.fetch(
|
|
|
|
|
"SELECT id::text, name, color FROM team WHERE tournament_id = $1 ORDER BY created_at",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
return [Team(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/teams",
|
|
|
|
|
response_model=Team,
|
|
|
|
|
status_code=201,
|
|
|
|
|
)
|
|
|
|
|
async def create_team(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
body: TeamCreate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> Team:
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
# Lås turneringsraden slik at to samtidige forespørsler ikke begge kan
|
|
|
|
|
# lese "1 lag" og begge sette inn et andre/tredje lag (TOCTOU).
|
|
|
|
|
locked = await conn.fetchrow(
|
|
|
|
|
"SELECT id FROM tournament WHERE id = $1 AND organization_id = $2 FOR UPDATE",
|
|
|
|
|
tournament_id,
|
|
|
|
|
organization_id,
|
|
|
|
|
)
|
|
|
|
|
if locked is None:
|
2026-07-17 21:40:42 +02:00
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
existing = await conn.fetchval(
|
|
|
|
|
"SELECT count(*) FROM team WHERE tournament_id = $1", tournament_id
|
|
|
|
|
)
|
|
|
|
|
if existing >= 2:
|
2026-07-17 21:40:42 +02:00
|
|
|
raise app_error(
|
|
|
|
|
409, "LIMIT_REACHED", "Turneringen har allerede to lag (ADR-011: nøyaktig to lag)."
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
INSERT INTO team (organization_id, tournament_id, name, color)
|
|
|
|
|
VALUES ($1, $2, $3, $4)
|
|
|
|
|
RETURNING id::text, name, color
|
|
|
|
|
""",
|
|
|
|
|
organization_id,
|
|
|
|
|
tournament_id,
|
|
|
|
|
body.name,
|
|
|
|
|
body.color,
|
|
|
|
|
)
|
|
|
|
|
return Team(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 21:25:31 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Turnering-nivå walkover/konsesjon (ADR-024)
|
|
|
|
|
#
|
|
|
|
|
# v1 er låst til nøyaktig to lag (ADR-011), så det finnes bare ÉN motstander
|
|
|
|
|
# uansett hvor mange sesjoner/matcher turneringen har -- "gi opp turneringen"
|
|
|
|
|
# er derfor bare "gi opp hver ikke-avgjorte match laget har" i én operasjon,
|
|
|
|
|
# gjenbruker match-nivå-logikken (apply_concession, scoring.py) per match.
|
|
|
|
|
# Samme autorisasjon som enkelt-match-konsesjon: kaptein for laget som gir
|
|
|
|
|
# seg selv, eller org-eier/admin.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ConcedeTournamentRequest(BaseModel):
|
|
|
|
|
conceding_team_id: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ConcedeTournamentOut(BaseModel):
|
|
|
|
|
matches_conceded: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/concede",
|
|
|
|
|
response_model=ConcedeTournamentOut,
|
|
|
|
|
status_code=201,
|
|
|
|
|
)
|
|
|
|
|
async def concede_tournament(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
body: ConcedeTournamentRequest,
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
organization_id: str,
|
2026-07-19 21:25:31 +02:00
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
) -> ConcedeTournamentOut:
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
team = await conn.fetchrow(
|
|
|
|
|
"SELECT tournament_id::text AS tournament_id FROM team WHERE id = $1",
|
|
|
|
|
body.conceding_team_id,
|
|
|
|
|
)
|
|
|
|
|
if team is None or team["tournament_id"] != tournament_id:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Laget finnes ikke i denne turneringen.")
|
|
|
|
|
|
|
|
|
|
if not await user_is_team_captain(conn, organization_id, body.conceding_team_id, user.user_id):
|
|
|
|
|
raise app_error(
|
|
|
|
|
403,
|
|
|
|
|
"NOT_TEAM_CAPTAIN",
|
|
|
|
|
"Du må være kaptein for laget som gir seg (eller organisasjonsadministrator) for å gi opp turneringen.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# FOR UPDATE OF m: låser alle berørte match-rader FØR de endres, samme
|
|
|
|
|
# race-vern som enkelt-match-konsesjon og recompute_and_cache_match_state.
|
|
|
|
|
matches = await conn.fetch(
|
|
|
|
|
"""
|
|
|
|
|
SELECT m.id::text AS id, m.team_a_id::text AS team_a_id, m.team_b_id::text AS team_b_id,
|
|
|
|
|
s.points_per_match::float AS points_per_match
|
|
|
|
|
FROM match m
|
|
|
|
|
JOIN session s ON s.id = m.session_id
|
|
|
|
|
WHERE s.tournament_id = $1
|
|
|
|
|
AND m.points_side_a IS NULL
|
|
|
|
|
AND (m.team_a_id = $2 OR m.team_b_id = $2)
|
|
|
|
|
FOR UPDATE OF m
|
|
|
|
|
""",
|
|
|
|
|
tournament_id,
|
|
|
|
|
body.conceding_team_id,
|
|
|
|
|
)
|
|
|
|
|
for m in matches:
|
|
|
|
|
side = "a" if m["team_a_id"] == body.conceding_team_id else "b"
|
2026-07-19 23:14:17 +02:00
|
|
|
await apply_concession(conn, m["id"], side, m["points_per_match"], tournament_id)
|
2026-07-19 21:25:31 +02:00
|
|
|
|
|
|
|
|
return ConcedeTournamentOut(matches_conceded=len(matches))
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Roster (spiller-pool per lag, ADR-007)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class RosterEntryCreate(BaseModel):
|
|
|
|
|
player_id: str
|
|
|
|
|
is_captain: bool = False
|
|
|
|
|
# None => fryses fra player.handicap_index ved innsetting (ADR-007).
|
|
|
|
|
handicap_index_snapshot: float | None = None
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
# Konkurranseklasse (2026-08-03) -- valgfri, se 053_tournament_classes.sql.
|
|
|
|
|
class_id: str | None = None
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class RosterEntry(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
player_id: str
|
|
|
|
|
display_name: str
|
|
|
|
|
handicap_index_snapshot: float | None
|
|
|
|
|
is_captain: bool
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
class_id: str | None
|
|
|
|
|
class_name: str | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_ROSTER_COLUMNS = """
|
|
|
|
|
tr.id::text, tr.player_id::text,
|
|
|
|
|
tr.handicap_index_snapshot::float AS handicap_index_snapshot,
|
|
|
|
|
tr.is_captain, p.display_name,
|
|
|
|
|
tr.class_id::text AS class_id, tc.name AS class_name
|
|
|
|
|
"""
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/orgs/{organization_id}/teams/{team_id}/roster", response_model=list[RosterEntry])
|
|
|
|
|
async def list_roster(
|
|
|
|
|
team_id: str,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> list[RosterEntry]:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
rows = await conn.fetch(
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
f"""
|
|
|
|
|
SELECT {_ROSTER_COLUMNS}
|
2026-07-16 09:16:22 +02:00
|
|
|
FROM team_roster tr
|
|
|
|
|
JOIN player p ON p.id = tr.player_id
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
LEFT JOIN tournament_class tc ON tc.id = tr.class_id
|
2026-07-16 09:16:22 +02:00
|
|
|
WHERE tr.team_id = $1
|
|
|
|
|
ORDER BY p.display_name
|
|
|
|
|
""",
|
|
|
|
|
team_id,
|
|
|
|
|
)
|
|
|
|
|
return [RosterEntry(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/orgs/{organization_id}/teams/{team_id}/roster",
|
|
|
|
|
response_model=RosterEntry,
|
|
|
|
|
status_code=201,
|
|
|
|
|
)
|
|
|
|
|
async def add_roster_entry(
|
|
|
|
|
team_id: str,
|
|
|
|
|
body: RosterEntryCreate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> RosterEntry:
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
team = await conn.fetchrow("SELECT tournament_id FROM team WHERE id = $1", team_id)
|
|
|
|
|
if team is None:
|
2026-07-17 21:40:42 +02:00
|
|
|
raise app_error(404, "NOT_FOUND", "Laget finnes ikke.")
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
# Skjemaet håndhever kun unikhet PER lag (UNIQUE(team_id, player_id)).
|
|
|
|
|
# Uten denne sjekken kunne en spiller rostres på BEGGE lag i samme
|
|
|
|
|
# turnering, og own_team_ids() (blind draw-filteret i matches.py) ville
|
|
|
|
|
# da regnet hen som "eget lag" på begge sider — hen ville sett
|
|
|
|
|
# motstanderens skjulte oppstilling før lås (ADR-013).
|
|
|
|
|
already_elsewhere = await conn.fetchval(
|
|
|
|
|
"""
|
|
|
|
|
SELECT EXISTS (
|
|
|
|
|
SELECT 1 FROM team_roster tr
|
|
|
|
|
JOIN team t ON t.id = tr.team_id
|
|
|
|
|
WHERE t.tournament_id = $1 AND tr.player_id = $2 AND tr.team_id <> $3
|
|
|
|
|
)
|
|
|
|
|
""",
|
|
|
|
|
team["tournament_id"],
|
|
|
|
|
body.player_id,
|
|
|
|
|
team_id,
|
|
|
|
|
)
|
|
|
|
|
if already_elsewhere:
|
2026-07-17 21:40:42 +02:00
|
|
|
raise app_error(
|
|
|
|
|
409,
|
|
|
|
|
"DUPLICATE",
|
|
|
|
|
"Spilleren er allerede rostret på det andre laget i denne turneringen.",
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
snapshot = body.handicap_index_snapshot
|
|
|
|
|
if snapshot is None:
|
|
|
|
|
snapshot = await conn.fetchval(
|
|
|
|
|
"SELECT handicap_index FROM player WHERE id = $1", body.player_id
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-19 11:41:57 +02:00
|
|
|
# Samme "kun én kaptein per lag"-invariant som update_roster_entry
|
|
|
|
|
# (ADR-023) -- håndhevet her også, selv om dagens frontend aldri
|
|
|
|
|
# sender is_captain=true ved opprettelse, for at API-et er korrekt
|
|
|
|
|
# uavhengig av klient.
|
|
|
|
|
if body.is_captain:
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"UPDATE team_roster SET is_captain = false WHERE team_id = $1", team_id
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
WITH inserted AS (
|
|
|
|
|
INSERT INTO team_roster
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
(organization_id, team_id, player_id, handicap_index_snapshot, is_captain, class_id)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
|
|
|
RETURNING id, player_id, handicap_index_snapshot, is_captain, class_id
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
|
|
|
|
SELECT inserted.id::text, inserted.player_id::text,
|
|
|
|
|
inserted.handicap_index_snapshot::float AS handicap_index_snapshot,
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
inserted.is_captain, p.display_name,
|
|
|
|
|
inserted.class_id::text AS class_id, tc.name AS class_name
|
|
|
|
|
FROM inserted
|
|
|
|
|
JOIN player p ON p.id = inserted.player_id
|
|
|
|
|
LEFT JOIN tournament_class tc ON tc.id = inserted.class_id
|
2026-07-16 09:16:22 +02:00
|
|
|
""",
|
|
|
|
|
organization_id,
|
|
|
|
|
team_id,
|
|
|
|
|
body.player_id,
|
|
|
|
|
snapshot,
|
|
|
|
|
body.is_captain,
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
body.class_id,
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
|
|
|
|
return RosterEntry(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
class RosterEntryUpdate(BaseModel):
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
"""Alle felt valgfrie -- exclude_unset, samme PATCH-semantikk som
|
|
|
|
|
TournamentUpdate/SponsorUpdate. `class_id` lagt til 2026-08-03 (bredere
|
|
|
|
|
enn tidligere, da feltet KUN var is_captain: bool påkrevd) -- utvidet
|
|
|
|
|
slik at klasse kan settes/endres uten å tvinge et samtidig
|
|
|
|
|
kaptein-valg."""
|
|
|
|
|
|
|
|
|
|
is_captain: bool | None = None
|
|
|
|
|
class_id: str | None = None
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch(
|
|
|
|
|
"/orgs/{organization_id}/teams/{team_id}/roster/{roster_id}",
|
|
|
|
|
response_model=RosterEntry,
|
|
|
|
|
)
|
|
|
|
|
async def update_roster_entry(
|
|
|
|
|
team_id: str,
|
|
|
|
|
roster_id: str,
|
|
|
|
|
body: RosterEntryUpdate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> RosterEntry:
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
updates = body.model_dump(exclude_unset=True)
|
|
|
|
|
if not updates:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.")
|
|
|
|
|
|
2026-07-19 11:41:57 +02:00
|
|
|
# 2026-07-19 (Brukerroller-runden, ADR-023): kaptein er nå en reell
|
|
|
|
|
# autorisasjonsrolle (se app/team_authz.py sin user_is_team_captain), så
|
|
|
|
|
# "kun én kaptein per lag" håndheves eksplisitt her -- å sette en NY
|
|
|
|
|
# kaptein fjerner automatisk merket fra en ev. tidligere kaptein på
|
|
|
|
|
# SAMME lag, i samme transaksjon (org_connection åpner allerede én).
|
|
|
|
|
# Å fjerne kapteinmerket (is_captain=false) rører ingen andre rader.
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
async with org_connection(organization_id) as conn:
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
if updates.get("is_captain"):
|
2026-07-19 11:41:57 +02:00
|
|
|
await conn.execute(
|
|
|
|
|
"UPDATE team_roster SET is_captain = false WHERE team_id = $1 AND id <> $2",
|
|
|
|
|
team_id,
|
|
|
|
|
roster_id,
|
|
|
|
|
)
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
|
|
|
|
|
set_clauses = [f"{key} = ${i}" for i, key in enumerate(updates, start=1)]
|
|
|
|
|
values = list(updates.values())
|
|
|
|
|
values.append(roster_id)
|
|
|
|
|
values.append(team_id)
|
|
|
|
|
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
row = await conn.fetchrow(
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
f"""
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
WITH updated AS (
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
UPDATE team_roster SET {', '.join(set_clauses)}
|
|
|
|
|
WHERE id = ${len(values) - 1} AND team_id = ${len(values)}
|
|
|
|
|
RETURNING id, player_id, handicap_index_snapshot, is_captain, class_id
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
)
|
|
|
|
|
SELECT updated.id::text, updated.player_id::text,
|
|
|
|
|
updated.handicap_index_snapshot::float AS handicap_index_snapshot,
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
updated.is_captain, p.display_name,
|
|
|
|
|
updated.class_id::text AS class_id, tc.name AS class_name
|
|
|
|
|
FROM updated
|
|
|
|
|
JOIN player p ON p.id = updated.player_id
|
|
|
|
|
LEFT JOIN tournament_class tc ON tc.id = updated.class_id
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
""",
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
*values,
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Roster-oppføringen finnes ikke.")
|
|
|
|
|
return RosterEntry(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
class RosterMove(BaseModel):
|
|
|
|
|
target_team_id: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/orgs/{organization_id}/teams/{team_id}/roster/{roster_id}/move",
|
|
|
|
|
response_model=RosterEntry,
|
|
|
|
|
)
|
|
|
|
|
async def move_roster_entry(
|
|
|
|
|
team_id: str,
|
|
|
|
|
roster_id: str,
|
|
|
|
|
body: RosterMove,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> RosterEntry:
|
|
|
|
|
"""FEATURE_BACKLOG.md "Turneringsoppsett: flytte/slette spillere mellom
|
|
|
|
|
lag" (notert 2026-07-25) -- atomisk flytting i én operasjon i stedet for
|
|
|
|
|
to separate DELETE+POST-kall (unngår en mellomtilstand der spilleren
|
|
|
|
|
midlertidig ikke er rostret noe sted)."""
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
current = await conn.fetchrow(
|
|
|
|
|
"SELECT tr.id, t.tournament_id::text AS tournament_id "
|
|
|
|
|
"FROM team_roster tr JOIN team t ON t.id = tr.team_id "
|
|
|
|
|
"WHERE tr.id = $1 AND tr.team_id = $2",
|
|
|
|
|
roster_id,
|
|
|
|
|
team_id,
|
|
|
|
|
)
|
|
|
|
|
if current is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Roster-oppføringen finnes ikke.")
|
|
|
|
|
if body.target_team_id == team_id:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Spilleren er allerede på dette laget.")
|
|
|
|
|
target_team = await conn.fetchrow(
|
|
|
|
|
"SELECT id FROM team WHERE id = $1 AND tournament_id = $2",
|
|
|
|
|
body.target_team_id,
|
|
|
|
|
current["tournament_id"],
|
|
|
|
|
)
|
|
|
|
|
if target_team is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Mållaget finnes ikke i denne turneringen.")
|
|
|
|
|
|
|
|
|
|
# Roster-raden er referert av match_participant.team_roster_id (ON
|
|
|
|
|
# DELETE RESTRICT) -- en spiller som allerede er satt opp i minst én
|
|
|
|
|
# match kan ikke flyttes trygt (matchens team_side ville da ikke
|
|
|
|
|
# lenger stemme med spillerens faktiske lag). Krev at spilleren
|
|
|
|
|
# fjernes fra matchen(e) FØRST, i stedet for å tillate en
|
|
|
|
|
# inkonsistent mellomtilstand.
|
|
|
|
|
already_in_match = await conn.fetchval(
|
|
|
|
|
"SELECT EXISTS(SELECT 1 FROM match_participant WHERE team_roster_id = $1)",
|
|
|
|
|
roster_id,
|
|
|
|
|
)
|
|
|
|
|
if already_in_match:
|
|
|
|
|
raise app_error(
|
|
|
|
|
409,
|
|
|
|
|
"ALREADY_IN_MATCH",
|
|
|
|
|
"Spilleren er allerede lagt til i en match og kan ikke flyttes -- "
|
|
|
|
|
"fjern spilleren fra matchen(e) først.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Kaptein-merket følger IKKE med til det nye laget -- "kun én
|
|
|
|
|
# kaptein per lag" (ADR-023) håndheves allerede ELLERS via
|
|
|
|
|
# update_roster_entry, men en flyttet spiller skal aldri bli
|
|
|
|
|
# kaptein på et nytt lag uten et eksplisitt valg der.
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
WITH moved AS (
|
|
|
|
|
UPDATE team_roster
|
|
|
|
|
SET team_id = $1, is_captain = false
|
|
|
|
|
WHERE id = $2
|
|
|
|
|
RETURNING id, player_id, handicap_index_snapshot, is_captain
|
|
|
|
|
)
|
|
|
|
|
SELECT moved.id::text, moved.player_id::text,
|
|
|
|
|
moved.handicap_index_snapshot::float AS handicap_index_snapshot,
|
|
|
|
|
moved.is_captain, p.display_name
|
|
|
|
|
FROM moved JOIN player p ON p.id = moved.player_id
|
|
|
|
|
""",
|
|
|
|
|
body.target_team_id,
|
|
|
|
|
roster_id,
|
|
|
|
|
)
|
|
|
|
|
return RosterEntry(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
@router.delete(
|
|
|
|
|
"/orgs/{organization_id}/teams/{team_id}/roster/{roster_id}",
|
|
|
|
|
status_code=204,
|
|
|
|
|
)
|
|
|
|
|
async def delete_roster_entry(
|
|
|
|
|
team_id: str,
|
|
|
|
|
roster_id: str,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> None:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
deleted = await conn.fetchval(
|
|
|
|
|
"DELETE FROM team_roster WHERE id = $1 AND team_id = $2 RETURNING id",
|
|
|
|
|
roster_id,
|
|
|
|
|
team_id,
|
|
|
|
|
)
|
|
|
|
|
if deleted is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Roster-oppføringen finnes ikke.")
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Økter (ADR-007: turnering = ordnet sekvens av økter)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class SessionCreate(BaseModel):
|
|
|
|
|
sequence: int
|
|
|
|
|
name: str | None = None
|
|
|
|
|
format: str
|
|
|
|
|
hole_config: str = "full_18"
|
|
|
|
|
course_id: str
|
|
|
|
|
points_per_match: float = 1.0
|
|
|
|
|
allowance_override: dict | None = None
|
2026-07-16 14:38:42 +02:00
|
|
|
# ADR-012: 'stroke' (hole_score, motoren utleder netto) eller 'hole_result'
|
|
|
|
|
# (match_hole_result, bare hvem som vant hullet). DB-default er 'stroke'.
|
|
|
|
|
scoring_mode: str = "stroke"
|
2026-07-17 21:40:42 +02:00
|
|
|
# Klokkeslett-modell (se plan): én starttid + ett intervall per økt.
|
|
|
|
|
# match.tee_time utledes av disse i matches.py, IKKE lagret per match.
|
|
|
|
|
scheduled_at: datetime | None = None
|
|
|
|
|
tee_interval_minutes: int | None = None
|
|
|
|
|
start_hole: int = 1
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
# KUN meningsfullt (og påkrevd) når format='shamble' -- antall av
|
|
|
|
|
# sidens individuelle resultater som teller per hull ("beste N av M").
|
|
|
|
|
shamble_best_n: int | None = Field(default=None, ge=1)
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class SessionOut(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
sequence: int
|
|
|
|
|
name: str | None
|
|
|
|
|
format: str
|
|
|
|
|
hole_config: str
|
|
|
|
|
course_id: str
|
|
|
|
|
points_per_match: float
|
|
|
|
|
allowance_override: dict | None
|
2026-07-16 14:38:42 +02:00
|
|
|
scoring_mode: str
|
2026-07-17 21:40:42 +02:00
|
|
|
scheduled_at: datetime | None
|
|
|
|
|
tee_interval_minutes: int | None
|
|
|
|
|
start_hole: int
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
shamble_best_n: int | None
|
2026-07-16 09:16:22 +02:00
|
|
|
locked_team_ids: list[str]
|
|
|
|
|
revealed: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _session_out(row, locked: set[str]) -> SessionOut:
|
|
|
|
|
return SessionOut(
|
|
|
|
|
id=row["id"],
|
|
|
|
|
sequence=row["sequence"],
|
|
|
|
|
name=row["name"],
|
|
|
|
|
format=row["format"],
|
|
|
|
|
hole_config=row["hole_config"],
|
|
|
|
|
course_id=row["course_id"],
|
|
|
|
|
points_per_match=row["points_per_match"],
|
|
|
|
|
allowance_override=json.loads(row["allowance_override"]) if row["allowance_override"] else None,
|
2026-07-16 14:38:42 +02:00
|
|
|
scoring_mode=row["scoring_mode"],
|
2026-07-17 21:40:42 +02:00
|
|
|
scheduled_at=row["scheduled_at"],
|
|
|
|
|
tee_interval_minutes=row["tee_interval_minutes"],
|
|
|
|
|
start_hole=row["start_hole"],
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
shamble_best_n=row["shamble_best_n"],
|
2026-07-16 09:16:22 +02:00
|
|
|
locked_team_ids=sorted(locked),
|
|
|
|
|
revealed=len(locked) >= 2,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
async def _fetch_sessions(conn, tournament_id: str) -> list[SessionOut]:
|
|
|
|
|
"""Delt mellom den innloggede org-ruten under og det offentlige
|
|
|
|
|
landingsside-endepunktet (app/routers/registration.py, ADR-018) -- samme
|
|
|
|
|
spørring, samme blind draw-låsing, ikke duplisert to steder."""
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
"""
|
|
|
|
|
SELECT id::text, sequence, name, format, hole_config::text AS hole_config,
|
|
|
|
|
course_id::text, points_per_match::float AS points_per_match,
|
|
|
|
|
allowance_override::text AS allowance_override, scoring_mode,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
scheduled_at, tee_interval_minutes, start_hole, shamble_best_n
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
FROM session
|
|
|
|
|
WHERE tournament_id = $1
|
|
|
|
|
ORDER BY sequence
|
|
|
|
|
""",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
result = []
|
|
|
|
|
for r in rows:
|
|
|
|
|
locked = await locked_team_ids(conn, r["id"])
|
|
|
|
|
result.append(_session_out(r, locked))
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
@router.get(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/sessions",
|
|
|
|
|
response_model=list[SessionOut],
|
|
|
|
|
)
|
|
|
|
|
async def list_sessions(
|
|
|
|
|
tournament_id: str,
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
organization_id: str,
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
2026-07-16 09:16:22 +02:00
|
|
|
) -> list[SessionOut]:
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
"""2026-07-21 (deltaker-tilgang-runden): org-medlem ELLER deltaker i
|
|
|
|
|
denne turneringen, samme mønster som list_teams over -- innholdet er
|
|
|
|
|
for øvrig identisk med det offentlige `/public/.../sessions`-
|
|
|
|
|
endepunktet (ADR-018) for enhver som allerede kan se turneringen, så
|
|
|
|
|
dette utvider ikke hva som er synlig, kun HVEM som kan nå det via
|
|
|
|
|
denne org-scopede stien."""
|
2026-07-16 09:16:22 +02:00
|
|
|
async with org_connection(organization_id) as conn:
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
exists = await conn.fetchval("SELECT id FROM tournament WHERE id = $1", tournament_id)
|
|
|
|
|
if exists is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
if not await is_org_member(conn, organization_id, user.user_id) and not await user_is_tournament_participant(
|
|
|
|
|
conn, organization_id, tournament_id, user.user_id
|
|
|
|
|
):
|
|
|
|
|
raise app_error(403, "NOT_ORG_MEMBER", "Du har ikke tilgang til denne turneringen.")
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
return await _fetch_sessions(conn, tournament_id)
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/sessions",
|
|
|
|
|
response_model=SessionOut,
|
|
|
|
|
status_code=201,
|
|
|
|
|
)
|
|
|
|
|
async def create_session(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
body: SessionCreate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> SessionOut:
|
|
|
|
|
allowance_json = json.dumps(body.allowance_override) if body.allowance_override is not None else None
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
INSERT INTO session
|
|
|
|
|
(organization_id, tournament_id, sequence, name, format,
|
2026-07-17 21:40:42 +02:00
|
|
|
hole_config, course_id, points_per_match, allowance_override, scoring_mode,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
scheduled_at, tee_interval_minutes, start_hole, shamble_best_n)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12, $13, $14)
|
2026-07-16 09:16:22 +02:00
|
|
|
RETURNING id::text, sequence, name, format, hole_config::text AS hole_config,
|
|
|
|
|
course_id::text, points_per_match::float AS points_per_match,
|
2026-07-17 21:40:42 +02:00
|
|
|
allowance_override::text AS allowance_override, scoring_mode,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
scheduled_at, tee_interval_minutes, start_hole, shamble_best_n
|
2026-07-16 09:16:22 +02:00
|
|
|
""",
|
|
|
|
|
organization_id,
|
|
|
|
|
tournament_id,
|
|
|
|
|
body.sequence,
|
|
|
|
|
body.name,
|
|
|
|
|
body.format,
|
|
|
|
|
body.hole_config,
|
|
|
|
|
body.course_id,
|
|
|
|
|
body.points_per_match,
|
|
|
|
|
allowance_json,
|
2026-07-16 14:38:42 +02:00
|
|
|
body.scoring_mode,
|
2026-07-17 21:40:42 +02:00
|
|
|
body.scheduled_at,
|
|
|
|
|
body.tee_interval_minutes,
|
|
|
|
|
body.start_hole,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
body.shamble_best_n,
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
|
|
|
|
return _session_out(row, set())
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
|
|
|
|
|
2026-07-18 22:21:22 +02:00
|
|
|
class SessionUpdate(BaseModel):
|
|
|
|
|
"""Bevisst IKKE format/scoring_mode/hole_config ennå -- disse har dypere
|
|
|
|
|
strukturelle konsekvenser for allerede registrerte scores (bytter man
|
|
|
|
|
scoring_mode havner gamle oppføringer i feil tabell; bytter man format
|
|
|
|
|
endres reglene for match_participant) som fortjener egen, senere design.
|
|
|
|
|
course_id er den ENESTE feltet som krever spesialhåndtering her (se
|
|
|
|
|
_remap_course) -- resten er en vanlig exclude_unset-oppdatering."""
|
|
|
|
|
|
|
|
|
|
name: str | None = None
|
|
|
|
|
scheduled_at: datetime | None = None
|
|
|
|
|
tee_interval_minutes: int | None = None
|
|
|
|
|
start_hole: int | None = Field(default=None, ge=1, le=18)
|
|
|
|
|
points_per_match: float | None = None
|
|
|
|
|
allowance_override: dict | None = None
|
|
|
|
|
course_id: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _remap_course(conn, session_id: str, new_course_id: str) -> None:
|
|
|
|
|
"""Reist av brukeren (2026-07-18): en organisator oppdager MIDT I RUNDEN
|
|
|
|
|
at feil bane var satt opp -- de faktisk registrerte slagene er ekte og
|
|
|
|
|
skal IKKE røres, men `match_participant.tee_id` peker til en tee som
|
|
|
|
|
hører til DEN GAMLE banen, og må flyttes til en tilsvarende tee på den
|
|
|
|
|
nye for at handicap-beregningen skal bli riktig videre.
|
|
|
|
|
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Presentere full plan og be om bekreftelse (ekte migrasjon + deploy)
Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer:
1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet.
3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19).
4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes.
2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret.
Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak?
Utrullingsplan — dette er første gang i denne runden noe rører ekte data:
Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
|
|
|
Matcher på tee-navn (case-insensitive) -- kjønn hører til RATINGEN, ikke
|
|
|
|
|
selve utslaget (ADR-029), så remap-en sjekker i tillegg at den nye
|
|
|
|
|
tee-en faktisk har en full_18-rating for hver berørte spillers kjønn.
|
|
|
|
|
Finnes ingen tilsvarende, gyldig tee på den nye banen for én eller
|
|
|
|
|
flere spillere, avvises HELE bane-byttet tydelig FØR noe skrives --
|
|
|
|
|
heller stoppe enn å gjette feil (samme "fail loudly" prinsipp som
|
|
|
|
|
ADR-019 sin importvalidering).
|
2026-07-18 22:21:22 +02:00
|
|
|
"""
|
|
|
|
|
course_exists = await conn.fetchval("SELECT id FROM course WHERE id = $1", new_course_id)
|
|
|
|
|
if course_exists is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Banen finnes ikke.")
|
|
|
|
|
|
|
|
|
|
participants = await conn.fetch(
|
|
|
|
|
"""
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Presentere full plan og be om bekreftelse (ekte migrasjon + deploy)
Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer:
1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet.
3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19).
4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes.
2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret.
Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak?
Utrullingsplan — dette er første gang i denne runden noe rører ekte data:
Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
|
|
|
SELECT mp.id::text AS id, t.name AS tee_name, p.gender AS player_gender
|
2026-07-18 22:21:22 +02:00
|
|
|
FROM match_participant mp
|
|
|
|
|
JOIN match m ON m.id = mp.match_id
|
|
|
|
|
JOIN tee t ON t.id = mp.tee_id
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Presentere full plan og be om bekreftelse (ekte migrasjon + deploy)
Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer:
1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet.
3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19).
4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes.
2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret.
Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak?
Utrullingsplan — dette er første gang i denne runden noe rører ekte data:
Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
|
|
|
JOIN team_roster tr ON tr.id = mp.team_roster_id
|
|
|
|
|
JOIN player p ON p.id = tr.player_id
|
2026-07-18 22:21:22 +02:00
|
|
|
WHERE m.session_id = $1
|
|
|
|
|
""",
|
|
|
|
|
session_id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
remap: dict[str, str] = {}
|
|
|
|
|
missing: set[str] = set()
|
|
|
|
|
for p in participants:
|
|
|
|
|
new_tee_id = await conn.fetchval(
|
|
|
|
|
"""
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Presentere full plan og be om bekreftelse (ekte migrasjon + deploy)
Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer:
1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet.
3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19).
4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes.
2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret.
Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak?
Utrullingsplan — dette er første gang i denne runden noe rører ekte data:
Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
|
|
|
SELECT t.id::text FROM tee t
|
|
|
|
|
JOIN tee_rating tr ON tr.tee_id = t.id AND tr.scope = 'full_18'
|
|
|
|
|
AND tr.gender IS NOT DISTINCT FROM $3
|
|
|
|
|
WHERE t.course_id = $1 AND lower(t.name) = lower($2)
|
2026-07-18 22:21:22 +02:00
|
|
|
""",
|
|
|
|
|
new_course_id,
|
|
|
|
|
p["tee_name"],
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Presentere full plan og be om bekreftelse (ekte migrasjon + deploy)
Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer:
1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet.
3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19).
4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes.
2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret.
Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak?
Utrullingsplan — dette er første gang i denne runden noe rører ekte data:
Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
|
|
|
p["player_gender"],
|
2026-07-18 22:21:22 +02:00
|
|
|
)
|
|
|
|
|
if new_tee_id is None:
|
|
|
|
|
missing.add(p["tee_name"])
|
|
|
|
|
else:
|
|
|
|
|
remap[p["id"]] = new_tee_id
|
|
|
|
|
|
|
|
|
|
if missing:
|
|
|
|
|
raise app_error(
|
|
|
|
|
400,
|
|
|
|
|
"VALIDATION_FAILED",
|
|
|
|
|
"Den nye banen mangler en tee som matcher: "
|
|
|
|
|
+ ", ".join(sorted(missing))
|
|
|
|
|
+ ". Legg til tilsvarende tee på den nye banen før du bytter.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for participant_id, new_tee_id in remap.items():
|
|
|
|
|
await conn.execute("UPDATE match_participant SET tee_id = $1 WHERE id = $2", new_tee_id, participant_id)
|
|
|
|
|
|
|
|
|
|
await conn.execute("UPDATE session SET course_id = $1 WHERE id = $2", new_course_id, session_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _recompute_session_matches(
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Presentere full plan og be om bekreftelse (ekte migrasjon + deploy)
Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer:
1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet.
3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19).
4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes.
2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret.
Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak?
Utrullingsplan — dette er første gang i denne runden noe rører ekte data:
Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
|
|
|
conn, session_id: str, format_: str, allowance_override: dict | None
|
2026-07-18 22:21:22 +02:00
|
|
|
) -> None:
|
|
|
|
|
"""Regner om handicap + matchstatus for HVER match i økten -- brukt etter
|
|
|
|
|
et bane- eller allowance-bytte. Bevisst UAVHENGIG av om en match allerede
|
|
|
|
|
er avgjort (brukeren bekreftet eksplisitt: en bane-korrigering skal kunne
|
|
|
|
|
endre et allerede cachet resultat, ikke bare blokkeres av den vanlige
|
|
|
|
|
ALREADY_DECIDED-sperren som gjelder NYE slag-innsendinger)."""
|
|
|
|
|
config = parse_allowance_config(format_, allowance_override)
|
|
|
|
|
matches = await conn.fetch("SELECT id::text AS id FROM match WHERE session_id = $1", session_id)
|
|
|
|
|
for m in matches:
|
|
|
|
|
for side in ("a", "b"):
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Presentere full plan og be om bekreftelse (ekte migrasjon + deploy)
Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer:
1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet.
3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19).
4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes.
2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret.
Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak?
Utrullingsplan — dette er første gang i denne runden noe rører ekte data:
Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
|
|
|
await compute_and_store_side_handicaps(conn, m["id"], side, format_, config)
|
2026-07-18 22:21:22 +02:00
|
|
|
await recompute_and_cache_match_state(conn, m["id"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/orgs/{organization_id}/sessions/{session_id}", response_model=SessionOut)
|
|
|
|
|
async def update_session(
|
|
|
|
|
session_id: str,
|
|
|
|
|
body: SessionUpdate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> SessionOut:
|
|
|
|
|
updates = body.model_dump(exclude_unset=True)
|
|
|
|
|
if not updates:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.")
|
|
|
|
|
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
session = await conn.fetchrow(
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Presentere full plan og be om bekreftelse (ekte migrasjon + deploy)
Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer:
1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet.
3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19).
4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes.
2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret.
Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak?
Utrullingsplan — dette er første gang i denne runden noe rører ekte data:
Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
|
|
|
"SELECT format, course_id::text AS course_id FROM session WHERE id = $1",
|
2026-07-18 22:21:22 +02:00
|
|
|
session_id,
|
|
|
|
|
)
|
|
|
|
|
if session is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
|
|
|
|
|
|
|
|
|
|
new_course_id = updates.pop("course_id", None)
|
|
|
|
|
allowance_or_points_changed = "allowance_override" in updates or "points_per_match" in updates
|
|
|
|
|
|
|
|
|
|
if updates:
|
|
|
|
|
set_clauses = []
|
|
|
|
|
values: list = []
|
|
|
|
|
for i, (key, value) in enumerate(updates.items(), start=1):
|
|
|
|
|
if key == "allowance_override":
|
|
|
|
|
set_clauses.append(f"allowance_override = ${i}::jsonb")
|
|
|
|
|
values.append(json.dumps(value) if value is not None else None)
|
|
|
|
|
else:
|
|
|
|
|
set_clauses.append(f"{key} = ${i}")
|
|
|
|
|
values.append(value)
|
|
|
|
|
values.append(session_id)
|
|
|
|
|
await conn.execute(
|
|
|
|
|
f"UPDATE session SET {', '.join(set_clauses)} WHERE id = ${len(values)}",
|
|
|
|
|
*values,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if new_course_id is not None and new_course_id != session["course_id"]:
|
|
|
|
|
await _remap_course(conn, session_id, new_course_id)
|
|
|
|
|
allowance_or_points_changed = True # banebytte krever alltid omregning
|
|
|
|
|
|
|
|
|
|
if allowance_or_points_changed:
|
|
|
|
|
current = await conn.fetchrow(
|
|
|
|
|
"SELECT allowance_override::text AS allowance_override FROM session WHERE id = $1",
|
|
|
|
|
session_id,
|
|
|
|
|
)
|
|
|
|
|
current_override = json.loads(current["allowance_override"]) if current["allowance_override"] else None
|
|
|
|
|
await _recompute_session_matches(
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Presentere full plan og be om bekreftelse (ekte migrasjon + deploy)
Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer:
1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet.
3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19).
4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes.
2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret.
Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak?
Utrullingsplan — dette er første gang i denne runden noe rører ekte data:
Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
|
|
|
conn, session_id, session["format"], current_override
|
2026-07-18 22:21:22 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
SELECT id::text, sequence, name, format, hole_config::text AS hole_config,
|
|
|
|
|
course_id::text, points_per_match::float AS points_per_match,
|
|
|
|
|
allowance_override::text AS allowance_override, scoring_mode,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
scheduled_at, tee_interval_minutes, start_hole, shamble_best_n
|
2026-07-18 22:21:22 +02:00
|
|
|
FROM session WHERE id = $1
|
|
|
|
|
""",
|
|
|
|
|
session_id,
|
|
|
|
|
)
|
|
|
|
|
locked = await locked_team_ids(conn, session_id)
|
|
|
|
|
|
|
|
|
|
return _session_out(row, locked)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/orgs/{organization_id}/sessions/{session_id}", status_code=204)
|
|
|
|
|
async def delete_session(
|
|
|
|
|
session_id: str,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Kun tomme økter kan slettes (ingen matcher opprettet ennå) -- dekker
|
|
|
|
|
"jeg satte opp feil økt, fjern den" FØR den er tatt i bruk. En økt med
|
|
|
|
|
matcher (og dermed potensielt registrerte scores/poeng) må rettes med
|
|
|
|
|
PATCH (bane-/felt-korrigering) i stedet for å kunne slettes -- hindrer
|
|
|
|
|
at noen ved et uhell sletter ekte kamper/resultater."""
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
session = await conn.fetchrow("SELECT id FROM session WHERE id = $1", session_id)
|
|
|
|
|
if session is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
|
|
|
|
|
|
|
|
|
|
match_count = await conn.fetchval("SELECT count(*) FROM match WHERE session_id = $1", session_id)
|
|
|
|
|
if match_count > 0:
|
|
|
|
|
raise app_error(
|
|
|
|
|
409,
|
|
|
|
|
"VALIDATION_FAILED",
|
|
|
|
|
"Økten har allerede matcher og kan ikke slettes -- bruk PATCH for å korrigere den i stedet.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await conn.execute("DELETE FROM session WHERE id = $1", session_id)
|
|
|
|
|
|
|
|
|
|
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Midlertidige spillere: etter-runde scorekort + innloggingsinvitasjon,
|
|
|
|
|
# økt-nivå (FEATURE_BACKLOG.md, avklart 2026-07-28, migrasjon 034).
|
|
|
|
|
#
|
|
|
|
|
# Eksplisitt organisator-handling (ikke en automatisk bakgrunnsjobb, se
|
|
|
|
|
# begrunnelse i backloggen) -- sender KUN til spillere UTEN egen konto ennå
|
|
|
|
|
# (player.user_id IS NULL) og med en registrert e-post, og KUN én gang per
|
|
|
|
|
# (økt, deltaker) via match_participant.invitation_sent_at. Locale er
|
|
|
|
|
# bevisst alltid 'nb' -- en spiller uten konto har ingen lagret
|
|
|
|
|
# språkpreferanse å lese.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class SendInvitationsOut(BaseModel):
|
|
|
|
|
sent: int
|
|
|
|
|
skipped_has_account: int
|
|
|
|
|
skipped_no_email: int
|
|
|
|
|
skipped_already_sent: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/orgs/{organization_id}/sessions/{session_id}/send-scorecard-invitations",
|
|
|
|
|
response_model=SendInvitationsOut,
|
|
|
|
|
)
|
|
|
|
|
async def send_scorecard_invitations(
|
|
|
|
|
session_id: str,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> SendInvitationsOut:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
session = await conn.fetchrow(
|
|
|
|
|
"SELECT tournament_id::text AS tournament_id, name FROM session WHERE id = $1",
|
|
|
|
|
session_id,
|
|
|
|
|
)
|
|
|
|
|
if session is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
|
|
|
|
|
tournament_name = await conn.fetchval(
|
|
|
|
|
"SELECT name FROM tournament WHERE id = $1", session["tournament_id"]
|
|
|
|
|
)
|
|
|
|
|
session_label = (session["name"] or "").strip() or "en økt"
|
|
|
|
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
"""
|
|
|
|
|
SELECT mp.id::text AS participant_id, mp.invitation_sent_at,
|
|
|
|
|
p.display_name, p.email, p.user_id::text AS user_id,
|
|
|
|
|
m.status_text
|
|
|
|
|
FROM match_participant mp
|
|
|
|
|
JOIN match m ON m.id = mp.match_id
|
|
|
|
|
JOIN team_roster tr ON tr.id = mp.team_roster_id
|
|
|
|
|
JOIN player p ON p.id = tr.player_id
|
|
|
|
|
WHERE m.session_id = $1
|
|
|
|
|
""",
|
|
|
|
|
session_id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
sent = 0
|
|
|
|
|
skipped_has_account = 0
|
|
|
|
|
skipped_no_email = 0
|
|
|
|
|
skipped_already_sent = 0
|
|
|
|
|
|
|
|
|
|
for r in rows:
|
|
|
|
|
if r["user_id"] is not None:
|
|
|
|
|
skipped_has_account += 1
|
|
|
|
|
continue
|
|
|
|
|
if not r["email"]:
|
|
|
|
|
skipped_no_email += 1
|
|
|
|
|
continue
|
|
|
|
|
if r["invitation_sent_at"] is not None:
|
|
|
|
|
skipped_already_sent += 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Kun meningsfull for individuell-ball-formater (singles/
|
|
|
|
|
# fourball) -- for delt-ball-formater er match_participant_id
|
|
|
|
|
# ALDRI satt i hole_score (ADR/scoring.py), så SUM blir naturlig
|
|
|
|
|
# NULL der uten noen egen format-sjekk her.
|
|
|
|
|
gross = await conn.fetchval(
|
|
|
|
|
"SELECT SUM(gross_strokes) FROM hole_score WHERE match_participant_id = $1",
|
|
|
|
|
r["participant_id"],
|
|
|
|
|
)
|
|
|
|
|
if r["status_text"]:
|
|
|
|
|
summary = f"Du spilte i {session_label} i {tournament_name} -- resultatet ble {r['status_text']}."
|
|
|
|
|
else:
|
|
|
|
|
summary = f"Du var satt opp for {session_label} i {tournament_name}."
|
|
|
|
|
if gross is not None:
|
|
|
|
|
summary += f" Du hadde {gross} slag totalt."
|
|
|
|
|
|
|
|
|
|
email = r["email"].strip().lower()
|
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
|
raw_token = secrets.token_urlsafe(32)
|
|
|
|
|
expires_at = now + timedelta(minutes=settings.MAGIC_LINK_MAX_AGE_MINUTES)
|
|
|
|
|
# Samme "ugyldiggjør eldre uforbrukte lenker"-mønster som
|
|
|
|
|
# request_magic_link (routers/auth.py) -- begrenser vinduet en
|
|
|
|
|
# eldre, lekket lenke kan misbrukes i.
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"UPDATE magic_link_token SET consumed_at = now() WHERE email = $1 AND consumed_at IS NULL",
|
|
|
|
|
email,
|
|
|
|
|
)
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"INSERT INTO magic_link_token (email, token_hash, expires_at, locale) VALUES ($1, $2, $3, 'nb')",
|
|
|
|
|
email,
|
|
|
|
|
_hash_secret(raw_token),
|
|
|
|
|
expires_at,
|
|
|
|
|
)
|
|
|
|
|
if settings.SMTP_CONFIGURED:
|
|
|
|
|
try:
|
|
|
|
|
await send_session_result_email(
|
|
|
|
|
email, r["display_name"], tournament_name, summary, raw_token, "nb"
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
# Se app/email.py sitt mønster -- driftsfeil i
|
|
|
|
|
# utsendingen skal aldri hindre resten av utsendingen
|
|
|
|
|
# (token-raden består uansett, lenken virker fortsatt
|
|
|
|
|
# hvis spilleren ber om en ny normalt).
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
elif settings.DEV_LOG_MAGIC_LINKS:
|
|
|
|
|
# Samme "[DEV] Magic link for ..."-linjeformat som
|
|
|
|
|
# request_magic_link (routers/auth.py) FØRST -- slik at
|
|
|
|
|
# eksisterende dev-/test-verktøy som harvester token derfra
|
|
|
|
|
# fungerer uendret her også -- pluss en egen, lesbar
|
|
|
|
|
# oppsummeringslinje.
|
|
|
|
|
print(f"[DEV] Magic link for {email} (nb): {raw_token}", flush=True)
|
|
|
|
|
print(f"[DEV] Scorekort-invitasjon til {email}: {summary}", flush=True)
|
|
|
|
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"UPDATE match_participant SET invitation_sent_at = now() WHERE id = $1",
|
|
|
|
|
r["participant_id"],
|
|
|
|
|
)
|
|
|
|
|
sent += 1
|
|
|
|
|
|
|
|
|
|
return SendInvitationsOut(
|
|
|
|
|
sent=sent,
|
|
|
|
|
skipped_has_account=skipped_has_account,
|
|
|
|
|
skipped_no_email=skipped_no_email,
|
|
|
|
|
skipped_already_sent=skipped_already_sent,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
# Sponsorer (ADR-018) -- navn+lenke+logo, alle aktive fra MinIO-runden.
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class SponsorCreate(BaseModel):
|
|
|
|
|
name: str = Field(min_length=1, max_length=200)
|
|
|
|
|
url: str | None = None
|
|
|
|
|
|
|
|
|
|
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
_SPONSOR_COLUMNS = "id::text, name, url, logo_key"
|
|
|
|
|
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
class Sponsor(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
name: str
|
|
|
|
|
url: str | None
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
logo_key: str | None
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
logo_url: str | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sponsor_from_row(row) -> Sponsor:
|
|
|
|
|
data = dict(row)
|
|
|
|
|
key = data["logo_key"]
|
|
|
|
|
return Sponsor(**data, logo_url=storage.public_url(key) if key else None)
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/sponsors",
|
|
|
|
|
response_model=list[Sponsor],
|
|
|
|
|
)
|
|
|
|
|
async def list_sponsors(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> list[Sponsor]:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
rows = await conn.fetch(
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
f"SELECT {_SPONSOR_COLUMNS} FROM tournament_sponsor WHERE tournament_id = $1 ORDER BY created_at",
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
tournament_id,
|
|
|
|
|
)
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
return [_sponsor_from_row(r) for r in rows]
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/sponsors",
|
|
|
|
|
response_model=Sponsor,
|
|
|
|
|
status_code=201,
|
|
|
|
|
)
|
|
|
|
|
async def add_sponsor(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
body: SponsorCreate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> Sponsor:
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
tournament = await conn.fetchval("SELECT id FROM tournament WHERE id = $1", tournament_id)
|
|
|
|
|
if tournament is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
row = await conn.fetchrow(
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
f"""
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
INSERT INTO tournament_sponsor (organization_id, tournament_id, name, url)
|
|
|
|
|
VALUES ($1, $2, $3, $4)
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
RETURNING {_SPONSOR_COLUMNS}
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
""",
|
|
|
|
|
organization_id,
|
|
|
|
|
tournament_id,
|
|
|
|
|
body.name,
|
|
|
|
|
body.url,
|
|
|
|
|
)
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
return _sponsor_from_row(row)
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
|
|
|
|
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
class SponsorUpdate(BaseModel):
|
|
|
|
|
name: str | None = Field(default=None, min_length=1, max_length=200)
|
|
|
|
|
url: str | None = None
|
|
|
|
|
# logo_key er BEVISST IKKE med -- settes kun via upload_sponsor_logo
|
|
|
|
|
# under, samme begrunnelse som tournament.hero_image_key over.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/sponsors/{sponsor_id}",
|
|
|
|
|
response_model=Sponsor,
|
|
|
|
|
)
|
|
|
|
|
async def update_sponsor(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
sponsor_id: str,
|
|
|
|
|
body: SponsorUpdate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> Sponsor:
|
|
|
|
|
updates = body.model_dump(exclude_unset=True)
|
|
|
|
|
if not updates:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.")
|
|
|
|
|
|
|
|
|
|
set_clauses = [f"{key} = ${i}" for i, key in enumerate(updates, start=1)]
|
|
|
|
|
values = list(updates.values())
|
|
|
|
|
values.append(sponsor_id)
|
|
|
|
|
values.append(tournament_id)
|
|
|
|
|
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
f"""
|
|
|
|
|
UPDATE tournament_sponsor SET {', '.join(set_clauses)}
|
|
|
|
|
WHERE id = ${len(values) - 1} AND tournament_id = ${len(values)}
|
|
|
|
|
RETURNING {_SPONSOR_COLUMNS}
|
|
|
|
|
""",
|
|
|
|
|
*values,
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Sponsoren finnes ikke.")
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
return _sponsor_from_row(row)
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/sponsors/{sponsor_id}/logo",
|
|
|
|
|
response_model=Sponsor,
|
|
|
|
|
)
|
|
|
|
|
async def upload_sponsor_logo(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
sponsor_id: str,
|
|
|
|
|
file: UploadFile,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> Sponsor:
|
|
|
|
|
if file.content_type not in storage.ALLOWED_INPUT_CONTENT_TYPES:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Ustøttet bildeformat.")
|
|
|
|
|
|
|
|
|
|
raw = await file.read(storage.MAX_UPLOAD_BYTES + 1)
|
|
|
|
|
if len(raw) > storage.MAX_UPLOAD_BYTES:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Bildet er for stort (maks 8 MB).")
|
|
|
|
|
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
exists = await conn.fetchval(
|
|
|
|
|
"SELECT id FROM tournament_sponsor WHERE id = $1 AND tournament_id = $2",
|
|
|
|
|
sponsor_id,
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
if exists is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Sponsoren finnes ikke.")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
key = await storage.upload_image("sponsors", sponsor_id, raw)
|
|
|
|
|
except storage.InvalidImageError:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Filen er ikke et gyldig bilde.")
|
|
|
|
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
f"UPDATE tournament_sponsor SET logo_key = $1 WHERE id = $2 RETURNING {_SPONSOR_COLUMNS}",
|
|
|
|
|
key,
|
|
|
|
|
sponsor_id,
|
|
|
|
|
)
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
return _sponsor_from_row(row)
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
@router.delete(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/sponsors/{sponsor_id}",
|
|
|
|
|
status_code=204,
|
|
|
|
|
)
|
|
|
|
|
async def delete_sponsor(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
sponsor_id: str,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> None:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
deleted = await conn.fetchval(
|
|
|
|
|
"DELETE FROM tournament_sponsor WHERE id = $1 AND tournament_id = $2 RETURNING id",
|
|
|
|
|
sponsor_id,
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
if deleted is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Sponsoren finnes ikke.")
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Konkurranseklasser (2026-08-03) -- fritt navngitte, valgfritt standard-
|
|
|
|
|
# utslag. Delt mellom lag- og individuelle turneringer (samme `tournament`-
|
|
|
|
|
# tabell). Brukes til å forhåndsutfylle riktig utslag når en deltaker legges
|
|
|
|
|
# til en match (her)/runde (individual_tournaments.py), og -- KUN for
|
|
|
|
|
# individuelle turneringer -- til å dele opp leaderboardet. Se
|
|
|
|
|
# 053_tournament_classes.sql for full begrunnelse.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class TournamentClassCreate(BaseModel):
|
|
|
|
|
name: str = Field(min_length=1, max_length=200)
|
|
|
|
|
default_tee_id: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TournamentClassUpdate(BaseModel):
|
|
|
|
|
"""Alle felt valgfrie -- exclude_unset, samme PATCH-semantikk som
|
|
|
|
|
TournamentUpdate/SponsorUpdate."""
|
|
|
|
|
|
|
|
|
|
name: str | None = Field(default=None, min_length=1, max_length=200)
|
|
|
|
|
default_tee_id: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_CLASS_COLUMNS = """
|
|
|
|
|
tc.id::text, tc.name, tc.default_tee_id::text AS default_tee_id, tee.name AS default_tee_name
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TournamentClass(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
name: str
|
|
|
|
|
default_tee_id: str | None
|
|
|
|
|
default_tee_name: str | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/classes",
|
|
|
|
|
response_model=list[TournamentClass],
|
|
|
|
|
)
|
|
|
|
|
async def list_tournament_classes(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> list[TournamentClass]:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
f"""
|
|
|
|
|
SELECT {_CLASS_COLUMNS}
|
|
|
|
|
FROM tournament_class tc
|
|
|
|
|
LEFT JOIN tee ON tee.id = tc.default_tee_id
|
|
|
|
|
WHERE tc.tournament_id = $1
|
|
|
|
|
ORDER BY tc.created_at
|
|
|
|
|
""",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
return [TournamentClass(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/classes",
|
|
|
|
|
response_model=TournamentClass,
|
|
|
|
|
status_code=201,
|
|
|
|
|
)
|
|
|
|
|
async def create_tournament_class(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
body: TournamentClassCreate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> TournamentClass:
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
tournament = await conn.fetchval("SELECT id FROM tournament WHERE id = $1", tournament_id)
|
|
|
|
|
if tournament is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
f"""
|
|
|
|
|
WITH inserted AS (
|
|
|
|
|
INSERT INTO tournament_class (organization_id, tournament_id, name, default_tee_id)
|
|
|
|
|
VALUES ($1, $2, $3, $4)
|
|
|
|
|
RETURNING id, name, default_tee_id
|
|
|
|
|
)
|
|
|
|
|
SELECT inserted.id::text, inserted.name,
|
|
|
|
|
inserted.default_tee_id::text AS default_tee_id, tee.name AS default_tee_name
|
|
|
|
|
FROM inserted LEFT JOIN tee ON tee.id = inserted.default_tee_id
|
|
|
|
|
""",
|
|
|
|
|
organization_id,
|
|
|
|
|
tournament_id,
|
|
|
|
|
body.name,
|
|
|
|
|
body.default_tee_id,
|
|
|
|
|
)
|
|
|
|
|
return TournamentClass(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/classes/{class_id}",
|
|
|
|
|
response_model=TournamentClass,
|
|
|
|
|
)
|
|
|
|
|
async def update_tournament_class(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
class_id: str,
|
|
|
|
|
body: TournamentClassUpdate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> TournamentClass:
|
|
|
|
|
updates = body.model_dump(exclude_unset=True)
|
|
|
|
|
if not updates:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.")
|
|
|
|
|
|
|
|
|
|
set_clauses = [f"{key} = ${i}" for i, key in enumerate(updates, start=1)]
|
|
|
|
|
values = list(updates.values())
|
|
|
|
|
values.append(class_id)
|
|
|
|
|
values.append(tournament_id)
|
|
|
|
|
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
f"""
|
|
|
|
|
WITH updated AS (
|
|
|
|
|
UPDATE tournament_class SET {', '.join(set_clauses)}
|
|
|
|
|
WHERE id = ${len(values) - 1} AND tournament_id = ${len(values)}
|
|
|
|
|
RETURNING id, name, default_tee_id
|
|
|
|
|
)
|
|
|
|
|
SELECT updated.id::text, updated.name,
|
|
|
|
|
updated.default_tee_id::text AS default_tee_id, tee.name AS default_tee_name
|
|
|
|
|
FROM updated LEFT JOIN tee ON tee.id = updated.default_tee_id
|
|
|
|
|
""",
|
|
|
|
|
*values,
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Klassen finnes ikke.")
|
|
|
|
|
return TournamentClass(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/classes/{class_id}",
|
|
|
|
|
status_code=204,
|
|
|
|
|
)
|
|
|
|
|
async def delete_tournament_class(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
class_id: str,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> None:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
deleted = await conn.fetchval(
|
|
|
|
|
"DELETE FROM tournament_class WHERE id = $1 AND tournament_id = $2 RETURNING id",
|
|
|
|
|
class_id,
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
if deleted is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Klassen finnes ikke.")
|
2026-07-18 18:22:10 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Leaderboard (turnering-total, summert på tvers av alle økter/matcher)
|
|
|
|
|
#
|
|
|
|
|
# `match.team_a_id`/`team_b_id` settes PER MATCH ved opprettelse (matches.py),
|
|
|
|
|
# ikke garantert konsistent på tvers av matcher -- en organisator kunne i
|
|
|
|
|
# prinsippet opprettet match 1 med team_a=Rød og match 3 med team_a=Blå. Derfor
|
|
|
|
|
# summeres ALT (både total og per-økt) på det EKTE lag-id-et, aldri på
|
|
|
|
|
# "a"/"b"-labelen -- se points_by_team under.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class TeamStanding(BaseModel):
|
|
|
|
|
team_id: str
|
|
|
|
|
team_name: str
|
|
|
|
|
color: str | None
|
|
|
|
|
points: float
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
# ADR-020 Beslutning B: hvis pågående matcher holder seg som de står nå --
|
|
|
|
|
# ikke-avgjorte matcher bidrar med full poengsum til leading_side (delt
|
|
|
|
|
# likt ved "AS"/ikke startet). Avgjorte matcher bidrar likt til begge.
|
|
|
|
|
projected_points: float
|
2026-07-18 18:22:10 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class SessionStanding(BaseModel):
|
|
|
|
|
session_id: str
|
|
|
|
|
sequence: int
|
|
|
|
|
name: str | None
|
|
|
|
|
points_by_team: dict[str, float]
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
projected_points_by_team: dict[str, float]
|
2026-07-18 18:22:10 +02:00
|
|
|
matches_total: int
|
|
|
|
|
matches_decided: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Leaderboard(BaseModel):
|
|
|
|
|
teams: list[TeamStanding]
|
|
|
|
|
matches_total: int
|
|
|
|
|
matches_decided: int
|
|
|
|
|
sessions: list[SessionStanding]
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 22:51:07 +02:00
|
|
|
async def fetch_leaderboard(conn, tournament_id: str) -> Leaderboard:
|
|
|
|
|
"""Delt mellom den innloggede (get_leaderboard under) og den offentlige
|
|
|
|
|
/public/tournaments/{id}/leaderboard (registration.py, tilskuer-rolle) --
|
|
|
|
|
ingen spiller-nivå-data i responsmodellene (kun lag-id/-navn/-farge/
|
|
|
|
|
poeng), så ingen ekstra synlighetsfiltrering trengs utover den vanlige
|
|
|
|
|
tournament.visibility-sjekken den offentlige ruten selv gjør FØR denne
|
|
|
|
|
kalles."""
|
|
|
|
|
teams = await conn.fetch(
|
|
|
|
|
"SELECT id::text, name, color FROM team WHERE tournament_id = $1 ORDER BY created_at",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
if len(teams) != 2:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Turneringen har ikke nøyaktig to lag ennå.")
|
2026-07-18 18:22:10 +02:00
|
|
|
|
2026-07-19 22:51:07 +02:00
|
|
|
rows = await conn.fetch(
|
|
|
|
|
"""
|
|
|
|
|
SELECT s.id::text AS session_id, s.sequence, s.name,
|
|
|
|
|
s.points_per_match::float AS points_per_match,
|
|
|
|
|
m.team_a_id::text AS team_a_id, m.team_b_id::text AS team_b_id,
|
|
|
|
|
m.points_side_a::float AS points_side_a, m.points_side_b::float AS points_side_b,
|
|
|
|
|
m.leading_side
|
|
|
|
|
FROM match m
|
|
|
|
|
JOIN session s ON s.id = m.session_id
|
|
|
|
|
WHERE s.tournament_id = $1
|
|
|
|
|
ORDER BY s.sequence, m.sequence
|
|
|
|
|
""",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
2026-07-18 18:22:10 +02:00
|
|
|
|
|
|
|
|
team_totals: dict[str, float] = {t["id"]: 0.0 for t in teams}
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
team_projected: dict[str, float] = {t["id"]: 0.0 for t in teams}
|
2026-07-18 18:22:10 +02:00
|
|
|
session_order: list[str] = []
|
|
|
|
|
sessions: dict[str, dict] = {}
|
|
|
|
|
matches_total = 0
|
|
|
|
|
matches_decided = 0
|
|
|
|
|
|
|
|
|
|
for r in rows:
|
|
|
|
|
matches_total += 1
|
|
|
|
|
sid = r["session_id"]
|
|
|
|
|
if sid not in sessions:
|
|
|
|
|
session_order.append(sid)
|
|
|
|
|
sessions[sid] = {
|
|
|
|
|
"session_id": sid,
|
|
|
|
|
"sequence": r["sequence"],
|
|
|
|
|
"name": r["name"],
|
|
|
|
|
"points_by_team": {t["id"]: 0.0 for t in teams},
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
"projected_points_by_team": {t["id"]: 0.0 for t in teams},
|
2026-07-18 18:22:10 +02:00
|
|
|
"matches_total": 0,
|
|
|
|
|
"matches_decided": 0,
|
|
|
|
|
}
|
|
|
|
|
sessions[sid]["matches_total"] += 1
|
|
|
|
|
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
# Projisert bidrag: avgjorte matcher gir samme tall som faktisk
|
|
|
|
|
# (de ER allerede det de blir). Ikke-avgjorte matcher gir hele
|
|
|
|
|
# øktens poengsum til den som leder NÅ, delt 0,5/0,5 ved "AS"
|
|
|
|
|
# eller en match som ennå ikke har noen registrerte hull.
|
|
|
|
|
if r["points_side_a"] is not None:
|
|
|
|
|
proj_a, proj_b = r["points_side_a"], r["points_side_b"]
|
|
|
|
|
else:
|
|
|
|
|
ppm = r["points_per_match"]
|
|
|
|
|
if r["leading_side"] == "a":
|
|
|
|
|
proj_a, proj_b = ppm, 0.0
|
|
|
|
|
elif r["leading_side"] == "b":
|
|
|
|
|
proj_a, proj_b = 0.0, ppm
|
|
|
|
|
else:
|
|
|
|
|
proj_a = proj_b = ppm / 2
|
|
|
|
|
team_projected[r["team_a_id"]] = team_projected.get(r["team_a_id"], 0.0) + proj_a
|
|
|
|
|
team_projected[r["team_b_id"]] = team_projected.get(r["team_b_id"], 0.0) + proj_b
|
|
|
|
|
sessions[sid]["projected_points_by_team"][r["team_a_id"]] = (
|
|
|
|
|
sessions[sid]["projected_points_by_team"].get(r["team_a_id"], 0.0) + proj_a
|
|
|
|
|
)
|
|
|
|
|
sessions[sid]["projected_points_by_team"][r["team_b_id"]] = (
|
|
|
|
|
sessions[sid]["projected_points_by_team"].get(r["team_b_id"], 0.0) + proj_b
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-18 18:22:10 +02:00
|
|
|
if r["points_side_a"] is None:
|
|
|
|
|
continue
|
|
|
|
|
matches_decided += 1
|
|
|
|
|
sessions[sid]["matches_decided"] += 1
|
|
|
|
|
team_totals[r["team_a_id"]] = team_totals.get(r["team_a_id"], 0.0) + r["points_side_a"]
|
|
|
|
|
team_totals[r["team_b_id"]] = team_totals.get(r["team_b_id"], 0.0) + r["points_side_b"]
|
|
|
|
|
sessions[sid]["points_by_team"][r["team_a_id"]] = (
|
|
|
|
|
sessions[sid]["points_by_team"].get(r["team_a_id"], 0.0) + r["points_side_a"]
|
|
|
|
|
)
|
|
|
|
|
sessions[sid]["points_by_team"][r["team_b_id"]] = (
|
|
|
|
|
sessions[sid]["points_by_team"].get(r["team_b_id"], 0.0) + r["points_side_b"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
team_standings = [
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
TeamStanding(
|
|
|
|
|
team_id=t["id"],
|
|
|
|
|
team_name=t["name"],
|
|
|
|
|
color=t["color"],
|
|
|
|
|
points=team_totals[t["id"]],
|
|
|
|
|
projected_points=team_projected[t["id"]],
|
|
|
|
|
)
|
2026-07-18 18:22:10 +02:00
|
|
|
for t in teams
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return Leaderboard(
|
|
|
|
|
teams=team_standings,
|
|
|
|
|
matches_total=matches_total,
|
|
|
|
|
matches_decided=matches_decided,
|
|
|
|
|
sessions=[SessionStanding(**sessions[sid]) for sid in sorted(session_order, key=lambda s: sessions[s]["sequence"])],
|
|
|
|
|
)
|
2026-07-19 22:51:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/orgs/{organization_id}/tournaments/{tournament_id}/leaderboard",
|
|
|
|
|
response_model=Leaderboard,
|
|
|
|
|
)
|
|
|
|
|
async def get_leaderboard(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> Leaderboard:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
return await fetch_leaderboard(conn, tournament_id)
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Individuell rangering PER ØKT (FEATURE_BACKLOG.md "Leaderboard for runder
|
|
|
|
|
# og turneringer", 2026-07-25/28 oppfølging) -- ved siden av, ikke i stedet
|
|
|
|
|
# for, lag-poeng-leaderboardet over. Bevisst AVGRENSET til én økt (ikke
|
|
|
|
|
# summert på tvers av turneringen, se avklaring i CLAUDE.md) -- og KUN
|
|
|
|
|
# meningsfullt for scoring_mode='stroke'-økter i individuell-ball-format
|
|
|
|
|
# (singles/fourball, se scoring.py sin _INDIVIDUAL_FORMATS) siden delt-ball-
|
|
|
|
|
# formater og hole_result-modus ikke har noen individuell brutto-score.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class IndividualLeaderboardEntry(BaseModel):
|
|
|
|
|
participant_id: str
|
|
|
|
|
display_name: str
|
|
|
|
|
team_id: str
|
|
|
|
|
team_name: str
|
|
|
|
|
team_color: str | None
|
|
|
|
|
thru: int
|
|
|
|
|
gross_score: int | None
|
|
|
|
|
gross_to_par: int | None
|
|
|
|
|
net_score: int | None
|
|
|
|
|
net_to_par: int | None
|
|
|
|
|
total_points: int | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class IndividualLeaderboard(BaseModel):
|
|
|
|
|
session_id: str
|
|
|
|
|
format: str
|
|
|
|
|
entries: list[IndividualLeaderboardEntry]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def fetch_individual_leaderboard(
|
|
|
|
|
conn, organization_id: str, session_id: str, viewer_user_id: str | None
|
|
|
|
|
) -> IndividualLeaderboard:
|
|
|
|
|
session = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
SELECT tournament_id::text AS tournament_id, format, scoring_mode,
|
|
|
|
|
hole_config::text AS hole_config, course_id::text AS course_id
|
|
|
|
|
FROM session WHERE id = $1
|
|
|
|
|
""",
|
|
|
|
|
session_id,
|
|
|
|
|
)
|
|
|
|
|
if session is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
|
|
|
|
|
if session["scoring_mode"] != "stroke" or session["format"] not in ("singles", "fourball"):
|
|
|
|
|
raise app_error(
|
|
|
|
|
400,
|
|
|
|
|
"VALIDATION_FAILED",
|
|
|
|
|
"Individuell rangering er kun tilgjengelig for slagspill-økter i format singles eller fourball.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
hole_rows = await conn.fetch(
|
|
|
|
|
"SELECT hole_number, par, stroke_index FROM hole WHERE course_id = $1 ORDER BY hole_number",
|
|
|
|
|
session["course_id"],
|
|
|
|
|
)
|
|
|
|
|
if len(hole_rows) != 18:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Banen mangler registrerte hull.")
|
|
|
|
|
par_by_hole = {r["hole_number"]: r["par"] for r in hole_rows}
|
|
|
|
|
all_18_si = [r["stroke_index"] for r in hole_rows]
|
|
|
|
|
played = played_hole_numbers(session["hole_config"])
|
|
|
|
|
|
|
|
|
|
# Samme reveal-respekterende synlighet som fetch_matches (ADR-013/026):
|
|
|
|
|
# egen side FØR reveal, hele feltet ETTER.
|
|
|
|
|
locked = await locked_team_ids(conn, session_id)
|
|
|
|
|
revealed = len(locked) >= 2
|
|
|
|
|
own = (
|
|
|
|
|
set()
|
|
|
|
|
if revealed
|
|
|
|
|
else await own_team_ids(conn, organization_id, session["tournament_id"], viewer_user_id)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
participant_rows = await conn.fetch(
|
|
|
|
|
"""
|
|
|
|
|
SELECT mp.id::text AS participant_id, mp.playing_handicap,
|
|
|
|
|
tr.team_id::text AS team_id, t.name AS team_name, t.color AS team_color,
|
|
|
|
|
p.display_name
|
|
|
|
|
FROM match_participant mp
|
|
|
|
|
JOIN match m ON m.id = mp.match_id
|
|
|
|
|
JOIN team_roster tr ON tr.id = mp.team_roster_id
|
|
|
|
|
JOIN team t ON t.id = tr.team_id
|
|
|
|
|
JOIN player p ON p.id = tr.player_id
|
|
|
|
|
WHERE m.session_id = $1 AND ($2 OR tr.team_id::text = ANY($3::text[]))
|
|
|
|
|
ORDER BY p.display_name
|
|
|
|
|
""",
|
|
|
|
|
session_id,
|
|
|
|
|
revealed,
|
|
|
|
|
list(own),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
entries: list[IndividualLeaderboardEntry] = []
|
|
|
|
|
for r in participant_rows:
|
|
|
|
|
score_rows = await conn.fetch(
|
|
|
|
|
"SELECT hole_number, gross_strokes FROM hole_score "
|
|
|
|
|
"WHERE match_participant_id = $1 ORDER BY hole_number",
|
|
|
|
|
r["participant_id"],
|
|
|
|
|
)
|
|
|
|
|
gross_score = gross_to_par = net_score = net_to_par = total_points = None
|
|
|
|
|
if score_rows:
|
|
|
|
|
gross_score = sum(s["gross_strokes"] for s in score_rows)
|
|
|
|
|
par_total = sum(par_by_hole[s["hole_number"]] for s in score_rows)
|
|
|
|
|
gross_to_par = gross_score - par_total
|
|
|
|
|
|
|
|
|
|
if r["playing_handicap"] is not None:
|
|
|
|
|
strokes_list = allocate_over_played_holes(r["playing_handicap"], all_18_si, played)
|
|
|
|
|
strokes_by_hole = dict(zip(played, strokes_list))
|
|
|
|
|
strokes_received_total = sum(strokes_by_hole[s["hole_number"]] for s in score_rows)
|
|
|
|
|
net_score = gross_score - strokes_received_total
|
|
|
|
|
net_to_par = gross_to_par - strokes_received_total
|
|
|
|
|
# Samme stableford-formel som round-leaderboardets
|
|
|
|
|
# total_points (rounds.py) -- 2 poeng for netto par, +/-1
|
|
|
|
|
# per slag, gulvet på 0.
|
|
|
|
|
total_points = sum(
|
|
|
|
|
max(
|
|
|
|
|
0,
|
|
|
|
|
par_by_hole[s["hole_number"]]
|
|
|
|
|
- (s["gross_strokes"] - strokes_by_hole[s["hole_number"]])
|
|
|
|
|
+ 2,
|
|
|
|
|
)
|
|
|
|
|
for s in score_rows
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
entries.append(
|
|
|
|
|
IndividualLeaderboardEntry(
|
|
|
|
|
participant_id=r["participant_id"],
|
|
|
|
|
display_name=r["display_name"],
|
|
|
|
|
team_id=r["team_id"],
|
|
|
|
|
team_name=r["team_name"],
|
|
|
|
|
team_color=r["team_color"],
|
|
|
|
|
thru=len(score_rows),
|
|
|
|
|
gross_score=gross_score,
|
|
|
|
|
gross_to_par=gross_to_par,
|
|
|
|
|
net_score=net_score,
|
|
|
|
|
net_to_par=net_to_par,
|
|
|
|
|
total_points=total_points,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return IndividualLeaderboard(session_id=session_id, format=session["format"], entries=entries)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/orgs/{organization_id}/sessions/{session_id}/individual-leaderboard",
|
|
|
|
|
response_model=IndividualLeaderboard,
|
|
|
|
|
)
|
|
|
|
|
async def get_individual_leaderboard(
|
|
|
|
|
session_id: str,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
) -> IndividualLeaderboard:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
return await fetch_individual_leaderboard(conn, organization_id, session_id, user.user_id)
|