# 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-16 --- ## 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. Endret fra Geminis «delt database direkte». | | 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. | --- ### 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. ### 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å ikke besluttet (deltakere/org/offentlig lenke). | | 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. | --- ## UX / frontend (senere fase) - 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp (banebruk i sollys/med solbriller). - ✅ Offline-first (ADR-006) — prinsipp besluttet; implementasjon senere. - 📋 PWA: manifest, service workers, «Legg til på hjemskjerm». --- ## Bevisst endret fra opprinnelige (Gemini-)råd - 🔀 **Banedata:** API mot teeoff (ADR-004), IKKE direkte delt database. Direkte DB-kobling ville låst TeeCup til teeoffs skjemaendringer. - 🔀 **Handicap-motor:** egen testet Python-modul, ikke den innlimte JS-funksjonen (som bl.a. ikke håndterte 9-hull eller konfigurerbare allowances korrekt). - 🔀 **Tenant-modell:** organisasjon som tenant med RLS, ikke bare «turnering-ID». - 🔀 **Sesjons-secret:** egne secrets for TeeCup, ikke fallback til teeoffs (teeoff selv bruker en slik fallback — bevisst unngått her).