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
|
2026-07-17 21:40:42 +02:00
|
|
|
from datetime import date, datetime
|
2026-07-16 09:16:22 +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
|
|
|
from fastapi import APIRouter, Depends, UploadFile
|
2026-07-16 09:16:22 +02:00
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
|
|
from ..auth import get_authorized_org
|
|
|
|
|
from ..blind_draw import locked_team_ids
|
|
|
|
|
from ..db import org_connection
|
2026-07-17 21:40:42 +02:00
|
|
|
from ..errors import app_error, translate_db_errors
|
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
|
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,
|
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
|
|
|
registration_overflow_policy, registration_requires_approval, hero_image_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
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
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
|
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]:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
rows = await conn.fetch(
|
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
|
|
|
f"SELECT {_TOURNAMENT_COLUMNS} FROM tournament ORDER BY created_at DESC"
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
2026-07-17 21:40:42 +02:00
|
|
|
return [Tournament(**dict(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():
|
2026-07-16 09:16:22 +02:00
|
|
|
row = await conn.fetchrow(
|
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
|
|
|
f"""
|
2026-07-17 21:40:42 +02:00
|
|
|
INSERT INTO tournament (organization_id, name, start_date, end_date)
|
|
|
|
|
VALUES ($1, $2, $3, $4)
|
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
|
|
|
RETURNING {_TOURNAMENT_COLUMNS}
|
2026-07-16 09:16:22 +02:00
|
|
|
""",
|
|
|
|
|
organization_id,
|
|
|
|
|
body.name,
|
2026-07-17 21:40:42 +02:00
|
|
|
body.start_date,
|
|
|
|
|
body.end_date,
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
2026-07-17 21:40:42 +02:00
|
|
|
return Tournament(**dict(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)."""
|
|
|
|
|
|
|
|
|
|
visibility: str | None = Field(default=None, pattern="^(public|org|participants)$")
|
|
|
|
|
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
|
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.
|
|
|
|
|
set_clauses = [f"{key} = ${i}" for i, key in enumerate(updates, start=1)]
|
|
|
|
|
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.")
|
|
|
|
|
return Tournament(**dict(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}/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,
|
|
|
|
|
)
|
|
|
|
|
return Tournament(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> list[Team]:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RosterEntry(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
player_id: str
|
|
|
|
|
display_name: str
|
|
|
|
|
handicap_index_snapshot: float | None
|
|
|
|
|
is_captain: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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(
|
|
|
|
|
"""
|
|
|
|
|
SELECT tr.id::text, tr.player_id::text,
|
|
|
|
|
tr.handicap_index_snapshot::float AS handicap_index_snapshot,
|
|
|
|
|
tr.is_captain, p.display_name
|
|
|
|
|
FROM team_roster tr
|
|
|
|
|
JOIN player p ON p.id = tr.player_id
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
WITH inserted AS (
|
|
|
|
|
INSERT INTO team_roster
|
|
|
|
|
(organization_id, team_id, player_id, handicap_index_snapshot, is_captain)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
|
|
|
RETURNING id, player_id, handicap_index_snapshot, is_captain
|
|
|
|
|
)
|
|
|
|
|
SELECT inserted.id::text, inserted.player_id::text,
|
|
|
|
|
inserted.handicap_index_snapshot::float AS handicap_index_snapshot,
|
|
|
|
|
inserted.is_captain, p.display_name
|
|
|
|
|
FROM inserted JOIN player p ON p.id = inserted.player_id
|
|
|
|
|
""",
|
|
|
|
|
organization_id,
|
|
|
|
|
team_id,
|
|
|
|
|
body.player_id,
|
|
|
|
|
snapshot,
|
|
|
|
|
body.is_captain,
|
|
|
|
|
)
|
|
|
|
|
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):
|
|
|
|
|
is_captain: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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:
|
|
|
|
|
# Bevisst enkelt: setter/fjerner kapteinmerket på NØYAKTIG denne raden,
|
|
|
|
|
# håndhever ikke "kun én kaptein per lag" -- kaptein er i dag bare et
|
|
|
|
|
# merke, ikke en egen autorisasjonsrolle (se FEATURE_BACKLOG).
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
WITH updated AS (
|
|
|
|
|
UPDATE team_roster
|
|
|
|
|
SET is_captain = $1
|
|
|
|
|
WHERE id = $2 AND team_id = $3
|
|
|
|
|
RETURNING id, player_id, handicap_index_snapshot, is_captain
|
|
|
|
|
)
|
|
|
|
|
SELECT updated.id::text, updated.player_id::text,
|
|
|
|
|
updated.handicap_index_snapshot::float AS handicap_index_snapshot,
|
|
|
|
|
updated.is_captain, p.display_name
|
|
|
|
|
FROM updated JOIN player p ON p.id = updated.player_id
|
|
|
|
|
""",
|
|
|
|
|
body.is_captain,
|
|
|
|
|
roster_id,
|
|
|
|
|
team_id,
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Roster-oppføringen finnes ikke.")
|
|
|
|
|
return RosterEntry(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
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
|
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"],
|
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,
|
|
|
|
|
scheduled_at, tee_interval_minutes, start_hole
|
|
|
|
|
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,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> list[SessionOut]:
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
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,
|
|
|
|
|
scheduled_at, tee_interval_minutes, start_hole)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12, $13)
|
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,
|
|
|
|
|
scheduled_at, tee_interval_minutes, start_hole
|
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,
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
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
|
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,
|
|
|
|
|
)
|
|
|
|
|
return [Sponsor(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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,
|
|
|
|
|
)
|
|
|
|
|
return Sponsor(**dict(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
|
|
|
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.")
|
|
|
|
|
return Sponsor(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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,
|
|
|
|
|
)
|
|
|
|
|
return Sponsor(**dict(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
|
|
|
@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.")
|