# 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-08-20 (funksjonelle hull funnet under design- > dokumentasjon-runden notert, se seksjonen nederst) --- ## 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 CHANGELOG.md 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 CHANGELOG.md 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) — ADR-023 + ADR-026 - **Status:** ✅ HELT FERDIG 2026-07-19 — kaptein/deltaker-delen (ADR-023) OG tilskuer-delen (ADR-026, se eget punkt lenger ned). - 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. - **2026-07-19, ✅ BYGGET (ADR-023):** Kaptein er nå en REELL autorisasjonsrolle, ikke bare et merke. `app/team_authz.py` sin nye `user_is_team_captain` (erstatter `user_may_act_for_team`) krever `is_captain=true` (eller org-eier/admin) for å legge til/fjerne deltakere og låse et lag (`matches.py`). Bevisst unntak — funnet ved å faktisk sjekke ekte produksjonsdata FØR utrulling: har laget INGEN utpekt kaptein ennå, godtas enhver rostret spiller i stedet (ellers ville «De Unge»-laget i den ekte «De Gamle er Eldst»-turneringen vært låst ute umiddelbart — 0 av 2 roster-rader er i dag merket kaptein der). - **2026-07-19, ✅ BYGGET (ADR-023):** «Kun én kaptein per lag» håndheves nå — `PATCH`/`POST .../roster` (`tournaments.py`) fjerner automatisk kapteinmerket fra andre rader på samme lag når en ny kaptein settes. Nødvendig konsekvens av at kaptein nå er en autorisasjonsrolle. Ingen eksisterende lag hadde flere kapteiner (sjekket mot ekte data), så ingen opprydning av data var nødvendig. - **2026-07-19, ✅ BYGGET (ADR-023):** Score-føring/-korrigering begrenset til matchens FAKTISKE deltakere (`app/team_authz.py` sin nye `user_is_match_participant`, brukt av `scoring.py`) — ikke lenger «noen på laget», og uavhengig av kapteinmerket. Erstatter «Scoring-autorisasjon»- punktet under, spørsmål (a) er dermed besvart. - **Tilskuer — ✅ HELT FERDIG, LIVE 2026-07-19 (ADR-026),** rett etter Kommunikasjon (ADR-025) som gjorde feed-synligheten (som denne skulle defineres sammen med) klar. Ingen ny rolle/tabell — «tilskuer» er ganske enkelt enhver som kan SE turneringen (`tournament.visibility`), nå også for LEADERBOARD, MATCH-LISTE og FULLT SCOREKORT (hull-for-hull), ikke bare info-siden/programtider som før (`GET /public/tournaments/{id}/ leaderboard`, `.../sessions/{id}/matches`, `.../matches/{id}/scorecard`, alle i `registration.py`, gjenbruker eksisterende `fetch_leaderboard`/`fetch_matches`/`fetch_scorecard`). Match-listen arver blind draw-skjuling (ADR-013) automatisk via `own_team_ids()` gjort null-sikker for en anonym leser (tom mengde = kun avslørte matcher). Scorekortet har en eksplisitt reveal-sjekk i tillegg. Ny `/t/[id]/live`- side (leaderboard-bar, øktliste, matcher, hull-for-hull-scorekort), lenket fra hovedsiden. Bruker valgte å bygge fullt scorekort med i denne runden (utover opprinnelig anbefaling om kun leaderboard+matchliste). - Se ARCHITECTURE_DECISIONS.md ADR-023 (kaptein/deltaker) og ADR-026 (tilskuer) for alle delbeslutningene, og CHANGELOG.md for scratch-verifiseringen (15 + 16 automatiserte sjekker). ### Scoring-autorisasjon: hvem fører, hvem korrigerer, hvem lukker (rejst 2026-07-16) - **Status:** ❓ delvis avgjort — direkte oppfølger av «Brukerroller» over. - **Hvem fører score i dag (2026-07-19, ADR-023):** kun matchens FAKTISKE deltakere, ELLER org-eier/admin — `app/team_authz.py` sin `user_is_match_participant`. Byttet fra «noen på laget» samme dag. Se «Brukerroller» over for full begrunnelse. - **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-status kan nå settes via API — se eget punkt lenger ned. - **Walkover/konsesjon — ✅ BYGGET OG LIVE 2026-07-19 (ADR-024).** Løste det opprinnelige hullet: en side som aldri stiller nok spillere kan nå avsluttes ved at den TAPENDE siden (kaptein, eller org-admin) erklærer walkover — ensidig, ingen bekreftelse fra motparten. `POST /orgs/{id}/ matches/{id}/concede` (én match) og `POST /orgs/{id}/tournaments/{id}/ concede` (gi opp ALLE ikke-avgjorte matcher laget har, i én operasjon — v1 er låst til nøyaktig to lag, så det finnes bare én motstander). Kan erklæres uansett hvor mange hull som allerede er registrert (poengmessig teller kun seier/tap/delt, ikke marginen) — allerede registrerte hull forblir urørt i scorekortet. Se ARCHITECTURE_DECISIONS.md ADR-024. Frontend: `session-scorecard.tsx` (gi opp én match) og `tournament-detail.tsx` (gi opp resten av turneringen, per lag). - **Turnering-status via API — ✅ BYGGET OG LIVE 2026-07-19.** `status` lagt til i `TournamentUpdate` (`app/routers/tournaments.py`), settes via det eksisterende `PATCH /orgs/{id}/tournaments/{id}` (ekte PATCH-semantikk uendret — et utelatt `status`-felt lar verdien stå urørt). `status` er, ulikt de andre PATCH-bare feltene, en EKTE Postgres ENUM-type (`tournament_status`, migrasjon 001) — den generiske SQL-byggeren i `update_tournament` fikk et eksplisitt `::tournament_status`-cast for akkurat dette feltet, funnet og løst FØR noe ble antatt riktig (verifisert i scratch at castet faktisk trengs). Ingen egen tilstandsmaskin/ overgangsregler — enhver org-medlem med skrivetilgang kan sette hvilken som helst av de fire verdiene, samme tillitsnivå som resten av appen. Frontend: `tournament-status-badge.tsx` fikk en ny redigerbar `TournamentStatusPicker` (dropdown, optimistisk oppdatering med rollback ved feil), koblet inn i `tournament-detail.tsx` sin header. - **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. - **Fortsatt åpent:** (a) ~~skal score-føring begrenses til faktiske matchdeltakere~~ ✅ avgjort/bygget 2026-07-19 (ADR-023, se over). (b) skal korrigering kreve motpartens godkjenning, eller er upsert-modellen god nok for v1? (c) ~~skal turnering-status kunne settes via API~~ ✅ avgjort/ bygget 2026-07-19 (se over). ### 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). **Frontend LIVE 2026-07-18:** `/tournaments/[id]/sessions/[sessionId]`, `components/session-blind-draw.tsx`. Ny `DELETE .../matches/{id}/ participants/{id}` (kunne ikke angre et valg før låsing uten den). Fant og fikset et "skriv blindt"-hull: org-admin kunne legge til deltakere på et lag de ikke er rostret på (forrige rundes utvidelse), men `own_team_ids()` i `app/blind_draw.py` viste dem aldri tilbake før reveal — utvidet til samme owner/admin-regel som skrive-siden. Se CHANGELOG.md for full runde, inkl. en tredje, urelatert 500-bug (manglende handicap-indeks) funnet og fikset samtidig. - Kapteinene låser oppstillingen skjult; matchene avsløres samtidig når begge er ferdige. - **2026-07-19 (ADR-023):** hvem som FÅR låse et lag er nå kaptein-spesifikt (eller org-eier/admin) — se «Brukerroller» over. Med fallback for lag uten utpekt kaptein ennå. - **Nytt 2026-07-18, ✅ BYGGET (fant og fikset rett før frontend-skjermen):** `match_participant.tee_id` er påkrevd, men det fantes INGEN vei til å liste EN banes tee-er (selv offisielt importerte), og egendefinerte baner har ALDRI hatt noen vei til å FÅ tee-er — et hull som fantes fra før ADR-019, ikke noe den innførte. Uten dette kunne en turnering på en manuelt navngitt bane aldri få en ekte deltaker på noen match. Ny `GET/POST /orgs/{id}/courses/{id}/tees` (kun full_18-rating, offisielle baner avviser manuell tee-opprettelse). **Presisert eksplisitt: dette er IKKE en teeoff.no-kodeendring** — egendefinerte baner er per definisjon utenfor teeoffs katalog, så dette er en ren teecup-intern funksjon. **Bevisst utenfor omfang:** hull-/stroke-index-data for egendefinerte baner (trengs i SCORING-fasen, ikke blind draw) — egen, senere sak når scorekort-skjermen bygges. ### 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`). - **Nytt 2026-07-18, ✅ BYGGET (forarbeid før scorekort-skjermen):** `GET/POST /orgs/{id}/courses/{id}/holes` løser hull-/stroke-index-hullet fra tee-runden (kun `stroke`-modus trengte det). `GET scorecard` utvidet med `stroke_entries`/`hole_result_entries` (rå registrerte tall, ikke bare utledet vinn/tap/delt) — uten dette kunne ikke en gjenlastet scorekort-side vise gjeldende tilstand. Verifisert med en full `stroke`-runde med ekte handicap-justering på en egendefinert bane. - **Frontend LIVE 2026-07-18:** `/tournaments/[id]/sessions/[sessionId]/ matches/[matchId]`, `components/session-scorecard.tsx`. Ett hull om gangen med store steppere/knapper (banebruk i sollys, ikke et regneark — matcher UX-visjonen lenger ned i denne fila). Lenket fra blind draw sin avslørte visning. Ingen nye backend-hull denne runden. ### 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). ### Leaderboard (turnering-total) - **Status:** ✅ HELT FERDIG 2026-07-18 — backend + frontend LIVE (`components/tournament-leaderboard.tsx`, `/tournaments/[id]/ leaderboard`). Siste skjerm i "bygg i rekkefølgen ting brukes"-serien for match-play-flyten (oppsett → program → blind draw → scorekort → leaderboard, alle live). - Summerer poeng på tvers av ALLE økter/matcher, total + per-økt-delsum. **Reelt korrekthetshull designet rundt før bygging:** `match.team_a_id`/ `team_b_id` er ikke garantert konsistent på tvers av matcher (settes per match) — summering skjer derfor KUN på ekte lag-id, aldri på "a"/"b"- labelen. Verifisert med et scratch-scenario med en bevisst byttet om team_a/team_b i én match — riktig total og per-økt-delsum likevel. ### Push-varsler - **Status:** ✅ HELT FERDIG 2026-07-28 — se "Varsler: push til telefon + in-app varslingssenter" lenger ned i denne filen for full detalj. Denne linjen var et tidlig notat som aldri ble fjernet da resten ble bygget (stale-opprydding 2026-08-14). ### Sanntid (WebSockets) - **Status:** ✅ HELT FERDIG, LIVE 2026-07-19 (ADR-025 + ADR-027). Koblet til BÅDE meldinger (ADR-025) OG `/t/[id]/live` (leaderboard/matcher/ scorekort, ADR-027). Ny, rutefri delt modul `app/realtime.py` (unngår sirkulær import mellom `messaging.py`/`registration.py`/`scoring.py`/ `tournaments.py`) — sender et "noe endret seg"-signal (ikke selve dataene) hver gang `recompute_and_cache_match_state`/`apply_concession` kjører, klienten henter de vanlige REST-endepunktene på nytt. Samme in-memory-per-prosess-begrensning som meldinger. ### 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. ### Flere turneringsformater utover Ryder Cup (reist 2026-07-19) - **Status (2026-07-30): ✅ BACKEND FERDIG for alle åtte formater** (Chapman/ Try-all, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball/Lone Ranger, High-low-high) — motor, migrasjoner, API-wiring for frittstående runder OG org-scopede turneringer (lag ELLER individuell, avhengig av formatets struktur), scratch-verifisert grundig i BEGGE systemer for hvert format (engine-enhetstester FØRST, deretter full API-verifisering mot ekte HTTP-endepunkter). **Frontend fortsatt IKKE bygget** — se eget avsnitt nederst i denne seksjonen for full detalj, migrasjonsnumre, og kjente v1-avgrensninger per format. "Robbins" ble bevisst DROPPET av bruker (ikke bekreftet i noen kilde). Se ADR-en i ARCHITECTURE_DECISIONS.md for arkitekturbeslutningene. - Brukeren ønsker at TeeCup etter hvert skal støtte flere turneringsformater enn dagens rene to-lags Ryder Cup-modell (ADR-011). Fire konkrete eksempler gitt, med ulik arkitektonisk konsekvens (fra «passer nesten inn i dag» til «krever en helt egen datamodell») — notert ordrett under, ikke forenklet, for at reglene skal være presise når dette tas fatt på: **«Københavner»** (engelsk: trolig **"Copenhagen"** — direkte oversettelse, brukes noen steder i engelskspråklig golf-litteratur om nettopp dette poengsystemet, men usikkert om det er en universelt anerkjent standardbetegnelse; verdt å dobbeltsjekke før navnet ev. brukes i UI-et). 3 spillere, alle mot alle (INGEN lag). 6 poeng fordeles på hvert hull etter relativ plassering: vinner alene → 4-2-0. Vinner + de to andre deler → 4-1-1. To deler beste score, én taper → 3-3-0. Alle likt → 2-2-2. Flest poeng totalt etter runden vinner. **Størst arkitektonisk avstand fra i dag:** ingen to-siders match i det hele tatt, individuelt felt med poeng-per-hull-fordeling — passer ikke inn i `match`/`team_a_id`/ `team_b_id`-modellen slik den er nå (se merknad ved ADR-011). **«High-low-high»** (4 spillere, 2 lag à 2). Per hull: beste spiller («high») på hvert lag møter hverandre i en del-match, dårligste spiller («low») på hvert lag møter hverandre i en egen del-match — resultatet av hele hullet i hovedmatchen avgjøres av disse to del-oppgjørene til sammen. Eksempel: hull 1 — spiller A (lag 1) får 3 poeng og slår begge på lag 2 (høyest slår høyest), spiller B (lag 1) stryker og taper mot lagets low-motpart. Hullet blir da delt 1-1 siden «high» vant for lag 1 og «low» vant for lag 2. **Arkitektonisk vrien del:** hvem som er «high»/«low» per hull avgjøres AV SCORENE selv, etter at de er registrert — ikke satt opp på forhånd slik dagens `match_participant`-oppsett (fast rolle/side satt ved blind draw) forutsetter. **«Robbins»** (foursome med partnerbytte). De første 6 hullene spilles som én foursome-match, deretter bytter alle makker og spiller neste 6 hull som en ny foursome-match, og de siste 6 hullene spilles med den tredje/siste kombinasjonen — slik at alle har spilt med og mot alle i løpet av runden. Seier i en 6-hulls delmatch gir 2 poeng, uavgjort gir 1 poeng, flest poeng totalt vinner. **Arkitektonisk konsekvens:** én økt blir egentlig TRE sekvensielle del-matcher med roterende partnerskap innad i samme runde — dagens modell (én match = ett fast lag-oppsett for hele økten) dekker ikke dette direkte. **«Try all»** (2 lag, variant av foursome). Begge spillerne slår egen ball fra tee, men BYTTER ball til andreslaget (spiller A slår spiller B sin ball og omvendt), og paret velger deretter hvilken av de to ballene som skal spilles videre — resten av hullet spilles som ordinær foursome på den valgte ballen. **Minst arkitektonisk avstand fra i dag:** sannsynligvis en ren spilleregel-variant av eksisterende foursome-format (samme poengmodell, bare en annen fremgangsmåte de to første slagene) — trenger trolig ikke ny datamodell, bare en presisering i regelverket/UI-teksten. **«Flaggturnering»** (Flag tournament), reist av bruker 2026-07-25 — IKKE del av den opprinnelige fire-formater-listen over, notert som et eget femte format. Hver spiller får et fast antall slag (typisk par + handicap for hele runden) og spiller til slagene er brukt opp — den som kommer lengst rundt banen før slagene tar slutt, vinner ("planter flagget" der siste slag ble brukt). **Konkret krav fra bruker, eksplisitt formulert:** en visning som viser GJENSTÅENDE slag for spilleren, oppdatert etter hvert spilte hull (nedtelling, ikke bare et sluttall). **Fremtidig idé, opprinnelig betinget av at GPS ble integrert i appen først:** bruk GPS til å markere/registrere HVOR på banen spilleren faktisk endte opp når slagene tok slutt, ikke bare hvilket hull. **Selve Flaggturnering-formatet ER bygget og live** (se "2026-07-30: alle åtte formater" lenger ned i denne filen — `flag_result`-motoren, hull-granularitet). **GPS-avhengigheten er også borte** — GPS/ avstandsmåling ble bygget 2026-08-10/12 (ADR-048 slag-for-slag-måling, ADR-064 rangefinder til grønn). **✅ GPS-posisjon-ved-siste-slag ER NÅ BYGGET (ADR-066, 2026-08-14), "Del A" av en tredelt utvidelse** (kartoversikt "Del B"/ADR-067 og nytt format Eclectic "Del C"/ADR-068 er OGSÅ bygget nå, se egne punkter -- alle tre deler ferdige): "Plant flagget"-knapp + guidet sheet (GPS auto-fanget → "Hullet du ut på hull N?" → evt. "Landet du på green?" + avstand i m/cm som påvirker rekkefølgen mot andre som gikk tom på samme hull) i BEGGE hjem (frittstående runder OG org-individuelle turneringer). Full runde 2+-scoreføring bygget for spillere med uvanlig gunstig slagbudsjett (isolert overflow-tabell, rører ikke delt `round_hole`/`tournament_round_hole`-infrastruktur). UI-sheeten er en Claude-skrevet V0-eksport (zip 11), ikke håndkodet. Migrasjon 069 (frittstående)/070 (org) — se ADR-066 for full detalj, CHANGELOG.md for byggelogg/verifisering. **✅ Kartoversikt ER NÅ BYGGET (ADR-067, 2026-08-14), "Del B"** — av/på- bryter (standard AV, eier/org-medlem) som viser ALLE deltakeres flagg på et satellittkart, lap 1 vs. lap 2+ skilt med farge+tekstbadge. Org-individuelle turneringers offentlige tilskuervisning er BEVISST utenfor omfang her (finnes ikke i det hele tatt ennå — `/t/[id]/live` er lagturnerings-only — egen, større oppgave hvis/når etterspurt). Migrasjon 071 — se ADR-067. **✅ Eclectic ER NÅ BYGGET (ADR-068, 2026-08-14), "Del C" — siste del, hele den tredelte utvidelsen er dermed ferdig.** Nytt format for org-individuelle turneringer (`scoring_method = eclectic_gross/ eclectic_net/eclectic_stableford`) -- beste resultat PER HULL på tvers av turneringens EGNE runder (krever samme bane, avvist tydelig ellers). IKKE det samme som OOM sin ennå-ubygde "Eclectic på tvers av lenkede turneringer" (se Order of Merit-seksjonen lenger ned) -- den motoren kan trolig gjenbrukes derfra senere, men selve OOM- integrasjonen er fortsatt ugjort. Migrasjon 072 — se ADR-068. - **Ingenting av dette er designet eller bygget ennå** — kun fanget her slik at det ikke går i glemmeboken. Naturlig neste steg når dette tas fatt på: vurder de fire hver for seg (ikke som én stor runde), start med «Try all» (lavest kostnad) hvis en rask seier er ønskelig, eller med «Københavner» hvis en bredere individuell/felt-basert turneringstype uansett skal bygges først som fundament for de andre. - **Ressurs, lagt til 2026-07-19:** brukeren har lastet opp tre PDF-er til prosjektroten (`spilletyper-og-spilleformer-2023.pdf`, `Live Tourney _ A Guide to Handicap Scoring in Golf for Tournaments.pdf`, `SCGA Club Digest.pdf`) som til sammen skal gi en tydelig beskrivelse av hvordan HCP og mottatte/tildelte slag beregnes/fordeles — les disse FØR design av handicap-/slagfordelingslogikken for disse fire formatene, se CLAUDE.md sin "Autoritative kilder"-seksjon. ### 2026-07-30: alle åtte formater — BACKEND ferdig, frontend gjenstår Brukeren utvidet listen fra fire til åtte format denne runden (Shamble, Chapman/Pinehurst, Bingo Bango Bongo, Money Ball/Lone Ranger, Nassau Match Play lagt til; «High-low-high»/«Try all» presist avklart med et fullt utregnet eksempel fra bruker; «Robbins» droppet). Full plan skrevet og godkjent (plan-modus), deretter bygget ett format om gangen i rekkefølgen Chapman → Nassau → Københavner → Bingo Bango Bongo → Flaggturnering → Shamble → Money Ball → High-low-high, motor→migrasjon→API→scratch- verifisering per format FØR neste startet, per prosjektets etablerte disiplin. **Arkitektonisk hjem, bekreftet av bruker FØR bygging («begge, fra start»):** - Flatt felt, ingen sider (Københavner, Bingo Bango Bongo, Flaggturnering): frittstående runder (`round.play_format`) OG den individuelle org- turnering-modellen (`tournament.scoring_method`, ADR-037) — ALDRI org-lagturneringer (ADR-011s to-lags-modell passer strukturelt ikke). - To-siders (Chapman, Nassau, Shamble, Money Ball, High-low-high): frittstående runder OG org-lagturneringer (`session.format`) — ALDRI den individuelle org-modellen. - **Shamble/Money Ball i frittstående runder:** bekreftet av bruker at ETT LAG = HELE RUNDENS deltakersett, INGEN `round_side`-involvering — flere lag som konkurrerer settes opp som separate runder koblet med det allerede byggede `flight_group_id`-leaderboardet (2026-07-28). **Per format, kort:** 1. **Chapman/Pinehurst ("Try all")** — INGEN ny motor. Ny `ENGINE_FORMAT_ALIASES`-mekanisme i `app/handicap.py` (`{"chapman": "foursome"}`, senere gjenbrukt for shamble/money_ball/ high_low_high) — strukturelt identisk med foursome. Migrasjon 041. 2. **Nassau Match Play** — INGEN ny motor, INGEN skjemaendring. Tre parallelle vinduer (hull 1-9/10-18/1-18) av EKSISTERENDE hull-for-hull- data, hver kjørt gjennom den allerede eksisterende `compute_match_ state`. Nye leseendepunkter (`/rounds/{id}/format-result/nassau`, `/orgs/.../matches/{id}/nassau`). **Reelt funn under scratch-testing:** org-matcher låser seg (409 ALREADY_DECIDED) straks OVERALL 18-hulls- matchen er avgjort (ADR-012) — en runaway-margin kan derfor hindre at alle 18 hull noensinne blir registrert, som igjen gir ufullstendige front9/back9-vinduer. Ikke fikset (ville krevd å løsne en etablert sikkerhetssperre) — dokumentert som en kjent v1-begrensning. 3. **Københavner** — ny motor (`copenhagen_points_for_hole`/`compute_ copenhagen_detail`, samme `(deltaker_id, verdi)`-per-hull-form som `compute_skins_detail`). Migrasjon 042 (`round.play_format` + `tournament.scoring_method` + `tournament_round_score.copenhagen_ points`). Alltid NETTO (individuell course handicap-allokering, ikke match-play-relativ). **Reelt funn:** Københavner-poeng avhenger av ALLE tre deltakerne samtidig — org-siden måtte derfor bryte det etablerte "regn om for ÉN deltaker om gangen"-mønsteret (`_recompute_copenhagen_ points` regner om for alle tre ved hver hull-innsending). 4. **Bingo Bango Bongo** — ny motor (`bbb_points_for_hole`/`compute_bbb`). Ny, liten dedikert tabell BEGGE steder (`round_bbb_hole`/`tournament_ round_bbb_hole`) siden bingo/bango/bongo er en per-HULL-fakta, ikke per-spiller/side — passer ikke i `round_hole`s eksisterende XOR-form. Migrasjon 043/044. Sveip-bonus (dobbelt poeng) er en valgfri organisator-innstilling (`bbb_sweep_bonus_enabled`), IKKE fast på. Lagvariant bevisst utenfor omfang v1. 5. **Flaggturnering** — ny motor (`flag_result`/`FlagResult`). INGEN nye kolonner — gjenbruker eksisterende individuell-ball-lagring og `course_handicap_snapshot`/`course_handicap` (allerede beregnet for ALLE formater ved deltaker-opprettelse). Migrasjon 045/046 er REN CHECK-utvidelse. v1: hull-granularitet, ingen fortsettelse på hull 19+. Org-siden fikk et eget, PER-RUNDE (ikke summert-på-tvers-av-turneringen) leseendepunkt (`/rounds/{id}/flag-result`), siden Flag strukturelt er en enkelt-rundes-konkurranse, ikke et akkumulerbart poengtall. 6. **Shamble** — ny motor (`shamble_hole_score`, "beste N av M"). Migrasjon 047 (`shamble_best_n` på BEGGE `round`/`session`). Org-siden: variabel lagstørrelse (2-4) håndteres via en NY, dedikert `_compute_shamble_hole_results` i `scoring.py` (rå brutto, IKKE via den generiske `_side_net`, som forutsetter en FAST enhetsstørrelse per format) — kun et TAK (maks 4) håndheves ved tilføyelse, en tilsvarende "minst 2"-fullstendighets-gate finnes bevisst IKKE for org-lagturneringer (samme begrensning som fourball allerede har). 7. **Money Ball/Lone Ranger** — ny motor (`money_ball_hole_score`, fast 4-manns rotasjon). Ny `lineup_order`-kolonne BEGGE steder (`round_participant`/`match_participant`, 0-3, unik per runde/side). Migrasjon 048/049. **Viktig presisering:** rotasjonen er basert på POSISJON i den faktisk spilte rekkefølgen (1. spilte hull = rotasjons- indeks 0), IKKE rått hullnummer — riktig også for en runde/økt som ikke starter på hull 1. 8. **High-low-high** — den mest nyskapende motoren (`high_low_high_points_for_hole`/`high_low_high_running_score`) — **verifisert tall for tall mot brukerens eget fullt utregnede eksempel** («1-1 etter hull 1», «2-1 til lag 2 før hull 3») i BÅDE enhetstestene og scratch-API-testene. Nøkkelinnsikt fra brukerens presisering: et uavgjort del-oppgjør («å dele et hull») gir NULL poeng til begge sider, IKKE en 0,5/0,5-splitt. Migrasjon 050 er ren CHECK-utvidelse (individuell ball, gjenbruker fourballs eksisterende lagring). Passer IKKE inn i den ternære `HoleResult`-cachen (`match.status_text`/`points_side_a/b`) — egen, dedikert leseendepunkt (`/orgs/.../matches/{id}/high-low-high`) for org-siden, samme "regn ut ved lesing"-filosofi som Nassau. **Kjent, ufarlig v1-kvirk:** den GENERISKE `match.status_text` viser en statisk "AS"-plassholder for High-low-high-matcher (siden `_compute_hole_ results` bevisst returnerer tom liste for dette formatet) — ikke en krasj, bare ikke spesielt informativt der; den ekte stillingen kommer fra det dedikerte endepunktet. **Verifisering, alle åtte formater:** isolerte `handicap_engine.py`- enhetstester FØRST (98 totalt etter denne runden, opp fra 63), deretter full scratch-API-verifisering i BEGGE relevante systemer per format (isolert `teecup_app_scratch`-rolle + isolert scratch-MinIO + engangs API-container, samme mønster som resten av prosjektet) — over 400 automatiserte sjekker totalt på tvers av de åtte formatenes test-skript, pluss `test_isolation.sql` 12/12 uendret gjennom hele runden (migrasjonene er rent additive). **IKKE bygget i denne runden, bevisst neste steg:** frontend for samtlige åtte formater — ingen ny UI for formatvalg (opprett-runde/opprett-økt- skjemaene), ingen nye resultatvisninger (Nassau-vinduer, Københavner-/BBB- poengtavler, Flag-nedtelling, Shamble/Money Ball-lagvisning, High-low- high-stilling). Backend er fullt funksjonelt og testet, men ubrukelig fra selve appen inntil frontend bygges i en egen, senere runde (samme lagdelings-mønster som ADR-038/039: motor/skjema/API FØR frontend). **Migrasjoner 041-050 er BYGGET OG SCRATCH-VERIFISERT, IKKE ENNÅ rullet ut mot ekte `teecup_db`** — venter på eksplisitt bekreftelse fra bruker før noen migrasjon kjøres mot produksjon, per CLAUDE.md sin ufravikelige regel. **RETTELSE 2026-08-03 — avsnittet over er UTDATERT, stod aldri oppdatert etter at frontend faktisk ble bygget:** verifisert direkte mot koden (ikke bare mot denne filen) samme dag som rundeleaderboardets V0-omskriving (se CHANGELOG.md 2026-08-03). Reell status nå: - **Migrasjoner 041-052 ER rullet ut mot ekte `teecup_db`** — bekreftet live (`round.play_format`-CHECK inkluderer alle 16 verdier, `round_bbb_hole`/`lineup_order`/`tournament_round`-familien finnes alle i produksjonsskjemaet). - **Frontend for formatvalg ER bygget, i alle tre arkitektoniske hjem:** `new-round.tsx` (frittstående runder, alle 8 + BBB-sveipbonus/Shamble- beste-N-konfig), `tournament-program.tsx` sin `CreateSessionCard` (org-lagturneringer, `SessionFormat` med alle 10 to-sidede format inkl. chapman/shamble/money_ball/high_low_high), `individual-tournament- detail.tsx` (org-individuell, `scoring_method`-bryter med copenhagen/bingo_bango_bongo/flag). - **Nye resultatvisninger ER bygget:** Nassau-vinduer og High-low-high- stilling i `session-scorecard.tsx` (org) og `round-detail.tsx`s `NassauPanel` (frittstående), Københavner-/BBB-poengtabeller i `individual-tournament-detail.tsx`, Shamble/Money Ball-lagvisning i `round-leaderboard.tsx`s `TeamFlightBoard` (frittstående — org-siden bruker den eksisterende to-siders matchvisningen). - Eneste sanne, fortsatt bekreftede GAP fra denne runden er separat og udiskutert her: organisator-vendt opplasting av hero-/sponsorbilder (se "Landingssider"-seksjonen lenger ned) — ikke relatert til de åtte formatene. ### Utvidelse 2026-07-26: individuelle turneringer, flerrunde-turneringer, og Order of Merit Reist av brukeren som svar på et spørsmål om et individuelt turnering-leaderboard — svaret avdekket at ønsket er STØRRE enn bare Københavner som ett format blant flere: TeeCup skal etter hvert kunne arrangere ekte INDIVIDUELLE turneringer (ikke bare lagturneringer), disse skal kunne gå over FLERE RUNDER, og det skal være mulig å sette opp et **Order of Merit** — sesong-sammenlagt poeng/rangering på tvers av flere separate arrangementer (brukerens eget eksempel: "klubbdager" som gjentas gjennom en hel sesong, med en løpende sammenlagt-tabell). **Ren notat- runde, ingen kode skrevet, ingen ADR skrevet ennå** — brukeren ba eksplisitt om at dette kun noteres nå. Tre distinkte, men beslektede strukturelle spørsmål — bevisst holdt fra hverandre siden de har ulik arkitektonisk tyngde: 1. **Individuelle turneringer (ingen lag i det hele tatt).** ADR-011 låser v1 til NØYAKTIG to lag — en individuell turnering (et flatt felt av spillere, som Københavner) bryter denne forutsetningen helt, ikke bare "trenger flere enn to lag". Dette er allerede notert i ADR-011 sitt eget "Merk (2026-07-19)"-avsnitt via Københavner-eksemplet, men brukerens presisering nå gjør det tydelig at individuelle turneringer er et EGET, generelt tilfelle — ikke bare én formatvariant blant fem. Trenger en egen ADR som avklarer om dette blir en helt egen turnering-TYPE (parallell til dagens to-lags-type, med sin egen `match`/scoring-modell) eller en utvidelse av eksisterende modell. 2. **Flerrunde-turneringer (samme arrangement, flere runder/dager, sammenlagt resultat).** Dagens lagturneringer har ALLEREDE flere `session`-er (f.eks. en Ryder Cup-helg med økter fredag/lørdag/søndag) — men poengsummeringen er PER MATCH innad i hver økt, ikke en sammenlagt SLAGSUM på tvers av runder slik en individuell flerrunde-turnering (f.eks. en 3-dagers slagspillturnering) ville trengt. **Reell strukturell kollisjon å avklare:** frittstående runder (ADR-033 sin `round`/`round_participant`/`round_hole`, Beslutning A) er BEVISST bygget helt UTENFOR organisasjon/RLS-systemet (`plain_connection()`, eid av `user_id`, ingen `organization_id` i det hele tatt) — mens turneringer er strengt org-scopet og RLS- beskyttet. En org-arrangert flerrunde-individuell-turnering trenger runder som lever INNENFOR en organisasjons/turnerings-kontekst — dette er IKKE det samme systemet som de personlige rundene, selv om datamodellen (hull-for-hull-registrering) sannsynligvis ligner mye. Må avklares eksplisitt: gjenbruke `round`-tabellene (utvidet med en valgfri turnering-/org-kobling), eller bygge en parallell, org-scopet rundemodell? Dette er trolig den vanskeligste enkeltbeslutningen av de tre. 3. **Order of Merit (sesong-sammenlagt på tvers av FLERE separate turneringer/arrangementer).** Krever et HELT NYTT overordnet konsept som ikke finnes i skjemaet i dag — noe a la en "sesong" eller "serie" som grupperer flere separate turnering-rader og akkumulerer poeng per spiller på tvers av dem, med sin egen løpende sammenlagt-rangering. Forutsetter sannsynligvis at (1) og (2) over er løst først (det er individuelle arrangementer som skal telle inn i et Order of Merit, ikke lag-baserte Ryder Cup-turneringer) — naturlig SISTE steg av de tre, ikke noe som kan designes isolert. **Ingenting av dette er designet eller bygget** — kun fanget presist her slik at retningen er dokumentert før noe glemmes. Se også ADR-011 sitt eget notat (samme sak, kortere) og "Åpne spørsmål"-listen i ARCHITECTURE_DECISIONS.md. ### Oppdatering 2026-07-26, samme dag: grunnstruktur for (1) og (2) AVKLART — se ADR-037 Brukeren ba om å starte ADR-runden på strukturspørsmålet direkte. Fire load-bærende beslutninger avklart eksplisitt (AskUserQuestion), full begrunnelse i **ADR-037** (ARCHITECTURE_DECISIONS.md): - **Ny, parallell org-scopet datamodell** — IKKE en utvidelse av ADR-033s `round`-tabeller (unngår hybrid/betinget RLS, bevarer et tidligere bevisst valg). - **Samme `tournament`-tabell**, ny `format_type`-diskriminator (`'team'`/`'individual'`) — gjenbruker synlighet/join-kode/status helt uendret. - **Flerrunde fra start**: ny `tournament_round`-tabell (økt-lignende), sammenlagt resultat summert ved lesing på ekte deltaker-id (samme prinsipp som det eksisterende lag-leaderboardet). - **Rå slag lagres OG poeng caches per format** — samme mønster som `match.points_side_a/b` i dag. Nye formater (Københavner m.fl.) blir dermed i hovedsak: én ny motorfunksjon i `handicap_engine.py` + én ny CHECK-verdi, ikke en skjemaendring. **Viktig presisering som oppsto underveis:** "flight" i en formell org-turnering er KUN en tee-tid-gruppering, IKKE en leaderboard-grense (leaderboardet spenner alltid hele feltet) — ULIKT den ad hoc "flere flighter i en frittstående runde"-ideen (der leaderboardet bevisst er avgrenset til det man selv satte opp). De to holdes bevisst ADSKILT nå, ikke forent slik forrige runde antydet — se egen seksjon lenger ned i denne filen ("Frittstående runder: flere flighter..."). **Fortsatt IKKE avgjort:** de fem konkrete formatenes egne poengregler (punkt utenfor denne strukturrunden), og (3) Order of Merit — bekreftet som naturlig SISTE steg, ikke designet. ### Oppdatering 2026-07-30: migrasjon + motor + API BYGGET OG SCRATCH-VERIFISERT, IKKE ENNÅ RULLET UT Migrasjon `040_individual_tournaments.sql` skrevet nøyaktig etter ADR-037s fire beslutninger (`tournament.format_type`/`scoring_method`, `tournament_round`, `tournament_participant`, `tournament_round_participant`, `tournament_round_hole`, `tournament_round_score` — full RLS org-isolasjon på alle fem nye tabellene). Scratch-verifisert alene FØR API-et ble bygget (alle 40 migrasjoner kjørte rent i rekkefølge, `test_isolation.sql` 12/12, 10 egne funksjonelle sjekker: kryss-org-isolasjon på lesing OG skriving, `format_type`-CHECK+default, unik-constraints, `gross_strokes`-CHECK, kaskade-sletting). **Motor** (`handicap_engine.py`, ny seksjon rett etter `allocate_over_played_holes`): `stroke_play_gross_total`/ `stroke_play_net_total`/`stableford_points_for_hole`/`stableford_total` — rene funksjoner, ingen ny fordelingslogikk (bruker samme `allocate_over_played_holes`-output som resten av motoren). 8 nye tester i `test_handicap_engine.py`, alle 63 (55 eksisterende + 8 nye) bestått. **API** (nytt `app/routers/individual_tournaments.py`, registrert i `main.py`): CRUD for runder/turnering-deltakere/rundedeltakere (med handicap-beregning ved tilføyelse -- v1 har INGEN allowance-prosent for individuelle turneringer, `playing_handicap` er alltid identisk med avrundet `course_handicap`, ulikt lagturneringenes `AllowanceStrategy`-familie som er bygget for et relativt to-siders oppgjør), hull-for-hull-scoring (`PATCH .../holes/{n}`, cacher brutto/netto/Stableford-total på nytt ved hver innsending -- samme mønster som `recompute_and_cache_match_state`), og et leaderboard som summerer `tournament_round_score` PÅ TVERS AV RUNDER ved lesing (Beslutning C). `tournaments.py` sin `TournamentCreate`/`TournamentUpdate`/`Tournament` utvidet med `format_type`/`scoring_method` (samme `exclude_unset`-PATCH- mønster som resten av filen). **Reelt funn under bygging, ikke antatt riktig:** leaderboard-endepunktet kunne IKKE hete `/orgs/{id}/tournaments/{id}/leaderboard` -- den stien er allerede `tournaments.py` sitt LAG-leaderboard (points_side_a/b, forventer nøyaktig to lag), og siden `tournaments.router` registreres FØR `individual_tournaments.router` i `main.py`, ville det eksisterende endepunktet stille skygget for det nye (funnet presist ved en ekte API-test som krasjet på `points_side_a`-formen den ikke fikk). Løst ved å gi det et eget navn, `/individual-leaderboard` -- samme kollisjonsklasse som `/rounds` vs. `/my-rounds` tidligere i prosjektet, denne gangen unngått fra start i stedet for oppdaget i produksjon. **Reelt funn under selve scratch-API-testingen, fikset FØR utrulling:** `list_rounds`/`list_tournament_participants` manglet en eksplisitt "finnes turneringen"-sjekk (samme mønster `list_sessions` allerede har for lagturneringer) -- ga stille en tom liste under RLS for en fremmed turnering-id i stedet for 404. Ingen sikkerhetslekkasje (RLS blokkerte fortsatt all faktisk data), men inkonsistent med resten av API-et. Begge rettet til å matche `list_sessions` sin konvensjon presist. **Scratch-API-verifisert grundig, 52/52 sjekker** (isolert `teecup_app_scratch`-rolle + isolert scratch-MinIO + engangs API-container, ekte HTTP via `requests`, ekte magic-link-innlogging via dev-log): full happy path (org→spillere→bane→18 hull→tee→individuell turnering→runde→to turnering-deltakere→to rundedeltakere med riktig beregnet course handicap→hull-for-hull-scoring→leaderboard), hånd- utregnet netto-kryssjekk for begge spillere (course handicap 11/20, stemte eksakt), `front_9`-runde avviser hull utenfor omfang, kryss-org- isolasjon (bekreftet BÅDE at en fremmed org ikke ser dataene OG at en FK-basert skriving på tvers av org blokkeres), slette-vern (runde MED deltakere avvist 409, turnering-deltaker fortsatt referert av en rundedeltaker avvist 400 RESTRICT-FK). `test_isolation.sql` 12/12 uendret (additiv migrasjon). **Autorisasjon — ✅ SCORING STRAMMET INN, SCRATCH-VERIFISERT OG LIVE 2026-07-30** (samme dag, egen del-runde etter frontend-utrullingen): ny `user_is_own_tournament_participant` (`app/team_authz.py`, samme mønster som ADR-023s `user_is_match_participant`) brukt av `update_hole` — kun deltakeren selv eller org-admin kan nå skrive score for en gitt rundedeltaker. Runde-/deltaker-oppsett forblir bevisst på org-medlemsnivå (samme presedens som `session`/`team_roster` i tournaments.py). 13 nye scratch-sjekker + full 52-punkts regresjon, se CHANGELOG.md 2026-07-30 for full detalj. De fem konkrete formatene (Københavner m.fl.) og Order of Merit fortsatt ikke designet. ### Oppdatering 2026-07-30, samme dag: frontend HÅNDKODET og LIVE Brukeren ba eksplisitt om å kode det selv (ikke V0), bruke Chrome DevTools til faktisk browserverifisering, og bruke mer enn grønn/oransje. Ny `components/individual-tournament-detail.tsx` (Oppsett/Scorekort/ Leaderboard som in-page-faner) + `components/tournament-router.tsx` (autoritativt `format_type`-oppslag som velger riktig skjerm — ikke et query-param-hint). `dashboard.tsx` sin "Ny turnering"-flyt fikk et Lag/Individuell-valg. `--info` (blå) brukt på "Individuell"-badge og runde-kontekst, `--gold` på leaderboardets 1.-plass — samme validerte tokens `round-card.tsx` allerede etablerte, ikke nye ukalibrerte farger. **Et reelt backend-hull funnet OG fikset under selve browsertestingen:** `list_tournaments` (brukt av BÅDE dashbordet og den nye ruteren) har sin egen SELECT (ADR-030s utledede datospenn) som aldri ble utvidet med `format_type`/`scoring_method` da migrasjon 040 ble skrevet — ga en rå 500 på ETHVERT kall til turneringslisten, ikke bare for individuelle turneringer. Rettet. Et layout-hull i headeren (navn ble avkuttet på smal mobil) og et manglende form-språk i scorekort-cellene (kun farge, ikke sirkel/firkant som `round-scorecard.tsx` sin `ScoreMark`) ble også funnet og rettet samme runde. Se CHANGELOG.md 2026-07-30 for full verifiseringsdetalj (håndregnet HCP-kryssjekk, databasebekreftet persistens, full opprett-ny-turnering-flyt testet fra bunnen). **RETTELSE 2026-08-14 — de to linjene under var stale, aldri oppdatert etter utrulling.** Migrasjon 040 er bekreftet live i ekte `teecup_db` (`tournament.format_type`/`scoring_method` finnes i produksjonsskjemaet med begge CHECK-constraints, verifisert direkte 2026-08-14). Individuelle turneringer må ha vært rullet ut en gang mellom 2026-07-30 og 2026-08-04 — Order of Merit (migrasjon 055, se eget punkt lenger ned) forutsetter at individuelle turneringer allerede er live, og ble selv bekreftet "BYGGET, SCRATCH-/BROWSERVERIFISERT OG LIVE 2026-08-04". Denne seksjonen fikk aldri sitt eget "rullet ut"-notat da det skjedde — historikken under er beholdt uendret, kun disse to linjene er korrigert: ~~IKKE rullet ut mot ekte systemer ennå~~ / ~~IKKE rullet ut mot ekte `teecup_db` ennå~~ → **✅ rullet ut, live siden senest 2026-08-04.** --- ## Varsler: push til telefon + in-app varslingssenter — ✅ HELT FERDIG 2026-07-28 (begge deler) **In-app varslingssenteret er nå komplett** (bygget 2026-07-25/26, utvidet 2026-07-28): bjelle-ikon i dashbord-header, `/my-notifications`, fire triggerpunkter (venneforespørsel sendt/akseptert, medspiller lagt til på en runde, en venn ser en synlig runde, en tilkoblet runde fullført) OG en e-post-fallback PER TYPE (`user_notification_email_pref`, trygg standard = ingen e-post). Se CHANGELOG.md for full detalj. Kun push til telefonens eget OS-varslingssystem (under) gjenstår av det opprinnelige forslaget. Brukeren spurte om dagens PWA-oppsett kan varsle telefonens eget varslingssystem (f.eks. ved en ny venneforespørsel), og ba om at det uansett finnes en in-app-fallback på dashbordet (varslingsindikator + en side med uleste varsler) — med et V0-prompt klart "i tilfelle". **Push-varsler til telefonens OS — ✅ BYGGET, SCRATCH-/BROWSERVERIFISERT OG LIVE 2026-07-28 (Web Push/VAPID):** - Ny migrasjon `037_push_subscriptions.sql` -- `push_subscription` (`user_id`, `endpoint` unik, `p256dh`/`auth`), samme `plain_connection()`- mønster som `notification`/`round`/`friendship` (ingen RLS, bruker-eid). Én bruker kan ha flere abonnement (flere enheter/nettlesere). - Nytt `app/push.py` (`send_push_to_user`) -- `pywebpush`, kalt fra `create_notification()` (den eneste skrivevegen inn, se `app/routers/notifications.py`) for ALLE fire varseltyper. **Bevisst ingen egen per-type opt-in** (ulikt e-post-fallbacken) -- selve det å abonnere ER samtykket, en abonnert enhet får push for alt. VAPID-nøkler er valgfrie (`settings.PUSH_CONFIGURED`, `app/config.py`) -- samme grasiøs-degraderings-mønster som SMTP, push forsøkes rett og slett ikke uten nøkler satt. En utløpt/tilbakekalt abonnement (404/410 fra push-tjenesten) ryddes automatisk bort, andre feil isoleres med `traceback.print_exc()` og lekker aldri til kalleren (samme mønster som e-post-utsendingen). Nye endepunkter: `GET /push/vapid-public-key` (offentlig), `POST/DELETE /push/subscribe` (krever sesjon). - Frontend: `public/sw.js` fikk `push`/`notificationclick`-håndtering (viser native OS-varsel selv når ingen fane er åpen, klikk fokuserer eller åpner appen på riktig `link_path`). Ny `lib/push-subscribe.ts` (feature-deteksjon inkl. iOS-hjemskjerm-kravet, abonner/avmeld). Ny seksjon "Push-varsler på denne enheten" i `/account` (`PushNotificationSection`), enkel av/på-bryter, håndterer avslått tillatelse med en forklarende tekst i stedet for en generisk feil. - **Viktig plattformbegrensning, uendret:** på iOS Safari fungerer Web Push KUN når PWA-en er lagt til på hjemskjermen -- dekket eksplisitt av `isPushSupported()`. - **Scratch-verifisert (27/27 sjekker, delt testløp med de tre andre punktene under samme dag):** VAPID-nøkkel korrekt eksponert, abonnement lagret, anonymt abonnement avvist (401), en EKTE `webpush()`-utsendelse forsøkt (mot en syntaktisk ugyldig test-nøkkel -- `WebPushException` fanget og logget, in-app-varselet ble uansett opprettet normalt, ingen krasj). **Browserverifisert:** UI-et rendrer korrekt, klikk kaller `Notification.requestPermission()` -- i denne automatiserte nettleser- økten var tillatelsen forhåndssatt til "denied" av miljøet selv (ingen ekte permission-dialog kan trigges av testverktøyet), og avslag ble vist med riktig forklarende tekst uten konsollfeil. **Ærlig begrensning:** en ekte innvilget tillatelse → ekte abonnement → ekte levert OS-varsel er IKKE bevist i denne runden -- krever en ekte enhet/nettleserøkt utenfor automatisert testing. Bruker bør selv teste dette før full tillit. **Rullet ut live 2026-07-28**, bruker bekreftet eksplisitt: migrasjon 037 kjørt mot ekte `teecup_db`, VAPID-nøkkelpar generert lokalt (`cryptography`, EC P-256) og lagt inn i ekte `.env` uten å noensinne vises i klartekst i chatten (samme regel som alle andre hemmeligheter i prosjektet), `docker compose up -d --build teecup_api teecup_frontend`. Verifisert: `/health`/`/dashboard` → 200, `GET /push/vapid-public-key` over ekte https ga korrekt offentlig nøkkel, `teeoff.no` upåvirket. **In-app varslingssenter — ✅ BYGGET, i hovedsak akkurat som beskrevet under (skjema/endepunkter/rute-navn stemmer med forslaget), pluss per-type e-post-fallback som ikke var en del av det opprinnelige forslaget:** - Ny tabell `notification` (arbeidsnavn): `id`, `user_id` (mottaker), `type`, `message` (ferdig norsk tekst, samme snapshot-prinsipp som `author_display_name` i meldinger — unngår å måtte slå opp relaterte data på nytt ved hver lesing), `link_path` (f.eks. `/my-friends`), `created_at`, `read_at` (nullable). - Triggerpunkter nå (matcher det som faktisk er bygget, ADR-036 fase 1): `POST /friends` → varsel til mottaker ("X har sendt deg en venneforespørsel"), `POST /friends/{id}/accept` → varsel til den opprinnelige forespørreren ("X godtok venneforespørselen din"). Åpent for flere triggerpunkter etter hvert som appen får flere hendelser verdt å varsle om. - Nye endepunkter (arbeidsnavn): `GET /notifications` (liste, nyeste først), `GET /notifications/unread-count` (lett, til selve indikator-tallet), `POST /notifications/{id}/read`, `POST /notifications/read-all`. - Frontend: bjelle-ikon + tall-merke i dashbordets header, ny rute `/my-notifications` (IKKE `/notifications` — det ville krasjet med det nye API-prefikset, samme kollisjonsklasse som `/rounds`/`/friends` tidligere, unngått fra start denne gangen). **V0-prompt skrevet, IKKE sendt til V0 ennå:** > Legg til en varslingsindikator i TeeCups dashbord-header (Next.js + > Tailwind + shadcn/ui, mobil-først, eksisterende merkevarefarger): et > bjelle-ikon ved siden av "Konto"/"Logg ut", med et lite, tydelig > tall-merke når det finnes uleste varsler (ingen merke når null). > > Design i tillegg en egen "Varsler"-side den lenker til: > - En liste over varsler, nyeste øverst, hver rad med kort tekst, et > tidspunkt (f.eks. "for 2 timer siden"), og en tydelig visuell > forskjell mellom lest/ulest (IKKE kun farge — bruk f.eks. en liten > prikk pluss fet skrift for uleste, ikke fargen alene). > - En "Merk alle som lest"-knapp øverst. > - Hver rad er klikkbar og fører videre til det varselet gjelder. > - En tydelig, vennlig tomtilstand ("Ingen varsler ennå"). > > Tilgjengelighet er et ufravikelig krav: god kontrast, stor nok skrift, > store trykkflater (min. 44px), aldri ikon-only uten tekstlabel på > viktige handlinger (bjelleikonet i header er et unntak siden det er et > universelt gjenkjent symbol, MEN skal ha en beskrivende > `aria-label` som inkluderer antall uleste). **Status: In-app varslingssenter BYGGET, SCRATCH-VERIFISERT OG RULLET UT LIVE 2026-07-26** (bruker bekreftet eksplisitt, se CLAUDE.md sin statuslogg for utrullingsdetalj — denne linjen var utdatert helt til den ble rettet i en senere gjennomgang samme dag). V0 kjørte prompten (zip 20), backend bygget for å matche eksakt: migrasjon `026_notifications.sql` (tabell `notification`, `plain_connection()`-mønster, ingen RLS — samme som personlig profil/ runder/venner), ny `app/routers/notifications.py` (`create_notification()` delt hjelpefunksjon + fire endepunkter: `GET /notifications`, `GET /notifications/unread-count`, `POST /notifications/{id}/read`, `POST /notifications/read-all`). To trigger-punkter koblet inn i `friends.py` (kun venneforespørsel-hendelsene som faktisk finnes i dag, ADR-036 fase 1): `POST /friends` varsler mottakeren, `POST /friends/{id}/ accept` varsler den opprinnelige forespørreren. `components/notifications.tsx` + ny rute `/my-notifications` (IKKE `/notifications` — samme kollisjonsklasse unngått fra start som `/rounds`/`/friends`). Bjelle-ikonet fra V0s dashbord-eksport portert inn i den LIVE `dashboard.tsx` sin header (ikke en full revert av filen — samme kirurgiske uttrekk-mønster som alltid), koblet til et ekte `GET /notifications/unread-count`-kall ved mount. **Scratch-verifisert i samme testløp som rundeleaderboardet under** (se den seksjonen for detaljer om selve scratch-infrastrukturen, totalt 39/39 sjekker på tvers av begge funksjonene): full forespørsel→varsel→lest-syklus begge retninger, `mark-all-read`, og eksplisitt kryss-bruker-isolasjon (bruker B kan ikke markere bruker A sitt varsel som lest via id-gjetting — stille no-op, ikke en feilmelding som ville lekket at id-en fantes). Ekte typesjekket produksjonsbuild kompilerte rent, `/my-notifications` listet blant rutene. **Ikke bygget i denne runden, bevisst utenfor omfang:** ekte push til telefonens OS (egen, større runde, se vurderingen over), e-post-fallback (se tillegget under, fortsatt kun foreslått). **Venter på brukerens bekreftelse før migrasjon 026 kjøres mot ekte `teecup_db` og containerne redeployes** — se CLAUDE.md for den samlede utrullingsplanen (denne runden + rundeleaderboardet under ble bygget sammen). ### Tillegg 2026-07-25: e-post som fallback-kanal, betinget av samtykke — ✅ BYGGET OG LIVE 2026-07-28 **Bygget rett til den "fremtidig presisering"-varianten under, ikke det enklere forslaget:** brukeren ba eksplisitt om at mottakeren skal kunne velge HVILKE varseltyper som skal gi e-post, ikke én global av/på-bryter — ny `user_notification_email_pref`-tabell (migrasjon 033, samme "tilstedeværelse = valgt"-mønster som `friend_categorization`/ `round_visible_category`, trygg standard = ingen typer valgt). E-post- utsendingen ligger ETT sted (inni `create_notification()` selv, `app/routers/notifications.py`), så den dekker automatisk alle nåværende OG fremtidige varseltyper (`friend`/`round`/`result`/`tournament`) uten at hvert kallsted må huske det selv. Ny `send_notification_email()` i `app/email.py`, ny seksjon "Varsler på e-post" i `/account`. Se CHANGELOG.md 2026-07-28 for full detalj (19/19 scratch-sjekker). Opprinnelig forslag, for historikkens skyld: brukeren foreslo at TeeCup i tillegg sender en e-post til mottakeren av en venneforespørsel ("du har fått en forespørsel, åpne appen for å se den") — MEN kun hvis brukeren har akseptert e-post som kommunikasjonskanal fra TeeCup. **Sjekket eksisterende kode:** det finnes I DAG ingen generell kommunikasjons-/varslings-samtykke-flagg på `app_user` — det eneste samtykket i skjemaet er `tournament_registration.consent_given_at` (ADR-017), som er noe HELT ANNET (samtykke til selve turneringspåmeldingen, org-scopet, ikke en kontoinnstilling). Dette må altså bygges som et nytt, eget felt, ikke gjenbrukes. **Foreslått, ikke bekreftet:** - Nytt `app_user.notification_emails_enabled boolean NOT NULL DEFAULT false` — OPT-IN, ikke opt-out (samme "trygg standard"-filosofi som resten av appen, f.eks. rundevisibilitet default `private`). Satt via en ny bryter i kontoinnstillinger (`/account`), IKKE en del av den obligatoriske profil-fullføringen (dette er valgfritt, ikke påkrevd). - Ny `send_friend_request_email()` i `app/email.py` — følger EKSAKT samme mønster som de fem eksisterende utsendingsfunksjonene der (nb/en-maler, `_send_sync` via `asyncio.to_thread`, driftsfeil lekker aldri til klientresponsen). Sendes fra `POST /friends`, KUN hvis mottakeren har `notification_emails_enabled = true`. - **Fremtidig presisering, ikke et problem nå:** med kun ÉN varseltype (venneforespørsel) holder én global boolean. Den dagen appen får flere varseltyper (ADR-036 fase 2s rundevisibilitet, fremtidige turnering-hendelser, osv.) bør dette trolig bli et SETT av brytere per type, ikke én global av/på — notert her for å ikke bli glemt, ikke løst nå. --- ## PWA-installasjon: hvordan få brukere til å installere raskt — ✅ BYGGET OG LIVE 2026-07-28 Brukeren reiste dette som en oppfølging av varsel-diskusjonen: hvordan få brukere til "nærmest umiddelbart" å installere TeeCup som app på telefonen, gitt at ADR-028 allerede har bygget selve PWA-fundamentet (manifest, service worker, ikoner, "Legg til på Hjemskjerm"-metadata) — men ingenting proaktivt OPPFORDRER til installasjon i dag. **Plattformvirkeligheten, avgjør hele designet:** - **Android/Chrome-familien:** nettleseren fyrer selv av et `beforeinstallprompt`-event når siden kvalifiserer (manifest+service worker+https — alt allerede på plass). Fanges opp med `event.preventDefault()` + lagres, og kan trigges SENERE fra en egen knapp via `event.prompt()` — full kontroll på NÅR spørsmålet stilles, ikke bare nettleserens egen timing. - **iOS Safari: INGEN programmatisk vei finnes i det hele tatt.** Apple har aldri implementert `beforeinstallprompt`. Eneste vei er den manuelle Del-ikon → "Legg til på Hjemskjerm"-flyten — appen kan KUN vise en instruksjonsoverlegg (f.eks. med skjermbilde/animasjon av hvilken knapp som skal trykkes), aldri utløse selve installasjonen. Dette er en hard Apple-begrensning, ikke noe TeeCup kan designe seg rundt. - **Deteksjon nødvendig for begge retninger:** `matchMedia( "(display-mode: standalone)")` (evt. `navigator.standalone` på eldre iOS) avslører om brukeren ALLEREDE kjører den installerte PWA-en — vis ALDRI noe installasjons-UI da. iOS-vs-Android/Chrome avgjøres med UA-sniffing (upresist, men standard og nødvendig her siden det ikke finnes noen bedre feature-deteksjon) for å velge riktig av de to variantene (ekte knapp vs. instruksjonsbanner) — andre nettlesere uten noen reell installasjonsvei bør ikke vise noe i det hele tatt. **Om "nærmest umiddelbart" — mild uenighet, med begrunnelse:** et prompt FØR brukeren har vist noen interesse (f.eks. på selve innloggingsskjermen) treffer typisk dårlig og føles påtrengende, OG `beforeinstallprompt` har ikke alltid rukket å fyres av så tidlig uansett. Appen har derimot ALLEREDE et universelt, høy-intensjons sjekkpunkt HVER ny bruker går gjennom: den obligatoriske profil-fullføringen (ADR-031/ "obligatorisk profil-fullføring ved innlogging"-runden). Å vise installasjons-oppfordringen RETT ETTER det steget — første gang brukeren faktisk når `/dashboard` med en komplett profil — er trolig det beste "nærmest umiddelbart"-tidspunktet som finnes: reelt tidlig, men etter at brukeren allerede har investert litt og vist ekte intensjon, ikke et kaldt overfall på innloggingssiden. **Foreslått, ikke besluttet:** - Vis KUN når `display-mode` ikke allerede er `standalone`. - Første visning: rett etter fullført profil, første gang `/dashboard` nås. - "Ikke nå"-avvisning lagres i `localStorage` med en avkjølingsperiode (f.eks. ikke vis på nytt før om N dager) — ingen server-side felt nødvendig, dette er et rent klient-signal. - To distinkte UI-varianter (Android: ekte "Installer"-knapp som kaller `event.prompt()`; iOS: instruksjonsbanner) — INGEN visning for andre nettlesere uten en reell installasjonsvei. **Status: BYGGET akkurat som foreslått over, håndkodet (ikke V0), 2026-07-28:** ny `lib/pwa-install.ts` (globalt fanget `beforeinstallprompt`-event via `components/sw-register.tsx`, som allerede mountes på alle sider -- eventet fyres kun én gang per side-liv og må derfor fanges tidligst mulig, ikke først når selve dashbord-komponenten mountes). Ny `components/install- prompt.tsx` (`InstallPrompt`) -- viser ALDRI noe hvis allerede installert (`display-mode: standalone`) eller nylig avvist (`localStorage`-cooldown, 14 dager). To varianter: Android/Chrome-familien får en ekte "Installer"-knapp (`event.prompt()`), iOS Safari får et instruksjonsbanner (Del-ikon → "Legg til på Hjem-skjerm", INGEN programmatisk vei finnes der). Andre nettlesere uten en reell installasjonsvei viser ingenting. Lagt inn i `dashboard.tsx` rett under hilsenen -- akkurat sjekkpunktet foreslått over (første/hver gang brukeren når dashbordet med komplett profil). **Browserverifisert:** Chrome fyrte faktisk `beforeinstallprompt` i testøkten, "Installer"-knappen rendret og var klikkbar, `event.prompt()` ble kalt uten konsollfeil (selve den native installasjonsdialogen kan ikke utløses i en automatisert nettleserøkt -- ærlig, forventet begrensning, samme klasse som all annen "ekte OS-installasjon"-testing i prosjektet). **Rullet ut live 2026-07-28** sammen med de tre andre punktene samme dag, se CHANGELOG.md for felles utrullingsdetalj. --- ## Leaderboard for runder og turneringer — ✅ HELT FERDIG (runder 2026-07-26, turnering-individuell-rangering 2026-07-28) Brukeren ba om et leaderboard for pågående og ferdige runder OG turneringer, usikker på om det bør ligge der man allerede ser rundens detaljer så langt (`round-stats.tsx`) eller integreres i selve detaljsiden (`round-detail.tsx`) — og lastet opp to skjermbilder av hvordan Golf GameBook har løst akkurat dette, som referanse. Bevisst drøftet her, ikke besluttet — brukeren ba selv om å "drodle", ikke bygge. **Referansen (Golf GameBook), oppsummert — IKKE noe TeeCup skal kopiere rett av, kun inspireres av struktur/konsept:** en egen, dedikert "Leaderboards"-fane nederst (sidestilt med "Rundeinfo" og "Spill-feed", ikke en del av noen av dem), med faner ØVERST for ulike scoringsmetoder ("Slagspill NET"/"Stableford NET"), en rangert liste (#, navn, HCP, score, til par, "F" for ferdig), en blå "HCP-RUNDE"- merkelapp, og at hver rad kan TRYKKES UT til å vise spillerens fulle horisontale scorekort inline (samme hull-for-hull-tabellformat TeeCup allerede har bygget i `round-scorecard.tsx`) pluss sosiale handlinger (Lik/Kommentar/Statistikk). **To reelle presiseringer funnet ved å faktisk sjekke koden, ikke antatt:** 1. **"Runder" kan få dette NÅ, ingen avhengighet til ADR-036 fase 3.** En frittstående runde støtter allerede flere deltakere i dag (eier + gjester, `round-detail.tsx` sine spiller-faner) med uavhengig hull-for-hull-score hver — et rangert leaderboard PÅ TVERS av disse deltakerne er fullt buildbart nå. Fase 3 (ekte medspillere med egen konto) endrer ikke dette, det utvider bare HVEM som kan være en deltaker. 2. **"Turneringer" har allerede et leaderboard** (`tournament- leaderboard.tsx`, live) — men det er et LAG-POENG-leaderboard for Ryder Cup-matchplay (ADR-011, to lag), strukturelt noe helt annet enn Golf GameBooks individuelle slagspill-rangering. **Åpent spørsmål, ikke avklart:** mener brukeren en individuell rangering INNAD i en turnering (f.eks. rangere spillere etter brutto/netto score i en økt, ved siden av det eksisterende lag-poeng-leaderboardet), eller var "turneringer" ment mer løst/generelt? Bør avklares før noe designes for turnering-siden av dette. **Plassering — min foreløpige vurdering, ikke en konklusjon:** verken `round-detail.tsx` (allerede tett under selve spillingen, samme "for mye stablet oppå hverandre"-fare som tidligere runder denne uken allerede ryddet opp i) eller `round-stats.tsx` (dedikert til DYP enkelt-spiller-statistikk, ikke tvers-sammenligning) er et perfekt hjem alene. To ideer, ikke gjensidig utelukkende: - En KOMPAKT leaderboard-oppsummering (topp/posisjon, ikke full tabell) øverst på `round-detail.tsx` — det man faktisk vil sjekke RASKT mens man spiller ("hvem leder nå"). - Gjenbruk EKSISTERENDE infrastruktur for full detalj i stedet for å duplisere Golf GameBooks "trykk ut for fullt scorekort inline": `round-stats.tsx` har ALLEREDE en spillervelger (pill-rad) bygget for flere-deltakere-runder — en leaderboard-rad kan trolig bare LENKE dit/bytte valgt spiller, i stedet for å bygge en helt ny inline- scorekort-mekanisme på nytt. - Flere scoringsmetode-visninger (Slagspill NET vs. Stableford NET) — TeeCup regner allerede Stableford klientside i `round-stats.tsx`, men har ingen tilsvarende "netto slagspill"-rangeringsvisning i dag. Verdt å designe eksplisitt om dette ønskes, ikke noe som følger gratis av det som allerede finnes. **Status: ingen kode, ingen V0-prompt ennå** — venter på at retning (og særlig turnering-spørsmålet over) avklares før noe designes ferdig. ### Oppdatering 2026-07-26: RUNDE-leaderboardet HELT FERDIG (håndkodet, ikke V0 -- se under) Brukeren ba eksplisitt om å få leaderboardet for frittstående runder på plass (kun runder, ikke turneringer — turnering-spørsmålet over fortsatt ikke avklart). Bygget som ny `GET /rounds/{round_id}/leaderboard` (`app/routers/rounds.py`), samme `plain_connection()`/eier-only- autorisasjon som resten av rundene (`_get_owned_round_or_404`). **Datakontrakt:** ``` GET /rounds/{round_id}/leaderboard { "holes_planned": 9 | 18, "completed": boolean, "entries": [ { "participant_id": string, "display_name": string, "is_owner": boolean, "holes_played": number, // "thru" "total_score": number | null, // null = ingen hull registrert ennå "score_to_par": number | null, "net_score_to_par": number | null // null hvis ingen course handicap // (f.eks. gjest uten HCP oppgitt) }, ... ] } ``` Sortert server-side stigende på `score_to_par` (lavest/best først, ingen registrerte hull sist). Samme allokeringsalgoritme (`allocate_strokes_by_index`) som resten av appen for netto — uavhengig kryssjekket mot `handicap_engine.py` direkte i scratch, ikke bare "kjørte uten feil". **Scratch-verifisert (39/39 sjekker, to separate testløp):** varsel- trigger-punktene fra samme runde (se "Varsler"-seksjonen), pluss full leaderboard-runde (3 deltakere — eier ferdig 9 hull til par, gjest 9 hull +1/hull, gjest kun 3 hull -1/hull — riktig rangering/thru/to-par for alle tre), kryss-bruker-autorisasjon (403 for en annen bruker), ukjent runde-id (404), OG en dedikert netto-kryssjekk (avvikende SI-rekkefølge, kun 9 av 18 hull spilt, resultatet sammenlignet mot en UAVHENGIG beregning via `handicap_engine.allocate_strokes_by_index` direkte — stemte eksakt). **V0-prompt sendt til bruker 2026-07-26, ikke kjørt i v0.app ennå:** > Design et "Leaderboard"-visning for en enkelt frittstående golfrunde i > TeeCup (Next.js + Tailwind + shadcn/ui, mobil-først, eksisterende > merkevarefarger — grønn primær, oransje sekundær). Runden kan ha > ALLE typer deltakerantall — fra kun eieren alene til flere flighter > samtidig (opptil 13+ spillere er reelt mulig), så designet må skalere > pent fra 1 til 15+ rader UTEN å bli en endeløs, monoton liste. > > Data kommer fra et allerede bygget API-endepunkt som returnerer, for > runden: om den er fullført eller pågår, planlagt hullantall (9/18), og > en liste med én rad per deltaker: navn, om det er rundens eier, antall > hull spilt ("thru"), total score, score til par (brutto), og score til > par netto (kan være fraværende — vises da ikke for den spilleren, > IKKE som "0" eller en feil). > > **Ranger deltakerne** etter brutto score til par (lavest/best først). > Gi et tydelig, men ikke overveldende, rangeringstall (#1, #2, ...) — > delt plassering (likt resultat) skal vises tydelig som delt (f.eks. > "T-2"), ikke to forskjellige tall for samme resultat. > > **Topp-plassering fortjener litt ekstra visuell vekt** (f.eks. en > diskret kant/bakgrunnstone eller et lite ikon) — men ALDRI kun farge > for å skille ledere fra resten (tilgjengelighetskrav, se under). > > **For en pågående runde:** vis "thru X" (f.eks. "thru 5") for spillere > som ikke har fullført alle planlagte hull ennå, i stedet for en > ferdig-markering. For en FULLFØRT runde: vis heller en tydelig > "Ferdig"-markering per spiller i stedet for "thru X av X". > > **Score-til-par-tall** skal formateres på golfvis: "E" for jevnt med > par (0), "+N" over, "−N" (ekte minustegn) under — ALDRI bare "0"/"-3" > uten fortegn. Bruk FORM i tillegg til farge der du fremhever over/ > under par (f.eks. en liten sirkel/firkant-indikator, ikke bare > tekstfarge) — samme "aldri kun farge"-prinsipp som resten av TeeCup. > > **Gi brukeren en brutto/netto-veksling** (to faner eller en enkel > switch øverst) som bytter både HVILKET tall som vises OG selve > rangeringsrekkefølgen mellom de to. Spillere uten et netto-tall (ingen > HCP registrert) skal vises tydelig nederst/uten rangering i > netto-visningen, ikke skjules eller krasje. > > **To distinkte merker, ikke ett:** et "Eier"-merke for rundens eier > (kommer direkte fra API-et sitt `is_owner`-felt), OG uavhengig av det et > "Deg"-merke for raden som tilhører DEN som ser på leaderboardet akkurat > nå (siden runden også kan sees av lenkede medspillere, ikke bare eieren > -- komponenten mottar hvilken `participant_id` som er "meg" som en egen > prop utenfra, ikke fra selve leaderboard-dataen). De to kan gjelde samme > rad (eieren ser sin egen runde) eller ulike rader (en medspiller ser > både sin egen "Deg"-rad og eierens "Eier"-rad) -- design for begge. > > Design også en kompakt "mini-leaderboard"-variant (topp 3 + evt. "og > N til") egnet til å vises øverst på selve rundens detaljside — et > raskt "hvem leder nå"-blikk uten å måtte navigere til hele > leaderboardet. > > Tilgjengelighet er et ufravikelig krav: god kontrast, stor nok skrift, > store trykkflater (min. 44px), aldri kun farge for å formidle > informasjon (rangering/over-under par), lesbar uten briller. **Rettet i selve prompten 2026-07-26, FØR den ble kjørt i v0.app:** den opprinnelige "Deg"-merke-instruksen antok feilaktig at kun eieren ser sin egen runde -- ADR-036 fase 3 (bygget samme dag) gjør nå at en lenket medspiller også kan se leaderboardet. Byttet til to uavhengige merker ("Eier" fra `is_owner`, "Deg" fra en egen `participant_id`-prop komponenten mottar utenfra) -- se prompten over. **Oppdatering, samme dag: HÅNDKODET i stedet for kjørt i v0.app.** Brukeren gikk tom for V0-credits (samme situasjon som spillerliste- redesignet rett over) og ba meg bygge direkte etter nøyaktig samme designspesifikasjon som prompten over. Ny `components/round-leaderboard.tsx` + rute `/my-rounds/[id]/leaderboard`, pluss en `RoundLeaderboardMini` (topp 3 + "og N til", lenket fra selve rundens detaljside) og en ny "Se leaderboard"-lenke på hvert rundekort i `/my-rounds`-listen (krevde å gjøre om `round-card.tsx` sin ytre `` til en `
` med to separate lenker -- unngår en nestet ``). Gjenbrukte bevisst etablerte mønstre (`ScoreMark`s form+farge-språk fra `round-scorecard.tsx`, WS-sanntid- mønsteret fra `round-stats.tsx`) fremfor å finne opp nye. **To oppfølgingspunkter samme dag, begge bygget:** (1) leaderboardet skulle vise hullscorer per spiller -- løst som en utvidbar rad (klikk for å vise en horisontal strip med hull-for-hull brutto-merker), krevde en liten backend-utvidelse (`LeaderboardHoleOut`/`holes`-felt, data var allerede hentet, bare ikke eksponert). (2) et tredje "Poeng" (stableford)-modus lagt til ved siden av Brutto/Netto (rangert synkende, siden høyere poengsum er bedre) -- krevde `strokes_received` per hull + `total_points` på hver leaderboard-rad (også backend, samme utvidelsesmønster). Netto/poeng vises nå også som en rolig sekundærlinje RETT under det fremhevede brutto-hullmerket (inspirert av et referansebilde fra en konkurrentapp bruker delte, bevisst IKKE en kopi av fargevalg/layout). Full verifiserings- og utrullingsdetalj i CHANGELOG.md (2026-07-26) -- ikke gjentatt her for å unngå duplisering. ### Oppdatering 2026-07-28: turnering-halvparten av spørsmålet AVKLART OG BYGGET Det tidligere åpne spørsmålet ("mener brukeren en individuell rangering INNAD i en turnering, ved siden av det eksisterende lag-poeng- leaderboardet?") ble avklart eksplisitt (AskUserQuestion) — svar: JA, en egen rangering PER ØKT (ikke summert på tvers av turneringen), ved siden av lag-poeng-leaderboardet, ikke i stedet for det. Bygget: ny `GET /orgs/{id}/sessions/{id}/individual-leaderboard` (`app/routers/tournaments.py`) — KUN meningsfull for `scoring_mode='stroke'`-økter i individuell-ball-format (singles/ fourball; delt-ball-formater og hole_result-modus avvises tydelig med 400, ikke krasj, siden de ikke har noen individuell brutto-score å rangere fra). Gjenbruker `mp.playing_handicap` (allerede beregnet, allowance-justert per format) + `allocate_over_played_holes` (samme mønster som eksisterende match-play-scoring) for netto/poeng — ingen ny regnelogikk. Respekterer samme reveal-gating (blind draw, ADR-013) som matchlisten. Frontend: ny side `/tournaments/[id]/sessions/[sessionId]/individual-leaderboard` (`session-individual-leaderboard.tsx`, gjenbruker rangerings-/ mode-toggle-mønsteret fra `round-leaderboard.tsx` i en enklere, sesjonsscopet variant), lenket fra blind draw-skjermen for kvalifiserende økter. Scratch- og browserverifisert 2026-07-28, se CHANGELOG.md. --- ## Spillerliste-redesign (vertikal, utslag/HCP/rediger inline) — ✅ HELT FERDIG, håndkodet (V0 tom for credits), live 2026-07-26 Samme dag som HCP-i-søk/rediger-utslag-per-deltaker (se ADR-033-loggen i CLAUDE.md), rett etter at den runden var rullet ut live. Brukeren viste et skjermbilde av "+ Medspiller"-skjermen og pekte på to ting: (1) utslag+HCP (nettopp bygget samme dag) bør flyttes OPP i selve spillerknappen i stedet for å ligge som en egen rad under statistikknivå-velgeren, med Navn/ Utslag/Hcp/Rediger inni knappen, og spillerne listet VERTIKALT i stedet for dagens horisontale scroll-rad. For en "midlertidig spiller" (gjest uten konto) skal Rediger-flyten også dekke Navn/Kjønn/E-post. (2) Under selve hull-registreringen bør det vises hvor mange mottatte slag aktiv spiller har på det hullet, eksempel "Hull 7 - Par 4 - Hcp 5 - -1". **Punkt 2 er ALLEREDE bygget og live** — ren frontend-tilføyelse i `round-detail.tsx` sin hull-header, bruker data (`strokes_received`) som allerede var hentet fra `GET .../holes` fra før (samme felt `ScoreSoFar`/ leaderboardet bruker til netto), bare aldri vist FØR scoring. Golfvis fortegn (ekte minustegn), vist kun når spilleren faktisk mottar minst ett slag på hullet (`Hull 7 · Par 4 · Hcp 5 · −1`). **Punkt 1 krevde ny backend, bygget og scratch-verifisert (22/22 sjekker + full regresjon av samme dags 27+35-punkts testsuiter) samme dag:** ny migrasjon `029_round_participant_guest_email.sql` (`round_participant. guest_email`, nullable). `ParticipantCreate` (`POST .../participants`) fikk et valgfritt `guest_email: EmailStr | None` (avvist sammen med `user_id` — en lenket bruker har allerede sin egen konto-e-post). `ParticipantUpdate` (`PATCH .../participants/{id}`) fikk `guest_name`/ `gender`/`guest_email` — men KUN gyldig for en gjest (`user_id IS NULL`); forsøk på en lenket deltaker avvises tydelig (400). `gender`-endring er nå en del av samme "rating_changed"-bunt som utslag/HCP (påvirker hvilken utslags-rating som er gyldig) — regnes om, avvist (400) hvis den nye kombinasjonen (nytt/uendret utslag × nytt kjønn) mangler rating, og bevisst avvist etter fullføring (409, samme presedens som utslag/HCP). `guest_name`-endring alene har INGEN rating-implikasjon og forblir derfor tillatt selv etter fullføring (ren metadata, samme som `RoundUpdate` sitt navnefelt). **V0-prompt sendt til bruker 2026-07-26, ikke kjørt i v0.app ennå:** > Design om spillerlisten på en frittstående golfrundes hull-for-hull- > registreringsside i TeeCup (Next.js + Tailwind + shadcn/ui, mobil-først, > eksisterende merkevarefarger — grønn primær, oransje sekundær). I dag er > spillerne en horisontal scroll-rad av små faner øverst på siden; dette > skal bli en VERTIKAL liste av spillerkort i stedet. Runden kan ha alt fra > kun eieren alene til mange spillere samtidig, så listen må skalere pent > til 10+ rader uten å bli en endeløs, monoton vegg. > > **Hvert spillerkort viser:** navn, utslagssted, HCP (eller "ikke satt" > hvis fraværende), og en tydelig "Rediger"-knapp/lenke. Kortet er OGSÅ > selve trykkflaten for å VELGE spilleren som aktiv for hull-registrering > (samme funksjon som dagens fane-klikk) — velg en tydelig, men ikke > overveldende, måte å vise "dette kortet betyr både 'velg meg' og > 'rediger meg'" på (f.eks. hele kortet velger, en distinkt undersone/ > knapp for rediger) uten at de to handlingene blandes sammen ved et > uhell. > > **To distinkte merker på kortet**, kan begge gjelde samme kort: "Deg" > (spilleren SOM SER PÅ siden akkurat nå — komponenten mottar egen > deltaker-id som prop, siden både eieren og en lenket medspiller kan se > og bruke siden) og "Eier" (rundens eier, fra et eget boolsk felt på > spilleren). > > **Rediger-flyten** (åpnes fra kortet, f.eks. en utvidbar seksjon eller et > ark/modal — din vurdering) inneholder: > - Utslagssted: en nedtrekksliste hentet fra et eget endepunkt (banens > faktiske utslag på DENNE runden), filtrert til utslag som har en > rating for spillerens (gjeldende, eller nylig valgte — se under) > kjønn. > - HCP: et tallfelt, valgfritt (kan stå tomt/fjernes). > - Statistikknivå for spilleren: tre valg ("Kun slag" / "Slag og putter" / > "All statistikk") — samme tre-valgs-mønster som resten av appen bruker > for dette (segmentert knapperad). > - **KUN hvis spilleren er en "midlertidig spiller" (gjest uten TeeCup- > konto — komponenten vet dette fra at spilleren mangler en konto-id)**: > TRE EKSTRA felt — Navn (fritekst), Kjønn (mann/kvinne/annet), E-post > (valgfritt, e-postformat). Endring av kjønn her skal oppdatere hvilke > utslag som er valgbare i utslag-nedtrekkslisten (samme filter som > over, reaktivt). > - En tydelig "gjelder kun denne runden, endrer ikke [spillerens] > profil"-forklaring for utslag/HCP-feltene (gjelder IKKE navn/kjønn/ > e-post for en gjest, siden en gjest ikke har noen egen profil å > bevare uendret). > > **For en spiller MED egen TeeCup-konto** (ikke en gjest) skal Rediger- > flyten KUN vise utslag/HCP/statistikknivå — ALDRI navn/kjønn/e-post- > feltene (gir ingen mening, personen har sin egen konto). > > **"Legg til medspiller"-knappen/flyten** (søk-som-du-skriver mot ekte > brukere, med et "legg til uten konto"-alternativ for gjester) beholdes > konseptuelt som i dag, men tilpasses visuelt til den nye vertikale > listen. Gjesteskjemaet i "legg til uten konto" bør få det samme > valgfrie e-post-feltet som Rediger-flyten nå støtter (kan sette e-post > allerede ved opprettelse, ikke bare i etterkant). > > **Skjul redigering/tilføyelse/fjerning helt** når komponenten får beskjed > om at brukeren IKKE har lov til å forvalte runden (en prop, f.eks. > `canManage`) — en medspiller uten forvaltningsrett skal fortsatt kunne > VELGE et kort for å registrere score, bare ikke redigere/legge til/ > fjerne noen. Skjul ALL redigering (også for eieren) når runden er > fullført (en `readOnly`-prop) — vis da utslag/HCP som ren tekst, ingen > Rediger-knapp. > > Tilgjengelighet er et ufravikelig krav: god kontrast, stor nok skrift, > store trykkflater (min. 44px), aldri kun farge for å formidle status > (Deg/Eier-merkene trenger tekst, ikke bare farge), lesbar uten briller. **Data komponenten skal jobbe mot (allerede live i produksjon):** ``` type Participant = { id: string user_id: string | null // null = gjest ("midlertidig spiller") guest_name: string | null guest_email: string | null // kun meningsfullt når user_id er null display_name: string // alltid utfylt, bruk denne til visning is_owner: boolean gender: "m" | "f" | "x" tee_name_snapshot: string handicap_index_snapshot: number | null course_handicap_snapshot: number | null stat_level: "strokes_only" | "strokes_and_putts" | "full" } GET /rounds/{round_id}/tee-options → [{ name: string, genders: ("m"|"f")[] }] PATCH /rounds/{round_id}/participants/{id} body (alle valgfrie, kun de som faktisk endres sendes): stat_level, tee_name, handicap_index (kan settes til null), guest_name, gender, guest_email (kan settes til null) -- guest_name/gender/guest_email avvises (400) hvis participant.user_id er satt (ikke en gjest) -- avvist (409) hvis runden er fullført OG tee_name/handicap_index/ gender er blant feltene som sendes POST /rounds/{round_id}/participants body: ENTEN { user_id, tee_name?, stat_level? } ELLER { guest_name, gender, handicap_index?, guest_email?, tee_name?, stat_level? } ``` **Oppdatering 2026-07-26: bygget HÅNDKODET i stedet for via V0.** Brukeren gikk tom for V0-credits rett etter at prompten over ble skrevet, og valgte eksplisitt (spurt via AskUserQuestion) at jeg bygger det direkte fremfor å vente på fornyede credits. Implementert etter nøyaktig samme prompt/ data-kontrakt som over, i samme Tailwind/shadcn-stil som resten av `round-detail.tsx`: ny `PlayerList`-komponent (erstatter `PlayerTabs`) — vertikal liste, hvert kort en stor `