teecup/FEATURE_BACKLOG.md
Erol Haagenrud 05a451ff75 Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.

Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.

Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.

Gjenstår, bevisst utsatt:

E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00

29 KiB

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. 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.
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. 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.

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 apiteeoff_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_selforganization) 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å ikke besluttet (deltakere/org/offentlig lenke) — SAMME trenivå-modell som landingssider under, bør trolig løses med ÉN delt mekanisme, ikke to separate.
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) — idé, ikke besluttet

Reist av brukeren 2026-07-18, rett etter registrerings-ADR-en (ADR-017). Egen ADR-runde planlagt SENERE — for mange åpne spørsmål til å bygges sammen med registrerings-API-et.

Del Status Notat
Synlighetsnivå: offentlig / kun org-medlemmer / kun turnering-deltakere Eksplisitt krav fra brukeren, må ikke glemmes. Samme trenivå-modell som «Banter Board» over — bør trolig dele mekanisme, ikke løses to ganger. Sannsynligvis et felt på turnering (og org?) som styrer hvilken av de tre gruppene som får se siden.
Turnering-landingsside: hero, tekst, program, sponsorer, påmelding 📋 Innhold bør endre seg med turneringens livssyklus (meld deg på → følg live → se resultater) — samme URL, ulikt hovedbudskap avhengig av dato/status.
Org-landingsside: klubbprofil, liste over turneringer Uavklart om dette i det hele tatt skal være en offentlig, søkbar profil, eller bare en intern hub med delbare turneringslenker.
Lesbar URL (slug) for organisasjon 📋 Trengs for at en delt lenke skal se ut som noe («teecup.teeoff.no/tjome-gents»), ikke en UUID. Nytt felt på organization.
Bilder (hero, sponsorlogoer) 📋 Krever objektlagring (MinIO) — arkitektur-invarianten finnes alt (se CLAUDE.md), men MinIO er ALDRI satt opp i praksis. Første funksjon som faktisk trenger det.
Del-metadata (Open Graph: og:title/og:image/og:description) 📋 Lett å glemme, høy verdi — dette ER siden folk faktisk deler i WhatsApp/e-post.
Blind draw-skjuling på offentlig side 📋 Må respektere ADR-013 (skjult oppstilling til begge lag er låst) akkurat som resten av appen — en offentlig side må IKKE lekke dette ved et uhell.
Antall påmeldte / ledige plasser vist åpent 📋 Bruker tournament.registration_capacity fra ADR-017 direkte, ingen ny data nødvendig.

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).