# TeeCup — Funksjons-backlog > Formål: fange ALT som er ønsket for TeeCup på ett sted, slik at intet krav går > tapt mellom økter, modeller eller samtaler. Kilder: de to opprinnelige > Gemini-samtalene (funksjonalitet + Docker) og arbeidet gjort med Claude. > > Status-koder: ✅ ferdig · 🔨 pågår · 📋 planlagt/fanget · ❓ trenger beslutning > · 🔀 endret fra opprinnelig råd · 💤 utsatt (bevisst) > > Sist oppdatert: 2026-07-17 --- ## Fundament (bygget) | Funksjon | Status | Notat | |---|---|---| | Handicap-/matchmotor (testet) | ✅ | 24 tester, R&A-verifisert. Erstatter Geminis løse JS-funksjon. | | Formater: singles, fourball, foursome, greensome, scramble | ✅ | I motoren, allowance som konfig. | | Brutto/netto + prosent-allowance (75 %, 90 %, 3/4) | ✅ | ADR-005. Prosentene bekreftet mot Golf GameBook (ADR-014). | | 9-hulls (front/back) slagfordeling | ✅ | ADR-008, egen funksjon + test. | | Miksede formater per turnering (økter) | ✅ | ADR-007. Ryder Cup-strukturen. | | Spiller-pool m/ reserver, delvis deltakelse | ✅ | ADR-007. | | Multi-tenant skjema + RLS | ✅ | ADR-001/003. Isolasjon bevist med oppførselstest. | | Dedikert app-rolle (ingen superuser/BYPASSRLS) | ✅ | migrasjon 002. | | API: oppsett (spillere/lag/roster/økter/matcher/blind draw) | ✅ | Verifisert for ekte mot scratch-db + engangscontainer. | | API: scoring (hole_score/match_hole_result, matchstatus, handicap-beregning) | ✅ | ADR-012/014. Verifisert for ekte, inkl. fourball better-ball og race-sikker recompute. | | Banedata fra teeoff via API | ✅ | ADR-004 → **ADR-019**, LIVE 2026-07-18. Import (kopi) ved eksplisitt organisator-valg, ikke live oppslag ved hver bruk. Se egen seksjon under. | | Konfigurerbar handicap-pipeline (4 brytere) | ✅ | ADR-014. Bygget i `app/handicap.py`, brukt av scoring-runden. | | Ekte autentisering (magic-link + JWT-sesjon) | ✅ | ADR-009. `app/routers/auth.py` + migrasjon `004_auth.sql`. `X-Debug-User-Id`-stubben er helt fjernet. | | RLS-tomstreng-fiks (`app_current_org()`) | ✅ | Migrasjon `005_rls_null_guard.sql`. Se detaljer under. | | Organisasjon-bootstrap (opprette ny org via API) | ✅ | `POST /orgs`, `app/routers/organizations.py`. Se detaljer under. | | Ekte SMTP-utsending av magic-link | ✅ | `app/email.py`. Se detaljer under. | | Containerisert, LIVE på `teecup.teeoff.no` | ✅ | `Dockerfile` + `docker-compose.yml`. Se egen seksjon under — to reelle driftshendelser funnet og rettet. | | Dato/klokkeslett på turnering/økt/match | ✅ | ADR-015. `session.scheduled_at` + `tee_interval_minutes` → utledet `match.tee_time`. Se egen seksjon under. | | Maskinlesbar feilkode-kontrakt (`{"code",...}`) | ✅ | ADR-015. Full retrofit, alle 39 tidligere steder. Se egen seksjon under. | | i18n-forberedelse (nb/en) | ✅ | ADR-015. `preferred_locale` + ekte engelsk e-postmal. Se egen seksjon under. | | Frontend: innlogging + verifisering, LIVE | ✅ | ADR-016. Next.js på `teecup.teeoff.no`, ekte magic-link-flyt bevist med reell e-post. Se egen seksjon under. | | Frontend: dashboard (org-bytter/-opprettelse + turneringsliste), LIVE | ✅ | `/dashboard`. Kablet mot `/auth/me`, `/orgs`, `/orgs/{id}/tournaments`. Skrive-flyt bekreftet med ekte data (org "Tjøme Gents" + turnering opprettet av bruker). | | Frontend: lag/roster-skjerm, LIVE | ✅ | `/tournaments/[id]`. To nye backend-endepunkter bygget samtidig (`PATCH`/`DELETE` roster). Skrive-flyt ikke testet med ekte data ennå. | | Selvregistrering + utvidet spillerprofil (API) | ✅ | ADR-017. `app/routers/registration.py` (offentlig, uautentisert), utvidet `players.py`, e-post-basert `player.user_id`-kobling ved innlogging. Backend komplett og live. Frontend-påmeldingsskjema/landingssider gjenstår (egen ADR-runde). | | Roster: endre kaptein / fjern spiller (`PATCH`/`DELETE`) | ✅ | `app/routers/tournaments.py`. Bevisst ingen "kun én kaptein"-håndhevelse ennå — se «Brukerroller»-punktet under. | | Egendefinerte baner (`course`) via API | ✅ | Nytt `app/routers/courses.py` (`GET`/`POST /orgs/{id}/courses`). Kun `source='custom'` — ADR-004s teeoff-integrasjon (`source='official'`) fortsatt kun vedtatt, ikke bygget. Se egen seksjon under. | | Frontend: program-skjerm (økter/tidsplan), LIVE | ✅ | `/tournaments/[id]/program`. Se egen seksjon under. | --- ### Frontend: innlogging + verifisering — ✅ BYGGET OG VERIFISERT LIVE 2026-07-17 - Første frontend-skjerm i prosjektet. Designet i V0 (login-skjerm, merkevare form/farge hentet fra Teeoff-logoen — IKKE navn/logo, se ADR-016s begrunnelse og tidligere samtale), hentet inn som `frontend/` (Next.js + Tailwind + shadcn/ui). - **Kvalitetsrunde på V0-output før bruk:** fargetokens i `globals.css` var OKLCH-TILNÆRMINGER av de forespurte hex-fargene (`#8bc24a`/`#ff5722`), ikke eksakte — regnet ut presise OKLCH-ekvivalenter og rettet alle 6 forekomster (lys/mørk/system-mørk). Fjernet `@vercel/analytics` helt (ga null verdi på egen-hostet infrastruktur, kun en unødvendig tredjeparts nettverkskall). Fjernet `typescript: { ignoreBuildErrors: true }` (bygget kjører nå ekte typesjekk). Fjernet dødt `pnpm.overrides`-felt. `frontend/.gitignore` manglet `.pnpm-store/` — årsaken til at VSCode viste 10 000 "ukjente" filer ved første commit-forsøk; rettet. - **Kablet mot ekte API** (`app/routers/auth.py` sine `/auth/request-link` og `/auth/verify-link`): `frontend/next.config.mjs` sin `rewrites()` proxyer `/auth/*`/`/orgs/*`/`/health` server-side til API-et — se ADR-016 for hele begrunnelsen (same-origin, ingen CORS, cookie uendret). `components/login-form.tsx` sender ekte `POST /auth/request-link`; ny `app/verify/page.tsx` + `components/verify-form.tsx` mottar `?token=...` fra e-postlenken (automatisk verifisering) ELLER viser et manuelt "lim inn koden"-felt (nødvendig fallback, ikke overflødig — se under). - **Backend-endring i samme runde:** `app/email.py`/`app/config.py` fikk en ny `PUBLIC_BASE_URL`-innstilling (default `https://teecup.teeoff.no`, valgfri) — e-postmalen sender nå en EKTE klikkbar lenke (`{base}/verify?token=...`) i tillegg til den rå koden som fallback (samme mal-mekanisme som i18n-runden, ADR-015). - **Containerisert og rullet ut LIVE** (ny `frontend/Dockerfile`, multi-stage, Next.js `output: "standalone"`; ny `teecup_frontend`-tjeneste i `docker-compose.yml`). Caddy (`teecup.teeoff.no`, i det SEPARATE `teeoff`-repoet) peker nå på `teecup_frontend` i stedet for `teecup_api` direkte — se ADR-016 for hvorfor, og CLAUDE.md-status for driftsdetaljene (samme stale-Caddy-inode-hendelse som containeriseringsrunden, løst likt). - **Verifisert med FAKTISK e-postlevering, ikke bare curl:** ekte `POST /auth/request-link` sendt til brukerens egen adresse over `https://teecup.teeoff.no`, ekte e-post mottatt med en ekte klikkbar lenke, lenken åpnet i nettleser, landet på en fungerende `/verify`-side, sesjon opprettet — brukeren bekreftet "jeg er tilsynelatende innlogget." Første gang en hel bruker-vendt flyt (ikke bare API-et isolert) er bevist ende-til-ende i produksjon. --- ### Dato/klokkeslett + feilkode-kontrakt + i18n-forberedelse — ✅ BYGGET OG VERIFISERT 2026-07-17 - Reist av brukeren rett før frontend-arbeidet: kan turnering/økt/match ha dato/klokkeslett, og må appen forberedes for flerspråklighet? Begge er API-kontraktspørsmål som er langt billigere å løse FØR frontend bygges enn å ettermontere — se ADR-015 for alle tre beslutningene i sin helhet. - **Migrasjon `006_scheduling_and_locale.sql`:** alt additivt (nullable/ default) — `tournament.end_date` (+ `CHECK` mot `start_date`), `session.scheduled_at`/`tee_interval_minutes`/`start_hole`, `match.tee_time_override`, `app_user.preferred_locale`, `magic_link_token.locale` (begge sistnevnte `CHECK IN ('nb','en')`). Kjørt permanent mot den ekte `teecup_db` (se under). - **Feilkoder:** ny `app_error(status_code, code, message)`-factory i `app/errors.py`, alle 39 tidligere norsk-hardkodede `HTTPException(..., detail="...")`-steder på tvers av 6 filer (`errors.py`, `auth.py`, `routers/auth.py`, `routers/tournaments.py`, `routers/matches.py`, `routers/scoring.py`) retrofittet til en delt ~15-kode-taksonomi. Endelig `grep -rn 'detail="' app/` ga NULL treff. - **Dato/tid i API-et:** `tournament.start_date` (fantes i skjemaet siden 001, men var ALDRI koblet til API-modellene før nå — funnet og fikset som en naturlig bivirkning av å legge til `end_date`) + `end_date` eksponert; `session` sine tre nye felt eksponert i `SessionCreate`/`SessionOut`; `match.tee_time` utledet i Python i både `create_match` og `list_matches` (ingen ekstra spørring — økten hentes allerede for andre formål der). - **i18n:** `MagicLinkRequest.locale` (`Literal["nb","en"]`, default `nb`) sendes av klienten, følger med i token-raden, brukes til å velge e-postmal OG (kun ved førstegangsopprettelse) `app_user.preferred_locale`. Ekte engelsk e-postmal lagt inn i `app/email.py` (ikke bare rørlegging) — konkret bevis på at flerspråklighet fungerer ende-til-ende, ikke bare at et felt eksisterer. - **Verifisert grundig mot en fersk `teecup_scratch`** (migrasjoner 001→006, `test_isolation.sql` 12/12 uendret, engangs-API-container): feilkode-form bekreftet på tvers av 5 filer (`NOT_FOUND`/`LIMIT_REACHED` fra tournaments.py, `DUPLICATE` fra roster, `NOT_AUTHENTICATED` uten cookie, `NOT_ROSTERED_ON_TEAM` fra matches.py sin `add_participant`); `tee_time` bekreftet å øke riktig per `sequence` (10 min intervall → 08:00/08:10/08:20), `tee_time_override` bekreftet å vinne over utledet verdi, økt uten `scheduled_at` bekreftet å gi `tee_time: null` (ikke feil); i18n bekreftet full runde — ny bruker med `locale:"en"` fikk `preferred_locale:"en"`, en ETTERFØLGENDE `request-link` for samme bruker med `locale:"nb"` skiftet e-postmalen men IKKE den lagrede preferansen (bekreftet uendret via `/auth/me`). - **Migrasjon 006 kjørt mot ekte `teecup_db` 2026-07-17**, bruker bekreftet eksplisitt i samme økt — kjørte rent, `test_isolation.sql` fortsatt 12/12. - **Mindre driftslærdom, funnet OG rettet samme runde:** et innledende `grep -v -i 'pass|secret|key'`-filter jeg brukte for å lese ikke-sensitive `.env`-nøkler fanget IKKE opp `TEECUP_DATABASE_URL`, som bar `teecup_app`-passordet innebygd i selve URL-en (i tillegg duplisert av det samme passordet som allerede lå rent i `TEECUP_APP_PASSWORD`) — passordet endte dermed synlig i et verktøyresultat. Flagget til brukeren umiddelbart (samme mønster som SMTP-passord-hendelsen under containeriseringsrunden). **Rettet:** `.env` bygget om til fem separate, rent navngitte felt (`TEECUP_DB_HOST/PORT/NAME/USER/PASS`), `app/config.py`+`app/db.py` bygger nå `asyncpg`-poolen fra disse i stedet for én DSN-streng, `docker-compose.yml` oppdatert tilsvarende. Se CLAUDE.md-status for full driftsdetalj, inkl. at dette krevde et fullt `docker compose up -d --build --force-recreate` (ikke bare `--force-recreate` — imaget bygger inn `app/` ved build-tid). Verifisert ende-til-ende mot den live stacken (`Application startup complete`, `/auth/me` ga rent `401` over ekte https, `teeoff.no` upåvirket). --- ### Containerisering og go-live — ✅ FERDIG 2026-07-16 - `POST /orgs` osv. var siste kodebit; dette var første gang noe rørte EKTE, PERMANENT infrastruktur (ekte `teecup_db`, ekte langtlevende container, den DELTE Caddy-instansen som også ruter live `teeoff.no`). - **Bygget:** ekte `teecup_db` opprettet, migrasjoner 001→005 kjørt permanent (samme filer, ingen endringer), `test_isolation.sql` bestått (ruller alltid tilbake, trygt å kjøre mot en database som skal bli stående). `Dockerfile` (speiler scratch-rundenes bevist-riktige volumoppsett: `app/` + `handicap_engine.py` på samme relative plassering) + `docker-compose.yml` (tjeneste `teecup_api`, joiner det eksisterende, eksterne `teeoff_default`-nettverket). Ny Caddy-blokk for `teecup.teeoff.no` i `/opt/teeoff/deploy/Caddyfile`. - **To reelle driftshendelser, begge funnet og rettet i sanntid:** 1. **Stale bind-mount-inode:** `teeoff_caddy` sin `Caddyfile`-mount er en ENKELTFIL-bind-mount, låst til inoden som fantes da containeren sist startet (13 dager tidligere). Fil-redigering via atomisk rename laget en ny inode på samme sti — containeren fortsatte å lese den GAMLE filen uansett hvor mange ganger `caddy validate`/`reload`/admin-API `/load` ble kjørt (alle opererte på den uendrede gamle filen, derav ingen synlig feil). Løsning: full `docker restart teeoff_caddy` (brukeren bekreftet eksplisitt — avvek fra planens "kun graceful reload"-løfte, ga noen sekunders nedetid for `teeoff.no`). 2. **Alvorlig nettverksalias-kollisjon** (oppdaget rett etter omstarten, da EKTE teeoff-trafikk — `/api/facilities?...` med ekte klubb-slugs som `borregaard-golfklubb` — dukket opp i `teecup_api` sin logg): `docker-compose.yml` sin service-nøkkel var `api:`, identisk med teeoffs eget `api`-servicenavn. Docker Compose registrerer nettverksalias etter service-NAVNET (ikke bare `container_name`) på delte nettverk, så begge containerne fikk alias `api` på `teeoff_default` — Caddys `reverse_proxy api:8000` i teeoffs egen config kunne da tilfeldig treffe enten ekte `teeoff_api` eller `teecup_api`, dvs. ekte brukertrafikk til teeoff.no kunne bli besvart av TeeCup-koden. **Rettet umiddelbart:** stoppet `teecup_api` først (hindre videre feilruting), ga service-nøkkelen navnet `teecup_api`, gjenopprettet, bekreftet med `docker network inspect` at alias `api` nå KUN peker på ekte `teeoff_api`. **Lærdom for fremtidige tjenester på delt nettverk:** ALLTID gi docker-compose sin service-nøkkel (ikke bare `container_name`) et prosjekt-unikt navn når flere uavhengige compose-prosjekter deler samme eksterne nettverk — service-navnet blir også et DNS-alias. - **Mindre driftslærdom (samlet):** (a) `.env` leses IKKE på nytt av en allerede kjørende container — `docker compose up -d --force-recreate` kreves etter enhver `.env`-endring; (b) et `#`-tegn i et upassordet `.env`-passord kuttes som kommentar av Compose sin parser, anførselstegn (helst enkle) løser det; (c) jeg eksponerte ved et uhell to secret-verdier i eget debug-output mens jeg feilsøkte en `.env`-korrupsjon (manglende linjeskift) — brukeren roterte passordet som forsiktighetsregel. - **Verifisert ende-til-ende mot den ekte, live stacken:** `teeoff.no` upåvirket gjennom hele prosessen; `https://teecup.teeoff.no/health` → 200 med automatisk utstedt TLS; full magic-link-innlogging (ekte e-post mottatt, `verify-link` ga `Secure`-flagget cookie siden vi nå er over ekte https, `/auth/me` fungerte med sesjonen). --- ### Ekte SMTP-utsending — ✅ BYGGET OG VERIFISERT 2026-07-16 - Brukeren la inn egne, uavhengige SMTP-credentials i `.env` (`TEECUP_SMTP_SERVER/PORT/USER/PASS`, `TEECUP_FROM_EMAIL` — ADR-009, ikke delt med teeoff). Kun nøkkelnavn ble lest for å bekrefte de fantes, ALDRI verdiene (CLAUDE.md sin sikkerhetsregel). - **Løsning:** ny `app/email.py` (`send_magic_link_email`, `smtplib` kjørt via `asyncio.to_thread` siden det er et synkront bibliotek — samme mønster som teeoffs egen fungerende utsending). Håndterer BEGGE vanlige SMTP-tilkoblingsmåter dynamisk (implisitt TLS på port 465 vs. STARTTLS på andre porter) siden porten bevisst ikke ble lest under planlegging. `app/config.py` fikk nye, valgfrie innstillinger (`SMTP_CONFIGURED` avledet fra at alle fem er satt) — IKKE `_required`, så scratch-/dev-testing fungerer fortsatt uten SMTP satt opp, via `TEECUP_DEV_LOG_MAGIC_LINKS`. - **Bevisst designvalg:** en driftsfeil i selve utsendingen (feil passord, SMTP nede, eller ingen leveringsmåte konfigurert i det hele tatt) logges kun server-side og endrer ALDRI klientens respons — alt annet ville brutt anti-enumereringsgarantien i `request-link` (klienten skal ikke kunne skille "e-posten finnes ikke" fra "e-posten finnes men utsendingen feilet"). - **Verifisert i to trinn:** (1) dev-log-flyten uendret uten SMTP satt (regresjonstest av eksisterende scratch-løype), (2) én ekte test-e-post sendt til en adresse brukeren oppga, med de ekte credentials videreført fra `.env` til scratch-containeren uten at jeg noensinne leste verdiene selv — **brukeren bekreftet mottak** av en e-post med innloggingskode. Dette er første gang noe i dette prosjektet er verifisert ved faktisk levering til en ekte, ekstern mottaker, ikke bare via curl/scratch-container. --- ### Organisasjon-bootstrap — ✅ BYGGET OG VERIFISERT 2026-07-16 - Gjennomgang av alle routere hadde avdekket at INGEN endepunkt opprettet en `organization`-rad — i alle testrunder denne økten var organisasjoner satt inn direkte med superbruker-SQL. En ekte førstegangsbruker hadde ingen vei til å opprette klubben/bedriften sin og bli owner. Reelt blokkerende, ikke en utsettbar produktbeslutning. - **Løsning:** nytt `POST /orgs {"name": ...}`, autorisert med `get_current_user` (ikke `get_authorized_org` — sirkulært før org-en finnes). Ingen ny migrasjon nødvendig. - **Selvrefererende RLS-bootstrap bekreftet å fungere** (kommentaren i 002 om en "privilegert sti" var ALDRI bygget og viste seg unødvendig): generer org-ens uuid i Python FØR innsetting, sett `app.current_org` til nøyaktig den verdien via den eksisterende `org_connection()`, sett så inn `organization`-raden med samme id. `org_self`-policyens implisitte `WITH CHECK` (id = `app_current_org()`) blir da trivielt sann. `teecup_app` (NOSUPERUSER/NOBYPASSRLS) trenger altså INGEN egen privilegert tilkobling for å bootstrappe sin egen første organisasjonsrad. - **Verifisert med 5 tester**, inkludert en negativ kontroll som beviser mekanismen er presis, ikke et RLS-hull: forsøkte å sette inn en organisasjon med en MISMATCHENDE id (annen enn `app.current_org`) — avvist med `insufficient_privilege`, som forventet. Også bekreftet: ny org fungerer normalt med eksisterende endepunkter (GET/POST tournaments), dukker opp riktig i `/auth/me`, og full kryss-org-isolasjon holder mellom to uavhengig opprettede organisasjoner. --- ### RLS-tomstreng-bug — ✅ FIKSET 2026-07-16 - Alle RLS-policyer i 001/003 (`org_isolation` på 14 tabeller + `org_self` på `organization`) brukte `current_setting('app.current_org', true)::uuid`. Denne håndterte NULL trygt (ga ingen rader, som tiltenkt), men IKKE tomstreng — en gjenbrukt asyncpg-pool-tilkobling der en TIDLIGERE forespørsel satte GUC-en via `SET LOCAL` kunne lese den tilbake som `''` etter at transaksjonen var ferdig, og casten kastet da en 500 i stedet for skjemaets lovede "trygg standard: se ingenting". - **Fiks:** samlet i én `STABLE` SQL-funksjon `app_current_org()` (migrasjon `005_rls_null_guard.sql`) som gjør `NULLIF(current_setting(...), '')::uuid` — konverterer tomstreng til NULL FØR cast. Alle 15 policyer alteret (`ALTER POLICY`) til å bruke funksjonen i stedet for det rå uttrykket. Verifisert med 3 nye regresjonstester i `test_isolation.sql` (Test 10-12: tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS ikke krasj, org-bootstrap-innsetting fungerer rett etter tomstreng- tilstand) OG ved å faktisk gjenskape original-buggen mot en ekte container (pool-størrelse 1, varm opp med `org_connection()`, deretter kall `/auth/me` på samme gjenbrukte tilkobling — gikk fra 500 til 200). - **Viktig presisering oppdaget underveis:** den opprinnelige planen antok at `/auth/me` kunne joine `organization` direkte igjen når tomstreng-buggen var fikset. Det var FEIL — fiksen gjør bare at tomstreng oppfører seg som NULL (trygt: se ingenting), den endrer IKKE at `org_self`-policyen krever en MATCHENDE `app.current_org` for å vise en rad i det hele tatt (riktig RLS-oppførsel, ikke en bug). En bruker kan tilhøre flere organisasjoner samtidig, så det finnes ingen ÉN kontekst å sette for en tverr-org- spørring. `/auth/me` slår derfor opp hvert org-navn ETT OM GANGEN via `org_connection()` (N+1 spørringer, N = antall org-er brukeren tilhører, typisk 1-3) — verifisert at dette faktisk returnerer navnet korrekt. --- ## Ønsket, men IKKE fanget før nå (fra Gemini-samtalene) ### Brukerroller (utover org-medlemskap) — 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 CLAUDE.md-status 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 CLAUDE.md-status 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:** 📋 planlagt (infrastruktur) - «BREAKING: X vant matchen». PWA push. Egen infrastruktur-bit. ### 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:** 📋 notert, IKKE designet/bygget — trenger egen ADR-runde senere. - 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é, uttrykkelig betinget av at GPS er integrert i appen først** (se GPS/avstandsmåling-notatet lenger opp i denne filen, ADR-033- seksjonen — samme avhengighet): bruk GPS til å markere/registrere HVOR på banen spilleren faktisk endte opp når slagene tok slutt, ikke bare hvilket hull. Ingen datamodell eller UI designet ennå for noen del av dette — rent notat. - **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. ### 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. **Ingen migrasjon skrevet** — ADR-037 er ren struktur-beslutning, neste steg er et konkret migrasjonsutkast til gjennomgang. --- ## 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 CLAUDE.md-status 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 CLAUDE.md-status 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 CLAUDE.md-status 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 `