teecup/FEATURE_BACKLOG.md
Erol Haagenrud 0e17a257dc display_name synkroniseres nå automatisk med for-/etternavn ved profilendring. Rettet også de to eksisterende kontoene som allerede hadde fylt ut profilen sin (erolhaagenrud@gmail.com → "Tore Morell", hei@erol.no → "Erol Haagenrud") — bekreftet med en tørrkjøring først, verifisert med RETURNING etterpå.
Varsel-spørsmålet
Ekte push til telefonens varslingssystem: teknisk mulig, men ikke gratis. Fundamentet finnes (ADR-028s PWA), men iOS Safari krever at PWA-en faktisk er lagt til på hjemskjermen for at Web Push skal fungere i det hele tatt — en vanlig fane kan aldri motta push på iPhone, uansett tillatelse. Krever i tillegg ny infrastruktur (VAPID-nøkler, abonnement-tabell, utsendingslogikk, eksplisitt tillatelsesspørsmål). Reelt eget byggeløft, ikke noe jeg vil anbefale som første steg.

Anbefaling: bygg et in-app varslingssenter først — fungerer på alle enheter uten noen tillatelse, og løser akkurat det du beskrev (venneforespørsel synlig på dashbordet). Design: en notification-tabell, varsler skapt når en forespørsel sendes/aksepteres, bjelle-ikon med tall-merke i header, ny side på /my-notifications.

V0-prompten er skrevet og ligger i FEATURE_BACKLOG.md, klar til å sendes. Ekte push kan bygges som egen, senere runde hvis dere fortsatt vil ha det etterpå.
2026-07-26 05:58:35 +02:00

