# TeeCup — Funksjons-backlog > Formål: fange ALT som er ønsket for TeeCup på ett sted, slik at intet krav går > tapt mellom økter, modeller eller samtaler. Kilder: de to opprinnelige > Gemini-samtalene (funksjonalitet + Docker) og arbeidet gjort med Claude. > > Status-koder: ✅ ferdig · 🔨 pågår · 📋 planlagt/fanget · ❓ trenger beslutning > · 🔀 endret fra opprinnelig råd · 💤 utsatt (bevisst) > > Sist oppdatert: 2026-07-17 --- ## Fundament (bygget) | Funksjon | Status | Notat | |---|---|---| | Handicap-/matchmotor (testet) | ✅ | 24 tester, R&A-verifisert. Erstatter Geminis løse JS-funksjon. | | Formater: singles, fourball, foursome, greensome, scramble | ✅ | I motoren, allowance som konfig. | | Brutto/netto + prosent-allowance (75 %, 90 %, 3/4) | ✅ | ADR-005. Prosentene bekreftet mot Golf GameBook (ADR-014). | | 9-hulls (front/back) slagfordeling | ✅ | ADR-008, egen funksjon + test. | | Miksede formater per turnering (økter) | ✅ | ADR-007. Ryder Cup-strukturen. | | Spiller-pool m/ reserver, delvis deltakelse | ✅ | ADR-007. | | Multi-tenant skjema + RLS | ✅ | ADR-001/003. Isolasjon bevist med oppførselstest. | | Dedikert app-rolle (ingen superuser/BYPASSRLS) | ✅ | migrasjon 002. | | API: oppsett (spillere/lag/roster/økter/matcher/blind draw) | ✅ | Verifisert for ekte mot scratch-db + engangscontainer. | | API: scoring (hole_score/match_hole_result, matchstatus, handicap-beregning) | ✅ | ADR-012/014. Verifisert for ekte, inkl. fourball better-ball og race-sikker recompute. | | Banedata fra teeoff via API | ✅ | ADR-004 → **ADR-019**, LIVE 2026-07-18. Import (kopi) ved eksplisitt organisator-valg, ikke live oppslag ved hver bruk. Se egen seksjon under. | | Konfigurerbar handicap-pipeline (4 brytere) | ✅ | ADR-014. Bygget i `app/handicap.py`, brukt av scoring-runden. | | Ekte autentisering (magic-link + JWT-sesjon) | ✅ | ADR-009. `app/routers/auth.py` + migrasjon `004_auth.sql`. `X-Debug-User-Id`-stubben er helt fjernet. | | RLS-tomstreng-fiks (`app_current_org()`) | ✅ | Migrasjon `005_rls_null_guard.sql`. Se detaljer under. | | Organisasjon-bootstrap (opprette ny org via API) | ✅ | `POST /orgs`, `app/routers/organizations.py`. Se detaljer under. | | Ekte SMTP-utsending av magic-link | ✅ | `app/email.py`. Se detaljer under. | | Containerisert, LIVE på `teecup.teeoff.no` | ✅ | `Dockerfile` + `docker-compose.yml`. Se egen seksjon under — to reelle driftshendelser funnet og rettet. | | Dato/klokkeslett på turnering/økt/match | ✅ | ADR-015. `session.scheduled_at` + `tee_interval_minutes` → utledet `match.tee_time`. Se egen seksjon under. | | Maskinlesbar feilkode-kontrakt (`{"code",...}`) | ✅ | ADR-015. Full retrofit, alle 39 tidligere steder. Se egen seksjon under. | | i18n-forberedelse (nb/en) | ✅ | ADR-015. `preferred_locale` + ekte engelsk e-postmal. Se egen seksjon under. | | Frontend: innlogging + verifisering, LIVE | ✅ | ADR-016. Next.js på `teecup.teeoff.no`, ekte magic-link-flyt bevist med reell e-post. Se egen seksjon under. | | Frontend: dashboard (org-bytter/-opprettelse + turneringsliste), LIVE | ✅ | `/dashboard`. Kablet mot `/auth/me`, `/orgs`, `/orgs/{id}/tournaments`. Skrive-flyt bekreftet med ekte data (org "Tjøme Gents" + turnering opprettet av bruker). | | Frontend: lag/roster-skjerm, LIVE | ✅ | `/tournaments/[id]`. To nye backend-endepunkter bygget samtidig (`PATCH`/`DELETE` roster). Skrive-flyt ikke testet med ekte data ennå. | | Selvregistrering + utvidet spillerprofil (API) | ✅ | ADR-017. `app/routers/registration.py` (offentlig, uautentisert), utvidet `players.py`, e-post-basert `player.user_id`-kobling ved innlogging. Backend komplett og live. Frontend-påmeldingsskjema/landingssider gjenstår (egen ADR-runde). | | Roster: endre kaptein / fjern spiller (`PATCH`/`DELETE`) | ✅ | `app/routers/tournaments.py`. Bevisst ingen "kun én kaptein"-håndhevelse ennå — se «Brukerroller»-punktet under. | | Egendefinerte baner (`course`) via API | ✅ | Nytt `app/routers/courses.py` (`GET`/`POST /orgs/{id}/courses`). Kun `source='custom'` — ADR-004s teeoff-integrasjon (`source='official'`) fortsatt kun vedtatt, ikke bygget. Se egen seksjon under. | | Frontend: program-skjerm (økter/tidsplan), LIVE | ✅ | `/tournaments/[id]/program`. Se egen seksjon under. | --- ### Frontend: innlogging + verifisering — ✅ BYGGET OG VERIFISERT LIVE 2026-07-17 - Første frontend-skjerm i prosjektet. Designet i V0 (login-skjerm, merkevare form/farge hentet fra Teeoff-logoen — IKKE navn/logo, se ADR-016s begrunnelse og tidligere samtale), hentet inn som `frontend/` (Next.js + Tailwind + shadcn/ui). - **Kvalitetsrunde på V0-output før bruk:** fargetokens i `globals.css` var OKLCH-TILNÆRMINGER av de forespurte hex-fargene (`#8bc24a`/`#ff5722`), ikke eksakte — regnet ut presise OKLCH-ekvivalenter og rettet alle 6 forekomster (lys/mørk/system-mørk). Fjernet `@vercel/analytics` helt (ga null verdi på egen-hostet infrastruktur, kun en unødvendig tredjeparts nettverkskall). Fjernet `typescript: { ignoreBuildErrors: true }` (bygget kjører nå ekte typesjekk). Fjernet dødt `pnpm.overrides`-felt. `frontend/.gitignore` manglet `.pnpm-store/` — årsaken til at VSCode viste 10 000 "ukjente" filer ved første commit-forsøk; rettet. - **Kablet mot ekte API** (`app/routers/auth.py` sine `/auth/request-link` og `/auth/verify-link`): `frontend/next.config.mjs` sin `rewrites()` proxyer `/auth/*`/`/orgs/*`/`/health` server-side til API-et — se ADR-016 for hele begrunnelsen (same-origin, ingen CORS, cookie uendret). `components/login-form.tsx` sender ekte `POST /auth/request-link`; ny `app/verify/page.tsx` + `components/verify-form.tsx` mottar `?token=...` fra e-postlenken (automatisk verifisering) ELLER viser et manuelt "lim inn koden"-felt (nødvendig fallback, ikke overflødig — se under). - **Backend-endring i samme runde:** `app/email.py`/`app/config.py` fikk en ny `PUBLIC_BASE_URL`-innstilling (default `https://teecup.teeoff.no`, valgfri) — e-postmalen sender nå en EKTE klikkbar lenke (`{base}/verify?token=...`) i tillegg til den rå koden som fallback (samme mal-mekanisme som i18n-runden, ADR-015). - **Containerisert og rullet ut LIVE** (ny `frontend/Dockerfile`, multi-stage, Next.js `output: "standalone"`; ny `teecup_frontend`-tjeneste i `docker-compose.yml`). Caddy (`teecup.teeoff.no`, i det SEPARATE `teeoff`-repoet) peker nå på `teecup_frontend` i stedet for `teecup_api` direkte — se ADR-016 for hvorfor, og CLAUDE.md-status for driftsdetaljene (samme stale-Caddy-inode-hendelse som containeriseringsrunden, løst likt). - **Verifisert med FAKTISK e-postlevering, ikke bare curl:** ekte `POST /auth/request-link` sendt til brukerens egen adresse over `https://teecup.teeoff.no`, ekte e-post mottatt med en ekte klikkbar lenke, lenken åpnet i nettleser, landet på en fungerende `/verify`-side, sesjon opprettet — brukeren bekreftet "jeg er tilsynelatende innlogget." Første gang en hel bruker-vendt flyt (ikke bare API-et isolert) er bevist ende-til-ende i produksjon. --- ### Dato/klokkeslett + feilkode-kontrakt + i18n-forberedelse — ✅ BYGGET OG VERIFISERT 2026-07-17 - Reist av brukeren rett før frontend-arbeidet: kan turnering/økt/match ha dato/klokkeslett, og må appen forberedes for flerspråklighet? Begge er API-kontraktspørsmål som er langt billigere å løse FØR frontend bygges enn å ettermontere — se ADR-015 for alle tre beslutningene i sin helhet. - **Migrasjon `006_scheduling_and_locale.sql`:** alt additivt (nullable/ default) — `tournament.end_date` (+ `CHECK` mot `start_date`), `session.scheduled_at`/`tee_interval_minutes`/`start_hole`, `match.tee_time_override`, `app_user.preferred_locale`, `magic_link_token.locale` (begge sistnevnte `CHECK IN ('nb','en')`). Kjørt permanent mot den ekte `teecup_db` (se under). - **Feilkoder:** ny `app_error(status_code, code, message)`-factory i `app/errors.py`, alle 39 tidligere norsk-hardkodede `HTTPException(..., detail="...")`-steder på tvers av 6 filer (`errors.py`, `auth.py`, `routers/auth.py`, `routers/tournaments.py`, `routers/matches.py`, `routers/scoring.py`) retrofittet til en delt ~15-kode-taksonomi. Endelig `grep -rn 'detail="' app/` ga NULL treff. - **Dato/tid i API-et:** `tournament.start_date` (fantes i skjemaet siden 001, men var ALDRI koblet til API-modellene før nå — funnet og fikset som en naturlig bivirkning av å legge til `end_date`) + `end_date` eksponert; `session` sine tre nye felt eksponert i `SessionCreate`/`SessionOut`; `match.tee_time` utledet i Python i både `create_match` og `list_matches` (ingen ekstra spørring — økten hentes allerede for andre formål der). - **i18n:** `MagicLinkRequest.locale` (`Literal["nb","en"]`, default `nb`) sendes av klienten, følger med i token-raden, brukes til å velge e-postmal OG (kun ved førstegangsopprettelse) `app_user.preferred_locale`. Ekte engelsk e-postmal lagt inn i `app/email.py` (ikke bare rørlegging) — konkret bevis på at flerspråklighet fungerer ende-til-ende, ikke bare at et felt eksisterer. - **Verifisert grundig mot en fersk `teecup_scratch`** (migrasjoner 001→006, `test_isolation.sql` 12/12 uendret, engangs-API-container): feilkode-form bekreftet på tvers av 5 filer (`NOT_FOUND`/`LIMIT_REACHED` fra tournaments.py, `DUPLICATE` fra roster, `NOT_AUTHENTICATED` uten cookie, `NOT_ROSTERED_ON_TEAM` fra matches.py sin `add_participant`); `tee_time` bekreftet å øke riktig per `sequence` (10 min intervall → 08:00/08:10/08:20), `tee_time_override` bekreftet å vinne over utledet verdi, økt uten `scheduled_at` bekreftet å gi `tee_time: null` (ikke feil); i18n bekreftet full runde — ny bruker med `locale:"en"` fikk `preferred_locale:"en"`, en ETTERFØLGENDE `request-link` for samme bruker med `locale:"nb"` skiftet e-postmalen men IKKE den lagrede preferansen (bekreftet uendret via `/auth/me`). - **Migrasjon 006 kjørt mot ekte `teecup_db` 2026-07-17**, bruker bekreftet eksplisitt i samme økt — kjørte rent, `test_isolation.sql` fortsatt 12/12. - **Mindre driftslærdom, funnet OG rettet samme runde:** et innledende `grep -v -i 'pass|secret|key'`-filter jeg brukte for å lese ikke-sensitive `.env`-nøkler fanget IKKE opp `TEECUP_DATABASE_URL`, som bar `teecup_app`-passordet innebygd i selve URL-en (i tillegg duplisert av det samme passordet som allerede lå rent i `TEECUP_APP_PASSWORD`) — passordet endte dermed synlig i et verktøyresultat. Flagget til brukeren umiddelbart (samme mønster som SMTP-passord-hendelsen under containeriseringsrunden). **Rettet:** `.env` bygget om til fem separate, rent navngitte felt (`TEECUP_DB_HOST/PORT/NAME/USER/PASS`), `app/config.py`+`app/db.py` bygger nå `asyncpg`-poolen fra disse i stedet for én DSN-streng, `docker-compose.yml` oppdatert tilsvarende. Se CLAUDE.md-status for full driftsdetalj, inkl. at dette krevde et fullt `docker compose up -d --build --force-recreate` (ikke bare `--force-recreate` — imaget bygger inn `app/` ved build-tid). Verifisert ende-til-ende mot den live stacken (`Application startup complete`, `/auth/me` ga rent `401` over ekte https, `teeoff.no` upåvirket). --- ### Containerisering og go-live — ✅ FERDIG 2026-07-16 - `POST /orgs` osv. var siste kodebit; dette var første gang noe rørte EKTE, PERMANENT infrastruktur (ekte `teecup_db`, ekte langtlevende container, den DELTE Caddy-instansen som også ruter live `teeoff.no`). - **Bygget:** ekte `teecup_db` opprettet, migrasjoner 001→005 kjørt permanent (samme filer, ingen endringer), `test_isolation.sql` bestått (ruller alltid tilbake, trygt å kjøre mot en database som skal bli stående). `Dockerfile` (speiler scratch-rundenes bevist-riktige volumoppsett: `app/` + `handicap_engine.py` på samme relative plassering) + `docker-compose.yml` (tjeneste `teecup_api`, joiner det eksisterende, eksterne `teeoff_default`-nettverket). Ny Caddy-blokk for `teecup.teeoff.no` i `/opt/teeoff/deploy/Caddyfile`. - **To reelle driftshendelser, begge funnet og rettet i sanntid:** 1. **Stale bind-mount-inode:** `teeoff_caddy` sin `Caddyfile`-mount er en ENKELTFIL-bind-mount, låst til inoden som fantes da containeren sist startet (13 dager tidligere). Fil-redigering via atomisk rename laget en ny inode på samme sti — containeren fortsatte å lese den GAMLE filen uansett hvor mange ganger `caddy validate`/`reload`/admin-API `/load` ble kjørt (alle opererte på den uendrede gamle filen, derav ingen synlig feil). Løsning: full `docker restart teeoff_caddy` (brukeren bekreftet eksplisitt — avvek fra planens "kun graceful reload"-løfte, ga noen sekunders nedetid for `teeoff.no`). 2. **Alvorlig nettverksalias-kollisjon** (oppdaget rett etter omstarten, da EKTE teeoff-trafikk — `/api/facilities?...` med ekte klubb-slugs som `borregaard-golfklubb` — dukket opp i `teecup_api` sin logg): `docker-compose.yml` sin service-nøkkel var `api:`, identisk med teeoffs eget `api`-servicenavn. Docker Compose registrerer nettverksalias etter service-NAVNET (ikke bare `container_name`) på delte nettverk, så begge containerne fikk alias `api` på `teeoff_default` — Caddys `reverse_proxy api:8000` i teeoffs egen config kunne da tilfeldig treffe enten ekte `teeoff_api` eller `teecup_api`, dvs. ekte brukertrafikk til teeoff.no kunne bli besvart av TeeCup-koden. **Rettet umiddelbart:** stoppet `teecup_api` først (hindre videre feilruting), ga service-nøkkelen navnet `teecup_api`, gjenopprettet, bekreftet med `docker network inspect` at alias `api` nå KUN peker på ekte `teeoff_api`. **Lærdom for fremtidige tjenester på delt nettverk:** ALLTID gi docker-compose sin service-nøkkel (ikke bare `container_name`) et prosjekt-unikt navn når flere uavhengige compose-prosjekter deler samme eksterne nettverk — service-navnet blir også et DNS-alias. - **Mindre driftslærdom (samlet):** (a) `.env` leses IKKE på nytt av en allerede kjørende container — `docker compose up -d --force-recreate` kreves etter enhver `.env`-endring; (b) et `#`-tegn i et upassordet `.env`-passord kuttes som kommentar av Compose sin parser, anførselstegn (helst enkle) løser det; (c) jeg eksponerte ved et uhell to secret-verdier i eget debug-output mens jeg feilsøkte en `.env`-korrupsjon (manglende linjeskift) — brukeren roterte passordet som forsiktighetsregel. - **Verifisert ende-til-ende mot den ekte, live stacken:** `teeoff.no` upåvirket gjennom hele prosessen; `https://teecup.teeoff.no/health` → 200 med automatisk utstedt TLS; full magic-link-innlogging (ekte e-post mottatt, `verify-link` ga `Secure`-flagget cookie siden vi nå er over ekte https, `/auth/me` fungerte med sesjonen). --- ### Ekte SMTP-utsending — ✅ BYGGET OG VERIFISERT 2026-07-16 - Brukeren la inn egne, uavhengige SMTP-credentials i `.env` (`TEECUP_SMTP_SERVER/PORT/USER/PASS`, `TEECUP_FROM_EMAIL` — ADR-009, ikke delt med teeoff). Kun nøkkelnavn ble lest for å bekrefte de fantes, ALDRI verdiene (CLAUDE.md sin sikkerhetsregel). - **Løsning:** ny `app/email.py` (`send_magic_link_email`, `smtplib` kjørt via `asyncio.to_thread` siden det er et synkront bibliotek — samme mønster som teeoffs egen fungerende utsending). Håndterer BEGGE vanlige SMTP-tilkoblingsmåter dynamisk (implisitt TLS på port 465 vs. STARTTLS på andre porter) siden porten bevisst ikke ble lest under planlegging. `app/config.py` fikk nye, valgfrie innstillinger (`SMTP_CONFIGURED` avledet fra at alle fem er satt) — IKKE `_required`, så scratch-/dev-testing fungerer fortsatt uten SMTP satt opp, via `TEECUP_DEV_LOG_MAGIC_LINKS`. - **Bevisst designvalg:** en driftsfeil i selve utsendingen (feil passord, SMTP nede, eller ingen leveringsmåte konfigurert i det hele tatt) logges kun server-side og endrer ALDRI klientens respons — alt annet ville brutt anti-enumereringsgarantien i `request-link` (klienten skal ikke kunne skille "e-posten finnes ikke" fra "e-posten finnes men utsendingen feilet"). - **Verifisert i to trinn:** (1) dev-log-flyten uendret uten SMTP satt (regresjonstest av eksisterende scratch-løype), (2) én ekte test-e-post sendt til en adresse brukeren oppga, med de ekte credentials videreført fra `.env` til scratch-containeren uten at jeg noensinne leste verdiene selv — **brukeren bekreftet mottak** av en e-post med innloggingskode. Dette er første gang noe i dette prosjektet er verifisert ved faktisk levering til en ekte, ekstern mottaker, ikke bare via curl/scratch-container. --- ### Organisasjon-bootstrap — ✅ BYGGET OG VERIFISERT 2026-07-16 - Gjennomgang av alle routere hadde avdekket at INGEN endepunkt opprettet en `organization`-rad — i alle testrunder denne økten var organisasjoner satt inn direkte med superbruker-SQL. En ekte førstegangsbruker hadde ingen vei til å opprette klubben/bedriften sin og bli owner. Reelt blokkerende, ikke en utsettbar produktbeslutning. - **Løsning:** nytt `POST /orgs {"name": ...}`, autorisert med `get_current_user` (ikke `get_authorized_org` — sirkulært før org-en finnes). Ingen ny migrasjon nødvendig. - **Selvrefererende RLS-bootstrap bekreftet å fungere** (kommentaren i 002 om en "privilegert sti" var ALDRI bygget og viste seg unødvendig): generer org-ens uuid i Python FØR innsetting, sett `app.current_org` til nøyaktig den verdien via den eksisterende `org_connection()`, sett så inn `organization`-raden med samme id. `org_self`-policyens implisitte `WITH CHECK` (id = `app_current_org()`) blir da trivielt sann. `teecup_app` (NOSUPERUSER/NOBYPASSRLS) trenger altså INGEN egen privilegert tilkobling for å bootstrappe sin egen første organisasjonsrad. - **Verifisert med 5 tester**, inkludert en negativ kontroll som beviser mekanismen er presis, ikke et RLS-hull: forsøkte å sette inn en organisasjon med en MISMATCHENDE id (annen enn `app.current_org`) — avvist med `insufficient_privilege`, som forventet. Også bekreftet: ny org fungerer normalt med eksisterende endepunkter (GET/POST tournaments), dukker opp riktig i `/auth/me`, og full kryss-org-isolasjon holder mellom to uavhengig opprettede organisasjoner. --- ### RLS-tomstreng-bug — ✅ FIKSET 2026-07-16 - Alle RLS-policyer i 001/003 (`org_isolation` på 14 tabeller + `org_self` på `organization`) brukte `current_setting('app.current_org', true)::uuid`. Denne håndterte NULL trygt (ga ingen rader, som tiltenkt), men IKKE tomstreng — en gjenbrukt asyncpg-pool-tilkobling der en TIDLIGERE forespørsel satte GUC-en via `SET LOCAL` kunne lese den tilbake som `''` etter at transaksjonen var ferdig, og casten kastet da en 500 i stedet for skjemaets lovede "trygg standard: se ingenting". - **Fiks:** samlet i én `STABLE` SQL-funksjon `app_current_org()` (migrasjon `005_rls_null_guard.sql`) som gjør `NULLIF(current_setting(...), '')::uuid` — konverterer tomstreng til NULL FØR cast. Alle 15 policyer alteret (`ALTER POLICY`) til å bruke funksjonen i stedet for det rå uttrykket. Verifisert med 3 nye regresjonstester i `test_isolation.sql` (Test 10-12: tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS ikke krasj, org-bootstrap-innsetting fungerer rett etter tomstreng- tilstand) OG ved å faktisk gjenskape original-buggen mot en ekte container (pool-størrelse 1, varm opp med `org_connection()`, deretter kall `/auth/me` på samme gjenbrukte tilkobling — gikk fra 500 til 200). - **Viktig presisering oppdaget underveis:** den opprinnelige planen antok at `/auth/me` kunne joine `organization` direkte igjen når tomstreng-buggen var fikset. Det var FEIL — fiksen gjør bare at tomstreng oppfører seg som NULL (trygt: se ingenting), den endrer IKKE at `org_self`-policyen krever en MATCHENDE `app.current_org` for å vise en rad i det hele tatt (riktig RLS-oppførsel, ikke en bug). 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 spørringer, N = antall org-er brukeren tilhører, typisk 1-3) — verifisert at dette faktisk returnerer navnet korrekt. --- ## Ønsket, men IKKE fanget før nå (fra Gemini-samtalene) ### Brukerroller (utover org-medlemskap) - **Status:** ❓ trenger beslutning - De opprinnelige samtalene beskriver: turneringsadmin, **lagkaptein**, spiller, **tilskuer** (les-only, følger live uten skriverettigheter). - Vi har i dag org-roller (owner/admin/member) + `is_captain` på roster. - **Mangler:** «tilskuer» som begrep. Henger sammen med hvem som ser den offentlige feeden (se Kommunikasjon). Kaptein-rollen bør kanskje gi spesifikke rettigheter (sette oppstilling), ikke bare være et flagg. - **Nytt 2026-07-18:** `PATCH .../roster/{id}` (sette/fjerne kaptein) håndhever bevisst IKKE «kun én kaptein per lag» — flere spillere kan i dag merkes kaptein samtidig på samme lag. Bør revurderes samtidig med resten av dette punktet, ikke løses isolert i roster-endepunktet. ### Scoring-autorisasjon: hvem fører, hvem korrigerer, hvem lukker (rejst 2026-07-16) - **Status:** ❓ trenger beslutning — direkte oppfølger av «Brukerroller» over. - **Hvem fører score i dag:** alle med en `team_roster`-rad på laget (samme minimale grense som deltaker/lås, se ADR-013-relatert kode). Ikke kaptein-only, ikke begrenset til de(n) som faktisk spiller matchen. - **Hvem kan korrigere en ført score i dag:** akkurat de samme — skriving er en upsert (`ON CONFLICT ... DO UPDATE`), så en korrigering er ikke skilt fra en førstegangs-innføring. Ingen godkjenning fra motstanderen, ingen audit-trail (forrige verdi overskrives sporløst). - **Match-lås ved avgjørelse: ✅ FIKSET 2026-07-16.** `submit_hole_score` og `submit_hole_result` (`app/routers/scoring.py`) avviser nå med 409 («Matchen er avgjort og kan ikke lenger endres.») FØR upserten kjøres, hvis `match.points_side_a IS NOT NULL` — dette er allerede et pålitelig, entydig signal siden kolonnen kun settes når `compute_match_state` sier matchen er avgjort. Gjelder BÅDE nye hull og korrigering av allerede talte hull. Verifisert: hull etter avgjørelse avvist, korrigering av et talt hull avvist, GET scorecard fortsatt leselig, ikke-avgjorte matcher upåvirket. **Bevisst IKKE bygget:** ingen manuell «avslutt match før den er avgjort»-handling (det grenser mot walkover under, fortsatt utsatt). Turnering har et `status`-felt (draft/active/completed/archived) men INGEN endepunkt endrer det ennå — organisator kan ikke markere en turnering ferdig via API-et i dag (eget, senere punkt). - **Manglende WO/konsesjon:** hvis en side aldri stiller nok spillere (`match_participant`-antallet når aldri det økten krever), beregnes handicap ALDRI (se `app/handicap.py`), og matchen kan derfor ALDRI få et hull-resultat — den blir hengende uavgjort for alltid. Det finnes ingen «gi bort hullet/matchen/turneringen»-mekanisme (walkover/konsesjon) i det hele tatt ennå — verken datamodell eller endepunkt. - **Avgjort 2026-07-16:** - **Match-lås ved avgjørelse: ✅ bygget** — se eget punkt over. Automatisk (ikke en handling noen utfører), så «hvem får låse» ble aldri et spørsmål som trengte avklaring. - **Walkover/konsesjon VENTER** til brukerroller (kaptein/organisator, punktet over) er avgjort — å bygge den nå på dagens løse «rostret på laget»-grense betyr sannsynligvis å bygge den om senere. - **Fortsatt åpent:** (a) skal score-føring begrenses til faktiske matchdeltakere (ikke bare «noen på laget»)? (b) skal korrigering kreve motpartens godkjenning, eller er upsert-modellen god nok for v1? (c) skal turnering-status (draft/active/completed/archived) kunne settes via API? ### Blind draw (skjult lagoppstilling) - **Status:** ✅ skjema (migrasjon 003, `lineup_lock`) + API bygget og verifisert (`app/routers/matches.py`: synlighetsfilter i SQL, ikke Python-filter — se ADR-013). - Kapteinene låser oppstillingen skjult; matchene avsløres samtidig når begge er ferdige. - **Gjenstår:** hvem som FÅR låse et lag er i dag bare «rostret på laget», ikke kaptein-spesifikt — se «Brukerroller» over. ### Forenklet scoreføring (uten slagtall) - **Status:** ✅ skjema (migrasjon 003) + API bygget og verifisert (`app/routers/scoring.py`: `hole-scores` for `stroke`-modus, `hole-results` for `hole_result`-modus, begge mater samme `compute_match_state`). ### Bøtekasse (Kangaroo Court) - **Status:** 📋 planlagt - Meld inn overtramp med bøter («kastet kølla på hull 4»). - Henger sammen med Kommunikasjon: bøter deles i feeden. Sannsynligvis en egen post-type i meldingsmodellen. ### Flerårig statistikk / historikk / MVP - **Status:** 📋 planlagt (datamodell støtter det allerede) - Seiersprosent i singelmatcher, historisk MVP, statistikk over år. - Datamodellen tillater dette fordi spillere lever på org-nivå og gjenbrukes på tvers av turneringer, og handicap fryses per turnering (reproduserbart). ### Push-varsler - **Status:** 📋 planlagt (infrastruktur) - «BREAKING: X vant matchen». PWA push. Egen infrastruktur-bit. ### Sanntid (WebSockets) - **Status:** ❓ trenger beslutning - Live leaderboard og chat som oppdateres uten refresh. Vi har leaderboard-viewet, men ikke sanntidsleveringen. Valg: WebSockets vs. polling. ### Knockout / cup-turnering (egen turneringstype) - **Status:** 💤 utsatt (egen fremtidig type, ADR-011) - Utslagsmatcher: 128 spillere → finale + bronsefinale. Rundene er *avhengige* (vinner går videre), krever bracket/progresjon, seeding, fripass. IKKE i v1. - v1 er låst til nøyaktig to lag (Ryder Cup-format). Match-modellen er generell, så denne typen kan legges til senere uten dataomskriving — den legger bare til et progresjons-lag. --- ## Kommunikasjon (under design) | Del | Status | Notat | |---|---|---| | Lag-intern chat («det hemmelige rommet») | 🔨 | Bekreftet ønsket. Kanal m/ scope `team`. | | Offentlig runde-feed («Banter Board») | ❓ | Synlighetsnivå fortsatt ikke besluttet for FEED-en spesifikt, men mekanismen finnes nå: `tournament.visibility` + `get_current_user_optional`/`_is_participant()` (ADR-018) er bygget og live — gjenbruk dette, ikke bygg en ny mekanisme. | | Bilder i feed/chat | 📋 | v1. Objektlagring (MinIO), presigned opplasting. | | Video | 💤 | Arkitekt for det, bygg senere (ADR-forslag). | | 1-til-1 direktemeldinger | 💤 | Gemini frarådet for v1; ikke etterspurt av deg. | | Moderering (for offentlig innhold) | ❓ | Kreves hvis «alle med lenken». Mønster finnes i teeoff. | --- ## Landingssider (turnering + organisasjon) — ADR-018 ✅ HELT FERDIG 2026-07-18 Reist av brukeren 2026-07-18, rett etter registrerings-ADR-en (ADR-017). Backend, begge frontend-skjermer, Open Graph-metadata OG MinIO-bilde- opplasting (med AVIF-konvertering) bygget og live samme dag. Kun selve dra-og-slipp-opplastingsskjermen i frontend (V0) gjenstår. | Del | Status | Notat | |---|---|---| | Synlighetsnivå: offentlig / kun org-medlemmer / kun turnering-deltakere | ✅ | `tournament.visibility` (default `org`, trygg standard) + `organization.public_profile`. Håndheves eksplisitt i `registration.py` — RLS løser IKKE dette alene (se ADR-018 Beslutning B, reell presisering funnet under bygging). Samme trenivå-modell som «Banter Board» under bør gjenbruke dette. | | Registrering følger samme synlighetsgrense | ✅ | ADR-018 Beslutning D, bekreftet med bruker FØR bygging. | | "Deltaker"-tilgang (ikke org-medlem, men rostret/registrert) | ✅ | Ny `get_current_user_optional` + `_is_participant()`. Testet: rostret spiller som logget inn fikk tilgang, tilfeldig fremmed ble avvist. | | Turnering-landingsside: tekst, program, sponsorer, påmelding (API) | ✅ | `description`-felt, `tournament_sponsor`-tabell (navn+lenke), `GET /public/tournaments/{id}/sessions` (gjenbruker blind draw-lås fra ADR-013). Selve SKJERMEN i frontend gjenstår. | | Org-landingsside: klubbprofil, liste over turneringer (API) | ✅ | `GET /public/orgs/{slug}` — kun `public`-synlige turneringer. Bekreftet: klubb-profil KAN være offentlig (brukerens valg). | | Lesbar URL (slug) for organisasjon | ✅ | Fantes faktisk allerede i skjemaet siden migrasjon 001 (oversett, funnet da migrasjon 009 feilet mot scratch — se CLAUDE.md-status). Kun `CHECK`-constraints lagt til i 009. | | Organisator kan faktisk SETTE disse feltene | ✅ | Implisitt hull fylt under bygging: `PATCH /orgs/{id}/tournaments/{id}` (visibility/description/registrering), `PATCH /orgs/{id}` (slug/public_profile), full sponsor-CRUD. | | Bilder (hero, sponsorlogoer), backend | ✅ | Ny `teecup-minio`-tjeneste, ekte multipart-opplasting → AVIF-konvertering (Pillow) → lagring, live på `teecup.teeoff.no/teecup-media/*`. Selve opplastingsskjermen i frontend (V0) gjenstår. | | Del-metadata (Open Graph: og:title/og:description) | ✅ | `generateMetadata()` på `/t/[id]`+`/clubs/[slug]`, ekte data fra API-et, verifisert mot produksjonsimaget. `og:image` gjenstår (MinIO). | | Blind draw-skjuling på offentlig side | ✅ | Arves automatisk via delt `_fetch_sessions()`-hjelpefunksjon (ADR-018 Beslutning E) — ikke reimplementert. | | Antall påmeldte / ledige plasser vist åpent | ✅ | `confirmed_count` i `GET /public/tournaments/{id}`. | | Frontend: turnering-landingsside + påmeldingsskjema, LIVE | ✅ | `/t/[id]`. Tre bekreftelsestilstander (bekreftet/venteliste/godkjenning venter). Verifisert med ekte `POST`-registrering mot scratch. | | Frontend: org-/klubb-landingsside, LIVE | ✅ | `/clubs/[slug]`. Gjenbruker `TournamentCard` (nå med valgfri `orgId`) for turneringslisten. Verifisert: viser kun `public`-synlige turneringer. | --- ### Program-skjerm (økter/tidsplan) — ✅ BYGGET OG SCRATCH-VERIFISERT 2026-07-18 Sjette V0-skjerm, første i "bygg i rekkefølgen ting brukes"-serien (etter lag/roster kommer program, så blind draw, scorekort, leaderboard). `components/tournament-program.tsx`, ny rute `/tournaments/[id]/program`. Tidslinje over turneringens økter i rekkefølge + opprett-skjema (format, hullomfang, scoringsmodus, poeng, klokkeslett+intervall, starthull, en kollapsbar "avansert"-seksjon med ADR-014s fire handicap-brytere). **Reelt blokkerende hull funnet FØR integrering, ikke etter:** `SessionCreate.course_id` er påkrevd, men det fantes INGEN vei til å skaffe en gyldig én — ingen `course`-endepunkt i API-et i det hele tatt, og ADR-004s teeoff-integrasjon er fortsatt bare vedtatt (ingen utgående HTTP-klient finnes noe sted i koden). Spurte bruker eksplisitt (samme mønster som andre scope-avklaringer) — svar: bygg enkel course-CRUD nå. Lagt til `app/routers/courses.py` (`GET`/`POST /orgs/{id}/courses`, kun `source='custom'`, ingen hull-/tee-/rating-detaljer denne runden — motoren bruker foreløpig kun `course_id` som fremmednøkkel). Program-skjemaet fikk et type-ahead-felt for bane (samme mønster som spiller-type-ahead i roster-skjermen: søk blant org-ens eksisterende baner, eller opprett ny inline). **Reell korrekthetsfeil funnet og rettet FØR den nådde V0-designet i det hele tatt hadde blitt integrert:** 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 (antall spillere per side er del av selve formatet) — ren `"scramble"` avvises med 400. Rettet i frontend-mappingen til to egne segment-knapper. **`allowance_override`-mapping verifisert eksakt mot motor-kontrakten:** `app/handicap.py` sin `parse_allowance_config`/`_strategy_from_json` forventer `{type: "combined"|"per_player", percentage: 0..1}` — IKKE en flat prosent. Frontend velger riktig `type` ut fra om formatet er side-enhet (foursome/ greensome/scramble_2/scramble_4 → `combined`) eller spiller-enhet (fourball/ singles → `per_player`), og konverterer skjemaets 0–100-prosentfelt til 0–1-brøk før sending. Full JSON-rundtur bekreftet i scratch (se under) — ikke bare antatt riktig fra å lese motorkoden. **Verifisert grundig mot fersk scratch-infrastruktur** (ny `teecup_scratch`- database 001→009 + en ISOLERT `teecup_app_scratch`-rolle som arver `teecup_app` sine grants via `GRANT teecup_app TO teecup_app_scratch` — bevisst IKKE den ekte `teecup_app`-rollen, siden den nå er produksjonskritisk og rollen er cluster-global på tvers av `teecup_db`/ `teecup_scratch`; en tidligere plandokument sin "drop teecup_app-rolle"- opprydning er utdatert etter go-live og ble bevisst IKKE fulgt + isolert scratch-MinIO-container): courses opprettet+listet, kryss-org-isolasjon bekreftet (org 2 ser ikke org 1 sin bane), økt opprettet med klokkeslett, økt opprettet med `scramble_4` + full `allowance_override`-rundtur, gammel `"scramble"`-verdi korrekt avvist (400), `test_isolation.sql` fortsatt 12/12. Ekte typesjekket PRODUKSJONSBUILD (samme `Dockerfile`/multi-stage som faktisk deployes, ikke bare en dev-server) kjørt og bekreftet — ny `/tournaments/[id]/program`-rute listet korrekt i build-output. **Diffet V0-eksporten mot live-treet før noe ble tatt inn** (samme mønster som alle tidligere runder): kun tre reelt nye filer (`tournament-program.tsx`, `program/page.tsx`, `ui/switch.tsx`) — resten var forventede full-reverts av allerede tilpassede filer, ikke rørt. **Mindre justeringer utover selve V0-promptet:** fjernet V0s dev-only "forhåndsvis tom/med økter"-knapperad (ikke noe en ekte organisator skal se); lagt til en fanerad ("Lag og spillere" / "Program") i BÅDE `tournament-detail.tsx` og den nye skjermen, siden V0 ikke visste om den andre skjermen når den ble generert i en egen prompt. **Rullet ut live 2026-07-18**, bruker bekreftet eksplisitt: begge containere (`teecup_api`, `teecup_frontend`) bygget og redeployet, live sjekker OK (`/health`, `/dashboard` → 200), `teeoff.no` upåvirket. --- ### Offisiell banedata fra teeoff — ✅ BYGGET OG LIVE 2026-07-18 (ADR-019) Bevisst sidesprang fra "bygg i rekkefølgen ting brukes" rett etter program-skjermen: brukeren påpekte at ADR-004s teeoff-integrasjon fortsatt bare var vedtatt, ikke bygget. Full design i ARCHITECTURE_DECISIONS.md ADR-019 (fem delbeslutninger). Kort: organisator søker blant teeoff sine baner i program-skjemaet, velger én, og teecup KOPIERER bane+hull+tee+ tee_rating inn i `teecup_db` (`source='official'`) — ikke et live oppslag ved hver bruk. Ny `app/teeoff_client.py` (ren HTTP-klient mot `http://teeoff_api:8000`, internt Docker-nettverk, ingen auth trengs — begge containere deler allerede `teeoff_default`). To nye endepunkter i `app/routers/courses.py`: `GET .../courses/official-search[/{slug}]` og `POST .../courses/official-import`. Migrasjon `010` (unik `external_course_ref` per org, hindrer dupliserte importer). **Bevisste avgrensninger for denne runden:** kun 18-hulls baner kan importeres (teeoffs skjema har ingen egen 9-hulls-inndeling); kun `full_18`-rating importeres (teeoff har ingen separat front9/back9-rating, samme valg som ADR-008 allerede tok for egendefinerte baner); ufullstendige teeoff-data (manglende par/hcp_index på et hull, eller en tee uten NOEN rating) avviser hele importen tydelig (`EXTERNAL_DATA_INCOMPLETE`) FØR noe skrives, ikke en delvis importert bane. **Verifisert grundig, inkludert mot EKTE `teeoff_api`** (ikke en simulert respons): søk, anlegg-/banevalg, og import kjørt reelt mot den kjørende produksjonscontaineren (kun lesing) — importerte Borregaard Golfklubb sin hovedbane, bekreftet alle 18 hull + 8 tee/tee_rating-rader riktig i databasen, og opprettet en ekte økt med den importerte banen (beviser hele veien til handicap-motoren, ikke bare selve importen). Reimport avvist (409), kryss-org-isolasjon bekreftet, ukjent teeoff-slug ga 404, `test_isolation.sql` 12/12, ekte typesjekket produksjonsbuild av frontend-utvidelsen. **Rullet ut live**, bruker bekreftet eksplisitt: migrasjon 010 mot ekte `teecup_db`, begge containere redeployet, `teeoff.no` upåvirket. **Reell bug funnet og fikset samme dag, av en bruker som faktisk testet funksjonen:** bane-søkeboksen ("Hent bane fra teeoff") var et `