teecup/CLAUDE.md
Erol Haagenrud 24af937fe0 Update Todos
Opprette app/realtime.py (delt WS-register, ingen router-avhengigheter)

Koble broadcast inn i recompute_and_cache_match_state + apply_concession

Legge til WS-endepunkt /ws/public/tournaments/{id}/live i messaging.py

Frontend: koble public-live.tsx til WebSocket med auto-refetch

Scratch-verifisere (REST + WebSocket)

Ekte typesjekket frontend-build

Skrive ADR-027 + oppdatere FEATURE_BACKLOG.md

Deploy etter bekreftelse + CLAUDE.md-statuslogg
"Følg live"-siden er nå faktisk live. Ny scores/walkover trigger en kringkasting over WebSocket til alle som følger den turneringen — siden henter automatisk leaderboardet på nytt, og matcher/scorekort som er åpne akkurat da, uten at noen trenger å laste siden på nytt.

Gjenbrukte hele infrastrukturen fra meldings-runden (samme /ws/*-Caddy-rute, ingen ny driftsendring), men måtte legge kringkastingsregisteret i en egen, rutefri modul (app/realtime.py) for å unngå en sirkulær import mellom filene. Verifisert med faktisk sanntidsmottak i scratch (ikke bare REST-svar) og et ekte wss://-håndtrykk mot produksjon.

.md-filene er oppdatert.
2026-07-19 23:16:38 +02:00

112 KiB
Raw Blame History

CLAUDE.md — arbeidsinstruks for TeeCup

Les dette først i hver økt. Det koder hva vi har bestemt og hvordan vi jobber.

Autoritative kilder (les før du gjør noe)

  • ARCHITECTURE_DECISIONS.md — hva som er bestemt og hvorfor (ADR-001…018). Fasit.
  • FEATURE_BACKLOG.md — hva som gjenstår, hva som er utsatt, hva som mangler.
  • Endres en beslutning: legg til en ny ADR, ikke slett historikk. Hold begge filene oppdatert når noe avgjøres.

Sikkerhetsregler (ufravikelige)

  • Rør ALDRI teeoff-databasen eller den ekte teecup_db uten at brukeren eksplisitt har bekreftet det i samme økt. Test alltid migrasjoner mot en egen scratch-database først, og rydd opp etterpå.
  • Vis planen (hvilke kommandoer, mot hvilken database) FØR du kjører noe som skriver, migrerer eller sletter. Vent på bekreftelse.
  • Hemmeligheter (passord, secrets) bor i .env (filrettigheter 600), dekkes av .gitignore, committes aldri, og skrives aldri i klartekst i chatten eller i SQL-filer. Generer dem på serveren (openssl rand -base64 32).
  • Kjør appen som databaserollen teecup_app (NOSUPERUSER, NOBYPASSRLS) — aldri som teeoff_admin/superuser i runtime.

Arkitektur-invarianter (ikke bryt uten en ny ADR)

  • Tenant = organisasjon. organization_id på alle domenetabeller, håndhevet av RLS. App-koden setter app.current_org med SET LOCAL per transaksjon.
  • Verifiser at brukeren er medlem av organisasjonen FØR org-konteksten settes. RLS stoler blindt på app.current_org.
  • Egen innlogging (uavhengig av teeoff). Banedata hentes fra teeoff via lesende API, ikke delt database.
  • v1 = nøyaktig to lag (Ryder Cup-format), håndhevet i app-laget. Match-modellen holdes generell (to sider) så knockout/flere lag kan komme senere.
  • Handicap-/matchlogikk skal ligge i handicap_engine.py (rent, testet, uten db/API-avhengigheter). Allowances er konfig, ikke hardkodet.
  • Media (bilder/video) skal i objektlagring (MinIO), ikke i Postgres. Postgres holder bare metadata + nøkkel.

Arbeidsmåte

  • Inkrementelt. Ingenting tas for gitt før det er testet. Bekreft hvert steg før du går videre.
  • Bruk git (remote: brukerens Forgejo). Commit i logiske steg med tydelige meldinger.
  • Er du usikker på omfang eller en beslutning: spør heller enn å gjette.

Status (oppdater denne når ting endres)

Ferdig og verifisert:

  • Handicap-motor + tester (24/24, R&A-verifisert).

  • Skjema 001 + roller 002 + scoring/blind draw 003. Isolasjon bevist med test_isolation.sql (RLS-oppførsel, ikke bare at skjemaet kjører).

  • 002 hadde en reell bug (psql interpolerer ikke :'var' inne i DO $$...$$) — permanent fikset, verifisert mot scratch to ganger.

  • API-et kjørt for ekte (ikke bare syntaks-sjekket) i en engangs Docker- container mot en scratch-database, RLS bevist gjennom hele asyncpg-pool-stacken (ikke bare i rå SQL).

  • Oppsett-endepunktene er bygget og verifisert: app/routers/players.py (spillerpool), tournaments.py (turnering/lag/roster/økter, ADR-011 to-lags-grense håndhevet med FOR UPDATE-lås), matches.py (matcher/ deltakere/blind draw-lås, ADR-013-synlighet push-down i SQL). Delt feiloversettelse i app/errors.py, delte synlighetsspørringer i app/blind_draw.py. main.py er nå bare app-factory + include_router.

  • Scoring-runden er bygget og verifisert for ekte mot scratch-db (18-hulls bane med tee_rating, 4 spillere for fourball-testing): app/handicap.py (ADR-014 fire brytere via parse_allowance_config, handicap beregnes i compute_and_store_side_handicaps rett etter deltaker-innsetting — singles/fourball per spiller umiddelbart, foursome/greensome/scramble kun når siden er komplett), app/routers/scoring.py (hole-scores/ hole-results-upsert, scorecard-GET, matchstatus-recompute med FOR UPDATE-lås mot race og SAMMENHENGENDE-prefiks-regel for uferdige hull). app/team_authz.py skilt ut fra matches.py (delt med scoring.py). Alle 10 planlagte tester bestått, inkl. fourball better-ball-aggregering (MIN av to nettoer, venter til begge partnere har registrert), poeng-caching ved tidlig avgjort match, og ADR-014-bryteren use_handicap=false. Fant og fikset underveis: tournaments.py sin SessionCreate manglet scoring_mode helt (økter kunne aldri opprettes i hole_result-modus via API-et) — lagt til. Bevisst utelatt/kjente begrensninger: en side som aldri når forventet deltakerantall (no-show) får aldri beregnet handicap og matchen kan da aldri avgjøres — ingen manuell overstyring bygget. Score-skriving er upsert (ingen avvisning ved duplikat) — ingen audit-trail på rettelser. Kapteins-only autorisasjon fortsatt ikke bygget (FEATURE_BACKLOG ); bar er «rostret på laget».

  • Match-lås ved avgjørelse (2026-07-16): submit_hole_score/ submit_hole_result avviser nå 409 hvis match.points_side_a IS NOT NULL (matchen er avgjort) — FØR upserten kjøres, både for nye hull og korrigering av allerede talte hull. Tetter en reell bug: uten dette kunne «spøkelses-hull» lagt inn etter avgjørelse endre en allerede cachet margin ved neste omregning. Automatisk, ingen ny autorisasjon involvert.

  • Ekte autentisering bygget og verifisert (2026-07-16): X-Debug-User-Id- stubben er HELT fjernet (ingen fallback). Magic-link + JWT-sesjon i app/routers/auth.py + app/auth.py (request-link/verify-link/ logout/me), ny migrasjon 004_auth.sql (magic_link_token-tabell + unik e-post-indeks på app_user). Token = secrets.token_urlsafe(32), kun SHA-256-hash lagres, atomisk forbruk (UPDATE ... RETURNING, ikke les-sjekk-skriv), generisk respons uansett om e-posten finnes (unngår enumerering), gamle uforbrukte lenker ugyldiggjøres når en ny utstedes, app_user opprettes FØRST ved vellykket verifisering (ikke ved forespørsel). Sesjons-JWT (PyJWT, algorithms=["HS256"] eksplisitt) i HttpOnly/SameSite=Lax/dynamisk-Secure-cookie, 30 dager, med et ekte eksistens-oppslag mot app_user på hver forespørsel (faktisk tilbakekalling — en slettet bruker kan ikke ri ut sesjonen). Alle 12 planlagte tester bestått. Fant og fikset underveis: ON CONFLICT (email) matchet ikke den nye PARTIELLE unike indeksen uten eksplisitt WHERE email IS NOT NULL (samme klasse feil som hole_scores partielle indekser i scoring-runden). Fant, IKKE fikset i denne runden (egen runde rett etterpå — se under): organization-tabellens RLS-policy kastet en 500 i stedet for skjemaets lovede "trygg standard: se ingenting" ved tomstreng-GUC.

  • RLS-tomstreng-bug FIKSET (2026-07-16): ny migrasjon 005_rls_null_guard.sql — delt STABLE SQL-funksjon app_current_org() gjør NULLIF(current_setting('app.current_org', true), '')::uuid i stedet for det rå uttrykket, brukt av alle 15 RLS-policyer (ALTER POLICY, 14 org_isolation + org_self). Verifisert med 3 nye regresjonstester i test_isolation.sql (Test 10-12) OG ved faktisk å gjenskape original- buggen mot en ekte container (pool-størrelse 1, varm opp med org_connection(), deretter /auth/me på samme gjenbrukte tilkobling — gikk fra 500 til 200). Viktig presisering fra denne runden: fiksen gjør IKKE at /auth/me kan joine organization direkte via plain_connection() — det var en feilaktig antakelse i forrige runde. org_self krever fortsatt en MATCHENDE app.current_org for å vise en rad (riktig RLS-design, ikke noe fiksen skulle endre), og en bruker kan tilhøre flere organisasjoner samtidig, så det finnes ingen ÉN kontekst å sette for en tverr-org-spørring. /auth/me slår derfor opp hvert org-navn ett om gangen via org_connection() (N+1, N = antall org-er brukeren tilhører) — dette er riktig løsning, ikke en omvei.

  • Organisasjon-bootstrap bygget og verifisert (2026-07-16): nytt POST /orgs (app/routers/organizations.py) — det ENESTE stedet i API-et som setter inn en organization-rad. Fant under statusgjennomgang at dette manglet helt (alle tidligere org-er var seedet med superbruker-SQL). Ingen ny migrasjon. Selvrefererende RLS-bootstrap bekreftet å fungere: generer org-ens uuid i Python, sett app.current_org til nøyaktig den via eksisterende org_connection(), sett inn organization-raden med samme id — org_selfs implisitte WITH CHECK blir da trivielt sann, ingen privilegert tilkobling nødvendig (i motsetning til hva 002s kommentar antydet). Verifisert med 5 tester inkl. en negativ kontroll (mismatchende id avvist med insufficient_privilege) og full kryss-org-isolasjon mellom to uavhengig opprettede organisasjoner.

  • Ekte SMTP-utsending bygget og verifisert (2026-07-16): ny app/email.py (send_magic_link_email, smtplib via asyncio.to_thread, håndterer både implisitt TLS/port 465 og STARTTLS dynamisk). Brukeren la egne SMTP-credentials i .env (TEECUP_SMTP_*, TEECUP_FROM_EMAIL — ADR-009, ikke delt med teeoff); jeg leste kun nøkkelnavnene for å bekrefte de fantes, aldri verdiene. app/config.py sin SMTP_CONFIGURED er valgfri (ikke _required) — dev-only logging (TEECUP_DEV_LOG_MAGIC_LINKS) fortsatt fungerer uendret når SMTP ikke er satt opp. Driftsfeil i utsendingen lekker aldri til klientresponsen (bevarer anti-enumerering). Verifisert med faktisk levering: sendte én ekte test-e-post til en adresse brukeren oppga — brukeren bekreftet mottak. Første gang noe i prosjektet er bevist ved ekte levering, ikke bare curl/scratch.

  • Containerisert og LIVE på teecup.teeoff.no (2026-07-16): ekte teecup_db opprettet (migrasjoner 001→005 kjørt permanent, test_isolation.sql består), Dockerfile + docker-compose.yml (tjeneste teecup_api, joiner det eksisterende teeoff_default-nettverket), Caddy-blokk lagt til i /opt/teeoff/deploy/Caddyfile. Ekte innlogging (magic-link → e-post → JWT-sesjon med Secure-cookie) verifisert ende-til-ende mot den live stacken. teeoff.no upåvirket gjennom hele prosessen. To reelle hendelser underveis, begge løst:

    1. Caddy plukket ikke opp filendringenteeoff_caddy sin Caddyfile-mount er en ENKELTFIL-bind-mount, låst til inoden som fantes da containeren sist startet. Min fil-redigering (atomisk rename) laget en ny inode på samme sti, så containeren fortsatte å lese den GAMLE filen uansett hvor mange ganger caddy validate/caddy reload/admin-API /load ble kjørt (alle validerte/lastet den uendrede gamle filen, derav ingen feilmelding). Løst med en full docker restart teeoff_caddy (brukeren bekreftet — avvek fra planens "kun graceful reload, ingen omstart"-løfte, noen sekunders nedetid for teeoff.no).
    2. Alvorlig nettverksalias-kollisjon (funnet RETT ETTER omstarten, da ekte teeoff-trafikk som /api/facilities?... med ekte klubb-slugs dukket opp i teecup_api sin logg): docker-compose.yml sin service-nøkkel var api: — SAMME nøkkel som teeoffs eget api-servicenavn (docker-compose.prod.yml). Docker Compose registrerer nettverksalias basert på service-NAVNET (ikke bare container_name) på delte nettverk, så BEGGE containerne fikk alias apiteeoff_default — Caddys reverse_proxy api:8000 i teeoff sin egen config kunne da tilfeldig treffe enten ekte teeoff_api eller teecup_api. Rettet umiddelbart (stoppet teecup_api først for å hindre videre feilruting av ekte teeoff-trafikk, ga service-nøkkelen navnet teecup_api i stedet, gjenopprettet — bekreftet med docker network inspect at alias api nå KUN peker på ekte teeoff_api). Mindre driftslærdom: (a) jeg eksponerte ved et uhell TEECUP_SMTP_PASS/TEECUP_FROM_EMAIL i eget debug-output mens jeg feilsøkte en .env-korrupsjon (manglende linjeskift fra min egen >>-tilføyelse) — brukeren roterte passordet som forsiktighetsregel; (b) Docker leser IKKE .env på nytt for en allerede kjørende container — docker compose up -d --force-recreate kreves etter enhver .env-endring som skal tas i bruk; (c) et #-tegn i et upassordet .env-passord kuttes som en kommentar av Compose sin parser — anførselstegn (fortrinnsvis enkle) løser dette.
  • Dato/klokkeslett + feilkode-kontrakt + i18n-forberedelse (2026-07-17, ADR-015): reist av brukeren rett før frontend-arbeidet. Ny migrasjon 006_scheduling_and_locale.sql (alt additivt): tournament.end_date, session.scheduled_at/tee_interval_minutes/start_hole, match.tee_time_override, app_user.preferred_locale, magic_link_token.locale. match.tee_time er UTLEDET i Python (scheduled_at + (sequence-1)*tee_interval_minutes, override vinner hvis satt) — aldri lagret per match. Full retrofit av ALLE 39 daværende HTTPException(..., detail="norsk streng")-steder på tvers av 6 filer til en delt app_error(status_code, code, message)-factory (app/errors.py) — responsformen er nå konsekvent {"detail":{"code":...,"message":...}} i hele API-et, verifisert med et siste grep -rn 'detail="' app/ som ga NULL treff. i18n: locale (nb/en) sendes av klienten ved request-link, styrer e-postmalen (ekte engelsk mal lagt inn i app/email.py, ikke bare rørlegging) OG settes som en HELT NY brukers preferred_locale — en eksisterende bruker som logger inn på et annet språk får IKKE sin lagrede preferanse overskrevet. Fant og fikset underveis: tournament.start_date har ligget i skjemaet siden migrasjon 001, men var ALDRI koblet til TournamentCreate/Tournament-modellene — funnet som en naturlig bivirkning av å legge til end_date. Verifisert grundig mot fersk teecup_scratch (001→006, test_isolation.sql fortsatt 12/12): feilkode-form bekreftet på tvers av 5 filer (NOT_FOUND/LIMIT_REACHED tournaments.py, DUPLICATE roster, NOT_AUTHENTICATED uten cookie, NOT_ROSTERED_ON_TEAM matches.py), tee_time-beregning bekreftet (10 min intervall → 08:00/08:10/08:20, override vinner, scheduled_at=null gir tee_time:null ikke feil), full i18n-runde bekreftet (ny bruker locale:"en"preferred_locale:"en"; påfølgende request-link for samme bruker med locale:"nb" skiftet e-postmalen men IKKE den lagrede preferansen, bekreftet via /auth/me). Kjørt mot ekte teecup_db 2026-07-17 (bruker bekreftet eksplisitt i samme økt): migrasjonen kjørte rent, test_isolation.sql fortsatt 12/12 (ruller alltid tilbake, ingen domenerader berørt). Mindre driftslærdom, funnet OG rettet samme runde: et .env-filter (grep -v -i 'pass|secret|key') jeg brukte for å lese ikke-sensitive nøkler fanget ikke opp TEECUP_DATABASE_URL, som bar teecup_app- passordet innebygd i selve URL-en (i tillegg til at det SAMME passordet allerede lå rent i TEECUP_APP_PASSWORD — duplisert, ikke bare skjult) — passordet ble dermed synlig i et verktøyresultat. Flagget til brukeren umiddelbart (samme mønster som SMTP-passord-hendelsen over).

  • .env/tilkobling ryddet opp (2026-07-17), samme runde: roten til hendelsen over var at TEECUP_DATABASE_URL var én sammensatt DSN-streng med passordet URL-kodet inni — usynlig for navnebaserte secret-filtre. Erstattet med fem separate, rent navngitte felt: TEECUP_DB_HOST/TEECUP_DB_PORT/TEECUP_DB_NAME/TEECUP_DB_USER/ TEECUP_DB_PASS (sistnevnte omdøpt fra TEECUP_APP_PASSWORD, kun nøkkelnavnet — verdien aldri lest eller skrevet av meg). app/config.py og app/db.py bygger nå asyncpg-poolen fra disse fem separate feltene (host=/port=/user=/password=/database=) i stedet for én DSN — fjerner også URL-prosentkoding-problemet fra SMTP-passord-hendelsen sin klasse av feil. docker-compose.yml sin environment:-liste oppdatert tilsvarende. Krevde et fullt image-rebuild, ikke bare --force-recreate: Dockerfile sin COPY app/ app/ bakes inn i imaget ved build-tid (ingen bind-mount i prod, i motsetning til scratch-verifiseringens engangscontainere) — en ren --force-recreate gjenbrukte det GAMLE imaget og krasjet umiddelbart på den nå fjernede TEECUP_DATABASE_URL. Rettet med docker compose up -d --build --force-recreate. Verifisert ende-til-ende mot den live stacken: containeren boot-et rent (Application startup complete i loggen, som krever en vellykket init_pool() — asyncpg ville kastet og forhindret akkurat den logglinjen ved feil tilkoblingsparametre), /auth/me over ekte https ga et rent 401 NOT_AUTHENTICATED (ikke 500/502), teeoff.no upåvirket (200 gjennom hele omstarten, kun teecup_api restartet — ikke delt Caddy-instans, ikke samme risikoklasse som Caddy-hendelsen fra containeriseringsrunden).

  • Frontend startet, innlogging LIVE (2026-07-17, ADR-016): første frontend-skjerm i prosjektet. Designet i V0 (Next.js + Tailwind + shadcn/ui), hentet inn som frontend/ — merkevare-form/farge fra Teeoff-logoen, IKKE navn/logo (egne, separate produkter, se ADR-009). Kvalitetsrunde før bruk: V0s fargetokens var OKLCH-TILNÆRMINGER, ikke eksakte — regnet ut presise verdier fra #8bc24a/#ff5722 og rettet alle 6 forekomster i globals.css. Fjernet @vercel/analytics (unødvendig på egen-hostet infra), fjernet typescript: { ignoreBuildErrors: true } (ekte typesjekk kjører nå), fjernet dødt pnpm.overrides-felt. frontend/.gitignore manglet .pnpm-store/ — årsaken til at brukerens VSCode viste 10 000 "ukjente" filer ved første commit-forsøk; rettet. Kablet mot ekte API: next.config.mjs sin rewrites() proxyer /auth/*//orgs/*//health server-side til teecup_api — same-origin, ingen CORS, cookie uendret (full begrunnelse i ADR-016). Login-skjermet sender ekte POST /auth/request-link; ny /verify-side mottar ?token=... fra e-postlenken (auto-verifiserer) eller viser et manuelt "lim inn koden"-felt. app/email.py fikk en ny PUBLIC_BASE_URL- innstilling og sender nå en EKTE klikkbar lenke (koden beholdes som fallback). Reell fallgruve funnet og fikset ved containerisering: Next.js sin rewrites() løses ved BUILD-tid for output: "standalone", ikke ved container-oppstart — en runtime -e TEECUP_API_ORIGIN=... ble stille ignorert (proxy-kall feilet med ECONNREFUSED mot localhost:8000). Løst med en Docker build-time ARG TEECUP_API_ORIGIN i frontend/Dockerfile, satt via docker-compose.yml sin build.args. Rullet ut live: ny teecup_frontend-tjeneste i docker-compose.yml. Caddy (teecup.teeoff.no, i det SEPARATE teeoff-repoet, /opt/teeoff/deploy/Caddyfile) endret fra å peke direkte på teecup_api til å peke på teecup_frontend — samme stale-inode-oppførsel som containeriseringsrunden (graceful reload plukket IKKE opp endringen, / fortsatte å gi teecup_api sin egen 404 i stedet for innloggingssiden til reload faktisk skjedde). Løst likt: full docker restart teeoff_caddy, brukeren bekreftet eksplisitt på forhånd. teeoff.no upåvirket gjennom hele omstarten. Verifisert med FAKTISK e-postlevering: ekte magic-link sendt til brukerens egen adresse over https://teecup.teeoff.no, ekte e-post mottatt med en ekte klikkbar lenke, åpnet i nettleser, landet på en fungerende /verify-side, sesjon opprettet — brukeren bekreftet innlogget status. Første gang en hel bruker-vendt flyt er bevist ende-til-ende i produksjon, ikke bare API-et isolert. Merk for neste økt: deploy/Caddyfile-endringen ligger uncommitted i det SEPARATE /opt/teeoff-repoet, ikke i teecup-repoet — lett å glemme siden denne økten ellers kun har jobbet i /opt/teecup.

  • Dashboard-skjerm LIVE (2026-07-18): andre V0-skjerm — organisasjon- bytter/-opprettelse + turneringsliste (/dashboard), samme mønster som login-runden. Reell integrasjonsfelle unngått: V0s eksport denne gangen var en FULL re-eksport av hele prosjektet (inkl. login-form.tsx, next.config.mjs, package.json), ikke bare de nye filene — en naiv utpakking ville stille reversert rewrites()-proxyen, output: "standalone", den ekte fetch-kablingen i login-skjemaet, og alle V0-uavhengige opprydninger fra forrige runde. Løst ved å pakke ut til et scratch-område FØRST, diffe mot live-treet fil for fil, og kun ta inn det som faktisk var nytt (dashboard.tsx, tournament-card.tsx, tournament-status-badge.tsx, wordmark.tsx, badge.tsx, dropdown-menu.tsx, app/dashboard/page.tsx) — next.config.mjs, package.json, globals.css, Docker-filene ble bevisst IKKE overskrevet. login-form.tsx fikk en kirurgisk patch (kun Wordmark flyttet til egen fil, som V0 selv hadde gjort — all egen fetch-/feilhåndteringslogikk urørt). dashboard.tsx sitt datalag skrevet om fra bunnen (V0 leverte kun mock useState): henter /auth/me for organisasjonsmedlemskap, /orgs/{id}/tournaments per valgt org, POST /orgs/POST /orgs/{id}/tournaments for opprettelse, POST /auth/logout for utlogging — presentasjonskomponentene (kort, bytter, tomme tilstander) beholdt uendret fra V0. /verify-siden oppdatert til å sende brukeren videre til /dashboard etter vellykket innlogging (fantes ingen dit å gå før nå). Verifisert: ekte typesjekket build, redeploy av kun teecup_frontend (ingen Caddy-endring nødvendig denne gangen — ADR-016s mønster holder), teecup.teeoff.no/dashboard → 200, teeoff.no upåvirket. Skrive-flyten bekreftet med EKTE data samme dag (brukeren testet selv, ikke meg): organisasjon "Tjøme Gents" og turnering "De Gamle er Eldst" opprettet via UI-et mot den ekte teecup_db — statusmerket viste riktig "Utkast", "Ingen datoer satt" håndtert korrekt (ingen krasj på manglende dato), ett-org-visningen viste riktig uten unødvendig bytter-UI. Første gang en HEL skrive-flyt (ikke bare lesing) er bevist ende-til-ende fra frontend mot ekte produksjonsdata.

  • Lag/roster-skjerm LIVE (2026-07-18): tredje V0-skjerm (/tournaments/[id]), samme re-eksport-mønster som dashboard-runden — diffet mot live-treet, tok kun inn tournament-detail.tsx og et Link-basert tournament-card.tsx (navigasjon fra dashbordet). URL-design bevisst avvikende fra V0s forslag: V0s genererte side leste aldri params.id og hadde ingen organization_id i det hele tatt — holdt derfor V0s flate /tournaments/[id]-struktur (i stedet for en nøstet /orgs/[orgId]/tournaments/[id], som ville krevd manuell ombygging ved HVER fremtidig V0-reeksport) og la org+name til som søkeparametre i tournament-card.tsx sin lenke — API-et krever organization_id på alle team-/roster-kall (RLS). Reelt hull funnet FØR integrering, ikke etter: V0-skjermen bygger inn "fjern spiller"/"gjør til kaptein"-handlinger, men backend hadde KUN GET/POST på team_roster — ingen DELETE eller PATCH. Spurte bruker eksplisitt (samme mønster som andre scope-avklaringer denne økten) — svar: bygg de to endepunktene nå. Lagt til i app/routers/tournaments.py: PATCH .../roster/{roster_id} (bevisst enkel — setter/fjerner is_captain på NØYAKTIG denne raden, håndhever IKKE "kun én kaptein per lag", siden kaptein fortsatt bare er et merke, ikke en egen autorisasjonsrolle) og DELETE .../roster/{roster_id} (204, idempotent NOT_FOUND ved dobbel sletting — ikke krasj). tournament-detail.tsx sitt datalag skrevet om fra V0s mock: henter lag + roster (roster-radene bærer allerede display_name/ handicap_index_snapshot fra APIet, så V0s separate poolById-oppslag ble fjernet som overflødig) og organisasjonens spillerpool (GET /orgs/{id}/players, brukt til type-ahead ved "legg til spiller"). Alle fem mutasjonene (opprett lag, legg til eksisterende spiller, opprett ny spiller inline + rostre, endre kaptein, fjern fra roster) kablet mot ekte endepunkter — presentasjonskomponentene (kort, type-ahead, fargevelger, bekreft-fjerning) beholdt uendret fra V0. Verifisert: de to nye endepunktene testet mot en fersk teecup_scratch (PATCH setter kaptein + riktig NOT_FOUND på ugyldig id, DELETE gir 204 + idempotent NOT_FOUND ved gjentak, test_isolation.sql fortsatt 12/12), ekte typesjekket frontend-build (5 ruter), redeploy av BEGGE containere (backend-endepunktene er nye), teecup.teeoff.no/dashboard → 200, teeoff.no upåvirket. Selve skrive-flyten på /tournaments/[id] (opprett lag/roster) ikke testet med ekte data i denne runden — venter på brukeren, samme mønster som dashboard-rundens skrive-test.

  • ADR-017 + migrasjon 007 (2026-07-18): brukeren reiste selvregistrering rett etter at roster-skrive-flyten var bekreftet. Full ADR skrevet (5 beslutninger: offentlig påmelding uten innlogging, e-post som sammenkoblingsnøkkel mot forhåndsopprettede spillere, egen tournament_registration-tabell atskilt fra team_roster med konfigurerbar godkjenning/kapasitet/venteliste, utvidet spillerprofil + obligatorisk samtykke, og public_tournament_org()). Migrasjon 007_registration_and_player_fields.sql skrevet og scratch-verifisert (001→007 kjører rent, test_isolation.sql fortsatt 12/12). Reelt arkitekturproblem løst underveis, ikke bare skjema: et offentlig (uautentisert) påmeldingskall kjenner en turnering-id, men ingen org-kontekst — og uten app.current_org slipper RLS ingen rader gjennom, heller ikke oppslaget for å FINNE riktig org. Løst med en snever SECURITY DEFINER-funksjon (public_tournament_org) som KUN eksponerer uuid→uuid-koblingen. Verifisert presist, ikke bare "kjørte uten feil": kalt funksjonen som teecup_app-rollen med ingen app.current_org satt — ga korrekt org-id for en kjent turnering, NULL (ikke feil) for en ukjent — OG et RÅTT SELECTtournament på SAMME tilkobling/rolle ga fortsatt 0 rader, som beviser RLS ikke er brutt generelt, bare dette ene smale unntaket eksisterer. Kjørt mot ekte teecup_db 2026-07-18 (bruker bekreftet eksplisitt i samme økt): migrasjonen kjørte rent, test_isolation.sql fortsatt 12/12.

  • Registrerings-API LIVE (2026-07-18), samme dag: ny app/routers/registration.pyGET /public/tournaments/{id} og POST /public/tournaments/{id}/register, begge UTEN get_current_user eller get_authorized_org (helt uautentisert, egen /public-prefiks, bevisst atskilt fra /orgs/... i koden). Bruker public_tournament_org() (migrasjon 007) til å slå opp org-kontekst FØR RLS kan håndheve noe. app/routers/players.py utvidet med alle sju nye ADR-017-feltene (mobile/email/birth_date/nickname/country/club/ club_member_number). frontend/next.config.mjs sin rewrites() utvidet med /public/* (ADR-016s konsekvens: enhver ny API-prefiks MÅ inn her). Ny brukers-oppdaget notat fanget FØR bygging, ikke etter: brukeren krevde eksplisitt at synlighet (offentlig/kun org/kun turnering- deltakere) må være et VALG for fremtidige landingssider — notert grundig i FEATURE_BACKLOG.md (koblet til samme åpne spørsmål for "Banter Board"-feeden) FØR dette registrerings-API-et ble bygget, akkurat for at det ikke skal gå i glemmeboken til landingsside-runden. Verifisert grundig mot fersk teecup_scratch (001→007, test_isolation.sql 12/12): hele registreringsløpet testet reelt — samtykke-avvisning (400), duplikat-avvisning (409 DUPLICATE), e-post-matching mot en organisator-forhåndsopprettet spiller (BEKREFTET: ingen duplikatrad, mobil fylt inn via COALESCE, display_name IKKE overskrevet), kapasitet+waitlist-policy (→ waitlisted), kapasitet+closed-policy (→ 409 LIMIT_REACHED), registration_ requires_approval (→ pending), utløpt frist (→ 409 REGISTRATION_CLOSED), og confirmed_count i GET-responsen talt riktig (kun confirmed+pending, ikke waitlisted/avviste). Rullet ut live: begge containere redeployet, teecup.teeoff.no/ dashboard og /health fortsatt 200, teeoff.no upåvirket, det offentlige endepunktet bekreftet nåbart over ekte https (ukjent turnering-id ga korrekt 404/NOT_FOUND, ikke-destruktiv sjekk — selve påmeldingsflyten med ekte data ikke testet mot prod i denne runden). E-post-basert kontosammenkobling LIVE, samme dag: ny migrasjon 008_link_player_by_email.sqllink_player_by_email(user_id, email), samme SECURITY DEFINER-mønster som public_tournament_org() (007), denne gangen for en tverr-org UPDATE i stedet for et lese-oppslag. verify_magic_link (app/routers/auth.py) kaller den på HVER innlogging (idempotent — funksjonens WHERE user_id IS NULL gjør gjentatte kall til en no-op), ikke bare ved førstegangsopprettelse. Verifisert presist: to separate org-er, hver med sin egen organisator-opprettede "Kari"-rad (samme e-post, ulik store/små bokstaver for å teste case-insensitivitet også) — ved Karis FØRSTE innlogging ble BEGGE radene koblet til kontoen hennes, i to org-er hun aldri har vært medlem av. Eksplisitt bekreftet: organization_membership har NULL rader for henne etterpå — ren identitetskobling, ingen privilegie-eskalering (å ha player.user_id satt gir ingen ny tilgang gjennom get_authorized_org, som fortsatt krever ekte org-medlemskap uavhengig av dette). Andre innlogging idempotent, ingen feil. test_isolation.sql fortsatt 12/12. Kjørt mot ekte teecup_db 2026-07-18, bruker bekreftet eksplisitt, backend redeployet, live sjekker OK, teeoff.no upåvirket. ADR-017s backend er dermed komplett (registrering + kontokobling). Gjenstående: frontend-påmeldingsskjema og landingssider — egen, senere ADR-runde (se FEATURE_BACKLOG.md).

  • ADR-018 + migrasjon 009: landingssider, backend LIVE (2026-07-18), samme dag: trenivås synlighet (tournament.visibility: public/org/participants, default org — trygg standard) + organization.public_profile. Reell presisering funnet underveis, ikke antatt på forhånd: RLS (org_isolation) beskytter kun TENANT-grenser (org A ser aldri org B), IKKE innholds-synlighet innenfor riktig org-kontekst — det eksisterende GET /public/tournaments/{id} (ADR-017) leste allerede fullt innhold uten synlighetssjekk, fordi RLS er fornøyd så snart org-konteksten er satt, uansett hvem som spør. visibility håndheves derfor eksplisitt i app/routers/registration.py, på BÅDE lesing og registrering (ADR-018 Beslutning D: kan du ikke se turneringen, kan du heller ikke melde deg på den — bekreftet med bruker FØR bygging). Ny get_current_user_optional i app/auth.py (som get_current_user, men returnerer None i stedet for 401 — offentlige endepunkter skal fungere for anonyme lesere også). Ny "deltaker"-autorisasjonsvei: en innlogget bruker med player.user_id koblet (ADR-017) OG en tournament_registration- eller team_roster-rad for NØYAKTIG den turneringen får se participants-synlige turneringer, uansett org-medlemskap. Ekte migrasjonsfeil funnet OG rettet UNDER scratch-verifisering, ikke antatt riktig: migrasjonen feilet først ("column slug already exists") — organization.slug har ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig", allerede med en plain UNIQUE), noe jeg hadde oversett fullstendig og forsøkt å legge til på nytt. Rettet ved å fjerne den doble ADD COLUMN + den overflødige partielle unik-indeksen (001 sin plain UNIQUE dekker "unik når satt" allerede, siden Postgres behandler NULL som distinkt), beholde kun de nye CHECK-constraintene. Kjørte rent på ny etter fiksen. Lærdom: grep alltid eksisterende skjema for feltnavn FØR en ny migrasjon skrives, ikke bare stol på hukommelsen om hva som "sikkert" ikke finnes fra før. Ny tabell tournament_sponsor (navn+lenke aktivt, logo_key inert til MinIO-runden — samme med tournament.hero_image_key). Tredje SECURITY DEFINER-bro i prosjektet: public_org_by_slug() (etter public_tournament_org 007, link_player_by_email 008) — returnerer NULL for BÅDE "finnes ikke" og "finnes, men er privat", samme anti-enumerering som magic-link. Fylte også et implisitt hull oppdaget underveis: ADR-en beskrev hvordan synlighet skulle håndheves, men ingen tidligere runde hadde bygget noen vei for organisator til faktisk å SETTE disse feltene. Lagt til: PATCH /orgs/{id}/tournaments/{id} (visibility/description/ registrerings-innstillinger, ekte PATCH-semantikk via Pydantic sin exclude_unset — et utelatt felt nullstilles IKKE), PATCH /orgs/{id} (slug/public_profile), full sponsor-CRUD. _fetch_sessions() trukket ut som delt hjelpefunksjon i tournaments.py (delt mellom den innloggede og den nye offentlige GET /public/tournaments/{id}/sessions — blind draw-hemmelighold, ADR-013, arves automatisk, ikke reimplementert). Verifisert grundig mot fersk teecup_scratch (001→009, test_isolation.sql 12/12): hele synlighetsmatrisen testet med ekte HTTP-kall — anonym avvist på org-synlig turnering (både lesing OG registrering), PATCH til public + beskrivelse + sponsor fungerte, anonym lesing fungerte deretter, participants-synlighet bekreftet reell chicken-and-egg-konsekvens av Beslutning D (ingen kan selv- registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — korrekt, ikke en bug), en organisator-rostret spiller som logget inn fikk tilgang, en tilfeldig innlogget FREMMED (ikke deltaker) ble fortsatt avvist, org-landingsside viste KUN public-synlige turneringer, CHECK-constraint (public_profile krever slug) avvist korrekt, ugyldig slug-format avvist av Pydantic, sponsor-sletting fungerte. Kjørt mot ekte teecup_db 2026-07-18, bruker bekreftet eksplisitt, backend redeployet, live sjekker OK, teeoff.no upåvirket. Ingen frontend-endring nødvendig for selve API-tilgangen (det brede /public/:path*-mønsteret fra ADR-016 dekker allerede /public/orgs/*). Gjenstår: selve landingsside-SKJERMENE i frontend (V0), og MinIO/bildeopplasting — bevisst utsatt, egen runde.

  • Offentlig turnering-landingsside LIVE (2026-07-18), samme dag: fjerde V0-skjerm (components/public-tournament.tsx, ny rute /t/[id]) — banner (bevisst permanent farge-/gradient-utseende, ikke en "bilde kommer"-plassholder), presenterende tekst, status (datoer + "X av Y plasser"), program, sponsorer (navn+lenke), og et påmeldingsskjema med navn+e-post synlig først og resten bak en "flere detaljer"-utvidelse — tre distinkte bekreftelsestilstander (bekreftet/venteliste/godkjenning venter), ikke én generisk "takk". Ingen ny reeksport-kollisjon denne gangen — kun étt genuint nytt filnavn (public-tournament.tsx), resten var kjent V0-revert av allerede-tilpassede filer, samme mønster som før. V0 opprettet komponenten, men ingen rute — la selv til app/t/[id]/ page.tsx (bevisst en FLAT /t/[id]-sti, ikke nøstet under /orgs/... som den innloggede turnering-detalj-siden, siden det offentlige API-et kun trenger turnering-id, ikke org-id). Datalaget skrevet om fra V0s mock: ekte fetch mot GET /public/tournaments/{id} + /sessions, ekte POST .../register. Håndterer 403 (NOT_VISIBLE, ADR-018) og 404 med en egen tilgang-avvist- tilstand V0 ikke hadde bedt om (fantes ikke i prompten, men trengs for at siden faktisk skal fungere for org/participants-synlige turneringer). Mappet status:"waitlisted" fra API-et til komponentens "waitlist", og skjemaets norske kjønnsvalg ("kvinne"/"mann"/"annet") til API-ets ^[mfx]$-mønster — to reelle navnekollisjoner mellom V0s UI-språk og API-kontrakten, ikke bare et rett-frem felt-for-felt-uttrekk. Reell driftsfeil funnet OG rettet under scratch-test, ikke i produksjon: pnpm install (uten --ignore-scripts) i dev-server- testoppsettet feilet stille med tom logg og exit 1 — corepack hadde hentet en NY pnpm-versjon (11.13.1 → 11.14.0) siden sist, som gjør "ignored builds"-varselet til en hard feil i stedet for bare en advarsel. Rettet ved å bruke samme --ignore-scripts-flagg som frontend/Dockerfile allerede bruker (upåvirket av denne — bekreftet ved at selve prod-buildet fortsatt gikk rent). Lærdom: Dockerfile sin pinning av kode er ikke det samme som å pinne verktøyene rundt (corepack henter alltid siste pnpm) — verdt å huske neste gang et scratch-dev-server-oppsett plutselig feiler uten åpenbar grunn. Verifisert grundig mot fersk teecup_scratch + en ekte kjørende frontend-dev-server (ikke bare next build): ekte turnering med beskrivelse, kapasitet, sponsor og økt opprettet via API-et, hentet gjennom frontend-proxyen (ikke direkte mot backend) og bekreftet byte-for-byte riktig — inkludert en ekte POST-registrering som økte confirmed_count fra 0 til 1. Rullet ut live, kun teecup_frontend (ingen backend-endring denne runden), teeoff.no upåvirket. Gjenstår: org-landingssiden (egen V0-prompt), Open Graph-metadata for deling, MinIO/bilder.

  • Offentlig klubb-landingsside LIVE (2026-07-18), samme dag: femte og siste V0-skjerm i ADR-018 (components/public-club.tsx, ny rute /clubs/[slug]). Samme banner-språk som turnering-siden, liste over klubbens turneringer (gjenbrukte eksisterende TournamentCard), egen tom- tilstand. Reell delt-komponent-kollisjon løst, ikke duplisert bort: TournamentCard var bygget for KUN den innloggede konteksten (krevde orgId, lenket til /tournaments/{id}?org=...). I stedet for en egen kopi av kortet for den offentlige siden, gjort orgId valgfri — satt (dashbordet) gir innlogget lenke, utelatt (klubbsiden) gir /t/{id} i stedet. Samme kort, to kontekster, ingen duplisering. Bekreftet dashbordets egen bruk uendret/upåvirket etterpå. V0 opprettet denne gangen selv en rute (app/clubs/[id]/page.tsx, uten noen props sendt inn i det hele tatt) — men navnga parameteren [id] selv om den faktisk er en SLUG (public_org_by_slug(), ADR-018). Skrev selv en ny, riktig app/clubs/[slug]/page.tsx i stedet for å bruke V0s (feilnavngitte og prop-løse) versjon. Verifisert mot fersk teecup_scratch + ekte kjørende frontend-dev- server: org med slug+public_profile, én turnering satt public, én latt stå på default org — klubbsiden viste GJENNOM frontend-proxyen kun den ene offentlige turneringen, den org-private var korrekt usynlig (samme filtermønster som API-et selv, bekreftet fra klientsiden også). Ukjent slug ga korrekt 404. Rullet ut live, kun teecup_frontend, teeoff.no upåvirket. ADR-018s planlagte skjermer er dermed komplette. Gjenstår: Open Graph-metadata for deling, MinIO/bilder — begge bevisst egne, senere runder.

  • Open Graph-metadata LIVE (2026-07-18), samme dag — ADR-018 dermed helt ferdig: generateMetadata() lagt til på /t/[id] og /clubs/[slug] (ekte tittel/beskrivelse fra API-et, og:site_name, trygg fallback-tittel ved ukjent id/slug — feil her skal ALDRI hindre selve siden i å laste). Reell driftsfeil funnet FØR den nådde produksjon, ikke etter: generateMetadata() kjører server-side ved REQUEST-tid, ikke i nettleseren — går derfor IKKE gjennom next.config.mjs sin rewrites() (som kun gjelder nettleser-trafikk inn til Next.js- serveren). Måtte derfor lese TEECUP_API_ORIGIN direkte, men den variabelen fantes KUN i Dockerfile sitt builder-steg — ENV satt i ett FROM-steg arves ikke til et senere. Rettet ved å sette samme ARG/ENV på nytt i runner-steget også. Verifisert presist at fiksen faktisk virker, ikke bare at bygget gikk gjennom: bygget det EKTE produksjonsimaget (ikke dev-server) pekt mot en scratch-backend med en ekte offentlig turnering+org, hentet den faktiske server-rendrede HTML-en og bekreftet ekte <title>/og:title/ og:description — ikke bare at TypeScript kompilerte. Ukjent turnering-id ga korrekt trygg fallback-tittel. Bevisst utenfor omfang: og:image — ingen ekte bilde finnes ennå (MinIO-runden). La til metadataBase i app/layout.tsx nå likevel, som forarbeid slik at et fremtidig relativt bilde-URL løses riktig uten en egen fiks da. Rullet ut live, kun teecup_frontend, teeoff.no upåvirket.

  • MinIO-runden LIVE (2026-07-18), samme dag — ADR-018 dermed HELT ferdig, ingenting utsatt igjen: ny teecup-minio-tjeneste (persistent volum, genererte credentials i .env), backend-endepunkter for ekte bildeopplasting til tournament.hero_image_key/tournament_sponsor. logo_key (som lå inerte siden migrasjon 009), offentlige API-svar bygger nå fulle URL-er, og:image koblet på i /t/[id] sin generateMetadata. Sent, men viktig presisert krav underveis: brukeren avbrøt en verktøyskall midtveis for å presisere at ALLE bilder skal konverteres til AVIF for plassbesparelse. Snudde HELE opplastingsarkitekturen på dette: droppet den opprinnelige planen om presignerte URL-er (nettleser laster opp DIREKTE til MinIO) til fordel for ekte multipart-opplasting GJENNOM API-et, som konverterer til AVIF (Pillow + pillow-avif-plugin) FØR lagring. Dette forenklet arkitekturen betydelig: kun ÉN MinIO-klient trengs nå (før: to separate klient-oppsett for henholdsvis internt admin-arbeid og offentlig presignering), og Caddy sin nye rute trenger ikke lenger bevare Host-headeren presist (relevant kun for SigV4- signaturverifisering av presignerte URL-er, ikke for anonym public-read). To reelle feil funnet UNDER scratch-verifisering, aldri i produksjon: (1) pillow-avif-plugin krever ingen ekstra systempakker i python:3.12-slim -- verifisert med et frittstående encode/decode- rundtrip FØR det ble tatt i bruk i selve API-et. (2) MinIO validerer Host-headeren STRENGT og avviser understrek som ugyldig vertsnavn -- et tjenestenavn med understrek (teecup_minio, konsistent med teecup_api/teecup_frontend) feilet umiddelbart ved oppstart ("Invalid Request (invalid hostname)"). Bekreftet presist ved å teste samme oppsett med bindestrek i stedet (teecup-minio) -- fungerte umiddelbart. Alle referanser rettet til bindestrek FØR noe ble forsøkt mot ekte infrastruktur. Caddy: ny /teecup-media/*-rute på det EKSISTERENDE teecup.teeoff.no-blokket (IKKE et nytt subdomene -- en tidlig sjekk avdekket at media.teecup.teeoff.no fantes som en wildcard DNS-post, men pekte til en IPv6-adresse denne serveren ikke har i det hele tatt; path-prefiks på et allerede fungerende domene unngikk hele den DNS- avhengigheten). Samme stale-inode-oppførsel som alle tidligere Caddy- runder -- full docker restart teeoff_caddy, brukeren bekreftet eksplisitt. teeoff.no upåvirket. Verifisert grundig, i flere lag: frittstående AVIF-encode/decode- test, full scratch-kjede (fersk Postgres + en ISOLERT scratch-MinIO- container) med et ekte opplastet bilde -- bekreftet konvertert til gyldig AVIF (800×400, 406 bytes for et helfarget testbilde), bekreftet lagret med riktig nøkkel, bekreftet lesbart ANONYMT direkte mot MinIO (uten Caddy, isolerer bucket-policyen), bekreftet hero_image_url/ logo_url bygget riktig i det offentlige API-svaret. Alle tre valideringsveier testet (ugyldig content-type, korrupt bildeinnhold, for stor fil >8MB) -- alle ga korrekt VALIDATION_FAILED. Etter Caddy-omstart: et ekte anonymt kall mot /teecup-media/... på produksjonsdomenet ga en ekte MinIO S3-XML-feilrespons (NoSuchKey for en scratch-nøkkel som naturligvis ikke finnes i prod) -- beviser ruten treffer MinIO selv, ikke frontend sin egen 404-side. test_isolation.sql fortsatt 12/12 gjennom hele runden. Bevisst utenfor omfang: ingen faktisk opplasting av et EKTE bilde til en EKTE, live turnering i denne runden (ville krevd å skrive test-data i brukerens ekte konto uten å bli spurt) -- tilbudt, ikke utført. Selve dra-og-slipp-opplastingsskjermen i frontend (V0) er fortsatt ikke bygget, som avtalt fra starten av runden.

  • Program-skjerm (økter/tidsplan), bygget og SCRATCH-verifisert, IKKE live ennå (2026-07-18): sjette V0-skjerm, første i "bygg i rekkefølgen ting brukes"-serien (etter lag/roster: program → blind draw → scorekort → leaderboard). components/tournament-program.tsx, ny rute /tournaments/[id]/program. Tidslinje over økter + opprett-skjema (format, hullomfang, scoringsmodus, poeng, klokkeslett+intervall, starthull, kollapsbar "avansert"-seksjon med ADR-014s fire brytere). Reelt blokkerende hull funnet FØR integrering: SessionCreate. course_id er påkrevd, men INGEN endepunkt kunne noensinne produsere en — ADR-004s teeoff-integrasjon er fortsatt bare vedtatt (ingen utgående HTTP-klient finnes). Spurte bruker eksplisitt — svar: bygg enkel course-CRUD nå. Ny app/routers/courses.py (GET/POST /orgs/{id}/ courses, kun source='custom'). Program-skjemaet fikk et type-ahead-felt for bane (samme mønster som spiller-type-ahead i roster-skjermen). Reell korrekthetsfeil rettet FØR integrering: V0-promptet mitt ba om ett generisk "Scramble"-valg, men skjemaets CHECK-constraint og handicap_engine.py sin Format-enum krever scramble_2/scramble_4 som distinkte verdier -- ren "scramble" avvises med 400. Rettet i frontend-mappingen til to segment-knapper. allowance_override-JSON-formen verifisert eksakt mot app/handicap.py sin parse_allowance_config/_strategy_from_json ({type: "combined"|"per_player", percentage: 0..1}, ikke en flat prosent) -- frontend velger riktig type ut fra om formatet er side-enhet eller spiller-enhet, konverterer 0100-skjemafelt til 01. Scratch-infrastruktur denne runden, bevisst forskjellig fra tidligere: brukte en ISOLERT teecup_app_scratch-rolle (GRANT teecup_app TO teecup_app_scratch) i stedet for den ekte teecup_app-rollen -- den er nå cluster-global og produksjonskritisk (delt Postgres-instans med teecup_db), så et eldre plandokuments "drop teecup_app-rolle"- opprydning (skrevet FØR go-live) er utdatert og ble bevisst IKKE fulgt. Egen isolert scratch-MinIO-container også (app-oppstart krever en nåbar MinIO for ensure_bucket()). Verifisert: courses opprettet+listet, kryss-org-isolasjon bekreftet, økt med klokkeslett, økt med scramble_4+full allowance_override- rundtur, gammel "scramble"-verdi avvist (400), test_isolation.sql 12/12, ekte typesjekket PRODUKSJONSBUILD (samme Dockerfile som faktisk deployes) kjørt og bekreftet. Diffet V0-eksporten mot live-treet FØR noe ble tatt inn (samme mønster som alle tidligere runder): kun tre reelt nye filer, resten forventede full-reverts, ikke rørt. Fjernet V0s dev-only forhåndsvisnings-toggle; lagt til fanerad ("Lag og spillere" / "Program") i BEGGE skjermene siden V0 ikke visste om den andre når den ble generert i egen prompt. Rullet ut live 2026-07-18, bruker bekreftet eksplisitt: docker compose up -d --build teecup_api teecup_frontend (kun disse to, teecup- minio urørt). Verifisert: begge containere boot-et rent (Application startup complete, Next.js Ready), teecup.teeoff.no/health og /dashboard → 200, teeoff.no upåvirket (200).

  • ADR-004 (teeoff-banedata) kartlagt, IKKE bygget (2026-07-18): brukeren påpekte rett etter program-skjerm-rullingen at ADR-004s teeoff- integrasjon fortsatt bare er vedtatt, ikke bygget (kun source='custom' finnes). Kartlagt /opt/teeoff/backend/main.py (annet repo, kun lest): GET /api/facilities/{slug} (offentlig, INGEN auth/API-nøkkel) returnerer allerede alt teecup trenger — courses[].holes[] (par, hcp_index = stroke index), courses[].tees[] (name, cr_men/slope_men, cr_women/slope_women — kjønnsdelt WHS-rating). CORS-lista på teeoff- siden ekskluderer teecups origin, men er IRRELEVANT for et server-til- server-kall (kun nettleser-fetch rammes av CORS). Ingen dokumentasjon (README/OpenAPI) finnes for dette API-et i teeoff-repoet — kartleggingen over er basert på å lese koden direkte. Stabil identifikator: facilities. slug (f.eks. borregaard-golfklubb) — selve banen har kun en intern serial-id, ingen egen slug, så course.external_course_ref må bære facility-slug + bane-id sammen. Oppdatering samme dag: brukeren ba meg ta fatt på dette NÅ (bevisst sidesprang fra "bygg i rekkefølgen ting brukes"-planen — blind draw- skjermen er fortsatt neste steg i den planen ETTERPÅ, ikke droppet). Design besluttet og skrevet som ADR-019 (se ARCHITECTURE_DECISIONS.md): import (kopi) ved organisators eksplisitte valg, ikke live oppslag ved hver bruk — fryser data på importtidspunktet, samme reproduserbarhets- prinsipp som handicap-snapshotten (ADR-007). Server-til-server-kall (teecup_apihttp://teeoff_api:8000, internt Docker-nettverk, ingen auth trengs). Ny migrasjon 010 (unik external_course_ref per org, hindrer dupliserte importer). Se ADR-019 for alle fem delbeslutningene. Bygget, scratch-verifisert MOT EKTE teeoff_api (ikke en simulert respons — ekte HTTP-kall til den kjørende produksjonscontaineren, kun lesing): søkte opp "Borregaard" i teeoff sine 174 publiserte anlegg, hentet Borregaard Golfklubb sin hovedbane (18 hull, 4 tee-farger × kjønn), importerte den til en scratch-org — alle 18 hull med riktig par/stroke-index (1-18, unik), alle 8 tee/tee_rating-rader med riktig full_18 course/slope-rating verifisert direkte i databasen, opprettet deretter en ekte økt med den importerte banen som course_id (beviser hele veien til handicap-motoren fungerer, ikke bare selve importen). Reimport av samme bane korrekt avvist (409 DUPLICATE, migrasjon 010 sin indeks). Kryss-org-isolasjon bekreftet, ukjent teeoff-slug ga korrekt 404, test_isolation.sql 12/12. Ekte typesjekket produksjonsbuild av frontend-utvidelsen (bane-søk i program-skjemaet: søk anlegg → velg bane → importer, samme UI-mønster som spiller-/bane-type-ahead ellers i appen). Rullet ut live 2026-07-18, bruker bekreftet eksplisitt: migrasjon 010 kjørt mot ekte teecup_db (kun én ny partiell unik-indeks, ingen eksisterende rader rørt), begge containere bygget+redeployet, live sjekker OK, teeoff.no upåvirket. "Bygg i rekkefølgen ting brukes"- planen gjenopptas nå — blind draw-skjermen er neste steg. Reell produksjonsbug funnet OG fikset samme dag, rapportert av bruker som faktisk brukte funksjonen: brukeren klikket seg korrekt via dashbord → turnering → Program-fane (bekreftet med skjermbilde + full klikk-sti, ikke gjettet), åpnet "Hent bane fra teeoff", søkte "Tjøme", og fikk feilmeldingen "Mangler organisasjon i lenken" — URL-en hadde da MISTET ?org=...&name=.... Root-cause: OfficialCourseSearch sitt eget søke-<form onSubmit={runSearch}> var rendret INNI CreateSessionCard sitt eksisterende <form onSubmit={handleSubmit}> -- nestede <form>-elementer er ugyldig HTML. Nettleseren slår sammen de to skjemaene i den faktiske DOM-en, så "Søk"-knappen submittet i praksis det YTRE økt-skjemaet som en ekte native side-navigasjon (GET til gjeldende sti, ingen navngitte felt => tom spørrestreng) -- dette vasket bort org-parameteren og landet brukeren på siden sin egen org-guard. Fikset ved å fjerne det indre <form>-elementet helt (vanlig <div> + Enter-tast-håndtering på inputet + type="button" i stedet for type="submit" på søkeknappen) -- gjør nestede skjemaer strukturelt umulig fremover for denne komponenten. Ekte typesjekket produksjonsbuild kjørt på nytt, kun teecup_frontend redeployet (ingen backend-endring). Lærdom for fremtidige skjermer: en ny søk-/underskjema-widget som skal plasseres INNI et eksisterende skjema (slik som denne bane-søk-widgeten ligger inni økt-opprett-skjemaet) må ALDRI være et eget <form> -- bruk <div> + eksplisitt klikk-/Enter-håndtering.

  • Organisator-overstyring i team_authz.py, LIVE (2026-07-18): reist av brukeren rett før blind draw-skjermen skulle designes: app/team_authz.py sin user_may_act_for_team krevde tidligere en team_roster-rad med lenket bruker-konto — ingen vei for organisatoren til å låse/legge til deltakere/føre score hvis ingen spiller hadde logget inn ennå (vanlig tidlig i en turnering). Utvidet til også å godta org-eier/admin (organization_membership.role IN ('owner','admin')), på ETHVERT lag, ingen unntak for at organisatoren selv er rostret på motstanderlaget. Det unntaket ble bevisst vurdert og avvist (brukeren spurte selv om det, jeg anbefalte det opprinnelig, men vi kom sammen frem til at det ville skapt en verre låsning: er organisatoren spillende på Lag A og Lag B heller ikke har noen innlogget spiller, ville Lag B blitt helt låst ute) — TeeCup er et tillitsbasert klubb-/vennegjeng-verktøy, ikke en sikkerhetsgrense mot en fiendtlig organisator som uansett allerede ser begge lags fulle troppe-liste (blind draw skjuler kun selve kamp-paringen). Alle 5 kallsteder oppdatert (matches.py sin add_participant/lock_lineup, scoring.py sin submit_hole_score/ submit_hole_result). Verifisert grundig i scratch: org-eier uten roster kan nå låse BEGGE lag + føre score, en vanlig 'member'-rolle fortsatt blokkert (uendret), en rostret spiller fungerer uendret uavhengig av org-rolle. test_isolation.sql 12/12. Ingen migrasjon (ren Python-endring) — kun teecup_api redeployet, teeoff.no upåvirket.

  • Tee-endepunkter bygget og LIVE (2026-07-18), rett før blind draw-skjermen: fant et hull som blokkerte selve blind draw-flyten: match_participant. tee_id er påkrevd, men det fantes INGEN GET-vei for å liste en banes tee-er (selv offisielt importerte baner har tee-rader, men ingenting eksponerte dem) OG egendefinerte («custom») baner har ALDRI hatt noen vei til å FÅ tee-er i det hele tatt — dette er ikke noe ADR-019 innførte, det var et hull som fantes fra før custom-baner ble lagt til i første omgang, bare usynlig til nå. Konsekvens før fiksen: en turnering satt opp på en manuelt navngitt bane kunne ALDRI få en ekte deltaker lagt til på noen match. Presisering (brukeren spurte eksplisitt): dette er IKKE noe som må løses i teeoff.no sin kode — egendefinerte baner er per definisjon baner teeoff ikke kjenner til, så dette er en ren teecup-intern funksjon, uavhengig av ADR-019-integrasjonen. Ny GET/POST /orgs/{id}/courses/{id}/tees i app/routers/courses.py. POST avviser eksplisitt forsøk på offisielle baner (400 VALIDATION_FAILED — de får tee-ene sine fra teeoff-importen, ikke manuelt). Kun full_18-rating dekket (samme begrunnelse som ADR-019 Beslutning D). Bevisst UTENFOR omfang denne runden, egen senere sak: hull-/stroke-index-data for egendefinerte baner (hole-tabellen forblir tom for custom-baner) — trengs for korrekt slagfordeling i SCORING-fasen (ADR-008), ikke i blind draw, så det løses naturlig når scorekort- skjermen bygges (samme "bygg i rekkefølgen ting brukes"-logikk). Verifisert grundig i scratch: tom tee-liste på fersk custom-bane, opprett+list tee på custom-bane, offisiell bane viser alle 8 importerte tee-er, manuell tee-opprettelse avvist på offisiell bane, og — den faktiske payoff-en — en deltaker lagt til en match på en custom-bane-økt for FØRSTE gang noensinne (POST .../matches/{id}/participants lykkes nå med en tee_id fra en nyopprettet custom-tee). test_isolation.sql 12/12. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.

  • Blind draw-skjermen LIVE (2026-07-18): syvende V0-skjerm, components/session-blind-draw.tsx, ny rute /tournaments/[id]/sessions/[sessionId]. To lag-kolonner, hver med sine matcher ("flights"), legg til/fjern spiller+tee per plass (antall plasser avhenger av format), lås-knapp med bekreftelse, avslørings-visning når begge lag har låst. Program-skjermens økt-kort er nå klikkbare inn hit. Datamodell tilpasset fra V0s mock til API-ets faktiske sett-modell: V0 designet faste, indekserte "Slot"-arrays (kontrollert skjema-state); API-et har verken PATCH på match_participant eller noen slot-indeks — kun opprett/slett av en løs deltaker-mengde per side. Løst med en "legg til spiller"-inline-form (samme mønster som spiller-/bane-type-ahead ellers i appen) i stedet for faste dropdown-rader; funksjonelt likeverdig, strukturelt riktigere for API-ets faktiske form. To reelle hull funnet og fikset FØR/UNDER integrering:

    1. Ingen DELETE fantes for match_participant — en kaptein kunne aldri angre et valg før låsing uten å etterlate en foreldreløs rad. Ny DELETE /orgs/{id}/matches/{match_id}/participants/{participant_id} (samme ALREADY_LOCKED/NOT_ROSTERED_ON_TEAM-sjekker som opprett).
    2. "Skriv blindt"-hull, funnet under selve scratch-testingen (ikke bare tenkt ut på forhånd): forrige rundes org-admin-overstyring i team_authz.py lot en organisator LEGGE TIL deltakere på et lag de ikke selv er rostret på, men app/blind_draw.py sin own_team_ids() sjekket KUN rostret spiller for SYNLIGHET — så organisatoren så aldri sine egne tilføyelser igjen før begge lag hadde låst. Fikset ved å gi own_team_ids() samme owner/admin-utvidelse som user_may_act_for_ team() — en org-admin ser nå BEGGE lag umiddelbart (konsistent med at de uansett allerede har full tilgang, se forrige rundes resonnement), mens en faktisk rostret kaptein fortsatt kun ser sitt eget lag før reveal. Kun ett reelt kallsted (matches.py sin list_matches). Tredje, urelatert bug fanget under samme scratch-økt og fikset på brukerens eksplisitte forespørsel: POST .../matches/{id}/participants krasjet med en rå 500 (TypeError i handicap_engine.py sin course_handicap_raw) hvis spilleren manglet handicap_index OG use_handicap var på (standard) — preeksisterende, ikke noe denne runden introduserte. Fikset i app/routers/matches.py sin add_participant: sjekker nå handicap_index_snapshot IS NULL EKSPLISITT FØR innsetting når use_handicap er sann, avviser med en klar VALIDATION_FAILED (400) i stedet for å krasje. Bekreftet at use_handicap=false fortsatt tillater en spiller uten handicap (scratch-spill), og at en spiller MED handicap fortsatt fungerer uendret. Verifisert grundig i scratch, flere runder: DELETE-endepunktet (fjern+idempotent 404 ved gjentak+409 etter lås), synlighetsfikset (org- admin ser begge sider, rostret kaptein ser fortsatt kun eget lag), null-handicap-fikset (avvist rent, scratch-modus upåvirket, normal spiller upåvirket), full opprett-match→legg-til-deltaker→lås→avslør- syklus ende-til-ende. test_isolation.sql 12/12 etter hver runde. Ekte typesjekket produksjonsbuild. Rullet ut live, bruker bekreftet eksplisitt: begge containere bygget+redeployet (ingen migrasjon), teeoff.no upåvirket.
  • Backend-forarbeid for scorekort-skjermen, LIVE (2026-07-18): to hull funnet ved gjennomlesing av scoring.py FØR V0-prompten ble skrevet, samme føre-var-mønster som resten av økten.

    1. Hull-/stroke-index-data for egendefinerte baner (bevisst utsatt fra tee-rundens status-notat) — hole-tabellen har ALDRI hatt noen vei inn for custom-baner, som blokkerte stroke-scoringsmodus helt (hole_ result-modus trenger den ikke). Ny GET/POST /orgs/{id}/courses/{id}/ holes i app/routers/courses.py — engangs alle-18-på-en-gang (avviser feil antall, avviser hvis banen allerede har hull, avviser på offisielle baner som får hullene sine fra teeoff-import).
    2. GET scorecard viste kun UTLEDET vinn/tap/delt per hull, aldri de faktiske tallene som var registrert — umulig å bygge en skjerm som viser gjeldende tilstand ved gjenlasting. Utvidet Scorecard-modellen i app/routers/scoring.py med stroke_entries/hole_result_entries (rå hole_score/match_hole_result-rader, nøyaktig ett av de to fylt ut avhengig av øktens scoring_mode). Verifisert grundig i scratch: hull-validering (feil antall avvist, gjentatt oppsett avvist), og en FULL stroke-modus scoringsrunde med ekte handicap-justert nettoberegning på en egendefinert bane for FØRSTE gang noensinne (ulikt slagtall — 4 mot 5 — ga korrekt "halved" etter handicap-utjevning), scorecard sin nye stroke_entries bekreftet å returnere nøyaktig det som ble registrert. test_isolation.sql 12/12. Rullet ut live, ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
  • Scorekort-skjermen LIVE (2026-07-18): åttende V0-skjerm, components/session-scorecard.tsx, ny rute /tournaments/[id]/sessions/ [sessionId]/matches/[matchId]. Ett hull i fokus om gangen (banebruk, ikke et regneark) — store slag-steppere for stroke-modus, tre-valgs vinner-knapper for hole_result-modus, hull-chip-navigasjon, kollapsbar full oversikt, feiret "avgjort"-banner som låser alt til read-only. Blind draw sin avslørte visning lenker nå til hver match sitt scorekort. Ingen nye backend-hull denne runden — forrige rundes forarbeid (hull-endepunkter + stroke_entries/hole_result_entries i scorecard) dekket akkurat det skjermen trengte. Verifisert grundig i scratch, inkludert et fullt oppsett som speiler NØYAKTIG frontend-ens egen last-sekvens (sessions→teams→matches→ holes→scorecard, deretter en ekte POST hole-scores for begge spillere på hull 1 og en refetch): status_text/derivert resultat oppdaterte seg korrekt ("1 UP (A)", hull 1 → "a"), alle feltnavn stemte eksakt med TypeScript-typene uten justering. test_isolation.sql 12/12, ekte typesjekket build. Rullet ut live, ren frontend-endring, teeoff.no upåvirket.

  • Leaderboard-backend LIVE (2026-07-18): ny GET /orgs/{id}/ tournaments/{id}/leaderboard i app/routers/tournaments.py — summerer poeng på tvers av ALLE økter/matcher i turneringen, både total og per-økt-delsum. Reelt korrekthetshull funnet OG designet rundt FØR koden ble skrevet, ikke oppdaget i ettertid: match.team_a_id/team_b_id settes PER MATCH ved opprettelse, ikke garantert konsistent på tvers av matcher — en organisator kunne i prinsippet opprettet match 1 med team_a=Rød og match 2 med team_a=Blå. En naiv summering av points_side_a/points_ side_b ville da blandet sammen poeng fra to ULIKE fysiske lag. Løst ved å ALDRI summere på "a"/"b"-labelen — kun på det ekte lag-id-et (points_by_team: dict[team_id, float], både i totalen og per økt). Verifisert presist, ikke bare "kjørte uten feil": bygget et scratch- scenario med 3 matcher over 2 økter der match 2 sin team_a/team_b BEVISST var byttet om i forhold til de to andre — satte poeng direkte (Rød vinner alle tre, ett delt) og bekreftet at leaderboardet likevel ga riktig total (Rød 2.5, Blå 0.5) og riktig per-økt-delsum, til tross for swap-en. test_isolation.sql 12/12. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket. Frontend-skjermen gjenstår.

  • Leaderboard-skjermen LIVE (2026-07-18), samme dag: niende og siste V0-skjerm i "bygg i rekkefølgen ting brukes"-serien for kamp-play-flyten. components/tournament-leaderboard.tsx, ny rute /tournaments/[id]/ leaderboard. Stort scoreboard-kort med de to lagenes totalpoeng (lederen fremhevet), pluss en per-økt poeng-fordelingsliste med fullført/ikke-startet-status. V0 la selv til et tredje faneelement ("Leaderboard") i BÅDE program- og roster-skjermens fanerad denne runden — portert inn i de LIVE versjonene av begge (med riktig org- parameter, som V0s eksport som vanlig manglet). Ingen nye backend-hull — forrige rundes leaderboard-endepunkt dekket akkurat det skjermen trengte. Verifisert i scratch: full datamodell-runde (samme felt-for-felt som backend-verifiseringen dagen før) OG et eget tomtilstand-scenario (fersk turnering med 2 lag, 0 økter — sessions: [], matches_total: 0) for å bekrefte at "ingen økter opprettet ennå"-meldingen vises riktig i stedet for å krasje på et tomt array. test_isolation.sql 12/12, ekte typesjekket build. Rullet ut live, ren frontend-endring, teeoff.no upåvirket. Hele "bygg i rekkefølgen ting brukes"-serien for match-play-flyten er dermed komplett: oppsett (lag/roster) → program → blind draw → scorekort → leaderboard.

  • Offisiell bane-import: idempotent + tydeligere navn, LIVE (2026-07-18), rapportert av brukeren som faktisk brukte funksjonen på ekte produksjonsdata: to reelle problemer, begge funnet ved å lese (kun lesing) de faktiske dataene for "De Gamle er Eldst" FØR noe ble antatt.

    1. POST .../courses/official-import var IKKE idempotent — å importere samme bane på nytt (helt vanlig: flere økter spilles ofte på samme bane) ga en 409 DUPLICATE-feil (migrasjon 010 sin sperre) i stedet for å bare gi tilbake den allerede importerte banen. Fikset: sjekker nå external_course_ref FØR noe teeoff-kall gjøres — finnes banen fra før, returneres den eksisterende raden direkte (også raskere, og robust mot at teeoff er nede akkurat da).
    2. Lagret navn var kun selve banens navn ("Hovedbanen"), ikke hvilken klubb — ubrukelig til å skille baner fra hverandre, siden mange klubber navngir hovedbanen sin identisk. Fikset: navnet kombineres nå til "{anlegg} {bane}" (f.eks. "Tjøme Golfklubb Hovedbanen") ved import. Reell konsekvens av hull #1 funnet i produksjonsdata: brukeren hadde, mens hen forsøkte å søke opp Tjøme via det ØVERSTE banefeltet (som søker organisasjonens EGNE baner, ikke teeoff), ved et uhell trigget «Opprett ny bane: «Tj»»-snarveien og fått en tom, søppel custom-bane hengende på Foursome-økten -- en ekte forvekslingsfelle mellom de to adskilte bane-søkeflatene (eget vs. teeoff), ikke en kodefeil i seg selv. Data ryddet opp i EKTE teecup_db, bruker bekreftet eksplisitt: Foursome-økten pekt om til den allerede importerte, ekte Tjøme-banen (samme bane som Fourball-økten allerede brukte -- nøyaktig det organisatoren egentlig ønsket), søppel-«Tj»-banen slettet (bekreftet ingen matcher/tee-er/hull hang på den FØR sletting), og den ekte banens navn oppdatert til "Tjøme Golfklubb Hovedbanen". Verifisert i etterkant at begge økter nå peker til samme, korrekt navngitte bane. Verifisert mot ekte teeoff_api i scratch FØR utrulling: importerte Borregaard på nytt to ganger — andre kallet ga nøyaktig samme course-id (ikke en duplikat-rad), navnet kom ut som "Borregaard Golfklubb Hovedbanen". test_isolation.sql 12/12. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket.
  • PATCH/DELETE for økter, LIVE (2026-07-18), samme dag: brukeren spurte rett etter opprydningen om det i det hele tatt var mulig å rette/slette en feiloppsatt økt — det var det ikke (kun POST/GET fantes). Ny PATCH /orgs/{id}/sessions/{id} (app/routers/ tournaments.py) for enkle felt (navn, klokkeslett/intervall, starthull, poeng, handicap-brytere) via vanlig exclude_unset-mønster, PLUSS en egen gren for bane-bytte. Ny DELETE /orgs/{id}/sessions/{id} — kun tomme økter (ingen matcher), avviser med 409 ellers (bruk PATCH til å korrigere i stedet). Banebytte-scenarioet brukeren selv reiste ("5 hull spilt, oppdager feil bane — slagene er ekte, utregningen er trolig feil") krevde egen design: match_participant.tee_id peker til en tee som HØRER til den gamle banen. Løst med _remap_course() — finner en tee med samme navn+kjønn på den nye banen for hver allerede tillagte deltaker, flytter dem dit, og avviser HELE bane-byttet tydelig (400, ingenting skrevet, bekreftet transaksjonell rollback) hvis den nye banen mangler en tilsvarende tee. Etter et vellykket bytte: handicap regnes om for alle berørte deltakere, og matchstatus/poeng regnes om for HVER match i økten — bevisst uavhengig av om matchen allerede er avgjort (brukeren bekreftet eksplisitt at en bane-korrigering skal kunne endre et allerede cachet resultat). recompute_and_cache_match_state i app/routers/ scoring.py gjort delt (fjernet ledende understrek) for gjenbruk fra tournaments.py. Reelt, urelatert funn underveis i scratch-testingen, IKKE fikset: tee_rating lages i dag ALLTID kun med full_18-omfang (både ved teeoff-import og manuell tee-opprettelse) — en økt satt til front_9/ back_9 i stroke-modus kan derfor ALDRI få handicap beregnet (compute_and_store_side_handicaps sin tee_rating-join finner aldri noen rad), og dermed aldri avgjøre noen hull. hole_result-modus upåvirket. Flagget til bruker, bevisst latt urørt denne runden. Verifisert grundig i scratch: enkelt feltbytte (kun navn), fullt banebytte-scenario bygget nøyaktig som brukerens eksempel (10 hull spilt under feil bane, matchen allerede avgjort 10&8, PATCH til riktig bane → tee-er ombyttet korrekt, handicap endret fra 7/9 til 10/13 under den nye banens rating, matchstatus regnet på nytt med UENDREDE rå slagtall), avvist bane-bytte ved manglende tee-match (bekreftet full rollback, også av det urelaterte navnefeltet i samme kall), DELETE avvist på økt med match (409) og godtatt på tom økt (204). test_isolation.sql 12/12. Ingen migrasjon, kun teecup_api redeployet, teeoff.no upåvirket. Frontend (rediger-/slett-knapper i program-skjermen) ikke bygget ennå — kun backend-kapasiteten denne runden.

  • Rediger/slett-UI for økter LIVE (2026-07-19): V0-utvidelse av den eksisterende Program-skjermen (ingen ny rute) — "..."-meny (samme DropdownMenu-mønster som roster-skjermens per-spiller-handlinger) med "Rediger"/"Slett" per øktkort. Rediger bytter kortet til et inline-skjema (samme felt som PATCH støtter: navn, poeng, starthull, klokkeslett/ intervall, bane). Slett viser en bekreftelse; treffer den ekte 409-en (økt har matcher), vises backend sin egen feiltekst i stedet for en forhåndsberegnet klient-tilstand. Reell regresjon funnet OG UNNGÅTT i selve V0-eksporten, ikke i etterkant: denne rundens V0-prompt handlet kun om rediger/slett, men eksporten hadde samtidig (utilsiktet) FJERNET bane-feltet fra "Legg til økt"-skjemaet helt — nye økter ville stille blitt satt til en hardkodet mock-bane. Fanget under diff-mot-live-treet FØR noe ble tatt inn (samme rutine som alltid) — CreateSessionCard (med sitt ekte bane-søk/ teeoff-import) beholdt fullstendig urørt; kun de nye rediger/slett- delene ble hentet inn. Bane-bytte i redigeringsskjemaet er bevisst ENKLERE enn opprett- skjemaets bane-felt — kun velg blant organisasjonens eksisterende baner eller hent fra teeoff, INGEN "opprett ny bane: X"-snarvei her (PATCH sin course_id må være en ekte, allerede eksisterende bane, ikke en tekststreng) — unngår at samme "Tj"-forvekslingsfelle fra forrige banebytte-hendelse kan gjenta seg via redigeringsveien. Verifisert: ekte PATCH med nøyaktig skjemaets feltform, ekte DELETE (204 tom økt, 409 med matcher — bekreftet at {"detail":{"code", "message"}}-formen leses riktig av UI-et), test_isolation.sql 12/12, ekte typesjekket build. Rullet ut live, ren frontend-endring, teeoff.no upåvirket.

  • Invitasjonskode + ledende side + projisert stilling, BACKEND LIVE (2026-07-19, ADR-020): brukeren reiste tre relaterte hull rett etter at "bygg i rekkefølgen ting brukes"-serien var ferdig: (1) ingen vei inn til en turnering for en spiller som bare har fått muntlig beskjed, (2) leaderboardet viser kun faktisk opptjente poeng, ikke hva stillingen ville blitt om pågående matcher holder seg, (3) ingen fargekoding i matchlister for hvem som leder. Full ADR-020 skrevet (4 delbeslutninger, se ARCHITECTURE_DECISIONS.md) — nøkkelbeslutning bekreftet eksplisitt av bruker FØR bygging: en invitasjonskode OVERSTYRER tournament.visibility helt (koden ER selve invitasjonen, ikke en snarvei som fortsatt krever eksisterende tilgang). Ny migrasjon 011_join_code_and_leading_side.sql: tournament.join_code (6 tegn, alfabet uten 0/O/1/I, globalt unikt, backfylt for eksisterende rader), match.leading_side (cachet fortegn av MatchState.lead, samme mønster som status_text/points_side_a/b), fjerde SECURITY DEFINER-bro public_tournament_by_code() (etter public_tournament_org 007, link_player_by_email 008, public_org_by_slug 009). Backend bygget: create_tournament genererer koden (retry-løkke ved kollisjon, astronomisk usannsynlig med 33^6 kombinasjoner). Ny GET /public/tournaments/by-code/{code} (MÅ registreres FØR /{tournament_id} i routeren, ellers tolkes "by-code" som en ugyldig UUID). GET /public/tournaments/{id}, GET .../sessions og POST .../register godtar alle en valgfri code-parameter som — når den matcher — hopper _check_visibility() helt over. recompute_and_cache_match_state (scoring.py) cacher nå leading_side ved HVER hull-innsending, ikke bare ved avgjørelse. Leaderboard- endepunktet fikk projected_points/projected_points_by_team: avgjorte matcher bidrar likt til faktisk og projisert, ikke-avgjorte gir hele points_per_match til leading_side (delt 0,5/0,5 ved "AS"/ikke startet) — speiler hvordan Ryder Cup-TV-dekning viser "hvis det sluttet nå". Reell hendelse underveis, håndtert transparent (ikke skjult): en feilformulert docker exec teeoff_db env | grep -i POSTGRES-kommando (ment å liste variabelNAVN) fanget opp POSTGRES_PASSWORD sin VERDI også, siden selve nøkkelnavnet matchet søkemønsteret — eksponerte teeoff_db sitt superbruker-passord (teeoff_admin) i verktøyresultatet. Alvorligere enn de to tidligere passord-hendelsene i prosjektet siden dette er superbrukeren for HELE den delte Postgres-klyngen (teeoff OG teecup), ikke en enkelt tjeneste-credential. Flagget til bruker umiddelbart, som valgte å rotere. Rotert trygt UTEN å noensinne re-eksponere gammel ELLER ny verdi i noe synlig kommandoresultat: ALTER ROLE kjørt via lokal Unix-socket-trust-auth (bekreftet ved å lese pg_hba.conf, ingen hemmelighet involvert i den sjekken) — krevde altså IKKE det gamle passordet i det hele tatt. Nytt passord generert med openssl rand -hex 32 (hex, ikke base64 — unngår SAMME klasse URL-enkodings-felle som TEECUP_DATABASE_URL-hendelsen tidligere, siden verdien også ligger i en postgres://-DSN). /opt/teeoff/.env sine TO forekomster (POSTGRES_PASSWORD og DATABASE_URL) oppdatert med sed-mønstre som ALDRI leser/skriver ut den gamle verdien. teeoff_api/teeoff_worker service-nøklene i docker-compose.prod.yml viste seg å hete api/ worker (ikke teeoff_api/teeoff_worker — det er kun container_name), samme "service-nøkkel ≠ container-navn"-fallgruve som nettverksalias-hendelsen fra containeriseringsrunden. docker compose up -d --force-recreate api worker gjenskapte OGSÅ teeoff_db selv (ikke eksplisitt navngitt) — Compose oppdager konfigurasjonsendring (${POSTGRES_PASSWORD} i db-tjenestens egen environment:) og gjenskaper uansett hvilke tjenester som ble navngitt. Verifisert grundig ETTERPÅ: teeoff_db-loggen viste "Skipping initialization" (datavolum urørt, ikke reinitialisert) + ren oppstart, teeoff_api/teeoff_worker ren oppstart uten en eneste feil-/auth-/passord-linje i hele loggen, teeoff.no OG teecup.teeoff.no/health begge 200 etterpå. Scratch-verifisert grundig (fersk teecup_scratch 001→011, isolert teecup_app_scratch-rolle, isolert scratch-MinIO, engangs API-container): test_isolation.sql 12/12 (måtte først rettes — tre RÅ INSERT INTO tournament-steder i selve testfilen predaterte join_code og traff den nye NOT NULL-constrainten, rettet med dummy-koder). Full ende-til-ende-runde: to turneringer opprettet, ulike koder bekreftet; by-code-oppslag bekreftet for kjent OG ukjent kode (404); anonym lesing av en org-synlig turnering BLOKKERT uten kode (403 NOT_VISIBLE), TILLATT med riktig kode (inkl. case-insensitivt), FORTSATT blokkert med feil kode; samme mønster bekreftet for POST .../register. Full hull-for-hull-simulering av en singel-match (hole_result-modus): leading_side/status_text fulgte hverandre eksakt gjennom "1 UP (A)" → "AS" (leading_side=null) → avgjort "9&7 (A)", leaderboardets projected_points traff nøyaktig 1.0/0.0 mens A ledet, 0.5/0.5 ved "AS", og ble likt points/projected_points (begge 1.0/0.0) etter avgjørelse. Rullet ut mot ekte teecup_db 2026-07-19, bruker bekreftet eksplisitt: migrasjon 011 kjørt (eneste eksisterende turnering fikk automatisk generert kode), test_isolation.sql fortsatt 12/12, kun teecup_api redeployet (ingen frontend-endring i denne del-runden), teeoff.no upåvirket. Frontend fullført samme dag, egen del-runde: login-form.tsx fikk et eget kode-modus (JoinByCode) — «Har du en invitasjonskode?»-lenke bytter ut e-post-skjemaet, slår opp /public/tournaments/by-code/{code} og navigerer til /t/{id}?code=... med Next sin useRouter. code query-param tres gjennom hele veien: app/t/[id]/page.tsx leser searchParams, public-tournament.tsx sender den med på BÅDE info-/sessions-lesingen og selve POST .../register (ikke bare det første oppslaget som tok deg dit). tournament-detail.tsx viser koden i en egen kopier-chip i headeren — ingen enkelt-turnering-GET fantes, så komponenten henter i stedet hele org-ens turneringsliste (som allerede bærer join_code) og finner egen rad, i stedet for å legge til et nytt endepunkt kun for dette. tournament-leaderboard.tsx fikk en ny SegmentedBar-komponent — ETT fargesegmentert rektangel per bar (ikke tall side om side), proporsjonalt med hvert lags poeng, 50/50 nøytralt ved 0-0. To slike bares rett under headeren: "Stilling nå" (faktisk) og "Projisert (hvis pågående matcher holder seg)" — den EKSISTERENDE store tall-scoreboarden beholdt uendret lenger ned som detaljvisning. session-blind-draw.tsx sin RevealedView: matchkortet får nå en farget toppkant (leaderens team.color) og en status_text-chip (fylt farge ved avgjort match m/ konfetti-ikon, lys tone ved pågående) i stedet for kun klokkeslettet; RevealSide for ledende side får en svak fargetonet bakgrunn. leading_side/status_text/points_side_a/b lagt til ApiMatch-typen (var der allerede i API-et, bare ikke konsumert frontend-siden før nå). Verifisert: ekte typesjekket produksjonsbuild (samme Dockerfile som deployes, ikke dev-server) kompilerte rent, alle 10 ruter listet. Rullet ut (kun teecup_frontend, ingen backend-endring i denne delen), teecup.teeoff.no/dashboard og / → 200, teeoff.no upåvirket. Ekte smoke-test i produksjon: by-code-oppslag for "De Gamle er Eldst" sin faktiske kode ga riktig turnering-id, /t/{id}?code=... ga 200. ADR-020 er dermed helt ferdig (backend + frontend, alle fire del-ønsker: kode-basert oppdagelse, kode-felt på login, projisert stilling, fargekoding av matcher) — bortsett fra kode-regenerering, som er bevisst utsatt (se FEATURE_BACKLOG.md). Reell driftshendelse underveis (mellom backend- og frontend-delen, under scratch-oppsett): en feilformulert grep -i POSTGRES-kommando eksponerte teeoff_db sitt superbruker-passord ved et uhell. Flagget umiddelbart, brukeren valgte å rotere — se detaljene under backend-avsnittet over for hele hendelsen og hvordan roteringen ble gjennomført uten å noensinne re-eksponere gammel eller ny verdi.

  • Sesjons-bug diagnostisert og FIKSET, LIVE (2026-07-19): brukeren rapporterte at hen måtte be om ny magic-link-kode ved HVERT besøk til teecup.teeoff.no, til tross for ADR-009s 30-dagers sesjonscookie. Bad brukeren sjekke den EKTE cookien i nettleseren fremfor å gjette — kom tilbake korrekt satt i alle henseender (Expires 30 dager frem, Secure/HttpOnly/SameSite=Lax). Rot-årsaken var derfor IKKE cookien eller backend-en: frontend/app/page.tsx (rot-siden) viste ALLTID innloggingsskjemaet uten noensinne å sjekke om en gyldig sesjon allerede fantes. Dashboard-komponenten sjekker /auth/me og sender til / ved MANGLENDE sesjon, men ingen kode gjorde det motsatte — en bruker som besøkte roten direkte (i stedet for å navigere til /dashboard) så derfor alltid innloggingsskjemaet uansett sesjonsstatus. Fikset: page.tsx gjort om til en async server-komponent som leser sesjonscookien via next/headers, kaller /auth/me server-til-server direkte mot TEECUP_API_ORIGIN (samme mønster som generateMetadata i app/t/[id]/page.tsx — IKKE gjennom next.config.mjs sin rewrites(), som kun gjelder nettleser-trafikk), og sender en allerede innlogget bruker videre til /dashboard med redirect() FØR innloggingsskjemaet når rendres. Verifisert presist mot den ekte, live stacken, med brukerens EGEN ekte sesjonscookie (ikke en syntetisk test): curl uten cookie mot https://teecup.teeoff.no/ ga 200 (skjemaet vises, riktig for en anonym besøkende); samme kall MED den ekte cookien ga 307 til /dashboard (riktig — sender en allerede innlogget bruker rett videre). Ekte typesjekket produksjonsbuild kjørt FØR utrulling (build-outputet viste selv at / nå er ƒ dynamisk i stedet for statisk — bekrefter at server-sjekken faktisk ble tatt i bruk). Rullet ut live, kun teecup_frontend, ingen migrasjon, teeoff.no upåvirket. Samme runde: brukeren stilte to oppfølgingsspørsmål om autentisering/autorisasjon — «er flere-organisasjoner-eierskap tenkt gjennom» (bekreftet: ja, ADR-002 fra dag én, ingen kodeendring nødvendig) og et ønske om passord (valgfritt tillegg)+2FA. Fullt design skrevet som ADR-021 (passord/2FA) og ADR-022 (dele/invitere/frasi seg eierskap + superadmin) — se ARCHITECTURE_DECISIONS.md.

  • ADR-021 (passord/2FA) + ADR-022 (org-eierskap) BYGGET OG LIVE (2026-07-19), samme dag: brukeren ba om begge sammen («Bygg det», bekreftet eksplisitt at det gjaldt begge ADR-ene i samme runde). Ny migrasjon 012_password_2fa_and_org_invitations.sql: app_user. password_hash/two_factor_method/totp_secret/is_super_admin, ny tabell two_factor_code (samme hash-og-utløp-mønster som magic_link_token), ny tabell organization_invitation (RLS org-isolert, INGEN egen klikkbar aksept-lenke — godtas automatisk ved neste innlogging med matchende e-post), femte SECURITY DEFINER-bro accept_pending_invitations_by_email() (etter public_tournament_org 007, link_player_by_email 008, public_org_by_slug 009, public_tournament_by_code 011). Sesjons-STADIER innført i app/auth.py: en sesjonscookie er ikke nødvendigvis en full sesjon lenger — create_session_token() tar nå en stage-parameter (full/pending_2fa/must_enroll_2fa), lagt inn som et JWT-claim. get_current_user avviser eksplisitt alt annet enn full ELLER en ELDRE token uten stage-claim i det hele tatt (utstedt før denne runden — behandlet som full for bakoverkompatibilitet, ingen eksisterende bruker logget brått ut). To nye avhengigheter: get_pending_user (kun for 2FA-verifiseringsendepunktene) og get_current_or_enrolling_user (godtar BÅDE en full sesjon — frivillig 2FA-oppsett fra kontoinnstillinger — OG must_enroll_2fa — tvunget oppsett rett etter innlogging — samme oppsett-logikk dekker begge veiene). get_current_user_optional fikk samme stage-sjekk for konsistens. Passord (Argon2id, ikke bcrypt): bevisst valg for å unngå bcrypt sin stille 72-byte-trunkering, siden brukeren eksplisitt ba om korrekt håndtering av spesialtegn/mellomrom. POST /auth/set-password/ /remove-password//login-password — sistnevnte svarer med IDENTISK 401 uansett om e-posten finnes, mangler passord, eller passordet er feil (samme anti-enumerering som magic-link). 2FA (TOTP via pyotp ELLER e-post-engangskode via eksisterende SMTP, brukerens eget valg): POST /auth/2fa/setup/start genererer en TOTP-secret UTEN å lagre den (rundturer til klienten, som ekkoer den tilbake i /setup/confirm — unngår en halvferdig 2FA-tilstand i databasen hvis brukeren forlater oppsettet). QR-kode generert server-side (qrcode-biblioteket + eksisterende Pillow-avhengighet, ingen ny ekstern tjeneste). POST /auth/2fa/verify fullfører en PÅGÅENDE innlogging. Tvungen 2FA for org-eier/admin (ADR-021 Beslutning D): user_requires_2fa_enrollment() sjekket ved HVER innlogging (ikke bare første gang, siden en bruker kan bli eier av en NY org etter at kontoen allerede eksisterer uten 2FA) — verifisert eksplisitt: en fersk org-eier uten 2FA ble korrekt blokkert fra all normal tilgang (401) og tvunget inn i oppsett-flyten før noe annet ble tilgjengelig. Organisasjonseierskap (ADR-022): POST/GET/DELETE /orgs/{id}/invitations (owner→enhver rolle, admin→KUN member — ellers en privilegie-eskaleringsvei), PATCH/DELETE /orgs/{id}/memberships/ {id} (owner kan endre/fjerne hvem som helst; en bruker kan ALLTID SENKE egen rolle selv — aldri heve den, ville vært selv-forfremmelse — og alltid forlate selv), «siste eier»-vern (409 LAST_OWNER, FOR UPDATE-lås mot race på alle eier-rader, samme TOCTOU-mønster som ADR-011s to-lags-grense). Superadmin (app_user.is_super_admin, KUN manuelt DB-tildelt — bevisst INGEN API-vei til å gi seg selv eller andre flagget) får en parallell autorisasjonssti (get_superadmin_user) som kan sette medlemskap på ENHVER org, uavhengig av eget medlemskap — bevisst avgrenset til nøyaktig dette, ikke generell tilgang til andres turnering-/spillerdata. Tre reelle bugs funnet OG fikset UNDER scratch-testing, ingen nådde produksjon:

    1. verify_magic_link sendte en rå asyncpg-UUID (ikke streng) videre til sesjonsutstedelse — jwt.encode() sin JSON-serialisering krasjet rått (500) på selve innloggingen. Fant umiddelbart ved første reelle innloggingstest. Rettet med en eksplisitt str().
    2. OG 3. Både 2fa/setup/confirm og 2fa/verify kalte først den delte _issue_login_result()-hjelpefunksjonen (ment for PRIMÆR autentisering) EN GANG TIL etter at 2FA nettopp var bekreftet — som så (korrekt, men feil kontekst) at two_factor_method nå var satt og krevde EN NY runde med 2FA for akkurat den samme innloggingen, en uendelig løkke. Fant ved å faktisk fullføre hele innloggings- syklusen med ekte genererte TOTP-koder (pyotp i test-scriptet), ikke bare ved å lese koden. Rettet ved at begge endepunktene nå utsteder en full sesjon DIREKTE etter vellykket 2FA-bekreftelse, ikke via gjenbruk av den generelle sjekken. Frontend: login-form.tsx fikk en tredje modus (passord, ved siden av magic-link og ADR-020s invitasjonskode-modus). Ny delt two-factor-flow.tsx (TwoFactorVerifyForm/TwoFactorSetupForm), brukt av BÅDE login-form.tsx og verify-form.tsx siden begge primær-autentiseringsveiene kan returnere samme 2FA-mellomtilstand. Ny /account-skjerm (sett/fjern passord, aktiver/deaktiver 2FA) og ny /orgs/[id]/members-skjerm (invitere, endre rolle, fjerne/forlate), begge lenket fra dashbordets header/org-visning. next.config.mjs sin rewrites() utvidet med /superadmin/:path* (ADR-016s konsekvens for enhver ny API-prefiks, selv om ingen frontend-UI faktisk bruker den ennå). Verifisert grundig mot fersk teecup_scratch (001→012, test_isolation.sql 12/12): full magic-link-bakoverkompatibilitet (eksisterende flyt uendret for brukere uten 2FA), passord med ekte spesialtegn/mellomrom/æøå satt og brukt til pålogging, full TOTP-runde (oppsett→bekreft→logg ut→logg inn→krev 2FA→verifiser med ekte pyotp-generert kode), full e-post-2FA-runde (samme mønster, feil kode avvist, kode ikke gjenbrukbar), tvungen 2FA-registrering for ny org-eier, invitasjon→auto-aksept ved førstegangsinnlogging, rolle-eskalering-forsøk avvist på tre distinkte måter (medlem kan ikke invitere, admin kan ikke gi eierskap, bruker kan ikke forfremme seg selv), siste-eier-vern (både PATCH og DELETE), duplikat-invitasjon avvist, superadmin-sti fungerer/avvises riktig i begge retninger. Ekte typesjekket produksjonsbuild av frontend (alle nye ruter listet). Rullet ut mot ekte systemer 2026-07-19, bruker bekreftet eksplisitt: migrasjon 012 kjørt mot ekte teecup_db, test_isolation.sql fortsatt 12/12, begge containere (teecup_api, teecup_frontend) redeployet og bekreftet ren oppstart, /health og /dashboard fortsatt 200 (eksisterende sesjoner uendret av stage-bakoverkompatibiliteten), /auth/login-password bekreftet nåbart over ekte https, teeoff.no upåvirket. ADR-021 og ADR-022 er dermed begge helt ferdig — backend + frontend. Bevisst utenfor omfang: dedikert superadmin-UI (brukes via API av en betrodd operatør), SMS som 2FA-metode.
  • Program-skjerm: tydeligere klikk-hint på øktkort (2026-07-19): brukeren påpekte at ingenting i grensesnittet indikerte at et øktkort er klikkbart inn til blind draw-skjermen. Lagt til en synlig "Sett opp flights og lås oppstilling →"-rad nederst i hvert kort (components/tournament-program.tsx). Ren frontend-endring, ingen backend-rørt.

  • Brukerroller: kaptein som reell autorisasjon, deltaker-avgrenset scoring (2026-07-19, ADR-023): direkte oppfølging av det lenge åpne "Brukerroller"-punktet i FEATURE_BACKLOG.md. Fire beslutninger avklart eksplisitt med bruker (AskUserQuestion) før bygging — alle anbefalte valg. Bygget: app/team_authz.py skrevet om — user_is_team_captain (erstatter user_may_act_for_team) krever is_captain=trueteam_roster (eller org-eier/admin) for å legge til/fjerne deltakere og låse et lag (matches.py); ny user_is_match_participant krever en ekte match_participant-rad for brukeren i AKKURAT den matchen (valgfritt side-spesifikk via team_side) for å føre/korrigere score (scoring.py) — uavhengig av kapteinmerket. Nye feilkoder NOT_TEAM_CAPTAIN og NOT_MATCH_PARTICIPANT (erstatter NOT_ROSTERED_ON_TEAM på disse fem stedene). app/routers/tournaments.py sin PATCH/POST .../roster håndhever nå "kun én kaptein per lag" (fjerner automatisk forrige kapteins merke i samme transaksjon). Reelt funn FØR utrulling, ikke antatt: sjekket (kun lesing, superbruker mot ekte teecup_db) om noen eksisterende lag ville blitt låst ute av en ren kaptein-only-regel — "De Unge" i "De Gamle er Eldst" har i dag 0 av 2 roster-rader merket kaptein. Designet derfor en bevisst fallback i user_is_team_captain: har laget INGEN utpekt kaptein ennå, godtas enhver rostret spiller i stedet for å låse laget helt ute. Ingen lag hadde flere kapteiner, så "kun én kaptein"-håndhevelsen krevde ingen data-opprydning. Reell bug funnet OG fikset UNDER scratch-testing: user_is_match_ participant sin SQL sammenlignet mp.team_side (enum-kolonne) direkte mot en tekst-parameter uten cast når team_side=None (hole_result-modus) — ga en rå 500 (UndefinedFunctionError: operator does not exist: team_side = text). Rettet med et eksplisitt mp.team_side::text = $3. Verifisert grundig mot fersk teecup_scratch (isolert teecup_app_scratch-rolle, isolert scratch-MinIO, engangs API-container): et 15-punkts Python/httpx-testskript som simulerte fire innloggede brukere (organisator + tre rostrede spillere på to lag) gjennom hele syklusen — lag uten kaptein tillater enhver rostret (fallback bekreftet), kaptein utpekt fjerner andre rostredes rettighet, "kun én kaptein" bekreftet (ny kaptein avsetter automatisk forrige), org-admin fungerer uendret uavhengig av kaptein, og scoring (hole_result-modus) bekreftet begrenset til faktiske matchdeltakere (en kaptein som IKKE selv spiller matchen ble korrekt avvist med NOT_MATCH_PARTICIPANT, mens en faktisk deltaker og org-admin begge fikk føre score). Måtte også oppdage og legge til et forutsetning-steg underveis: get_authorized_org krever organization_membership for ALLE org-scopede endepunkter uansett — en rostret spiller må derfor også være invitert som org-medlem (minimum 'member', ADR-022s invitasjonsflyt) for i det hele tatt å nå team_authz-vurderingen; ikke en bug, men en forutsetning testskriptet først manglet. test_isolation.sql 12/12 uendret (ingen skjemaendring). Ekte typesjekket produksjonsbuild av frontend (øktkort-hintet fra samme runde) kjørt og bekreftet, alle 11 ruter listet. Rullet ut live 2026-07-19, bruker bekreftet eksplisitt: ingen migrasjon, docker compose up -d --build teecup_api teecup_frontend, begge containere boot-et rent (Application startup complete, Next.js Ready), /health og /dashboard → 200, teeoff.no upåvirket.

  • Walkover/konsesjon LIVE (2026-07-19, ADR-024): direkte oppfølging av Brukerroller-runden samme dag — brukeren ba eksplisitt om å ta fatt på dette som naturlig neste steg. Løser det lenge kjente hullet: en side som aldri stiller nok spillere fikk aldri beregnet handicap og matchen kunne derfor aldri avgjøres — hang uendelig. Fire beslutninger avklart eksplisitt med bruker (AskUserQuestion) før bygging — tre anbefalte valg, ett (omfang: match+turnering-nivå samtidig, ikke bare match) valgt utover anbefalingen. Bygget: ny apply_concession-hjelpefunksjon i app/routers/ scoring.py (skriver til de samme fire kolonnene som recompute_and_cache_match_state -- status_text/points_side_a/b/ leading_side -- men direkte, ikke utledet fra hull). Ny POST /orgs/{id}/matches/{id}/concede (kun kaptein for det TAPENDE laget, speiler ekte golf-etikette -- du gir bort DITT tap, krever ikke seier på motstanderens vegne -- eller org-admin, gjenbruker user_is_team_captain fra ADR-023 uendret). Ny POST /orgs/{id}/tournaments/{id}/concede (app/routers/tournaments.py) som gir opp ALLE ikke-avgjorte matcher laget har i turneringen i én operasjon -- v1s to-lags-grense (ADR-011) gjør dette trivielt (bare én motstander uansett), FOR UPDATE-låser alle berørte match-rader (samme race-vern som ellers). Kan erklæres uansett hvor mange hull som allerede er registrert (match-play teller kun seier/tap/delt for poeng, ikke marginen) -- allerede registrerte hull i hole_score/match_hole_result forblir urørt, kun matchens avgjørelses-felt endres. Frontend: session-scorecard.tsx fikk en kollapsbar "Gi opp matchen (walkover)"-seksjon (to knapper, én per lag, med bekreftelsessteg) synlig når matchen ikke er avgjort. tournament-detail.tsx sin TeamPanel fikk en tilsvarende "Gi opp resten av turneringen for laget"-knapp nederst i hvert lagkort. Ingen klientside-forhåndsfiltrering på kapteinstatus noe sted -- begge knappene vises alltid, en 403 fra backend vises bare som vanlig feiltekst (samme mønster som resten av appen). Verifisert grundig mot fersk teecup_scratch (isolert teecup_app_scratch-rolle, isolert scratch-MinIO, engangs API-container): to separate 15-punkts Python/httpx-testløp. Match-nivå: vinnende lags kaptein NEKTES å konsedere på vegne av det tapende laget (kan ikke kreve seier på andres vegne), tapende lags kaptein FÅR, allerede avgjort match avvist 409, fremmed team_id avvist 400, konsesjon ETTER at ett hull allerede er registrert bekreftet å fungere OG bekreftet at det registrerte hull-resultatet forblir synlig i scorekortet etterpå (ikke overskrevet), org-admin FÅR konsedere direkte uavhengig av kapteinmerke. Turnering-nivå: feil lags kaptein nektes, riktig kaptein FÅR gi opp resten (kun de faktisk ikke-avgjorte matchene telles -- allerede avgjorte matcher fra match-nivå-testene i samme løp ble korrekt hoppet over), gjentatt kall er trygt (0 nye, idempotent i praksis), ukjent team_id gir 404. test_isolation.sql 12/12 uendret (ingen skjemaendring). Ekte typesjekket produksjonsbuild av frontend kjørt og bekreftet, alle 11 ruter listet. Rullet ut live 2026-07-19, bruker bekreftet eksplisitt: ingen migrasjon, docker compose up -d --build teecup_api teecup_frontend, begge containere boot-et rent, /health og /dashboard → 200, teeoff.no upåvirket.

  • Turnering-status via API, SCRATCH-VERIFISERT (2026-07-19): brukeren valgte dette som neste steg etter walkover/konsesjon-runden (fikk velge mellom denne lille opprydningen og å starte Kommunikasjon-runden). status (draft/active/completed/archived) har ligget i skjemaet siden migrasjon 001, men INGEN endepunkt kunne endre det — kun INSERT-defaulten 'draft' fra create_tournament. Bygget: lagt til i TournamentUpdate (app/routers/tournaments.py), settes via det eksisterende generiske PATCH /orgs/{id}/tournaments/{id} (exclude_unset-mønsteret, ekte PATCH-semantikk uendret). Reelt funn UNDER scratch-testing, ikke antatt riktig på forhånd: status er -- ulikt visibility (som er ren text+CHECK) -- en EKTE Postgres ENUM-type (tournament_status). Den generiske set_clauses-byggeren (f"{key} = ${i}") hadde derfor trengt et eksplisitt cast for at asyncpg sin ukjent-typede parameter skulle løses riktig mot en enum-kolonne -- lagt til en spesialsjekk (::tournament_status kun for status-nøkkelen) FØR jeg antok mekanismen "bare fungerer" fordi den gjør det for de andre feltene. Bevisst INGEN tilstandsmaskin/overgangsregler bygget (kan f.eks. gå fra completed tilbake til draft fritt) -- samme tillitsnivå som resten av appen, ikke etterspurt. Frontend: tournament-status-badge.tsx fikk en ny redigerbar TournamentStatusPicker (dropdown over de fire verdiene, optimistisk UI-oppdatering med rollback ved feil), koblet inn i tournament-detail.tsx sin header ved siden av invitasjonskode-chipen. Den eksisterende skrivebeskyttede TournamentStatusBadge (dashbordets kortliste) urørt. Verifisert i scratch: ny turnering får riktig default draft, PATCH til active bekreftet enum-castet faktisk løser problemet, PATCH med status+et annet felt samtidig fungerer, PATCH UTEN status-felt lar verdien stå urørt (regresjon på eksisterende PATCH-semantikk), ugyldig status-verdi avvist med 422 (Pydantic-mønster), GET-listen viser samme verdi etterpå. test_isolation.sql 12/12 uendret (ingen skjemaendring). Ekte typesjekket produksjonsbuild kjørt og bekreftet. Rullet ut live 2026-07-19, bruker bekreftet eksplisitt: ingen migrasjon, docker compose up -d --build teecup_api teecup_frontend, begge containere boot-et rent, /health og /dashboard → 200, teeoff.no upåvirket.

  • Kommunikasjon LIVE (2026-07-19, ADR-025) — det største enkeltløftet i prosjektet så langt: lag-intern chat («det hemmelige rommet») + offentlig runde-feed («Banter Board»), begge med bilder og ekte WebSocket-sanntid, bygget i samme runde. Fire hovedbeslutninger avklart eksplisitt med bruker (AskUserQuestion) før bygging — brukeren valgte den mest ambisiøse kombinasjonen på alle fire (begge deler nå, WebSockets fremfor polling, ekte privat chat, bilder fra start). Datamodell: ny migrasjon 013_messaging.sql — delt message-tabell med scope-diskriminator (team/tournament_feed) i stedet for to separate tabeller, RLS org_isolation som ellers. author_display_name FRYSES ved skrivetidspunkt (samme prinsipp som handicap-snapshot, ADR-007) — spillerens player.display_name i org-en hvis den finnes, ellers e-postens lokaldel (dekker org-ansatte uten egen spillerprofil). Lag-chat er BEVISST ekte privat — ny user_is_rostered_on_team i app/team_authz.py, med VILJE uten org-admin-fallback, ulikt de to andre funksjonene i samme fil (user_is_team_captain/user_is_match_participant, ADR-023, som begge har et slikt unntak). Første sted i hele appen der org-eier/admin er strukturelt utestengt fra noe. Offentlig feed: LESING gjenbruker registration.py sitt eksisterende trenivå-visibility-mønster (ADR-018) helt uendret, inkl. anonym tilgang. POSTING er strengere enn lesing — krever ekte innlogging OG org- medlemskap/faktisk deltakelse, selv på en public-synlig turnering (en helt urelatert innlogget bruker skal ikke kunne poste på en fremmed offentlig side). Moderering: forfatteren selv ELLER org-eier/admin kan slette et feed-innlegg (motsatt av lag-chatten, som ikke har noen ekstern moderator). registration.py sine fire interne hjelpefunksjoner gjort delt (fjernet ledende understrek — samme "gjort delt for gjenbruk"-mønster som tidligere runder): resolve_org, is_participant, code_matches, check_visibility. team_authz.py sin _is_org_admin likeens → is_org_admin. Ingen atferdsendring, kun navn, for at messaging.py skulle kunne gjenbruke dem uendret i stedet for å duplisere logikk. WebSockets, ikke polling: in-memory tilkoblingsregister PER PROSESS i app/routers/messaging.py — trygt med dagens ene teecup_api-container, men deles IKKE på tvers av flere prosesser/containere (samme klasse begrensning som den allerede aksepterte in-memory-cachen, se ARCHITECTURE_ DECISIONS.md "Åpne spørsmål"). Ny get_current_user_from_websocket i app/auth.py — WS-ruter kan ikke bruke get_current_user/ get_authorized_org direkte via Depends() (de er Request-typet, ingen ekte HTTP Request finnes i en WS-scope), derfor en bevisst minimal, egen kopi av samme cookie-dekode-/oppslagslogikk. Reell infrastrukturoppdagelse FØR noe ble forsøkt, ikke i etterkant: Next.js sin rewrites() proxyer ikke WebSocket-oppgraderinger pålitelig i "standalone"-modus — løst likt som MinIO-media-ruten (ADR-018): en egen Caddy-rute (handle /ws/* { reverse_proxy teecup_api:8000 }) rett til API-et, forbi Next.js/teecup_frontend helt. Caddyfile ligger i det SEPARATE /opt/teeoff-repoet — samme stale-bind-mount-inode-oppførsel som ALLE tidligere Caddyfile-runder (graceful reload plukker ikke opp endringen), løst likt: full docker restart teeoff_caddy, brukeren bekreftet eksplisitt på forhånd, noen sekunders nedetid for teeoff.no. Verifisert grundig mot fersk teecup_scratch (isolert teecup_app_scratch-rolle, isolert scratch-MinIO, engangs API-container med websockets-Python-biblioteket installert kun for testen): et 20-punkts asyncio/httpx/websockets-testskript som dekket BEGGE meldingstyper ende-til-ende. Kritiske personvern-/sanntid-funn, alle bekreftet med ekte tilkoblinger (ikke bare REST):

    • Rostret spiller på lag A FÅR lese/skrive lag A sin chat; rostret spiller på lag B NEKTES; organisatoren (org-eier) NEKTES OGSÅ — bekreftet BÅDE over REST (GET) og over selve WebSocket-håndtrykket (avvist med lukkekode 4403 før accept() i det hele tatt kalles).
    • Sanntid bekreftet reelt: A1 koblet til lag A sin chat-socket, A2 sendte en melding over vanlig REST, A1 mottok den umiddelbart over den åpne WebSocket-tilkoblingen (ikke bare at REST-svaret så riktig ut).
    • Bildeopplasting i chat bekreftet (ekte AVIF-konvertert image_url returnert).
    • Offentlig feed: anonym NEKTES å poste (401), en tilfeldig INNLOGGET men uvedkommende bruker NEKTES (403 NOT_A_PARTICIPANT), org-medlem FÅR, en faktisk deltaker (rostret, IKKE org-medlem) FÅR — beviser is_participant-veien fungerer uavhengig av is_member-veien. Anonym WebSocket-tilkobling til en public-synlig turnerings feed FÅR lov og mottar sanntidsoppdateringer.
    • Moderering bekreftet: forfatter sletter eget innlegg, org-admin sletter ANDRES innlegg (feeden), uvedkommende NEKTES å slette andres innlegg. test_isolation.sql 12/12 uendret (additiv migrasjon). Ekte typesjekket produksjonsbuild av frontend kjørt og bekreftet, inkl. den nye /tournaments/[id]/teams/[teamId]/chat-ruten. Frontend: ny components/team-chat.tsx (meldingsliste med egen/andres- styling, bildeopplasting, sanntid via nettleserens native WebSocket, slett-egen-melding), lenket fra en ny chat-ikon-knapp i tournament- detail.tsx sin TeamPanel. Ny seksjon TournamentFeed i components/public-tournament.tsx (nederst på den offentlige turneringssiden) — viser 401/403-svar fra posting som forklarende inline-tekst ("logg inn for å poste" / "du må være medlem/deltaker") i stedet for en generisk feilmelding. Rullet ut live 2026-07-19, bruker bekreftet eksplisitt for alle tre stegene (migrasjon, containere, Caddy-restart): migrasjon 013 kjørt mot ekte teecup_db, test_isolation.sql fortsatt 12/12, begge containere boot-et rent, Caddy validert (caddy validate — "Valid configuration") FØR restart, restarten ren (ingen feil i loggen). Verifisert grundig etterpå: /health/dashboard → 200, teeoff.no → 200, et ekte wss://-håndtrykk over produksjons-https bekreftet å nå helt frem til applikasjonslaget (testet mot en ukjent turnering-id — ingen ekte data berørt — ga korrekt 404 fra selve WS-ruten, ikke en Caddy/Next.js-feil), og et ren-HTTP-kall mot en /ws/*-sti bekreftet å returnere FastAPI sin egen JSON-404 ({"detail":"Not Found"}) og ikke Next.js sin HTML-404 — beviser Caddy-ruten faktisk treffer teecup_api, ikke teecup_frontend.
  • Tilskuer-rolle LIVE (2026-07-19, ADR-026): brukeren valgte dette som neste steg rett etter Kommunikasjon-runden — "tilskuer" var bevisst utsatt til feed-synligheten (ADR-025) fantes, og nå gjorde den det. Kjernebeslutning: ingen ny rolle/tabell — "tilskuer" er ganske enkelt enhver som kan SE en turnering per tournament.visibility (ADR-018), utvidet til også å dekke LIVE-data (leaderboard, matcher, scorekort), ikke bare info-siden/programtidene som før. Bygget: GET /orgs/.../leaderboard, .../sessions/{id}/matches og .../matches/{id}/scorecard fantes allerede (organisator-/spiller-siden), men krevde org-medlemskap — en ren spectator kunne aldri se dem. Løst ved å ekstrahere den delte kjernelogikken til gjenbrukbare funksjoner (fetch_leaderboard i tournaments.py, fetch_matches i matches.py, fetch_scorecard i scoring.py — samme "gjort delt"-mønster som tidligere runder), og la tre nye offentlige endepunkter i registration.py kalle dem etter egen visibility-sjekk. own_team_ids() (blind_draw.py) gjort null-sikker (user_id: str | None) -- en anonym leser har per definisjon ingen egne lag, korrekt oppførsel er tom mengde (ser kun avslørte matcher), ikke en feil. To nye sikkerhetssjekker funnet under DESIGN, ikke i etterkant, samme disiplin som tidligere ADR-018 Beslutning B-lærdommen:

    1. session_id/match_id i URL-en må eksplisitt verifiseres å høre til NØYAKTIG tournament_id i samme URL — org_connection() setter kun TENANT-grensen (RLS), ikke at stiens id-er faktisk henger sammen. Uten dette kunne noen med tilgang til én offentlig turnering i en organisasjon lest en HVILKEN SOM HELST økt/match i samme organisasjon (inkl. en privat en) ved å gjette/prøve id-er.
    2. Scorekortet krever eksplisitt at BEGGE lag har låst oppstillingen (blind draw, ADR-013) — leaderboard/matchliste arver reveal-skjuling automatisk via own_team_ids(), men scorekortet har ingen tilsvarende innebygd sjekk. Bruker valgte omfang utover anbefalingen: BÅDE leaderboard+matchliste OG fullt hull-for-hull-scorekort per match i samme runde (anbefalingen var kun de to første). Frontend: ny /t/[id]/live-side (components/public-live.tsx) — fargesegmentert stillingsbar (faktisk + projisert, samme visuelle idé som den org-autentiserte leaderboard-skjermen, men egen enklere implementasjon siden komponentene har ulik autentiseringskontekst), utvidbar øktliste → matchliste → hull-for-hull-scorekort (fargede hull-chips per lag). Lenket fra hovedsiden (public-tournament.tsx) med en ny "Følg live"-knapp. Verifisert grundig mot fersk teecup_scratch (isolert teecup_app_scratch-rolle, isolert scratch-MinIO, engangs API-container): 16 automatiserte sjekker, inkl. en PRESIS test av den nye tenant-vs-sti- sjekken (ikke bare en ukjent id, men en EKTE ANNEN turnering i SAMME org — bekreftet at match/økt fra turnering A fortsatt ikke kan leses via turnering B sin offentlige URL), full blind-draw-skjuling FØR/ETTER reveal for en anonym leser, kode-overstyring (ADR-020) fungerer uendret for de nye endepunktene, scorekort eksplisitt nektet før reveal. Ekte typesjekket produksjonsbuild av frontend, inkl. den nye /t/[id]/live- ruten. Rullet ut live 2026-07-19, bruker bekreftet eksplisitt: ingen migrasjon, docker compose up -d --build teecup_api teecup_frontend, begge containere boot-et rent, /health/dashboard → 200, teeoff.no upåvirket.
  • "Følg live"-siden koblet til sanntid (2026-07-19, ADR-027): brukeren fulgte anbefalingen fra forrige runde — /t/[id]/live (bygget i tilskuer-runden samme dag) krevde omlasting for nye resultater. Bygget: ny, RUTEFRI modul app/realtime.py (in-memory tilkoblingsregister + broadcast_live_update(tournament_id)) -- ligger bevisst BAK alle routere i importgrafen for å unngå en sirkulær import (registration.py importerer allerede fra scoring.py/tournaments.py/ matches.py, og messaging.py importerer fra registration.py; en kringkastingsfunksjon i noen av routerne ville derfor bitt seg selv i halen). Kringkastingen er lagt INN I recompute_and_cache_match_state og apply_concession selv (sistnevnte fikk en ny påkrevd tournament_id- parameter) -- kallerne (hole-score/hole-result-innsending, walkover på både match- og turnering-nivå) trenger ikke huske å gjøre noe selv. Nytt WS-endepunkt /ws/public/tournaments/{id}/live i messaging.py (gjenbruker /ws/*-Caddy-ruten fra ADR-025 uendret -- ingen ny infrastruktur). Sender bevisst kun et "noe endret seg, hent på nytt"- signal, ikke selve dataene -- unngår å duplisere leaderboardets projeksjons-regnestykke/blind draw-filtrering i kringkastings-payloaden. Frontend: components/public-live.tsx åpner en WebSocket ved montering; hver melding teller opp en refreshKey som utløser refetch av leaderboardet ALLTID, og av en økts matcher/et scorekort KUN hvis det faktisk er utvidet/åpent på skjermen akkurat da (ingen unødvendige kall for lukket innhold). Liten pulserende prikk lagt til ved siden av "Følg live"-teksten som visuell bekreftelse. Verifisert i scratch: anonym avvist på live-WS for en org-synlig turnering (samme visibility-sjekk som REST), satt til public, deretter bekreftet FAKTISK sanntidsmottak (ikke bare at REST-svaret så riktig ut) for BÅDE et vanlig hull-resultat OG en walkover-konsesjon -- en åpen WebSocket-tilkobling mottok kringkastings-signalet i begge tilfeller. Leaderboard fortsatt korrekt lesbar over REST etterpå (regresjon). Ekte typesjekket produksjonsbuild kjørt og bekreftet. Rullet ut live 2026-07-19, bruker bekreftet eksplisitt: ingen migrasjon, ingen ny Caddy-endring, docker compose up -d --build teecup_api teecup_frontend, begge containere boot-et rent. Verifisert grundig etterpå: /health/dashboard → 200, teeoff.no upåvirket, OG et ekte wss://-håndtrykk mot den NYE /live-ruten over produksjons-https (ukjent turnering-id, ingen ekte data berørt) ga korrekt 404 fra selve applikasjonslaget.

Neste steg:

  1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
  2. Fortsatt åpne beslutninger fra FEATURE_BACKLOG.md: kode-regenerering for ADR-020, korrigering-godkjenning fra motpart, video/1-til-1-meldinger (bevisst utsatt i ADR-025).
  3. Flere turneringsformater utover Ryder Cup (Københavner/High-low-high/ Robbins/Try all, notert 2026-07-19) — ingen ADR-runde startet ennå.
  4. Merk for neste økt: deploy/Caddyfile-endringen (ny /ws/*-rute, ADR-025) ligger uncommitted i det SEPARATE /opt/teeoff-repoet, ikke i teecup-repoet — samme fallgruve som ADR-016-runden sin Caddy-endring, lett å glemme siden denne økten ellers kun har jobbet i /opt/teecup.
  5. Ved fremtidige nye V0-skjermer/-design i V0 (fortsett i samme prosjekt), FORVENT en full re-eksport hver gang — diff mot live-treet i et scratch-område før noe pakkes ut over eksisterende filer, og sjekk om V0-skjermen bygger inn handlinger backend ikke støtter ennå FØR integrering.