2192 lines
136 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
`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`
`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`
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`
`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.
---
## Varsler: push til telefon + in-app varslingssenter — 📋 DESIGNET 2026-07-25, IKKE bygget
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 — teknisk mulig, men reell ny
infrastruktur, ikke en liten utvidelse:**
- Fundamentet finnes allerede (ADR-028: service worker, installerbar PWA).
Web Push (Push API + Notification API) fungerer UTEN en native app.
- **Viktig plattformbegrensning:** på iOS Safari fungerer Web Push KUN når
PWA-en er lagt til på hjemskjermen (iOS 16.4+) — en vanlig Safari-fane
kan ALDRI motta push, uansett tillatelse gitt. Android/desktop er langt
mer tilgivende.
- Krever: et VAPID-nøkkelpar, en ny `push_subscription`-tabell (én bruker
kan ha flere enheter/abonnementer), en backend-sende-funksjon (f.eks.
`pywebpush`), og et eksplisitt tillatelsesspørsmål fra nettleseren (kan
ikke sendes stille, brukeren kan avslå).
- **Vurdering:** reell verdi, men et eget, moderat-til-stort byggeløft med
en hard plattformbegrensning å designe rundt — IKKE anbefalt som første
steg.
**In-app varslingssenter — anbefalt første steg, fungerer overalt, ingen
tillatelse kreves:**
- 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: ren design-/dokumentasjonsrunde, ingen kode skrevet** — venter på
brukerens beslutning om (a) bygge in-app-senteret først, (b) sende
V0-prompten, eller (c) også gå videre med ekte push-varsler som en egen,
senere runde.
---
## Scramble: statistikk over utslag brukt per spiller — 📋 NOTERT 2026-07-25, IKKE bygget
Brukeren ba om at det i scramble-turneringer skal føres statistikk over
hvor mange utslag hver spiller har hatt (dvs. hvor mange ganger den
enkelte spillerens drive ble VALGT av laget som ballen man fortsetter
med).
**Reelt ny type data, ikke en liten utvidelse:** dagens
`hole_score`/`match_hole_result` er en DELT rad per side for scramble
(se det allerede eksisterende åpne punktet "Individuell-vs-delt-ball i
`hole_score`" i ARCHITECTURE_DECISIONS.md sin "Åpne spørsmål"-seksjon,
og "Scramble-grensesnitt" rett over det samme stedet) — det finnes i dag
INGEN kobling mellom en registrert hull-score og HVILKEN spiller sitt
utslag som faktisk ble valgt. Å telle "utslag brukt" krever et nytt,
eksplisitt datapunkt per hull (f.eks. hvem sitt utslag ble valgt), ikke
noe som kan utledes fra det som allerede lagres.
**Sannsynligvis samme problemstilling for greensome** (ikke eksplisitt
nevnt av bruker, men samme spilleregel-mekanikk: begge partnere slår
egen ball fra tee, ett velges) — verdt å vurdere sammen når dette
designes, ikke som to separate ting.
**Åpne spørsmål, ikke besluttet:**
- Hvem registrerer dette — kapteinen/den som fører score for laget
(samme autorisasjonsmodell som resten av match-scoring, ADR-023), og
på hvilket tidspunkt (samtidig med selve hull-scoren, eller separat)?
- Hvor vises statistikken — per match, aggregert for hele turneringen,
eller begge deler?
- Teller uspilte/ikke-registrerte hull annerledes enn et hull der ingen
eksplisitt valgte utslag ble registrert (skal det være mulig å hoppe
over)?
**Ingen datamodell eller UI designet ennå** — rent notat, fanget opp slik
at det ikke går i glemmeboken til scramble/greensome-scoring tas fatt på
som egen runde.
---
## Kommunikasjon — ✅ HELT FERDIG 2026-07-19 (ADR-025)
| Del | Status | Notat |
|---|---|---|
| Lag-intern chat («det hemmelige rommet») | ✅ LIVE | `/tournaments/[id]/teams/[teamId]/chat`. Ekte privat — kun rostrede spillere, INGEN unntak for org-eier/admin (bevisst avvik fra appens vanlige autorisasjonsmønster). `user_is_rostered_on_team` i `app/team_authz.py`. |
| Offentlig runde-feed («Banter Board») | ✅ LIVE | Egen seksjon nederst på `/t/[id]`. Lesing gjenbruker `tournament.visibility`-mønsteret (ADR-018) uendret, inkl. anonym tilgang for `public`-synlige turneringer. Posting er strengere: krever innlogging OG org-medlemskap/deltakelse. |
| Bilder i feed/chat | ✅ LIVE | Med fra start (ikke utsatt) — gjenbruker `app/storage.py` sin MinIO+AVIF-pipeline fra ADR-018 uendret. |
| Sanntid-levering | ✅ LIVE | WebSockets, ikke polling — løser samtidig det tidligere åpne "sanntid vs. polling"-spørsmålet generelt. In-memory tilkoblingsregister per prosess (kun trygt med dagens ene `teecup_api`-container, se ARCHITECTURE_DECISIONS.md "Åpne spørsmål"). Egen Caddy-rute `/ws/*` rett til `teecup_api`. |
| Video | 💤 | Fortsatt utsatt, egen ADR om/når etterspurt. |
| 1-til-1 direktemeldinger | 💤 | Fortsatt utsatt, ikke etterspurt. |
| Moderering (offentlig feed) | ✅ LIVE | Forfatteren selv, ELLER org-eier/admin, kan slette et innlegg. Lag-chatten har ingen moderering utover forfatteren (rommet er privat, org-admin har uansett ikke lesetilgang). |
Se ARCHITECTURE_DECISIONS.md ADR-025 for alle fire hovedbeslutningene og
CLAUDE.md-status for full byggerunde (datamodell, autorisasjon, scratch-
verifisering med 20 automatiserte sjekker inkl. reell WebSocket-sanntid,
og utrulling).
---
## Landingssider (turnering + organisasjon) — ADR-018 ✅ HELT FERDIG 2026-07-18
Reist av brukeren 2026-07-18, rett etter registrerings-ADR-en (ADR-017).
Backend, begge frontend-skjermer, Open Graph-metadata OG MinIO-bilde-
opplasting (med AVIF-konvertering) bygget og live samme dag. Kun selve
dra-og-slipp-opplastingsskjermen i frontend (V0) gjenstår.
| Del | Status | Notat |
|---|---|---|
| Synlighetsnivå: offentlig / kun org-medlemmer / kun turnering-deltakere | ✅ | `tournament.visibility` (default `org`, trygg standard) + `organization.public_profile`. Håndheves eksplisitt i `registration.py` — RLS løser IKKE dette alene (se ADR-018 Beslutning B, reell presisering funnet under bygging). Samme trenivå-modell som «Banter Board» under bør gjenbruke dette. |
| Registrering følger samme synlighetsgrense | ✅ | ADR-018 Beslutning D, bekreftet med bruker FØR bygging. |
| "Deltaker"-tilgang (ikke org-medlem, men rostret/registrert) | ✅ | Ny `get_current_user_optional` + `_is_participant()`. Testet: rostret spiller som logget inn fikk tilgang, tilfeldig fremmed ble avvist. |
| Turnering-landingsside: tekst, program, sponsorer, påmelding (API) | ✅ | `description`-felt, `tournament_sponsor`-tabell (navn+lenke), `GET /public/tournaments/{id}/sessions` (gjenbruker blind draw-lås fra ADR-013). Selve SKJERMEN i frontend gjenstår. |
| Org-landingsside: klubbprofil, liste over turneringer (API) | ✅ | `GET /public/orgs/{slug}` — kun `public`-synlige turneringer. Bekreftet: klubb-profil KAN være offentlig (brukerens valg). |
| Lesbar URL (slug) for organisasjon | ✅ | Fantes faktisk allerede i skjemaet siden migrasjon 001 (oversett, funnet da migrasjon 009 feilet mot scratch — se CLAUDE.md-status). Kun `CHECK`-constraints lagt til i 009. |
| Organisator kan faktisk SETTE disse feltene | ✅ | Implisitt hull fylt under bygging: `PATCH /orgs/{id}/tournaments/{id}` (visibility/description/registrering), `PATCH /orgs/{id}` (slug/public_profile), full sponsor-CRUD. |
| Bilder (hero, sponsorlogoer), backend | ✅ | Ny `teecup-minio`-tjeneste, ekte multipart-opplasting → AVIF-konvertering (Pillow) → lagring, live på `teecup.teeoff.no/teecup-media/*`. Selve opplastingsskjermen i frontend (V0) gjenstår. |
| Del-metadata (Open Graph: og:title/og:description) | ✅ | `generateMetadata()``/t/[id]`+`/clubs/[slug]`, ekte data fra API-et, verifisert mot produksjonsimaget. `og:image` gjenstår (MinIO). |
| Blind draw-skjuling på offentlig side | ✅ | Arves automatisk via delt `_fetch_sessions()`-hjelpefunksjon (ADR-018 Beslutning E) — ikke reimplementert. |
| Antall påmeldte / ledige plasser vist åpent | ✅ | `confirmed_count` i `GET /public/tournaments/{id}`. |
| Frontend: turnering-landingsside + påmeldingsskjema, LIVE | ✅ | `/t/[id]`. Tre bekreftelsestilstander (bekreftet/venteliste/godkjenning venter). Verifisert med ekte `POST`-registrering mot scratch. |
| Frontend: org-/klubb-landingsside, LIVE | ✅ | `/clubs/[slug]`. Gjenbruker `TournamentCard` (nå med valgfri `orgId`) for turneringslisten. Verifisert: viser kun `public`-synlige turneringer. |
---
### Program-skjerm (økter/tidsplan) — ✅ BYGGET OG SCRATCH-VERIFISERT 2026-07-18
Sjette V0-skjerm, første i "bygg i rekkefølgen ting brukes"-serien (etter
lag/roster kommer program, så blind draw, scorekort, leaderboard).
`components/tournament-program.tsx`, ny rute `/tournaments/[id]/program`.
Tidslinje over turneringens økter i rekkefølge + opprett-skjema (format,
hullomfang, scoringsmodus, poeng, klokkeslett+intervall, starthull, en
kollapsbar "avansert"-seksjon med ADR-014s fire handicap-brytere).
**Reelt blokkerende hull funnet FØR integrering, ikke etter:**
`SessionCreate.course_id` er påkrevd, men det fantes INGEN vei til å skaffe
en gyldig én — ingen `course`-endepunkt i API-et i det hele tatt, og ADR-004s
teeoff-integrasjon er fortsatt bare vedtatt (ingen utgående HTTP-klient
finnes noe sted i koden). Spurte bruker eksplisitt (samme mønster som andre
scope-avklaringer) — svar: bygg enkel course-CRUD nå. Lagt til
`app/routers/courses.py` (`GET`/`POST /orgs/{id}/courses`, kun
`source='custom'`, ingen hull-/tee-/rating-detaljer denne runden — motoren
bruker foreløpig kun `course_id` som fremmednøkkel). Program-skjemaet fikk et
type-ahead-felt for bane (samme mønster som spiller-type-ahead i
roster-skjermen: søk blant org-ens eksisterende baner, eller opprett ny
inline).
**Reell korrekthetsfeil funnet og rettet FØR den nådde V0-designet i det hele
tatt hadde blitt integrert:** V0-promptet mitt ba om ett generisk
"Scramble"-valg, men skjemaets `CHECK`-constraint og `handicap_engine.py` sin
`Format`-enum krever `scramble_2`/`scramble_4` som DISTINKTE verdier (antall
spillere per side er del av selve formatet) — ren `"scramble"` avvises med
400. Rettet i frontend-mappingen til to egne segment-knapper.
**`allowance_override`-mapping verifisert eksakt mot motor-kontrakten:**
`app/handicap.py` sin `parse_allowance_config`/`_strategy_from_json` forventer
`{type: "combined"|"per_player", percentage: 0..1}` — IKKE en flat prosent.
Frontend velger riktig `type` ut fra om formatet er side-enhet (foursome/
greensome/scramble_2/scramble_4 → `combined`) eller spiller-enhet (fourball/
singles → `per_player`), og konverterer skjemaets 0100-prosentfelt til
01-brøk før sending. Full JSON-rundtur bekreftet i scratch (se under) —
ikke bare antatt riktig fra å lese motorkoden.
**Verifisert grundig mot fersk scratch-infrastruktur** (ny `teecup_scratch`-
database 001→009 + en ISOLERT `teecup_app_scratch`-rolle som arver
`teecup_app` sine grants via `GRANT teecup_app TO teecup_app_scratch`
bevisst IKKE den ekte `teecup_app`-rollen, siden den nå er
produksjonskritisk og rollen er cluster-global på tvers av `teecup_db`/
`teecup_scratch`; en tidligere plandokument sin "drop teecup_app-rolle"-
opprydning er utdatert etter go-live og ble bevisst IKKE fulgt + isolert
scratch-MinIO-container): courses opprettet+listet, kryss-org-isolasjon
bekreftet (org 2 ser ikke org 1 sin bane), økt opprettet med klokkeslett,
økt opprettet med `scramble_4` + full `allowance_override`-rundtur, gammel
`"scramble"`-verdi korrekt avvist (400), `test_isolation.sql` fortsatt
12/12. Ekte typesjekket PRODUKSJONSBUILD (samme `Dockerfile`/multi-stage som
faktisk deployes, ikke bare en dev-server) kjørt og bekreftet — ny
`/tournaments/[id]/program`-rute listet korrekt i build-output.
**Diffet V0-eksporten mot live-treet før noe ble tatt inn** (samme mønster
som alle tidligere runder): kun tre reelt nye filer
(`tournament-program.tsx`, `program/page.tsx`, `ui/switch.tsx`) — resten var
forventede full-reverts av allerede tilpassede filer, ikke rørt.
**Mindre justeringer utover selve V0-promptet:** fjernet V0s dev-only
"forhåndsvis tom/med økter"-knapperad (ikke noe en ekte organisator skal se);
lagt til en fanerad ("Lag og spillere" / "Program") i BÅDE
`tournament-detail.tsx` og den nye skjermen, siden V0 ikke visste om den
andre skjermen når den ble generert i en egen prompt.
**Rullet ut live 2026-07-18**, bruker bekreftet eksplisitt: begge containere
(`teecup_api`, `teecup_frontend`) bygget og redeployet, live sjekker OK
(`/health`, `/dashboard` → 200), `teeoff.no` upåvirket.
**Nytt 2026-07-18, ✅ HELT FERDIG (backend + frontend live):** `PATCH`/
`DELETE` for økter (`app/routers/tournaments.py`) — kunne tidligere verken
rettes eller slettes etter opprettelse. `DELETE` kun for tomme økter (409
hvis den har matcher). `PATCH` dekker enkle felt fritt, pluss en egen,
forsiktig gren for bane-bytte (finner/flytter tilsvarende tee per allerede
tillagt deltaker, regner om handicap+matchstatus for hele økten etterpå —
også for allerede AVGJORTE matcher, bekreftet eksplisitt av bruker). Se
CLAUDE.md-status for det fulle scenarioet (verifisert med et 10-hulls
avgjort-match-eksempel) og et urelatert funn (`front_9`/`back_9` +
`stroke`-modus kan aldri få handicap i dag, siden `tee_rating` alltid kun
lages med `full_18`-omfang).
**Rediger-/slett-UI LIVE 2026-07-19** — utvidelse av den eksisterende
Program-skjermen (ingen ny rute). Fanget en reell regresjon i selve
V0-eksporten før den ble tatt inn: samme runde hadde utilsiktet fjernet
bane-feltet fra "Legg til økt"-skjemaet — kun de nye rediger/slett-delene
ble hentet inn, det ekte opprett-skjemaet urørt. Se CLAUDE.md-status for
detaljer.
---
### Offisiell banedata fra teeoff — ✅ BYGGET OG LIVE 2026-07-18 (ADR-019)
Bevisst sidesprang fra "bygg i rekkefølgen ting brukes" rett etter
program-skjermen: brukeren påpekte at ADR-004s teeoff-integrasjon fortsatt
bare var vedtatt, ikke bygget. Full design i ARCHITECTURE_DECISIONS.md
ADR-019 (fem delbeslutninger). Kort: organisator søker blant teeoff sine
baner i program-skjemaet, velger én, og teecup KOPIERER bane+hull+tee+
tee_rating inn i `teecup_db` (`source='official'`) — ikke et live oppslag
ved hver bruk. Ny `app/teeoff_client.py` (ren HTTP-klient mot
`http://teeoff_api:8000`, internt Docker-nettverk, ingen auth trengs — begge
containere deler allerede `teeoff_default`). To nye endepunkter i
`app/routers/courses.py`: `GET .../courses/official-search[/{slug}]` og
`POST .../courses/official-import`. Migrasjon `010` (unik
`external_course_ref` per org, hindrer dupliserte importer).
**Bevisste avgrensninger for denne runden:** kun 18-hulls baner kan
importeres (teeoffs skjema har ingen egen 9-hulls-inndeling); kun
`full_18`-rating importeres (teeoff har ingen separat front9/back9-rating,
samme valg som ADR-008 allerede tok for egendefinerte baner); ufullstendige
teeoff-data (manglende par/hcp_index på et hull, eller en tee uten NOEN
rating) avviser hele importen tydelig (`EXTERNAL_DATA_INCOMPLETE`) FØR noe
skrives, ikke en delvis importert bane.
**Verifisert grundig, inkludert mot EKTE `teeoff_api`** (ikke en simulert
respons): søk, anlegg-/banevalg, og import kjørt reelt mot den kjørende
produksjonscontaineren (kun lesing) — importerte Borregaard Golfklubb sin
hovedbane, bekreftet alle 18 hull + 8 tee/tee_rating-rader riktig i
databasen, og opprettet en ekte økt med den importerte banen (beviser hele
veien til handicap-motoren, ikke bare selve importen). Reimport avvist
(409), kryss-org-isolasjon bekreftet, ukjent teeoff-slug ga 404,
`test_isolation.sql` 12/12, ekte typesjekket produksjonsbuild av
frontend-utvidelsen.
**Rullet ut live**, bruker bekreftet eksplisitt: migrasjon 010 mot ekte
`teecup_db`, begge containere redeployet, `teeoff.no` upåvirket.
**Reell bug funnet og fikset samme dag, av en bruker som faktisk testet
funksjonen:** bane-søkeboksen ("Hent bane fra teeoff") var et `<form>`
rendret INNI det ytre økt-opprett-skjemaet — ugyldig, nestet HTML. Å klikke
"Søk" submittet i praksis det ytre skjemaet som en ekte side-navigasjon og
vasket bort `?org=...`-parameteren fra URL-en. Fikset ved å fjerne det
indre `<form>`-elementet (vanlig `<div>` + Enter-tast/knapp-klikk i
stedet). Se CLAUDE.md-status for full root cause.
**Nok en reell bug funnet og fikset samme uke, rapportert fra ekte bruk mot
`teecup.teeoff.no`:** import av samme teeoff-bane til flere økter (helt
normalt — flere runder spilles ofte på samme bane) ga en 409-feil i stedet
for å bare gjenbruke banen, og det lagrede navnet ("Hovedbanen" alene) ga
ingen måte å se hvilken klubb det gjaldt. Fikset: `POST .../courses/
official-import` er nå idempotent (gir tilbake eksisterende rad ved
reimport, sjekket FØR teeoff-kallet), og navnet lagres nå som
"{anlegg} {bane}". Ekte produksjonsdata for "De Gamle er Eldst" ryddet
opp (en økt hadde ved et uhell fått en tom søppel-`custom`-bane — se
CLAUDE.md-status for full hendelse og rotårsak).
**Åpent, ikke løst i denne runden:** de to bane-søkeflatene (øverste felt
= organisasjonens egne baner + "opprett ny"-snarvei, "Hent bane fra
teeoff"-knappen lenger ned = offisielt søk) er lette å forveksle — det var
nettopp dette som forårsaket søppel-banen. Vurder en tydeligere UI-
sammenslåing eller rekkefølge-endring i en senere runde.
---
## Invitasjonskode, ledende side og projisert stilling — ADR-020
Reist av brukeren 2026-07-19, rett etter at kamp-play-flyten var komplett.
Full design i ARCHITECTURE_DECISIONS.md ADR-020.
| Del | Status | Notat |
|---|---|---|
| Kort invitasjonskode per turnering, overstyrer visibility | ✅ backend | `tournament.join_code` (migrasjon 011), genereres automatisk ved opprettelse. `GET /public/tournaments/by-code/{code}` + kode-bypass i `registration.py`. Løser "muntlig invitert spiller finner ikke turneringen"-hullet. |
| `match.leading_side` (strukturert, ikke tekst-parsing) | ✅ backend | Cachet i `recompute_and_cache_match_state` ved hver hull-innsending, eksponert på `MatchOut`. |
| Projisert stilling (hvis pågående matcher holder seg) | ✅ backend | `TeamStanding.projected_points` + `SessionStanding.projected_points_by_team` i leaderboard-endepunktet. Ikke-avgjorte matcher gir full poengsum til `leading_side`, delt 0,5/0,5 ved "AS". |
| Login-skjerm: kode-felt, tar deg direkte til turneringen | ✅ LIVE 2026-07-19 | `login-form.tsx` sin `JoinByCode`. `code` tres gjennom `/t/[id]` sin lesing OG registrering. |
| Turnering-detalj: vis invitasjonskode (kopier-knapp) | ✅ LIVE 2026-07-19 | `tournament-detail.tsx` sin `JoinCodeChip` — henter fra org-ens turneringsliste, ikke et nytt endepunkt. |
| Leaderboard: to stillingsbarer (faktisk + projisert) | ✅ LIVE 2026-07-19 | `tournament-leaderboard.tsx` sin `SegmentedBar` — fargesegmentert rektangel, ikke tall side om side. Eksisterende tall-scoreboard beholdt som detaljvisning. |
| Fargekoding av matchlister etter ledende lag | ✅ LIVE 2026-07-19 | `session-blind-draw.tsx` sin `RevealedView`/`RevealSide` — farget toppkant + status-chip, bruker `leading_side` + eksisterende `team.color`, ingen ny fargemodell. |
| Kode-regenerering (ved lekket kode) | 💤 bevisst utsatt | Ikke bygget denne runden — ingen organisator-vei til å bytte ut en kode ennå. Egen sak hvis etterspurt. |
**ADR-020 er dermed helt ferdig** — backend + frontend, alle fire
del-ønsker levert samme dag. Se CLAUDE.md-status for full runde inkl. en
reell (og transparent håndtert) passord-eksponeringshendelse underveis.
---
## Innlogging: sesjon-bug + passord/2FA — ADR-021 (reist 2026-07-19)
| Del | Status | Notat |
|---|---|---|
| Flere organisasjoner per bruker | ✅ bekreftet allerede dekket | ADR-002 fra dag én. `POST /orgs` har ingen begrensning på antall org-er samme bruker kan eie. Ingen kodeendring — kun bekreftet ved gjennomgang 2026-07-19. |
| Sesjon holder seg ikke — ny magic-link-kode kreves ved hvert besøk | ✅ FIKSET og LIVE 2026-07-19 | Ikke en cookie-bug (brukerens ekte cookie var korrekt satt, 30 dager, `Secure`/`HttpOnly`). Root cause: `app/page.tsx` sjekket aldri om en gyldig sesjon allerede fantes før den viste innloggingsskjemaet. Fikset med server-side sesjonssjekk + redirect til `/dashboard`. Se CLAUDE.md-status for full diagnose og verifisering. |
| Passord som valgfritt tillegg til magic-link | ✅ LIVE 2026-07-19 | Argon2id-hashing (ikke bcrypt — unngår 72-byte-trunkering). Verifisert med et ekte passord med mellomrom+æøå+spesialtegn. `POST /auth/login-password`, `/auth/set-password`, `/auth/remove-password`. Passord er ALDRI påkrevd. |
| 2FA: TOTP eller e-post-engangskode, brukerens eget valg | ✅ LIVE 2026-07-19 | SMS bevisst utenfor omfang (krever betalt leverandør). `POST /auth/2fa/setup/start`+`/confirm`, `/auth/2fa/verify`, `/auth/2fa/disable`. |
| 2FA påkrevd for org-eier/admin, valgfritt for medlemmer | ✅ LIVE 2026-07-19 | Håndheves ved hver innlogging via en `stage: "pending_2fa"`/`"must_enroll_2fa"`-mellomtilstand i sesjons-JWT-en. Verifisert: en fersk org-eier uten 2FA ble korrekt tvunget inn i oppsett ved neste innlogging. |
| Frontend: passord-innlogging, 2FA-verifisering/-oppsett, kontoinnstillinger | ✅ LIVE 2026-07-19 | `login-form.tsx` (passord-modus), `two-factor-flow.tsx` (delt mellom login/verify), `/account`. |
**ADR-021 er dermed helt ferdig.** Se CLAUDE.md-status for full byggerunde,
inkl. tre reelle bugs funnet og fikset under scratch-testing.
### Organisasjonseierskap: dele, invitere, frasi seg, superadmin — ADR-022
Reist rett etter ADR-021. Avdekket et bredere, mer fundamentalt hull enn
bare "del eierskap": det fantes tidligere INGEN vei til å legge til et
organisasjonsmedlem etter opprettelse i det hele tatt — kun grunnleggerens
egen `owner`-rad ble noensinne satt inn.
| Del | Status | Notat |
|---|---|---|
| Flere eiere per organisasjon | ✅ LIVE | Skjemaet støttet det allerede (ingen unikhetssperre); nå faktisk brukbart via API. |
| E-post-invitasjon (owner→hvilken som helst rolle, admin→kun member) | ✅ LIVE 2026-07-19 | `POST/GET/DELETE /orgs/{id}/invitations`. Auto-akseptert ved neste innlogging (magic-link ELLER passord), samme mønster som spiller-e-post-kobling (ADR-017). Verifisert: admin som forsøkte å invitere som eier ble korrekt avvist. |
| Rollestyring + frasi seg eierskap (selvbetjent) | ✅ LIVE 2026-07-19 | `PATCH`/`DELETE /orgs/{id}/memberships/{id}`. «Siste eier»-vern (409 `LAST_OWNER`) OG selv-forfremmelse-vern verifisert eksplisitt i scratch. |
| Superadmin (manuelt DB-tildelt, ikke selvbetjent) | ✅ LIVE 2026-07-19 | `POST /superadmin/orgs/{id}/memberships`. Verifisert: ikke-superadmin avvist (403), superadmin kan sette eierskap på en org de selv ikke er medlem av, ukjent e-post avvist (404). |
| Fjernet eiers roster-/spillerdata | ✅ avgjort (Beslutning E) | Forblir urørt — organisasjons-styring ≠ deltakelse-historikk. Bekreftet av bruker 2026-07-18. |
| Frontend: medlemsstyring/invitasjon | ✅ LIVE 2026-07-19 | `org-members.tsx`, `/orgs/[id]/members`, lenket fra dashbordet. |
**ADR-022 er dermed helt ferdig.** Ingen dedikert superadmin-UI bygget
(bevisst — brukes via API av en betrodd operatør, matcher «sjelden,
manuelt tildelt makt»-designet).
---
## Rapporterte hull, blind draw + scorekort (2026-07-19)
Fire punkter rapportert av brukeren fra faktisk testing av
`/tournaments/.../sessions/...` (blind draw-skjermen) og scorekort-skjermen,
foursome-format. Root cause funnet ved kodegjennomgang for alle fire (det
fjerde, hcp, ble bekreftet mot EKTE produksjonsdata — ikke gjettet). Alle
fire er nå ✅ FIKSET OG SCRATCH-VERIFISERT samme dag — punkt 2 ble omdefinert
etter en presisering fra brukeren (se under), fikk en egen ADR (ADR-029) og
en reell skjemamigrasjon (`014_tee_gender_to_rating.sql`).
1. **✅ FIKSET 2026-07-19: valgt spiller forsvant ikke fra listen over
velgbare spillere.** Root cause: `AddSlotForm` i
`components/session-blind-draw.tsx` merket allerede brukte roster-rader
som `disabled` på selve `<option>`-elementet — et `disabled`
HTML-`<option>` blir stående synlig (kun gråtonet), forsvinner ikke.
Fikset: `roster`-listen FILTRERES nå ned til kun ledige spillere før
den rendres, i stedet for å deaktivere valget.
2. **✅ FIKSET OG LIVE 2026-07-19 (ADR-029).** Brukeren presiserte at min opprinnelige forståelse
var feil: en golfbane har IKKE fysisk kjønnsdelte utslag — begge kjønn
kan som regel spille fra ethvert utslag. Det eneste som faktisk
varierer per kjønn er om klubben har VALGT å slope (rate) et gitt
utslag for det respektive kjønnet (typisk: noen klubber sloper bevisst
ikke det lengste utslaget for damer). Bekreftet mot ekte produksjonsdata
(Tjøme Golfklubb, importert via ADR-019): hvert fysisk utslag ("32",
"44", "50", "55") lå som TO separate `tee`-rader med SAMME navn, én per
kjønn — selve konflateringen brukeren påpekte. **Fikset:** `gender`
flyttet fra `tee` til `tee_rating` (migrasjon
`014_tee_gender_to_rating.sql`, se ADR-029 for full detalj), tee-valg i
blind draw er nå HELT automatisk (organisator velger kun fysisk
utslag, riktig kjønnsrating løses fra spillerens `player.gender`
server-side — bruker bekreftet dette fremfor et forhåndsutfylt-men-
overstyrbart alternativ), manglende kjønn/rating avvises tydelig FØR
innsetting (bruker bekreftet "feil høyt" fremfor stille fallback).
ADR-019s teeoff-import, manuell tee-opprettelse og bane-bytte-remap
(`_remap_course`) alle oppdatert til samme modell.
3. **✅ FIKSET 2026-07-19: tallvelger (19 + utvidbar "10 eller flere" →
1019) i stedet for pluss/minus-steppere.** Ny `StrokePicker`-komponent
i `components/session-scorecard.tsx`, erstatter den gamle ±-stepperen
helt (`stepStroke`/`Minus`/`Plus` fjernet som død kode). Ren
frontend-endring.
4. **✅ FIKSET 2026-07-19: HCP ble faktisk ALDRI beregnet for
front_9/back_9-økter — bekreftet ekte kodebug, ikke bare en
synlighetsmangel.** Diagnostisert presist ved å lese EKTE
produksjonsdata (read-only, `teecup_db`) for brukerens rapporterte
testøkt: `hole_config='front_9'`, `format='foursome'`, og ALLE fire
`match_participant`-radene hadde `course_handicap`/`playing_handicap =
NULL`. Root cause: `app/handicap.py` sin
`compute_and_store_side_handicaps` joinet `tee_rating` på ØKTENS
`hole_config` som ratingens `scope` — men `tee_rating`-rader lages i
praksis KUN med `scope='full_18'` (ADR-019 Beslutning D + tee-
endepunktet), og ADR-008 sin allerede etablerte design tilsier nettopp
dette: course handicap skal ALLTID regnes fra full_18-ratingen, uansett
øktens hole_config — front/back-9-fordelingen skjer SENERE, ved selve
slagtildelingen (`allocate_over_played_holes`), ikke ved rating-
oppslaget. Joinen matchet dermed aldri noen rad for en front_9/back_9-
økt, og handicap ble stille aldri beregnet — påvirket ALLE formater på
front_9/back_9-økter, ikke bare foursome. **Fikset:** scope hardkodet
til `'full_18'`, `hole_config`-parameteren fjernet helt fra
`compute_and_store_side_handicaps`/`_recompute_session_matches` (var
død etter fiksen) og de tre kallstedene i `matches.py`/`tournaments.py`.
**Bekreftet at selve UTREGNINGEN matcher brukerens egen beskrivelse
nøyaktig** (kombinert course handicap / 2 via `CombinedPercentage(0.5)`,
laveste side satt til 0 mottatte slag via `match_play_strokes`, resten
fordelt fra stroke index 1 via `allocate_over_played_holes`) — bugen lå
i at beregningen ALDRI kjørte for front_9/back_9, ikke i selve formelen.
**Scratch-verifisert presist:** gjenskapte nøyaktig samme scenario
(foursome + front_9, hcp 10/20 vs. 5/15) — course/playing handicap
beregnet korrekt (10/21 vs. 5/16, kombinert 16/10), og et hull med
IDENTISK bruttoscore (5-5) på begge sider ga et ikke-delt resultat
("a" vant, ikke "halved") — direkte bevis på at handicap nå faktisk
brukes. Ingen migrasjon (ren Python-logikk-fiks).
**Ekte, urelatert bug funnet UNDER samme scratch-test, IKKE fikset
ennå:** et stroke-modus scoreinnsending på en bane UTEN registrerte
hull (`hole`-tabellen tom) krasjer med en rå 500
(`IndexError: list index out of range` i `handicap_engine.py` sin
`allocate_over_played_holes`, kalt fra `scoring.py` sin
`_compute_hole_results`) i stedet for en tydelig `VALIDATION_FAILED`.
Samme klasse feil som den allerede kjente/fikset
manglende-handicap-indeks-krasjen fra blind draw-runden (2026-07-18) —
bør fikses likt (eksplisitt sjekk FØR beregning, ikke en try/except
rundt symptomet). Ikke fikset i denne runden, kun oppdaget og notert.
**Designspørsmålene for punkt 2 er avklart** (bruker valgte det anbefalte
alternativet på begge, se ADR-029 Beslutning B/C): helautomatisk tee-valg
(ingen manuell kjønnsvelger), og "feil høyt" ved manglende kjønn/rating
(ingen stille fallback). Migrasjon 014 kjørt mot ekte `teecup_db`
2026-07-19, bruker bekreftet eksplisitt — Tjømes 8 tee-rader slått sammen
til 4, alle referanser intakte.
**Alle fire punkter rullet ut live 2026-07-19**, bruker bekreftet
eksplisitt: migrasjon 014 + `docker compose up -d --build teecup_api
teecup_frontend`. Verifisert: `/health`/`dashboard` → 200, `teeoff.no`
upåvirket, 0 brutte `match_participant.tee_id`-referanser etter
sammenslåingen.
---
## Rapporterte UI-/UX-hull (2026-07-19) — notert, IKKE fikset ennå
Fire punkter rapportert av brukeren fra faktisk bruk av `teecup.teeoff.no`.
Root cause funnet ved kodegjennomgang for de tre første (ikke bare gjettet);
det fjerde er et reelt manglende UI-element, ingen kodefeil. Ingen av de fire
er fikset i denne runden — kun dokumentert slik at de ikke går i glemmeboken.
1. **✅ FIKSET 2026-07-19/20 (ADR-030).** Dashboard-turneringskortet viste
"Ingen datoer satt" selv om øktene (rundene) hadde dato/klokkeslett
satt — bekreftet av brukeren med et faktisk skjermbilde (en økt tydelig
planlagt til "lør. 11. juli", men kortet viste fortsatt "Ingen datoer
satt"). Root cause: `tournament.start_date`/`end_date` var et EGET,
frittstående felt (ADR-015) som INGEN UI noensinne satte — helt atskilt
fra `session.scheduled_at`. Valgte retning (b) fra de to opprinnelig
skisserte alternativene: `GET /orgs/{id}/tournaments` utleder nå
datospennet fra øktenes `scheduled_at` (COALESCE med et evt. eksplisitt
satt `start_date`/`end_date`, som fortsatt vinner om det noensinne
settes). Se ADR-030 for full detalj og verifisering.
2. **✅ FIKSET 2026-07-20: `/orgs/{id}/members`-siden ga en rå API-404
(`{"detail":"Not Found"}`), ikke medlemssiden.** Bekreftet root cause,
ikke bare reprodusert: `frontend/next.config.mjs` sin `rewrites()`
returnerer en PLAIN ARRAY (implisitt "afterFiles"-semantikk i Next.js)
— statiske filer/sider sjekkes FØR rewrites, men DYNAMISKE sider (som
`app/orgs/[id]/members/page.tsx`) sjekkes ETTER. Rewrite-regelen
`{ source: "/orgs/:path*", ... }` (ADR-016) fanget derfor kallet FØR
Next.js noensinne nådde selve siden, og sendte det til FastAPI i
stedet (som naturligvis ikke har noen `GET /orgs/{id}/members`-rute).
**Fikset:** siden flyttet til `app/organizations/[id]/members/page.tsx`
(utenfor det proxyede `/orgs/*`-prefikset), eneste lenke til den
(`dashboard.tsx`) oppdatert tilsvarende. Lagt til en forklarende
kommentar direkte i `next.config.mjs` sin `rewrites()` slik at samme
feil ikke gjentas for en fremtidig ny side. Verifisert med ekte
produksjonsbuild + container-boot: den nye ruten rendrer faktisk
`OrgMembers`-komponenten (ikke en 404 eller innloggingssiden).
3. **Dashboard: ingen vei til å opprette/legge til en ANDRE organisasjon.**
Bekreftet i `dashboard.tsx`: `CreateOrganizationState`
(opprett-organisasjon-skjemaet) vises KUN når `hasOrg` er `false`, altså
når brukeren har null organisasjoner fra før. Har brukeren allerede én
org, finnes ingen knapp/lenke noe sted i UI-et for å opprette en til —
selv om backend-et støtter dette fullt ut og uten begrensning (ADR-021,
bekreftet: `POST /orgs` har ingen grense på antall org-er én bruker kan
eie). Ren manglende UI, ikke en backend-begrensning.
4. **Ingen måte å se, på ett blikk, at ALLE runder/økter i en turnering har
fått dato/klokkeslett satt.** `tournament-program.tsx` viser
`scheduled_at` per øktkort hvis satt, ingenting spesielt (ingen
fremhevet "mangler dato"-tilstand) hvis ikke. Ingen sammendrag/telling
noe sted ("X av Y runder har dato") — organisatoren må åpne
program-skjermen og lese hvert kort manuelt. Ren UX-mangel, ingen
bakenforliggende datamodell-begrensning (all nødvendig data finnes
allerede i `GET .../sessions`).
Punkt 1 (datovisning, ADR-030) og punkt 2 (medlemsside-ruten) er nå ✅
fikset, se over.
3. **✅ FIKSET 2026-07-21: ingen vei til å opprette en ANDRE organisasjon.**
Ny `NewOrganizationControl`-komponent i `dashboard.tsx` (identisk mønster
som `NewTournamentControl` — inline-ekspanderende navnefelt), plassert i
`OrganizationView` sin header ved siden av «Medlemmer»/«Ny turnering»,
uavhengig av antall org-er brukeren allerede har. Ingen backend-endring
nødvendig (`POST /orgs` hadde aldri noen begrensning).
4. **✅ FIKSET 2026-07-21: intet sammendrag for "alle runder har dato".**
Ny `DateCoverageSummary`-komponent i `tournament-program.tsx`, vist
øverst i øktlisten (kun når minst én økt finnes): «X av Y runder har
fått dato og klokkeslett» (uthevet/grønn når alle er satt). Ren
klientside-telling av allerede lastet `scheduled_at`-data, ingen
backend-endring.
Alle fire punktene fra 2026-07-19 er dermed fikset. Begge siste fikser
verifisert med ekte typesjekket produksjonsbuild, rullet ut live
2026-07-21 (kun `teecup_frontend`, ingen migrasjon), bruker bekreftet
eksplisitt.
---
### Rediger spiller (spillerpool) — ✅ BYGGET OG SCRATCH-VERIFISERT 2026-07-19/20
Reist av brukeren samme runde som ADR-030: ingen vei fantes til å rette en
feilregistrert spiller (f.eks. en HCP-skrivefeil) etter opprettelse — kun
`POST` fantes for `player`.
Ny `PATCH /orgs/{id}/players/{id}` (`app/routers/players.py`), vanlig
`exclude_unset`-PATCH-mønster, dekker alle spillerfelt (navn, HCP, kjønn,
mobil, e-post, fødselsdato, kallenavn, land, klubb, medlemsnummer). Frontend:
ny "Rediger spiller"-handling i rosterradens "⋮"-meny
(`tournament-detail.tsx`), inline skjema for navn/HCP/kjønn.
**Viktig, bevisst grense — kommunisert i selve UI-et, ikke skjult:** dette
endrer spilleren i POOLEN (brukes ved fremtidig rostring), IKKE et lags
allerede FROSNE `team_roster.handicap_index_snapshot` (ADR-007 — reproduser-
barhet for allerede opprettede turneringer/lag er et bevisst designvalg, ikke
noe denne rundens fiks endrer på). Skjemaet viser en tydelig forklarende
tekst om dette; å oppdatere et allerede rostret lags viste HCP-tall krever
fortsatt å fjerne og legge til spilleren på nytt (eksisterende funksjon).
**Scratch-verifisert:** HCP-endring lykkes, delvis PATCH (kun navn) lar HCP
stå urørt, tomt PATCH avvist (400), ukjent spiller-id gir 404, og — den
kritiske sjekken — en spillers allerede frosne roster-snapshot for et
EKSISTERENDE lag forble uendret etter en påfølgende spiller-PATCH (bekrefter
ADR-007 fortsatt holder). Ekte typesjekket produksjonsbuild kjørt og
bekreftet.
**Rullet ut live 2026-07-20**, bruker bekreftet eksplisitt, ingen migrasjon.
---
## Personlig landingsside for ENHVER registrert bruker + personlig profil — ✅ BYGGET OG SCRATCH-VERIFISERT 2026-07-20 (ADR-031)
Reist av brukeren 2026-07-20 som en "tenk igjennom og foreslå"-instruks,
deretter et "gjør det" med utvidet omfang (personlig profil-CRUD lagt til:
profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb). Full
detalj i ARCHITECTURE_DECISIONS.md ADR-031 — kort her:
| Del | Status | Notat |
|---|---|---|
| Ett samlet dashboard (ikke to atskilte ruter) | ✅ | Ny "Mine runder"-seksjon øverst i `dashboard.tsx`, organisasjonsseksjonen uendret under, begge vises hvis begge finnes. |
| "Mine runder" — tverr-org spiller-oppslag | ✅ | Ny `player_organizations_for_user()`-bro (migrasjon 015, samme mønster som fire tidligere), `/auth/me` utvidet med `my_tournaments`. Kun rostrede lag i v1 (ikke rene påmeldinger uten roster). |
| Personlig profil (fornavn/etternavn/fødselsdato/kjønn/HCP/hjemmeklubb) | ✅ | Nye felt på `app_user` (migrasjon 015) — ETT sett per konto, BEVISST atskilt fra org-scopede `player`-rader (se ADR-031 Beslutning B for hvorfor). `PATCH /auth/profile`, ny seksjon i `/account`. |
| Profilbilde | ✅ | `POST`/`DELETE /auth/profile/avatar`, samme ekte multipart→AVIF-opplasting som turnering-hero-bilder (ADR-018). |
| **Sikkerhetsutvidelse funnet UNDER bygging:** deltaker-tilgang uansett synlighetsnivå | ✅ | `check_visibility()` ga tidligere kun deltaker-tilgang for `visibility='participants'` — IKKE for `'org'` (DEFAULT for enhver ny turnering), som ville stengt ute enhver spiller uten org-medlemskap fra sin EGEN turnering. Utvidet til å gjelde begge ikke-offentlige tier. Verifisert: deltaker FÅR nå tilgang, fremmed+anonym fortsatt avvist (ingen innstramming, ren utvidelse). |
**Bevisst UTENFOR omfang, kjent gjenstående begrensning (se ADR-031 for full
begrunnelse):** "Mine runder" lenker til den offentlige turnering-siden
(`/t/{id}`), IKKE til lagets private chat eller scorekortet — disse krever
fortsatt ekte organisasjonsmedlemskap (`get_authorized_org`), en strengere
sperre enn deltaker-status alene, brukt av dusinvis av endepunkter på tvers
av appen. Å utvide DEN sperren til også å godta "faktisk deltaker" er en
egen, større og mer risikofylt endring (påvirker autorisasjonsarkitekturen
bredt, ikke ett enkelt endepunkt) — naturlig neste steg, men bevisst ikke
gjort i denne runden. Notifikasjons-/aktivitetsfeed og HCP-historikk over
tid også bevisst utenfor omfang v1 (samme begrunnelse som opprinnelig
forslag).
**Oppdatering 2026-07-21 — deltaker-tilgang til lag-chat/scorekort er
dermed ✅ BYGGET OG LIVE, se egen seksjon lenger ned.**
**Scratch-verifisert, 15 sjekker** (profil-CRUD komplett, inkl. sletting av
enkeltfelt og avatar; en EKTE ren spiller uten org-medlemskap ser riktig
`my_tournaments`; den kritiske sikkerhetssjekken: samme spiller får nå se
sin `'org'`-synlige turnering, mens fremmed/anonym fortsatt avvises). Ekte
typesjekket produksjonsbuild kjørt og bekreftet.
**Rullet ut live 2026-07-20**, bruker bekreftet eksplisitt: migrasjon 015
kjørt mot ekte `teecup_db`, begge containere redeployet, `/health`/
`/dashboard`/`/account` → 200, `teeoff.no` upåvirket.
### Naturlig neste steg (ikke bygget, notert for senere)
- **Deltaker-tilgang (uten org-medlemskap) til lag-chat og scorekort — ✅
BYGGET OG LIVE 2026-07-21**, se egen seksjon lenger ned.
- **HCP-historikk over tid — ✅ BYGGET OG LIVE 2026-07-21**, se egen seksjon
lenger ned.
- Notifikasjons-/aktivitetsfeed på "Mine runder" — fortsatt IKKE bygget.
- "Mine runder" for RENE påmeldinger (`tournament_registration` uten
roster ennå) — v1 viser kun rostrede lag, fortsatt IKKE bygget.
### Oppfølging samme dag: e-post + mobil — ✅ BYGGET OG SCRATCH-VERIFISERT (ADR-032)
Brukeren påpekte rett etter forrige runde at "identifikatoren" (e-post)
manglet i profilen, og at mobil (med landsnummer) burde være en opsjon. Full
detalj i ADR-032.
| Del | Status | Notat |
|---|---|---|
| Mobil (landsnummer + nummer, to separate felt) | ✅ | Del av samme `PATCH /auth/profile` som resten av profilen — ren tilføyelse. |
| E-post — verifisert to-stegs bytte, IKKE en enkel PATCH | ✅ | Ny `email_change_token`-tabell (migrasjon 016, samme mønster som `magic_link_token`). `POST /auth/profile/email` (krever sesjon, sender lenke til den NYE adressen) + `POST /auth/profile/email/confirm` (forbruker token atomisk, ingen sesjon påkrevd — samme som selve magic-link-verifiseringen). E-posten endres ALDRI før lenken faktisk åpnes. Ny `/verify-email`-side. |
**Scratch-verifisert, 10 sjekker** — inkl. at et bytte til en allerede brukt
adresse avvises, at e-posten forblir uendret helt til bekreftelse, at samme
kode ikke kan gjenbrukes, og at en ny innlogging med den GAMLE adressen
oppretter en fersk, tom konto (beviser byttet er reelt, ikke kosmetisk).
Ekte typesjekket produksjonsbuild kjørt og bekreftet.
**Rullet ut live 2026-07-20**, bruker bekreftet eksplisitt: migrasjon 016
kjørt mot ekte `teecup_db`, begge containere redeployet, `/health`/
`/dashboard`/`/account`/`/verify-email` → 200, `teeoff.no` upåvirket.
---
## Midlertidige spillere + automatisk etter-runde-invitasjon — 📋 FORESLÅTT 2026-07-20, IKKE bygget ennå
Reist samme runde som punktet over, uttalt som punkt 2 (ikke like prioritert
som "Mine runder"-dashbordet, men skal likevel dokumenteres grundig nå).
**Viktig presisering, funnet ved kodegjennomgang FØR noe ble antatt:** det
meste av "midlertidig spiller"-behovet er allerede dekket av eksisterende
funksjonalitet, ikke et hull i seg selv — `POST /orgs/{id}/players` krever
ALDRI at spilleren har en konto (`player.user_id` er nullable, kobles først
automatisk når/hvis noen logger inn på matchende e-post, ADR-017
Beslutning B). En organisator kan altså allerede legge til "Ola Nordmann,
ola@example.com" uten at Ola noensinne har hørt om TeeCup. Det som
FAKTISK mangler er den PROAKTIVE oppfølgingen brukeren ber om: et
automatisk e-post-utsendelse-steg etter runden, med scorekort + invitasjon
til å logge inn og "ta eierskap" over spiller-profilen sin — dette finnes
ikke i noen form i dag (en spiller må selv, uoppfordret, logge inn for at
koblingen skal skje).
**Foreslått design, IKKE bygget:**
- **Utløses av en EKSPLISITT organisator-handling, ikke en automatisk
bakgrunnsjobb** ("Send scorekort og invitasjon til alle med e-post i
denne økten", en knapp på øktnivå når øktens matcher er avgjort).
Anbefalt fremfor helautomatisk utsendelse ved et gjettet
"runden er ferdig"-tidspunkt — unngår uventede e-poster fra en
feilaktig auto-deteksjon, og matcher prosjektets øvrige mønster (blind
draw krever eksplisitt lås, walkover er en eksplisitt handling — ingen
"magisk" auto-trigger noe annet sted i appen).
- **Ingen ny databasekolonne nødvendig for selve "midlertidig"-begrepet**
— enhver `player`-rad UTEN `user_id` ER allerede "midlertidig" i praksis.
Kun en NY, liten `sent_at`-lignende sporingskolonne kan trengs for å
unngå dobbel utsending ved gjentatt klikk (åpent spørsmål, se under).
- **E-posten gjenbruker eksisterende infrastruktur** (`app/email.py`,
samme SMTP-oppsett som magic-link/2FA) — innhold: spillerens
hull-for-hull-resultat for økten + en ekte innloggingslenke (vanlig
magic-link, ingen ny auth-mekanisme nødvendig siden `link_player_by_
email()` allerede kobler kontoen automatisk ved første innlogging).
**Åpne spørsmål, trengs FØR bygging:**
- Skal utsendingsknappen ligge på ØKT-nivå (send til alle i denne ene
runden) eller TURNERING-nivå (send til alle på tvers av alle økter, når
hele turneringen er ferdig)? Økt-nivå virker riktigst — en spiller kan
ha spilt kun én av flere økter.
- Skal systemet spore "allerede sendt til denne spilleren for denne økten"
for å hindre dobbel utsending ved et nytt klikk (sannsynligvis ja — én
liten ny tabell/kolonne)?
- Skal e-posten sendes på spillerens/organisasjonens foretrukne språk
(samme `locale`-mønster som magic-link-e-posten, ADR-015 Beslutning C)?
---
## Deltaker-tilgang til lag-chat og scorekort (uten org-medlemskap) — ✅ BYGGET OG LIVE 2026-07-21
Direkte oppfølging av ADR-031s kjente, notert begrensning: "Mine runder"
lenket til den offentlige turnering-siden, men IKKE til lagets private
chat eller det skrivbare scorekortet — begge krevde fortsatt ekte
organisasjonsmedlemskap (`get_authorized_org`), noe en ren, rostret/
påmeldt spiller (uten organisasjonsmedlemskap) ikke har.
**Kjernefunn ved gjennomlesing (ikke antatt):** de faktiske
autorisasjonsprimitivene (`user_is_rostered_on_team`,
`user_is_match_participant`, `user_is_team_captain`,
`own_team_ids` — alle i `app/team_authz.py`/`app/blind_draw.py`) støttet
ALLEREDE ikke-org-medlemmer korrekt overalt — de var bare plassert BAK en
ekstra, blank `Depends(get_authorized_org)`-sperre på ni endepunkter på
tvers av fire filer. Fikset ved kirurgisk å fjerne akkurat den sperren fra
disse ni (lag-chat lese/skrive/slette i `messaging.py`; scorekort-lesing,
slag-/hull-resultat-innsending, walkover i `scoring.py`; match-/lag-/
økt-listing i `matches.py`/`tournaments.py`; walkover-på-turnering-nivå i
`tournaments.py`; bane-hull i `courses.py`) — de eksisterende
domene-sjekkene (som allerede har egen org-admin-fallback der det er
tiltenkt) er den REELLE sikkerhetsgrensen, ikke `get_authorized_org`.
**For endepunkter som IKKE hadde noen finkornet sjekk i det hele tatt**
(f.eks. `get_scorecard`, `list_sessions`, `list_teams` — disse stolte
UTELUKKENDE på org-medlemskap) ville en ren fjerning av sperren latt EN
HVILKEN SOM HELST innlogget bruker se dem — løst med et nytt, eksplisitt
`is_org_member(...) OR user_is_tournament_participant(...)`-OR (begge nye
hjelpefunksjoner i `team_authz.py`). `user_is_tournament_participant` er
FLYTTET dit fra `registration.py` (het `is_participant` der) — org-scopede
routere kan ikke importere fra `registration.py` uten sirkulær import
(registration.py importerer FRA dem), men alle importerer allerede fritt
fra den avhengighetsfrie `team_authz.py`. `courses.py` sin `list_holes`
fikk en bevisst LØSERE sjekk (`user_is_org_player` — kun "koblet til NOEN
spillerprofil i org-en", ikke bundet til én turnering) siden par/
stroke-index er lavsensitiv banedata, ikke spillerdata.
**`/auth/me` utvidet** med `my_session_id`/`my_match_id` per rad i
`my_tournaments` (en av spillerens egne matcher, ikke-avgjort foretrukket)
— lar frontend lenke direkte til riktig chat/scorekort uten at spilleren
selv må navigere via program-/blind draw-skjermene (som fortsatt krever
org-medlemskap for andre formål). "Mine runder"-kortet i `dashboard.tsx`
fikk to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når
`my_match_id` finnes).
**Scratch-verifisert grundig, 43 automatiserte sjekker** (isolert
`teecup_app_scratch`-rolle, isolert scratch-MinIO, engangs API-container):
en rostret spiller UTEN org-medlemskap fikk korrekt tilgang til alt de ni
endepunktene dekker (inkl. faktisk å SENDE en chat-melding og et
hull-resultat); samme spiller fortsatt korrekt AVVIST fra det andre laget
sin chat; en helt fremmed innlogget bruker (ingen spillerkobling i org-en
i det hele tatt) avvist overalt; org-eier beholder full tilgang til alt
UNNTATT lag-chat (ekte privat, med vilje uendret — ADR-025); kryss-org-
isolasjon bekreftet (kan ikke nå egen turnering via en ANNEN org-id);
og — et reelt funn UNDER testingen, ikke en bug — en rostret-men-ikke-
utpekt-kaptein spiller ble først FEILAKTIG godtatt til walkover fordi
laget ennå ikke hadde noen utpekt kaptein (allerede dokumentert,
tiltenkt fallback i `user_is_team_captain`: "ingen kaptein ennå = enhver
rostret spiller godtas") — testen ble korrigert (la til en faktisk
kaptein) og bekreftet deretter riktig avvisning av ikke-kapteinen.
`test_isolation.sql` fortsatt 12/12 (ingen skjemaendring). Ekte
typesjekket produksjonsbuild av frontend kjørt og bekreftet.
**Rullet ut live 2026-07-21**, bruker bekreftet eksplisitt: ingen
migrasjon, `docker compose up -d --build teecup_api teecup_frontend`,
begge containere boot-et rent, `/health`/`/dashboard` → 200, `teeoff.no`
upåvirket.
---
## HCP-historikk over tid — ✅ BYGGET OG LIVE 2026-07-21
Siste av de tre konkrete følgepunktene brukeren bekreftet i dashboard/
konto-runden (2026-07-21) — ADR-031s "naturlig neste steg"-punkt: den
personlige profilens `app_user.handicap_index` endres i dag stille ved
hver `PATCH /auth/profile`, uten noen logg over tidligere verdier.
**Migrasjon `018_handicap_history.sql`:** ny append-only-tabell
`handicap_history` (`user_id`, `handicap_index`, `recorded_at`) — kun for
den PERSONLIGE profilens HCP, bevisst atskilt fra de org-scopede
`player.handicap_index`-radene og `team_roster.handicap_index_snapshot`
(som allerede har sitt eget reproduserbarhets-prinsipp, ADR-007, ikke rørt
her).
**Backend:** `update_profile` (`PATCH /auth/profile`) leser gjeldende HCP
FØR den overskrives, og logger en ny historikk-rad KUN når verdien faktisk
ENDRES til en tallverdi — ikke ved nullstilling (ingen "HCP fjernet"-
hendelse gir mening i en verdi-over-tid-logg), og ikke ved et PATCH som
gjentar samme verdi uendret (unngår støy fra en form som lagres på nytt
uten reell endring). Ny `GET /auth/profile/handicap-history`.
**Frontend:** en «Vis HCP-historikk»-lenke i `/account` sin
`ProfileSection`, ekspanderer til en dato+verdi-liste, hentes på nytt
automatisk rett etter en lagring.
**Scratch-verifisert, 18 sjekker:** tom historikk for en fersk bruker,
riktig logging ved første HCP-verdi, INGEN duplikat ved gjentatt lagring
av uendret verdi (selv sammen med en annen felt-endring i samme PATCH),
ny rad ved faktisk endring, kronologisk rekkefølge riktig, ingen logg ved
nullstilling, ny rad ved gjeninnsetting etter nullstilling, og full
isolasjon mellom to ulike brukeres historikk. `test_isolation.sql`
fortsatt 12/12. Ekte typesjekket produksjonsbuild kjørt og bekreftet.
**Rullet ut live 2026-07-21**, bruker bekreftet eksplisitt: migrasjon 018
kjørt mot ekte `teecup_db` (tabell bekreftet, `test_isolation.sql`
fortsatt 12/12), deretter `docker compose up -d --build teecup_api
teecup_frontend`. Begge containere boot-et rent, `/health`/`/dashboard`/
`/account` → 200, `teeoff.no` upåvirket.
**Dermed er alle tre bekreftede punktene fra dashboard/konto-runden
(2026-07-21) ferdig bygget:** deltaker-tilgang til lag-chat/scorekort,
sekundær e-postadresse (del 1), og HCP-historikk. Gjenstående, bevisst
utsatte punkter fra samme runde: dashbordets tom-tilstand-redesign
(venter på retning), frittstående rundeføring + statistikk (trenger egen
ADR), og konto-sammenslåing (del 2 av multi-e-post).
---
## Obligatorisk profil-fullføring ved innlogging — ✅ BYGGET OG LIVE 2026-07-22
Bygget som direkte svar på "hva skal møte en fersk bruker aller først"-
spørsmålet reist i tom-tilstand-diskusjonen under. Brukeren observerte selv
at en fersk konto (`hei@erol.no`, opprettet bevisst for å se førstegangs-
innloggingen) kun viste et tomt skall + opprett-organisasjon-skjermet, og
avklarte at riktig oppførsel er: **kontoinnstillinger/personlig profil skal
være det aller første som vises, og alt der (utenom bilde) skal være
obligatorisk**, før noe annet i appen (inkl. dashbordet) er tilgjengelig.
**Design:**
- Ny migrasjon `019_profile_country_bio.sql`: `app_user.country` +
`app_user.bio` (samme nullable-kolonne-mønster som resten av
ADR-031-profilen — "obligatorisk" håndheves i app-laget via et beregnet
`profile_complete`-felt på `/auth/me`, ikke som en DB `NOT NULL`).
- Obligatoriske felt: fornavn, etternavn, fødselsdato, kjønn, HCP,
hjemmeklubb, land. Valgfrie: beskrivelse, profilbilde.
- **HCP-grensetilfellet avklart eksplisitt med bruker før bygging** (via
AskUserQuestion): en fersk golfspiller har sjelden en offisiell HCP
ennå. Løsning: WHS-maksimum 54 er forhåndsutfylt i skjemaet som
utgangspunkt, og `handicap_index` har en hard `le=54`-validering i
`ProfileUpdate` (kan aldri registreres høyere) — ingen egen "har ikke
HCP ennå"-avkrysning trengtes.
- `/account` grener på `profile_complete`: ufullstendig → et nytt,
fokusert `ProfileOnboarding`-skjema (kun de obligatoriske feltene +
valgfri beskrivelse, «Logg ut» tilgjengelig, INGEN annen navigasjon) —
komplett → den vanlige innstillingssiden (nå med land+beskrivelse lagt
til i det ordinære profilskjemaet for redigering i etterkant, per
brukerens eget ønske: "Når dette er på plass kan informasjonen heller
kunne redigeres i 'Konto'-visningen").
- `app/page.tsx` (rot) og `Dashboard`-komponenten sender en innlogget
bruker til `/account` i stedet for `/dashboard` når profilen er
ufullstendig — dekker alle innloggingsveier (magic-link/passord/2FA
lander alle på `/dashboard` uansett hvilken flyt som ble brukt, som selv
gjør sjekken ved mount, så ingen av de tre separate login-komponentene
måtte endres).
- **Bevisst avgrenset:** gaten håndheves kun ved disse to inngangspunktene,
ikke ved dypere direktelenker til andre autentiserte sider (f.eks. en
bokmerket turnering-URL) — samme skope-disiplin som tidligere runder.
**Verifisert:** se full detalj i CLAUDE.md-status 2026-07-22 — 16/16
scratch-backend-sjekker, `test_isolation.sql` 12/12, ekte typesjekket
produksjonsbuild, og et ekte HTTP-nivå-bevis mot en kjørende
produksjonscontainer (anonym → 200 innloggingsskjema, ekte innlogget-men-
ufullstendig sesjonscookie → `307 → /account`). Rullet ut mot ekte
`teecup_db`/containere, bruker bekreftet eksplisitt.
**Kjent, tilsiktet konsekvens:** brukerens BEGGE egne kontoer
(`erol.haagenrud@envide.no` og `hei@erol.no`) manglet alle disse feltene
og vil derfor begge se profil-fullførings-skjemaet ved neste innlogging.
---
## Dashboard: tom-tilstand ved første innlogging — 📋 KONKRET FORSLAG LAGT FREM (2026-07-25), IKKE bygget
Brukeren påpekte 2026-07-20 at dagens tomme-tilstand ("Du har ingen
organisasjon ennå — opprett en") er organisator-vridd og ikke stemmer med
hva en fersk bruker faktisk trenger å se/gjøre. Et opprinnelig forslag
(utvid "Mine runder" til påmeldinger + nøytral to-valgs tom-skjerm, se
historikk under) ble lagt frem 2026-07-20 — brukeren ba 2026-07-21
eksplisitt om å justere retningen i lys av en dypere refleksjon, se under.
**2026-07-21 — premisset er endret, ikke bare forslaget:** brukeren stilte
selv spørsmålet om organisasjon fortsatt bør være "det som meldes først" —
gitt ADR-031 (Mine runder), ADR-032 (e-post/mobil som personlig identitet)
og det nye ønsket om frittstående rundeføring med statistikk (se egen
seksjon rett under), er en vanlig bruker først og fremst en GOLFSPILLER,
og det å arrangere turneringer er én av flere ting en spiller *kan* gjøre —
ikke forutsetningen for å bruke appen i det hele tatt. **Vurdering: ja,
organisasjon bør slutte å være default/første-handling**, og bli ett
likestilt valg blant flere fremtidige "første ting du kan gjøre"
(bli med i en turnering via kode, registrere en runde selv, ELLER
arrangere/opprette organisasjon) — ikke lenger den ENESTE synlige veien
inn.
**2026-07-22 — delvis besvart, ikke fullt løst:** brukeren avklarte at
det ALLER første en innlogget bruker med en ufullstendig personlig profil
skal se, er en obligatorisk «Fullfør profilen din»-visning (fornavn/
etternavn/fødselsdato/kjønn/HCP/hjemmeklubb/land — alt utenom bilde og
beskrivelse) — se CLAUDE.md-status, ✅ BYGGET OG LIVE. Dette svarer på
"hva møter en fersk bruker aller først", men IKKE på det opprinnelige
spørsmålet i denne seksjonen: hva skal dashbordets tom-tilstand vise for
en bruker som HAR fullført profilen, men ennå ikke har noen organisasjon/
turnering å vise? Den vurderingen (organisasjon bør slutte å være
default/første-handling) står fortsatt ved lag og er fortsatt IKKE bygget.
**Konsekvens for byggerekkefølgen:** selve tom-skjerm-redesignet er satt
PÅ VENT til frittstående runder (under) er avklart nok til å vite hvilken
tredje kortform den skal ha på tom-skjermen — å bygge en to-valgs versjon
nå og redesigne den på nytt om kort tid ville vært dobbeltarbeid. Punktet
"utvid Mine runder til rene påmeldinger" (fra 2026-07-20-forslaget)
henger IKKE sammen med denne avhengigheten og kan bygges uavhengig når som
helst — fortsatt et åpent, godt avgrenset TODO.
**Opprinnelig forslag (2026-07-20), for historikkens skyld:**
1. Utvid "Mine runder" til også å vise rene påmeldinger (ikke bare
rostrede lag).
2. Gjør selve tom-skjermen nøytral: to likestilte valg side ved side —
"Har du en kode?" og "Skal du arrangere selv? Opprett organisasjon".
**Venter på:** en videre avklaring av frittstående runder (under) før
tom-skjermens endelige form kan bestemmes.
---
### 2026-07-25 — avhengigheten er løst, konkret forslag lagt frem (📋 DESIGNET, IKKE BYGGET)
Frittstående runder (ADR-033) er nå ferdig bygget (backend+frontend, alle
oppfølgingsrunder), så blokkeringen over er borte. Brukeren reiste samtidig
det dypere spørsmålet "hvorfor har vi organisasjon i det hele tatt" —
besvart og designet som **ADR-035** (organisasjon beholdes, men
opprettelsen gjøres usynlig/automatisk — Beslutning B, se
ARCHITECTURE_DECISIONS.md for full A-vs-B-avveining og
reversibilitetsvurdering).
**Konkret blokk-forslag for det nye dashbordet** (rekkefølge, topp til
bunn):
1. **Hurtighandlinger** — tre likestilte kort/knapper: "Ny runde", "Ny
turnering" (oppretter/gjenbruker organisasjon usynlig, ADR-035), "Bli
med med kode" (ADR-020). Ingen av de tre skal kreve noe org-steg
synlig for brukeren.
2. **Kommende runder** — egne runder som ikke er fullført
(`completed_at IS NULL`), sortert på dato, inntil 3 vist + "se alle"
til `/my-rounds`. Tomtilstand: kort tekst + snarvei til "Ny runde".
3. **Kommende turneringer** — SLÅR SAMMEN turneringer man er deltaker i
(dagens "Mine runder"-data) OG turneringer man arrangerer (dagens
organisasjons-turneringsliste, på tvers av ALLE organisasjoner
brukeren er medlem i, flatt) til ÉN tidssortert liste. Kort man
arrangerer får en liten "Arrangør"-merkelapp. Tomtilstand: snarvei til
"Ny turnering"/"Bli med med kode".
4. **Statistikk** — smått aggregert: antall runder spilt, HCP-trend
(sparkline fra den eksisterende `handicap_history`-tabellen, kun vist
ved ≥2 datapunkter), snitt til par siste 5 runder. Tomtilstand til
minst én runde er fullført.
5. **Spilte baner** — utledet fra `round.course_name_snapshot`, gruppert
med besøksantall + sist spilt. Ingen ny datamodell trengs.
6. **Venner** — kort med antall ventende forespørsler + snarvei til en ny
`/friends`-side (se ADR-036 under). Tomtilstand: "Du har ingen venner
ennå — søk etter noen".
7. **Organisasjoner** — KUN vist hvis brukeren er medlem i mer enn den
auto-opprettede sin egen (dvs. har en ekte, navngitt klubb-tilknytning)
— én liten, nedtonet lenke, ikke en egen fremtredende seksjon. Dette er
selve poenget med ADR-035: organisasjon skal ikke lenger dominere
dashbordet.
**V0-prompt skrevet, IKKE sendt til V0 ennå** (venter på brukerens
gjennomgang av blokk-forslaget over først):
> Design et nytt dashbord for TeeCup (golf-app, Next.js + Tailwind +
> shadcn/ui, mobil-først, eksisterende merkevarefarger: grønn primær,
> oransje sekundær — bruk appens eksisterende design-tokens, ikke nye
> farger). Dette ERSTATTER dagens dashbord, som feilaktig satte
> "organisasjon" som det første og viktigste en bruker møtte — ny
> retning: brukeren er først og fremst en GOLFSPILLER, organisasjon er en
> liten, valgfri detalj lengre ned.
>
> Innhold, i denne rekkefølgen, som distinkte kort/seksjoner (ikke faner):
> 1. Hurtighandlinger: tre like store, likestilte knapper/kort side ved
> side (stables på smal skjerm) — "Ny runde", "Ny turnering", "Bli med
> med kode". Tydelige, tekstede (ikke kun ikon), store trykkflater.
> 2. "Kommende runder" — liste over inntil 3 pågående/ikke-fullførte
> runder (banenavn eller eget rundenavn, dato, en liten
> fremdriftsindikator "X/18 hull"), med en "se alle"-lenke. Vis en
> tydelig, vennlig tomtilstand med snarvei til "Ny runde" hvis ingen.
> 3. "Kommende turneringer" — liste over turneringer brukeren enten
> deltar i eller arrangerer, sortert på dato, en liten
> "Arrangør"-merkelapp på kort man selv arrangerer. Tomtilstand med
> snarveier til "Ny turnering"/"Bli med med kode".
> 4. "Statistikk" — en kompakt rad med 2-3 nøkkeltall (antall runder,
> HCP nå + en liten trendpil/sparkline, snitt til par) i staselige
> tall-fliser (stort, fet skrift, høy kontrast).
> 5. "Spilte baner" — en kompakt liste/rutenett av baner med
> besøksantall og sist spilt-dato.
> 6. "Venner" — et lite kort: avatar-stabel av noen få venner (om noen),
> et tall-merke for ventende forespørsler, snarvei "Se venner".
> Tomtilstand: oppfordring til å søke opp noen.
> 7. "Organisasjoner" — KUN når relevant: én liten, nedtonet tekstlenke
> nederst, IKKE et fremtredende kort — dette skal se ut som en
> bakgrunnsdetalj, ikke en hovedseksjon.
>
> Tilgjengelighet er et ufravikelig krav, ikke en estetisk sluttpuss: god
> kontrast, stor nok skrift, store trykkflater (min. 44px), ALDRI
> ikon-only uten tekstlabel på viktige handlinger, appen skal være
> brukbar uten finmotorikk eller skarpt syn. Design ALLE tomtilstander
> eksplisitt, ikke bare den fylte varianten — de fleste nye brukere vil
> se flere tomme blokker samtidig, og det skal fortsatt se innbydende ut,
> ikke ufullstendig.
**Åpne spørsmål før bygging:**
- Skal blokk 3 og 4 lenke til nye, dedikerte sider (f.eks. en egen
aggregert statistikk-side), eller er de rene dashbord-widgets uten
"se mer"? Statistikk-tallene over er enkle å beregne, men en FULL
aggregert statistikk-side (grafer over tid på tvers av alle runder) er
et større, eget stykke arbeid — ikke inkludert i dette forslaget.
- Nøyaktig terskel for når "Organisasjoner"-lenken vises (mer enn 1 org
totalt? Eller kun når minst én org har et eksplisitt satt `slug`/
`public_profile`, dvs. faktisk er gjort til en "ekte" klubb?).
---
## Venner, kategorisert deling av runder, og tiered personsøk — 🔨 FASE 1 BACKEND+FRONTEND BYGGET 2026-07-25 (se under), fase 2/3 fortsatt kun designet
Reist av brukeren samme runde som dashbord-forslaget over. Fullt design
skrevet som **ADR-036** i ARCHITECTURE_DECISIONS.md — se der for
datamodell, autorisasjonslogikk og søke-algoritme i detalj. Kort
oppsummert her, pluss åpne spørsmål og foreslått byggerekkefølge.
**Tre sammenhengende deler:**
1. Et ekte vennekonsept — gjensidig forespørsel/aksept (som org-
invitasjoner), pluss et fast sett kategorier en venn kan settes i
(flere samtidig): Make, Nær familie, Storfamilie, Nære venner,
Golfvenner, Kollegaer, Forretningsforbindelser, Studiekamerater,
Perifere bekjente, Ymse. **Kategoriseringen er privat** — vennen vet
ikke hvilken/hvilke grupper du har satt dem i.
2. Rundevisibilitet — ny `round.visibility_mode`
(`public`/`private`/`friends`, default `private`). Ved `friends`
velger man EKSPLISITT hvilke av gruppene sine som får se runden (ikke
"alle venner"). Styrer kun tredjeparts innsyn — en faktisk lagt-til
medspiller ser alltid runden uansett innstilling.
3. Tiered personsøk — samme søkbare-liste-mønster som bane-/klubbsøket
(`HomeClubField`/`OfficialSearchStep`), navnerekkefølge-uavhengig
(skriv for- ELLER etternavn først, begge treffer), rangert: venner →
samme hjemmeklubb → samme land → globalt. Ett delt endepunkt brukt
BÅDE til "finn en venn" og (i en senere fase) "legg til medspiller på
en runde".
**Reell synergi funnet under design, ikke tilfeldig:** forrige rundes
redesign av "Hjemmeklubb" til en ekte dropdown-verdi (i stedet for
fritekst) gjør nå "samme klubb"-rangeringen i søket pålitelig — et
eksakt strengmatch fungerer nå, noe det ikke ville gjort med den gamle
fritekst-versjonen.
**Foreslått byggerekkefølge (IKKE bekreftet), tre uavhengig leverbare
faser:**
1. Venner-kjernen (vennskap, kategorisering, `/people/search`, ny
`/friends`-side) — leverbar og nyttig helt alene.
2. Rundevisibilitet (visibility-valg ved rundestart, gatede
lese-endepunkter, sanntids-livevisning for venner/offentlighet).
3. Ekte medspillere (ikke bare gjester) — utvider
`POST /rounds/{id}/participants` til å godta et søkt `user_id`.
**Avklart med bruker 2026-07-25: JA, runden skal vises i spillerens
egen rundeliste.** Teknisk konsekvens (se ADR-036 for full detalj):
`list_rounds` må utvides fra kun `owner_user_id` til også lenkede
`round_participant.user_id`-rader, og `RoundOut` sine
`owner_*`-statistikkfelt må bli "viewer-relative" (vise DEN som ser
på sin egen score, ikke alltid eierens). **Skrivetilgang også
avklart 2026-07-25: en medspiller skal kunne registrere score for
ALLE i flighten**, ikke bare egen rad — samme rettighet som eieren
har i dag, utvidet til enhver lenket ekte deltaker. Sletting av
runden og bane-/utslagsbytte forblir eier-eksklusivt (se ADR-036 for
presis autorisasjonssjekk).
**Andre åpne spørsmål (se ADR-036 for full drøfting):**
- Skal en bruker kunne gjøre seg "usøkbar"? Foreslått default: alle med
fullført profil er søkbare, ingen opt-out i første omgang.
- Minimum antall tegn før søket returnerer globale (ikke-venn/klubb/land)
treff — foreslått 2, for å hindre triviell enumerering av alle
brukere.
- Skal en "offentlig" runde være synlig for ANONYME besøkende (som
turneringers offentlige side), eller kreve innlogging? Foreslått:
samme mønster som turnering — anonymt tilgjengelig.
**Ingen kode skrevet ennå** — dette var en design-/dokumentasjonsrunde,
ikke en byggerunde, på brukerens eksplisitte instruks.
---
### Fase 1 (venner-kjernen) — ✅ BACKEND BYGGET OG SCRATCH-VERIFISERT 2026-07-25, IKKE ennå rullet ut, frontend gjenstår
Migrasjon `025_friends.sql` (`friendship` + `friend_categorization`, ingen
RLS — samme `plain_connection()`-mønster som runder), nytt
`app/routers/friends.py`: `GET /people/search` (tiered, navnerekkefølge-
uavhengig, min. 2 tegn), `POST /friends` (send forespørsel),
`POST /friends/{id}/accept`, `DELETE /friends/{id}` (avslå/kanseller/
avvenn — én operasjon dekker alle tre), `GET /friends` (venner +
inn-/utgående forespørsler), `PUT /friends/{friend_user_id}/categories`
(erstatter hele settet, krever akseptert vennskap).
**Verifisert grundig i scratch** (isolert `teecup_app_scratch`-rolle +
isolert scratch-MinIO + engangs API-container, 5 syntetiske brukere): 31
sjekker — navnerekkefølge-uavhengig søk begge veier, selv-ekskludering,
anti-enumerering under 2 tegn, dupliserte forespørsler avvist i BEGGE
retninger, kun mottaker kan akseptere, tiered rangering bekreftet presist
(venn → samme klubb → samme land → globalt, i RIKTIG rekkefølge for 4
distinkte brukere samtidig), kategorisering avvist for ikke-venn,
kategorisering BEKREFTET PRIVAT (B ser ALDRI kategoriene A har satt B i),
uvedkommende kan ikke slette andres vennskap, avvenning + ny forespørsel
etterpå fungerer. `test_isolation.sql` fortsatt 12/12. Ekte typesjekket
produksjonsbuild kompilerte rent (ingen frontend-endring ennå utover
`next.config.mjs` sine nye rewrites for `/people`/`/friends`).
**Frontend BYGGET 2026-07-25, samme dag** — zip 19 mottatt (V0-prompten
under kjørt av bruker), integrert som `components/friends.tsx` + ny rute
`app/my-friends/page.tsx` (IKKE `app/friends/page.tsx` slik V0 selv
foreslo — flyttet bevisst for å unngå kollisjon med API-prefikset
`/friends`, samme klasse feil som `/rounds` vs. `/my-rounds` tidligere,
denne gangen unngått fra start). Datalag skrevet om fra mock til ekte
fetch, kategori-koder mappet mot norske visningsnavn i samme rekkefølge
som backend. Dashbordets "Venner"-blokk (`dashboard.tsx`) koblet til
ekte `GET /friends`-data i samme runde — viser nå ekte antall
venner/ventende forespørsler, lenker til `/my-friends`. Ekte
typesjekket produksjonsbuild kompilerte rent.
**V0-prompten som ble brukt (til referanse):**
> Design en ny "Mine venner"-side for TeeCup (golf-app, Next.js +
> Tailwind + shadcn/ui, mobil-først, eksisterende merkevarefarger).
>
> Innhold:
> 1. Et søkefelt øverst ("Søk etter navn …") — søkbar liste som filtrerer
> live mens man skriver (samme mønster som bane-/klubbsøket ellers i
> appen), viser navn + evt. hjemmeklubb per treff, med en
> "Send forespørsel"-knapp per rad.
> 2. En "Forespørsler"-seksjon (kun synlig når det finnes noen): to
> undergrupper — "Mottatt" (med "Godta"/"Avslå"-knapper per rad) og
> "Sendt" (med en "Kanseller"-knapp per rad, og tekst som "Venter på
> svar").
> 3. En "Venner"-liste — hver rad har navn, avatar (initialer som
> fallback), hjemmeklubb, og en utvidbar kategori-velger: ti faste
> avkrysningsbare kategorier (Make, Nær familie, Storfamilie, Nære
> venner, Golfvenner, Kollegaer, Forretningsforbindelser,
> Studiekamerater, Perifere bekjente, Ymse) — flere kan velges
> samtidig for samme venn. Vis tydelig, med liten tekst under
> kategori-velgeren: "Kun du ser hvilke kategorier du har satt en venn
> i." En "Fjern venn"-knapp med bekreftelsessteg.
> 4. Tomtilstander for alle tre seksjonene (ingen venner ennå, ingen
> forespørsler).
>
> Tilgjengelighet er et ufravikelig krav: god kontrast, stor nok skrift,
> store trykkflater (min. 44px), aldri ikon-only uten tekstlabel på
> viktige handlinger.
---
## Turneringsoppsett: flytte/slette spillere mellom lag — 📋 NOTERT 2026-07-25, IKKE bygget
Reist av brukeren samme runde som dashbord-integreringen. I dag finnes
`DELETE .../roster/{roster_id}` (fjerner en spiller fra ETT lag) og
`PATCH .../roster/{roster_id}` (kun `is_captain`, ADR-023) — men INGEN vei
til å FLYTTE en allerede rostret spiller til et ANNET lag i samme
turnering i én operasjon. I dag må organisatoren gjøre det som to separate
kall (fjern fra lag A, legg til på lag B), og det finnes ingen slik
UI-handling i `tournament-detail.tsx` sin `TeamPanel` i det hele tatt —
kun fjerning.
**Ikke designet i detalj ennå** — rent notert som et reelt hull. Naturlig
retning ved bygging: enten et eget `POST .../roster/{roster_id}/move`
(target-team-id) som gjør begge operasjonene atomisk (unngår en
mellomtilstand der spilleren midlertidig ikke er rostret noe sted), eller
en UI-snarvei som bare kjører de to eksisterende kallene i sekvens — det
første er tryggere (ingen delvis fullført tilstand ved feil midtveis).
---
## Frittstående runder: flere flighter i én "vanlig" runde — 📋 NOTERT 2026-07-25, IKKE bygget
Reist av brukeren samme runde: eksempel gitt — "jeg går i den første
flighten sammen med tre andre, mens fire venner går i flighten bak."
Dagens datamodell (`round`, ADR-033 Beslutning A) er implisitt ÉN flight
= ÉN runde: `round.owner_user_id` er entydig, og alle
`round_participant`-rader (eier + gjester/medspillere) spiller sammen i
SAMME flight på SAMME hull-for-hull-registrering.
**Reell modelleringsspenning, ikke bare en UI-mangel:** å støtte flere
flighter "i samme runde" krever et valg mellom to prinsipielt ulike
retninger:
1. **Løs gruppering av flere separate `round`-rader** — hver flight er
fortsatt sin egen `round` (egen eier, egne deltakere, egen
hull-registrering, uendret datamodell), men et nytt, tynt
"delt arrangement"-konsept binder flere runder sammen visuelt (samme
dag, samme bane, "spilt sammen med disse flightene") — minst
invasivt, gjenbruker alt som allerede er bygget og scratch-verifisert.
2. **`round` blir en beholder for flere flighter** — ligner
turneringers `session`→`match`-struktur (én økt, flere matcher).
Større omskriving: `round_participant` må da vite hvilken flight den
hører til, og eierskap/autorisasjon (i dag: "eieren av runden ser/
redigerer alt") må revurderes for en modell med flere selvstendige
flighter under samme paraply.
**Ikke besluttet hvilken retning** — kun notert som et reelt, ikke-trivielt
spørsmål. Henger dessuten sammen med det pågående ADR-036-arbeidet
(medspillere/venner) — en avklaring bør trolig vente til minst fase 1 av
ADR-036 er bygget, siden "hvem er i min flight" og "hvem er min venn/
medspiller" er beslektede, men ikke identiske spørsmål.
---
## Frittstående rundeføring + detaljert statistikk (uten turnering/organisasjon) — ✅ BACKEND + FRONTEND LIVE, løpende oppfølging t.o.m. 2026-07-25 (se ADR-033 for full detalj per dag)
**Fremdrift 2026-07-22:** HCP-indeks-motor (43/43 tester), databaseskjema
(`020`+`021`, sistnevnte en fiks for manglende rating-snapshot-kolonner),
OG et fullt API-lag (`app/routers/rounds.py`) er alle bygget,
scratch-verifisert og rullet ut mot ekte `teecup_db`/`teecup_api`.
**Frontend bygget og rullet ut 2026-07-23** — se ADR-033 i
ARCHITECTURE_DECISIONS.md for full detalj om alle tre lagene (engine,
skjema, API) og hele frontend-runden (nye endepunkter, komponenter,
verifisering, utrulling). Kort: `/rounds` (liste), `/rounds/new`
(bane-søk teeoff/egen + opprett egen bane, utslag/dato/hull), `/rounds/[id]`
(deltakere, hull-for-hull-registrering med automatisk GIR-visning,
fullfør-runde med HCP-differensial). Lenket fra dashbordet som «Egne
runder». Ingen migrasjon i denne del-runden — kun tre nye,
organisasjonsuavhengige endepunkter i `rounds.py`
(`official-search`/`official-search/{slug}`/`personal-courses/{id}`/
`.../holes`) og en endring av hull-PATCH-responsen.
**Frontend-presentasjonen ERSTATTET med V0-design 2026-07-23, samme dag:**
den første frontend-runden var hånd-kodet direkte av meg (avvik fra
prosjektets ellers gjennomgående V0-mønster, påpekt av bruker). Skrev tre
V0-prompter, mottok tre zip-eksporter, diffet mot levende tre (samme
rutine som alltid — kun fire filer reelt nye), erstattet de hånd-bygde
komponentene med V0s presentasjon og kablet ekte data inn på samme måte
som enhver annen skjerm i appen. Se ADR-033 for full detalj om
tilpasningene (to-stegs teeoff-oppslag, tredje kjønnsvalg «Annet»,
merge-før-PATCH beholdt, dev-forhåndsvisningskontroller fjernet). Ingen
backend-endring i denne del-runden. Rullet ut live, `teeoff.no`
upåvirket.
**Reell produksjonsbug funnet og fikset 2026-07-23, samme dag, rapportert
av bruker med skjermbilder:** `/rounds` var samtidig frontend-side og
API-prefiks — samme fellesklasse som ADR-016s medlemsside-hendelse, men
rammet begge presedens-retninger samtidig (listesiden nådde aldri
backend, og rundedetalj-siden var helt uoppnåelig). Fikset ved å flytte
frontend til `/my-rounds/*`, API uendret. Se ADR-033 for full detalj,
inkl. `curl`-bevis før/etter.
**Sju punkter rapportert av bruker 2026-07-24 etter faktisk bruk, BYGGET
OG LIVE samme dag** (unntatt punkt 6, se eget notat under): starthull-bug
(currentHole respekterte aldri `round.start_hole`, forklarte trolig også
det rapporterte GIR-avviket — feil hull ga feil par inn i en ellers
korrekt formel), kølle-bag i profilen (28 faste kølletyper, maks 14 --
den ekte golfregelen -- brukt som knapp-utvalg for "kølle brukt ved
utslaget", kun for eieren selv siden gjester ikke har profil), nytt
statistikkfelt "Anywayslag" (siste punkt i "Flere detaljer", samme
tallvelger-stil som slag/putter), valgfritt statistikknivå per deltaker
(`strokes_only`/`strokes_and_putts`/`full`, default `strokes_only` --
kun slag er strengt tatt nødvendig for resultat/HCP, resten er valgfritt
og skjules helt til det slås på), putt-avstand endret fra fritekst-tall
til seks faste bøtter (`<1m`…`8m+`), og "Hullet er spilt"-avkrysningen
fjernet helt (var reelt overflødig -- spilt settes allerede automatisk
når et slagtall velges). Ny migrasjon `022_round_stats_and_bag.sql`.
**Punkt 6 (numpad-layout for tallvelgerne + retningskors-ikoner for
utslag/innspill + vurdering av sveip vs. scroll) er BEVISST IKKE bygget
selv** brukeren ba eksplisitt om at dette prompres til V0 for en egen
vurdering, se egen V0-prompt utarbeidet samme dag (ikke kjørt av
brukeren ennå ved denne loggens skriving).
**Fanget, IKKE bygget (brukerens egen kommentar mens punkt 7 ble
avklart):** "plukket opp"-mulighet for Stableford-format -- i Stableford
er det vanlig å plukke opp ballen uten å fullføre hullet når det er
klart 0 poeng uansett. Frittstående runder har i dag INGEN
Stableford-poengberegning i det hele tatt (kun slagtall for HCP-
differensial) -- dette er et helt eget, udesignet format-spørsmål, ikke
løst av at "spilt"-avkrysningen ble fjernet. Trenger egen designrunde
(scoring-format-valg per runde, poengberegning, og en "plukket opp"-
tilstand som sannsynligvis bør lagres som en cap nettoscore, samme
prinsipp som WHS sin Net Double Bogey) den dagen Stableford faktisk
bygges.
**Notat fra bruker, IKKE designet/bygget ennå (fanget 2026-07-22):**
brukeren har tenkt å ha med (a) måling av lengde slag, og (b) å kunne
opplyst avstand til forskjellige steder banen (typisk pin/hazard/
layup-punkter, à la en golf-GPS/rangefinder). **Reell, ikke-triviell
avhengighet, verdt å notere nå:** dette krever faktiske GPS-/geografiske
koordinater for banens features (pin-plassering, hazarder osv.) data
INGEN av dagens kilder har. Verken teeoff sitt API (kun par/stroke-
index/rating, ingen geometri) eller den nye `personal_course`-katalogen
(samme enkle skjema som org-scopet `course`/`hole`) inneholder noe slikt
i dag. "Lengde slag" krever i tillegg selve GPS-posisjonering av
SPILLEREN i sanntid (nettleser-Geolocation API, ikke bare statiske
baneddata) en annen klasse funksjonalitet enn resten av appen, som til
ikke har hatt noe geografisk/posisjonsbasert element i det hele tatt.
Ingen beslutning tatt om omfang, datakilde (manuelt kartlagt per bane?
en ekstern golf-GPS-database?) eller UI kun fanget som en kjent,
fremtidig ambisjon som statistikk-modellen (Beslutning B) og
banedata-modellen (Beslutning C) bør ha i bakhodet, siden begge kan
trenge en utvidelse den dagen dette faktisk designes.
**Reconfirmed 2026-07-25** (scorekort-redesign-runden): brukeren
gjentok at avstandsmåling "ligger i kortene". Fortsatt IKKE designet
eller bygget -- eneste konkrete tiltak er en kode-KOMMENTAR i
`round-detail.tsx` sin hull-header som reserverer visuell plass ved
siden av GIR-merket, slik at en fremtidig avstand-indikator kan legges
til uten en layout-endring. Ingen data, ingen funksjonalitet.
**Se ADR-033 i ARCHITECTURE_DECISIONS.md for den fulle, besluttede
arkitekturen** (eierskapsmønster, statistikk-datamodell, HCP-indeksmotor).
Brukeren bekreftet 2026-07-22 at teeoff-banedata skal hentes via LIVE
oppslag (ikke import), og lastet opp den offisielle "WHS Rules of
Handicapping 2024" (USGA/R&A) som kilde for HCP-indeksberegningen alle
tre store åpne punktene fra brainstorm-runden er dermed enten bekreftet
eller presist kildebelagt, ikke lenger antatt. Notatene under er
brainstorm-historikken som ledet frem til ADR-en beholdt for
sporbarhet, ikke lenger den autoritative kilden for dette punktet.
Reist av brukeren 2026-07-21, eksplisitt begrunnet som relevant for
hvordan dashbordet skal se ut fremover (se punktet over) derfor fanget
grundig her selv om ingenting bygges i denne runden.
**2026-07-22 — brukeren sier dette skal bli HOVEDFOKUS i appen** (det mest
"kontroversielle" premisset i samtalen): når frittstående rundeføring med
detaljert statistikk er plass, skal det deretter bli ekstremt enkelt å
sette opp turneringer i ulike formater altså en reell prioritets-
omveltning, ikke bare en ny funksjon ved siden av de eksisterende.
Fortsatt IKKE designet/bygget dette er en brainstorm-runde (bedt
eksplisitt om av bruker), ikke en beslutningsrunde. Neste steg når
brukeren er klar: en egen, dedikert ADR-runde (se arkitektur-gaffelen
under).
**Nye statistikk-elementer lagt til 2026-07-22** (i tillegg til de fem fra
2026-07-21 under): kølle brukt ved utslaget, om utslaget traff fairway
eller var til høyre/venstre for den, automatisk beregnet "green in
regulation" (GIR), og utfallet av innspillet til green (traff/lang/kort/
høyre/venstre).
**Viktig presisering fra min side, IKKE avklart med bruker ennå:** GIR er
en DERIVERT stat (kan regnes ut fra antall slag brukt + om ballen var
green) men fairway-treff og innspill-retning krever at SPILLEREN
vurderer og taster inn utfallet etter hvert slag, appen kan ikke "beregne"
dette uten GPS. Dette betyr i praksis SLAG-FOR-SLAG-registrering (hvert
slag = kølle + utfall/posisjon), ikke bare noen aggregerte tall per hull
slik dagens scorekort gjør en vesentlig UX-heving fra dagens modell.
**Fire konkrete spørsmål brukeren stilte, med retning:**
- **Egendefinerte baner hvis de ikke finnes i TeeOff:** ja. Åpent
delspørsmål: uten organisasjon, hvor bor en custom-bane? Custom-baner er
i dag org-scopet (`course.organization_id`). Anbefaling: behold
offisiell teeoff-import (ADR-019) som primærvei (gir korrekt rating
"gratis", avgjørende for HCP-matte), og lag en NY, GLOBAL (ikke
org-scopet) pool for egendefinerte baner med søk-før-opprett for å
unngå tusenvis av private duplikater av samme bane.
- **Tvinge 18/front9/back9:** nei, kun standardvalg. Konsekvens: par-sum
for statistikk regnes fra hullene FAKTISK spilt, ikke anta 72.
Øktenes `start_hole`-felt (ADR-015, allerede bygget for turneringer) er
direkte gjenbrukbart. Bør også kunne avsluttes midt i (f.eks. 14 hull)
uten forhåndsdeklarert totalt antall.
- **HCP-tellende krever minst 9 hull:** riktig prinsipp (WHS aksepterer
9-hulls score), MEN WHS sin faktiske konvertering av en 9-hulls-score
til en Score Differential er en EGEN, presis justering ikke "halvparten
av 18-hulls-formelen". Nøyaktig den typen regel de tre HCP-PDF-ene
(lastet opp 2026-07-19, se CLAUDE.md) er ment å dekke leses før
denne logikken bygges. **Strukturelt større gap oppdaget under
drodlingen:** `handicap_engine.py` regner i dag KUN course handicap/
slagfordeling fra en ALLEREDE KJENT indeks den regner IKKE ut selve
HCP-indeksen fra en historikk av runder (WHS sin Score Differential +
snitt-av-beste-8-av-20-algoritme). Skal frittstående runder faktisk
oppdatere `app_user.handicap_index` automatisk, er dette en HELT NY
motor-komponent, ikke et lite tillegg til den eksisterende.
- **Spiller velger starthull:** ja, gjenbruk av samme `start_hole`-konsept
som over.
**Shotgun vs. fortløpende start ved turneringsoppsett (eget spørsmål,
egentlig et TURNERING/økt-konsept, ikke selve rundeførings-pivoten):**
- I dag er `session.start_hole` økt-bredt. Shotgun trenger starthull PER
FLIGHT/MATCH (typisk trukket/tildelt), ikke ett felles for økten.
- Shotgun har samme klokkeslett for alle grupper ikke en variant av
`tee_interval_minutes` (som gjelder fortløpende start), kun
`start_hole` varierer mellom gruppene.
- Konkret forslag: `session.start_type: 'sequential' | 'shotgun'`,
`start_hole` blir settbart PER MATCH når shotgun velges (økt-nivået
forblir default/fallback for fortløpende). `tee_time`-utledningen
(ADR-015) trenger en egen shotgun-gren.
**Andre punkter fra drodlingen, ikke avklart med bruker ennå:**
- Spenningen "detaljert statistikk" vs. "ekstremt enkelt": anbefaler at
ALLE detalj-felt er valgfrie per hull rask "bare slagtall"-
registrering skal alltid fungere, detaljer legges for dem som vil.
Ellers risikerer man at hovedfokuset blir for tungvint til daglig bruk.
- Personlig køllebag (driver/hybrid/jern/wedge/putter) som naturlig
følgefunksjon til "kølle brukt ved utslag" kvikk-valg fremfor fritekst
hver gang.
- Flight-partnere uten TeeCup-konto: gjenbruk det allerede etablerte
mønsteret for org-scopede spillere uten konto + senere e-post-kobling
(`link_player_by_email`-familien), ikke finn opp noe nytt.
- Bør turnering-scoring til slutt bruke SAMME rike statistikk-registrering
som frittstående runder (én delt scoring-komponent), i stedet for to
ulike scoring-opplevelser i samme app? Ikke avklart, men verdt å ha i
bakhodet fra design-start siden brukeren kaller dette "hovedfokus".
- Historikk/trender over tid (beste runde, HCP-trend, snitt putter/runde)
naturlig, senere konsekvens når data finnes, ingen egen beslutning
nødvendig .
**Brukerens beskrevne behov, fanget presist:** en bruker skal kunne
registrere en golfrunde HELT UAVHENGIG av enhver turnering eller
organisasjon verken tilhørighet til et lag, en turnering, eller en
organisasjon skal være en forutsetning. Kan føres kun for seg selv, ELLER
for andre man spiller sammen med i flighten (ikke nødvendigvis
TeeCup-brukere). Statistikk utover selve slagtallet:
- Antall slag hullet (allerede dekket av eksisterende `hole_score`-form)
- Antall putter
- Antall chip
- Antall bunkerslag
- Antall straffeslag
- Lengde første putt
- Kølle brukt ved utslaget (2026-07-22)
- Om utslaget traff fairway, eller var til høyre/venstre for den (2026-07-22)
- "Green in regulation" (GIR), automatisk beregnet (2026-07-22 se
presisering under om DERIVERT vs. OBSERVERT stat)
- Utfallet av innspillet til green: traff/lang/kort/høyre/venstre
(2026-07-22)
**Dette er IKKE en liten dashboard-finpuss det utfordrer en av
arkitektur-invariantene i CLAUDE.md direkte:** "Tenant = organisasjon.
`organization_id` alle domenetabeller, håndhevet av RLS." En
frittstående runde har per definisjon INGEN organisasjon å henge
`organization_id` dagens RLS-modell (org_isolation-policyer som
alle stoler `app.current_org`) dekker rett og slett ikke dette
tilfellet. Dette krever en ny, egen beslutning (sannsynligvis en helt ny
ADR) om et PARALLELT eierskaps-/isolasjonsmønster keyet `user_id`
(`app_user.id`) i stedet for `organization_id` ikke en utvidelse av et
eksisterende mønster, men en ny gren i tenant-modellen. Presist hvilke
tabeller som trengs (egen `personal_round`? egen
`personal_round_hole_stat`? gjenbruk av eksisterende `hole_score`-form med
en nullable `organization_id` og en NY RLS-policy for "eier = current
user"?) er IKKE avklart bevisst ikke gjettet her.
**Andre åpne spørsmål som trengs FØR design/bygging, ikke besvart av
brukerens beskrivelse ennå:**
- Hvordan identifiseres "andre man spiller med i flighten" når de ikke
nødvendigvis er TeeCup-brukere frittstående "midlertidige" spiller-
rader (ala `player`, men uten organisasjonstilhørighet), eller rene
navn uten noen kobling i det hele tatt?
- Skal disse rundene noensinne telle inn i HCP-beregning/-historikk (se
eget punkt under), eller er de rent loggførende (som en digital
scorekort-dagbok)?
- Skal banedata (hull/par/stroke index/tee-rating) hentes fra samme
`course`-modell som i dag (org-scopet), eller trengs en egen,
org-uavhengig banekatalog for dette bruksmønsteret (en spiller uten
noen organisasjon i det hele tatt fortsatt kunne velge en bane)?
- Skal frittstående runder vises i "Mine runder" dashbordet sammen med
turnering-rundene, eller i en egen seksjon?
**Bevisst IKKE startet i denne runden** dette bør bli sin egen,
dedikerte ADR-runde (arkitektur-invariant-nivå beslutning, ikke et
tillegg til en dashboard/konto-poleringsrunde), men er tatt med i
vurderingen av tom-tilstand-redesignet over siden det direkte påvirker
hvilke "første handling"-alternativer dashbordet bør vise i fremtiden.
---
## Én person, flere e-postadresser — DEL 1 (det enkle tilfellet) ✅ BYGGET OG LIVE 2026-07-21, DEL 2 (kontosammenslåing) fortsatt 📋 NOTERT
Reist av brukeren rett etter ADR-032 (verifisert e-postbytte). Et beslektet,
men DISTINKT behov: én person kan ha flere e-postadresser i omløp samtidig
(f.eks. registrert seg privat med én adresse, men fått en turnering-
invitasjon rettet mot en jobb-adresse organisator la inn) ikke et BYTTE
(ADR-032 sin løsning), men en TILLEGGS-tilknytning. I dag oppretter et
magic-link-innlogg en ny adresse alltid en HELT NY, tom `app_user`-konto
(ADR-009) nøyaktig det som gjør at spilleren aldri "finner" turneringen
sin med sin vanlige, primære konto.
**Brukerens beskrevne flyt, ordrett fanget:** innlogget med
eksempel@domene.no, sier "jeg eier også test@domain.com". Ved dette kravet
sendes en e-post til test@domain.com med en nøkkel som limes inn et sted
dashbordet. Etter bekreftelse dukker inviterte turneringer den adressen
opp. Annen data (spilte runder, personlig informasjon) skal "forespørres
slått sammen eller justert". Fremtidige innlogginger skal kunne gjøres med
ENHVER av de tilknyttede adressene.
**Foreslått retning, basert på gjenbruk av allerede bygget mønster:**
samme token-i-e-post-bevis-eierskap-mekanisme som ADR-032 sin
e-postbytte-flyt (`email_change_token`), men ADDITIV i stedet for
ERSTATTENDE en ny tabell for verifiserte SEKUNDÆRE e-poster knyttet til
kontoen (i stedet for å overskrive `app_user.email`). Login (`/auth/
request-link` m.fl.) da slå opp BÅDE primær- og sekundær-e-poster.
`link_player_by_email()`/organisasjonsinvitasjon-aksept (som i dag kun
kjører mot `app_user.email`) kjøres for HVER av kontoens verifiserte
adresser naturlig utløst rett etter en ny adresse er bekreftet, og
sannsynligvis også trygt å kjøre nytt ved hver innlogging (idempotent,
samme mønster som i dag).
**Det virkelig vanskelige, uløste spørsmålet, IKKE adressert av brukerens
beskrevne flyt:** hva skjer hvis den "krevde" adressen ALLEREDE er
primær- (eller sekundær-)adressen til en ANNEN, eksisterende `app_user`-
konto altså at spilleren faktisk har logget inn med DEN adressen
tidligere og dermed har to helt separate kontoer med egen historikk
(ulike org-medlemskap, ulike spillerkoblinger, kanskje ulikt passord/2FA)?
Da holder det ikke å bare "legge til" adressen det er en ekte KONTO-
SAMMENSLÅING (slå sammen organisasjonsmedlemskap uten å bryte "én rolle
per bruker per org"-unikheten, deduplisere spiller-koblinger, avgjøre
hvilken konto som "vinner" for tvetydige felt som `preferred_locale`/2FA
når begge har satt noe ulikt). Dette er en betydelig større og mer
risikofylt operasjon enn "legg til en frisk, ukrevd adresse" bør
utredes og besluttes som en egen, separat sak, ikke antas løst av samme
runde som det enkle tilfellet.
**Plassering:** brukeren presiserte eksplisitt at dette skje FRA
dashbord-siden (ikke `/account`, der ADR-032 sin e-postbytte-flyt ellers
naturlig ville hørt hjemme) trolig fordi selve GEVINSTEN (nye turneringer
dukker opp) er noe som vises dashbordet, handlingen bør ligge der
resultatet vises.
**Ble delt i to separate runder, som foreslått:** (1) legg til en frisk,
ukrevd sekundær-e-post BYGGET, se under. (2) Ekte konto-sammenslåing
for det vanskelige tilfellet fortsatt IKKE designet, egen fremtidig
runde.
### Del 1 (det enkle tilfellet) — ✅ BYGGET OG LIVE 2026-07-21
Migrasjon `017_secondary_email.sql`: to nye tabeller
(`secondary_email_token` midlertidig, samme token-hash-og-utløp-mønster
som `email_change_token`; `user_secondary_email` den faktiske,
verifiserte adressen, globalt UNIQUE). Nye endepunkter i
`app/routers/auth.py`: `POST /auth/secondary-email` (send
bekreftelseslenke), `POST /auth/secondary-email/confirm` (ingen sesjon
påkrevd, samme mønster som selve magic-link-verifiseringen),
`DELETE /auth/secondary-email/{id}`.
**Kjernestykket, ikke bare CRUD:** `verify_magic_link` og
`login_with_password` sjekker `user_secondary_email` FØR de gjør sitt
vanlige `app_user.email`-oppslag finner de en match, løses innloggingen
til DEN EKSISTERENDE eierens konto i stedet for å (som før) stille
opprette en helt ny, separat konto. Dette er selve mekanismen som gjør
adressen nyttig, ikke bare en liste over "andre adresser".
**Plassering, bevisst avvik fra brukerens opprinnelige "fra dashbordet"-
instruks:** lagt i `/account` (samme sted som ADR-032 sin e-postbytte),
IKKE dashbordet begrunnet med at dette kun er del 1 (det enkle
tilfellet); når/hvis del 2 (kontosammenslåing, "data dukker opp") bygges,
er dashbordet trolig riktigere siden GEVINSTEN vises der. Flagget
eksplisitt til bruker, ikke stille besluttet.
**Scratch-verifisert, 20 sjekker:** adresse legges IKKE til før bekreftet;
token ikke gjenbrukbart; adresse som allerede er en ANNEN kontos
hovedadresse ELLER sekundæradresse avvist tydelig (409 DUPLICATE) i begge
retninger; innlogging via sekundæradressen (BÅDE magic-link OG passord)
løses korrekt til samme, eksisterende konto (bekreftet: samme `id`,
`email` i responsen forblir hovedadressen); en fremmed kan ikke slette
andres sekundæradresse; og den kritiske sjekken en ny innlogging
adressen ETTER at den er fjernet oppretter en genuint NY, separat konto
(beviser fjerningen er reell, ikke kosmetisk). `test_isolation.sql`
fortsatt 12/12 (additiv migrasjon). Ekte typesjekket produksjonsbuild av
frontend kjørt og bekreftet.
**Rullet ut live 2026-07-21**, bruker bekreftet eksplisitt: migrasjon 017
kjørt mot ekte `teecup_db` (bekreftet begge nye tabeller finnes,
`test_isolation.sql` fortsatt 12/12), deretter `docker compose up -d
--build teecup_api teecup_frontend`. Begge containere boot-et rent,
`/health`/`/dashboard`/`/account`/`/verify-email` 200, `teeoff.no`
upåvirket.
### Del 2 (ekte kontosammenslåing) — fortsatt 📋 NOTERT, IKKE designet
Uendret fra den opprinnelige analysen: hva skjer hvis adressen som legges
til ALLEREDE er primær- eller sekundæradressen til en ANNEN, eksisterende
konto (spilleren har altså to helt separate kontoer med egen historikk
ulike org-medlemskap, ulike spillerkoblinger, kanskje ulikt passord/2FA)?
Dagens del 1-løsning avviser dette tydelig (409 DUPLICATE) i stedet for å
gjette en ekte sammenslåing (slå sammen org-medlemskap uten å bryte
"én rolle per bruker per org", deduplisere spillerkoblinger, avgjøre
hvilken konto som "vinner" for motstridende felt) er en betydelig større
og mer risikofylt operasjon, fortsatt bevisst utsatt til en egen,
dedikert designrunde.
---
## UX / frontend (senere fase)
- 🔀 **Tilgjengelighet STÅENDE krav, ikke lenger et enkeltpunkt
(skjerpet 2026-07-22, se CLAUDE.md):** all frontend, eksisterende og
fremtidig (inkl. alle nye V0-skjermer), skal være lesbar/forståelig/
betjenbar for noen med noe redusert syn UTEN briller. Det tidligere,
vagere punktet under ("høy kontrast, store knapper...") er en
KONKRET instans av dette generelle, varige kravet ikke en egen,
isolert senere-fase-oppgave. Ingen dedikert retrofit-runde igangsatt
ennå; rettes opportunistisk når skjermer likevel røres, og tas inn i
enhver ny V0-prompt fremover.
- 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp
(banebruk i sollys/med solbriller) konkret eksempel punktet over.
- Offline-first (ADR-006) BYGGET 2026-07-19 (ADR-028), se eget punkt
under. Scoreregistrering (hole-scores/hole-results) fungerer offline
med automatisk synk.
- PWA: manifest, service worker, «Legg til hjemskjerm» BYGGET
2026-07-19 (ADR-028).
### PWA — ✅ BYGGET OG LIVE 2026-07-19 (ADR-028)
Full design i ARCHITECTURE_DECISIONS.md ADR-028. Kort:
| Del | Status | Notat |
|---|---|---|
| Installerbar app (manifest + ikoner + «Legg til hjemskjerm») | bygget | `app/manifest.ts` (Next.js sin innebygde manifest-generator), `components/sw-register.tsx`, `appleWebApp`-metadata for iOS. |
| Ikoner | bygget, **MIDLERTIDIG** | Enkelt grønt golf-flagg generert programmatisk (`public/icons/*`, `public/apple-icon.png`) **skal erstattes med ekte design senere.** Erstattet samtidig den gamle v0.app-plassholderlogoen som i `apple-icon.png` fra før (var aldri TeeCup-merkevare). |
| Service worker: cache app-navigasjon + `/orgs/*`-GET-er | bygget | `public/sw.js`, nettverk-først/cache-fallback (bevisst IKKE stale-while-revalidate, se ADR-028). `public/offline.html` som siste utvei. |
| Offline scoreregistrering (hole-scores/hole-results) | bygget | `lib/offline-queue.ts` (IndexedDB-kø) + `components/session-scorecard.tsx`. Synker automatisk ved `window`s `online`-event, pluss manuell "Synkroniser "-knapp. Bevisst IKKE Background Sync API (iOS Safari støtter den ikke). |
| Andre skrivehandlinger offline (walkover, chat/feed, oppsett) | 💤 bevisst utenfor omfang | Kun de to scoreregistrerings-endepunktene er køet se ADR-028 Beslutning B for begrunnelse per type. |
| Faktisk browser-testet (DevTools Offline-modus) | **FORTSATT IKKE GJORT — OPPFØLGINGSPUNKT** | Kun verifisert med typesjekket build + container-boot/curl, aldri i en ekte nettleser. Ingen nettleserverktøy tilgjengelig i byggeøkten. **Brukeren bør selv åpne et scorekort, skru på Chrome DevTools sin Offline-bryter, registrere et par slag, skru nettet på igjen, og bekrefte at de faktisk synkes** først da er offline-flyten reelt bevist, ikke bare kodegjennomgått. |
**Rullet ut live 2026-07-19**, bruker bekreftet eksplisitt: `docker compose
up -d --build teecup_frontend` (ingen migrasjon). Verifisert:
`/health`/`dashboard` 200, `/manifest.webmanifest`/`sw.js`/ikoner alle 200
over ekte https, `teeoff.no` upåvirket.
---
## Bevisst endret fra opprinnelige (Gemini-)råd
- 🔀 **Banedata:** API mot teeoff (ADR-004), IKKE direkte delt database. Direkte
DB-kobling ville låst TeeCup til teeoffs skjemaendringer.
- 🔀 **Handicap-motor:** egen testet Python-modul, ikke den innlimte JS-funksjonen
(som bl.a. ikke håndterte 9-hull eller konfigurerbare allowances korrekt).
- 🔀 **Tenant-modell:** organisasjon som tenant med RLS, ikke bare «turnering-ID».
- 🔀 **Sesjons-secret:** egne secrets for TeeCup, ikke fallback til teeoffs
(teeoff selv bruker en slik fallback bevisst unngått her).