teecup/FEATURE_BACKLOG.md
Erol Haagenrud 83c4749516 Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.

Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.

Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00

193 KiB
Raw Blame History

TeeCup — Funksjons-backlog

Formål: fange ALT som er ønsket for TeeCup på ett sted, slik at intet krav går tapt mellom økter, modeller eller samtaler. Kilder: de to opprinnelige Gemini-samtalene (funksjonalitet + Docker) og arbeidet gjort med Claude.

Status-koder: ferdig · 🔨 pågår · 📋 planlagt/fanget · trenger beslutning · 🔀 endret fra opprinnelig råd · 💤 utsatt (bevisst)

Sist oppdatert: 2026-07-17


Fundament (bygget)

Funksjon Status Notat
Handicap-/matchmotor (testet) 24 tester, R&A-verifisert. Erstatter Geminis løse JS-funksjon.
Formater: singles, fourball, foursome, greensome, scramble I motoren, allowance som konfig.
Brutto/netto + prosent-allowance (75 %, 90 %, 3/4) ADR-005. Prosentene bekreftet mot Golf GameBook (ADR-014).
9-hulls (front/back) slagfordeling ADR-008, egen funksjon + test.
Miksede formater per turnering (økter) ADR-007. Ryder Cup-strukturen.
Spiller-pool m/ reserver, delvis deltakelse ADR-007.
Multi-tenant skjema + RLS ADR-001/003. Isolasjon bevist med oppførselstest.
Dedikert app-rolle (ingen superuser/BYPASSRLS) migrasjon 002.
API: oppsett (spillere/lag/roster/økter/matcher/blind draw) Verifisert for ekte mot scratch-db + engangscontainer.
API: scoring (hole_score/match_hole_result, matchstatus, handicap-beregning) ADR-012/014. Verifisert for ekte, inkl. fourball better-ball og race-sikker recompute.
Banedata fra teeoff via API ADR-004 → ADR-019, LIVE 2026-07-18. Import (kopi) ved eksplisitt organisator-valg, ikke live oppslag ved hver bruk. Se egen seksjon under.
Konfigurerbar handicap-pipeline (4 brytere) ADR-014. Bygget i app/handicap.py, brukt av scoring-runden.
Ekte autentisering (magic-link + JWT-sesjon) ADR-009. app/routers/auth.py + migrasjon 004_auth.sql. X-Debug-User-Id-stubben er helt fjernet.
RLS-tomstreng-fiks (app_current_org()) Migrasjon 005_rls_null_guard.sql. Se detaljer under.
Organisasjon-bootstrap (opprette ny org via API) POST /orgs, app/routers/organizations.py. Se detaljer under.
Ekte SMTP-utsending av magic-link app/email.py. Se detaljer under.
Containerisert, LIVE på teecup.teeoff.no Dockerfile + docker-compose.yml. Se egen seksjon under — to reelle driftshendelser funnet og rettet.
Dato/klokkeslett på turnering/økt/match ADR-015. session.scheduled_at + tee_interval_minutes → utledet match.tee_time. Se egen seksjon under.
Maskinlesbar feilkode-kontrakt ({"code",...}) ADR-015. Full retrofit, alle 39 tidligere steder. Se egen seksjon under.
i18n-forberedelse (nb/en) ADR-015. preferred_locale + ekte engelsk e-postmal. Se egen seksjon under.
Frontend: innlogging + verifisering, LIVE ADR-016. Next.js på teecup.teeoff.no, ekte magic-link-flyt bevist med reell e-post. Se egen seksjon under.
Frontend: dashboard (org-bytter/-opprettelse + turneringsliste), LIVE /dashboard. Kablet mot /auth/me, /orgs, /orgs/{id}/tournaments. Skrive-flyt bekreftet med ekte data (org "Tjøme Gents" + turnering opprettet av bruker).
Frontend: lag/roster-skjerm, LIVE /tournaments/[id]. To nye backend-endepunkter bygget samtidig (PATCH/DELETE roster). Skrive-flyt ikke testet med ekte data ennå.
Selvregistrering + utvidet spillerprofil (API) ADR-017. app/routers/registration.py (offentlig, uautentisert), utvidet players.py, e-post-basert player.user_id-kobling ved innlogging. Backend komplett og live. Frontend-påmeldingsskjema/landingssider gjenstår (egen ADR-runde).
Roster: endre kaptein / fjern spiller (PATCH/DELETE) app/routers/tournaments.py. Bevisst ingen "kun én kaptein"-håndhevelse ennå — se «Brukerroller»-punktet under.
Egendefinerte baner (course) via API Nytt app/routers/courses.py (GET/POST /orgs/{id}/courses). Kun source='custom' — ADR-004s teeoff-integrasjon (source='official') fortsatt kun vedtatt, ikke bygget. Se egen seksjon under.
Frontend: program-skjerm (økter/tidsplan), LIVE /tournaments/[id]/program. Se egen seksjon under.

Frontend: innlogging + verifisering — BYGGET OG VERIFISERT LIVE 2026-07-17

  • Første frontend-skjerm i prosjektet. Designet i V0 (login-skjerm, merkevare form/farge hentet fra Teeoff-logoen — IKKE navn/logo, se ADR-016s begrunnelse og tidligere samtale), hentet inn som frontend/ (Next.js + Tailwind + shadcn/ui).
  • Kvalitetsrunde på V0-output før bruk: fargetokens i globals.css var OKLCH-TILNÆRMINGER av de forespurte hex-fargene (#8bc24a/#ff5722), ikke eksakte — regnet ut presise OKLCH-ekvivalenter og rettet alle 6 forekomster (lys/mørk/system-mørk). Fjernet @vercel/analytics helt (ga null verdi på egen-hostet infrastruktur, kun en unødvendig tredjeparts nettverkskall). Fjernet typescript: { ignoreBuildErrors: true } (bygget kjører nå ekte typesjekk). Fjernet dødt pnpm.overrides-felt. frontend/.gitignore manglet .pnpm-store/ — årsaken til at VSCode viste 10 000 "ukjente" filer ved første commit-forsøk; rettet.
  • Kablet mot ekte API (app/routers/auth.py sine /auth/request-link og /auth/verify-link): frontend/next.config.mjs sin rewrites() proxyer /auth/*//orgs/*//health server-side til API-et — se ADR-016 for hele begrunnelsen (same-origin, ingen CORS, cookie uendret). components/login-form.tsx sender ekte POST /auth/request-link; ny app/verify/page.tsx + components/verify-form.tsx mottar ?token=... fra e-postlenken (automatisk verifisering) ELLER viser et manuelt "lim inn koden"-felt (nødvendig fallback, ikke overflødig — se under).
  • Backend-endring i samme runde: app/email.py/app/config.py fikk en ny PUBLIC_BASE_URL-innstilling (default https://teecup.teeoff.no, valgfri) — e-postmalen sender nå en EKTE klikkbar lenke ({base}/verify?token=...) i tillegg til den rå koden som fallback (samme mal-mekanisme som i18n-runden, ADR-015).
  • Containerisert og rullet ut LIVE (ny frontend/Dockerfile, multi-stage, Next.js output: "standalone"; ny teecup_frontend-tjeneste i docker-compose.yml). Caddy (teecup.teeoff.no, i det SEPARATE teeoff-repoet) peker nå på teecup_frontend i stedet for teecup_api direkte — se ADR-016 for hvorfor, og CLAUDE.md-status for driftsdetaljene (samme stale-Caddy-inode-hendelse som containeriseringsrunden, løst likt).
  • Verifisert med FAKTISK e-postlevering, ikke bare curl: ekte POST /auth/request-link sendt til brukerens egen adresse over https://teecup.teeoff.no, ekte e-post mottatt med en ekte klikkbar lenke, lenken åpnet i nettleser, landet på en fungerende /verify-side, sesjon opprettet — brukeren bekreftet "jeg er tilsynelatende innlogget." Første gang en hel bruker-vendt flyt (ikke bare API-et isolert) er bevist ende-til-ende i produksjon.

Dato/klokkeslett + feilkode-kontrakt + i18n-forberedelse — BYGGET OG VERIFISERT 2026-07-17

  • Reist av brukeren rett før frontend-arbeidet: kan turnering/økt/match ha dato/klokkeslett, og må appen forberedes for flerspråklighet? Begge er API-kontraktspørsmål som er langt billigere å løse FØR frontend bygges enn å ettermontere — se ADR-015 for alle tre beslutningene i sin helhet.
  • Migrasjon 006_scheduling_and_locale.sql: alt additivt (nullable/ default) — tournament.end_date (+ CHECK mot start_date), session.scheduled_at/tee_interval_minutes/start_hole, match.tee_time_override, app_user.preferred_locale, magic_link_token.locale (begge sistnevnte CHECK IN ('nb','en')). Kjørt permanent mot den ekte teecup_db (se under).
  • Feilkoder: ny app_error(status_code, code, message)-factory i app/errors.py, alle 39 tidligere norsk-hardkodede HTTPException(..., detail="...")-steder på tvers av 6 filer (errors.py, auth.py, routers/auth.py, routers/tournaments.py, routers/matches.py, routers/scoring.py) retrofittet til en delt ~15-kode-taksonomi. Endelig grep -rn 'detail="' app/ ga NULL treff.
  • Dato/tid i API-et: tournament.start_date (fantes i skjemaet siden 001, men var ALDRI koblet til API-modellene før nå — funnet og fikset som en naturlig bivirkning av å legge til end_date) + end_date eksponert; session sine tre nye felt eksponert i SessionCreate/SessionOut; match.tee_time utledet i Python i både create_match og list_matches (ingen ekstra spørring — økten hentes allerede for andre formål der).
  • i18n: MagicLinkRequest.locale (Literal["nb","en"], default nb) sendes av klienten, følger med i token-raden, brukes til å velge e-postmal OG (kun ved førstegangsopprettelse) app_user.preferred_locale. Ekte engelsk e-postmal lagt inn i app/email.py (ikke bare rørlegging) — konkret bevis på at flerspråklighet fungerer ende-til-ende, ikke bare at et felt eksisterer.
  • Verifisert grundig mot en fersk teecup_scratch (migrasjoner 001→006, test_isolation.sql 12/12 uendret, engangs-API-container): feilkode-form bekreftet på tvers av 5 filer (NOT_FOUND/LIMIT_REACHED fra tournaments.py, DUPLICATE fra roster, NOT_AUTHENTICATED uten cookie, NOT_ROSTERED_ON_TEAM fra matches.py sin add_participant); tee_time bekreftet å øke riktig per sequence (10 min intervall → 08:00/08:10/08:20), tee_time_override bekreftet å vinne over utledet verdi, økt uten scheduled_at bekreftet å gi tee_time: null (ikke feil); i18n bekreftet full runde — ny bruker med locale:"en" fikk preferred_locale:"en", en ETTERFØLGENDE request-link for samme bruker med locale:"nb" skiftet e-postmalen men IKKE den lagrede preferansen (bekreftet uendret via /auth/me).
  • Migrasjon 006 kjørt mot ekte teecup_db 2026-07-17, bruker bekreftet eksplisitt i samme økt — kjørte rent, test_isolation.sql fortsatt 12/12.
  • Mindre driftslærdom, funnet OG rettet samme runde: et innledende grep -v -i 'pass|secret|key'-filter jeg brukte for å lese ikke-sensitive .env-nøkler fanget IKKE opp TEECUP_DATABASE_URL, som bar teecup_app-passordet innebygd i selve URL-en (i tillegg duplisert av det samme passordet som allerede lå rent i TEECUP_APP_PASSWORD) — passordet endte dermed synlig i et verktøyresultat. Flagget til brukeren umiddelbart (samme mønster som SMTP-passord-hendelsen under containeriseringsrunden). Rettet: .env bygget om til fem separate, rent navngitte felt (TEECUP_DB_HOST/PORT/NAME/USER/PASS), app/config.py+app/db.py bygger nå asyncpg-poolen fra disse i stedet for én DSN-streng, docker-compose.yml oppdatert tilsvarende. Se CLAUDE.md-status for full driftsdetalj, inkl. at dette krevde et fullt docker compose up -d --build --force-recreate (ikke bare --force-recreate — imaget bygger inn app/ ved build-tid). Verifisert ende-til-ende mot den live stacken (Application startup complete, /auth/me ga rent 401 over ekte https, teeoff.no upåvirket).

Containerisering og go-live — FERDIG 2026-07-16

  • POST /orgs osv. var siste kodebit; dette var første gang noe rørte EKTE, PERMANENT infrastruktur (ekte teecup_db, ekte langtlevende container, den DELTE Caddy-instansen som også ruter live teeoff.no).
  • Bygget: ekte teecup_db opprettet, migrasjoner 001→005 kjørt permanent (samme filer, ingen endringer), test_isolation.sql bestått (ruller alltid tilbake, trygt å kjøre mot en database som skal bli stående). Dockerfile (speiler scratch-rundenes bevist-riktige volumoppsett: app/ + handicap_engine.py på samme relative plassering) + docker-compose.yml (tjeneste teecup_api, joiner det eksisterende, eksterne teeoff_default-nettverket). Ny Caddy-blokk for teecup.teeoff.no i /opt/teeoff/deploy/Caddyfile.
  • To reelle driftshendelser, begge funnet og rettet i sanntid:
    1. Stale bind-mount-inode: teeoff_caddy sin Caddyfile-mount er en ENKELTFIL-bind-mount, låst til inoden som fantes da containeren sist startet (13 dager tidligere). Fil-redigering via atomisk rename laget en ny inode på samme sti — containeren fortsatte å lese den GAMLE filen uansett hvor mange ganger caddy validate/reload/admin-API /load ble kjørt (alle opererte på den uendrede gamle filen, derav ingen synlig feil). Løsning: full docker restart teeoff_caddy (brukeren bekreftet eksplisitt — avvek fra planens "kun graceful reload"-løfte, ga noen sekunders nedetid for teeoff.no).
    2. Alvorlig nettverksalias-kollisjon (oppdaget rett etter omstarten, da EKTE teeoff-trafikk — /api/facilities?... med ekte klubb-slugs som borregaard-golfklubb — dukket opp i teecup_api sin logg): docker-compose.yml sin service-nøkkel var api:, identisk med teeoffs eget api-servicenavn. Docker Compose registrerer nettverksalias etter service-NAVNET (ikke bare container_name) på delte nettverk, så begge containerne fikk alias apiteeoff_default — Caddys reverse_proxy api:8000 i teeoffs egen config kunne da tilfeldig treffe enten ekte teeoff_api eller teecup_api, dvs. ekte brukertrafikk til teeoff.no kunne bli besvart av TeeCup-koden. Rettet umiddelbart: stoppet teecup_api først (hindre videre feilruting), ga service-nøkkelen navnet teecup_api, gjenopprettet, bekreftet med docker network inspect at alias api nå KUN peker på ekte teeoff_api. Lærdom for fremtidige tjenester på delt nettverk: ALLTID gi docker-compose sin service-nøkkel (ikke bare container_name) et prosjekt-unikt navn når flere uavhengige compose-prosjekter deler samme eksterne nettverk — service-navnet blir også et DNS-alias.
  • Mindre driftslærdom (samlet): (a) .env leses IKKE på nytt av en allerede kjørende container — docker compose up -d --force-recreate kreves etter enhver .env-endring; (b) et #-tegn i et upassordet .env-passord kuttes som kommentar av Compose sin parser, anførselstegn (helst enkle) løser det; (c) jeg eksponerte ved et uhell to secret-verdier i eget debug-output mens jeg feilsøkte en .env-korrupsjon (manglende linjeskift) — brukeren roterte passordet som forsiktighetsregel.
  • Verifisert ende-til-ende mot den ekte, live stacken: teeoff.no upåvirket gjennom hele prosessen; https://teecup.teeoff.no/health → 200 med automatisk utstedt TLS; full magic-link-innlogging (ekte e-post mottatt, verify-link ga Secure-flagget cookie siden vi nå er over ekte https, /auth/me fungerte med sesjonen).

Ekte SMTP-utsending — BYGGET OG VERIFISERT 2026-07-16

  • Brukeren la inn egne, uavhengige SMTP-credentials i .env (TEECUP_SMTP_SERVER/PORT/USER/PASS, TEECUP_FROM_EMAIL — ADR-009, ikke delt med teeoff). Kun nøkkelnavn ble lest for å bekrefte de fantes, ALDRI verdiene (CLAUDE.md sin sikkerhetsregel).
  • Løsning: ny app/email.py (send_magic_link_email, smtplib kjørt via asyncio.to_thread siden det er et synkront bibliotek — samme mønster som teeoffs egen fungerende utsending). Håndterer BEGGE vanlige SMTP-tilkoblingsmåter dynamisk (implisitt TLS på port 465 vs. STARTTLS på andre porter) siden porten bevisst ikke ble lest under planlegging. app/config.py fikk nye, valgfrie innstillinger (SMTP_CONFIGURED avledet fra at alle fem er satt) — IKKE _required, så scratch-/dev-testing fungerer fortsatt uten SMTP satt opp, via TEECUP_DEV_LOG_MAGIC_LINKS.
  • Bevisst designvalg: en driftsfeil i selve utsendingen (feil passord, SMTP nede, eller ingen leveringsmåte konfigurert i det hele tatt) logges kun server-side og endrer ALDRI klientens respons — alt annet ville brutt anti-enumereringsgarantien i request-link (klienten skal ikke kunne skille "e-posten finnes ikke" fra "e-posten finnes men utsendingen feilet").
  • Verifisert i to trinn: (1) dev-log-flyten uendret uten SMTP satt (regresjonstest av eksisterende scratch-løype), (2) én ekte test-e-post sendt til en adresse brukeren oppga, med de ekte credentials videreført fra .env til scratch-containeren uten at jeg noensinne leste verdiene selv — brukeren bekreftet mottak av en e-post med innloggingskode. Dette er første gang noe i dette prosjektet er verifisert ved faktisk levering til en ekte, ekstern mottaker, ikke bare via curl/scratch-container.

Organisasjon-bootstrap — BYGGET OG VERIFISERT 2026-07-16

  • Gjennomgang av alle routere hadde avdekket at INGEN endepunkt opprettet en organization-rad — i alle testrunder denne økten var organisasjoner satt inn direkte med superbruker-SQL. En ekte førstegangsbruker hadde ingen vei til å opprette klubben/bedriften sin og bli owner. Reelt blokkerende, ikke en utsettbar produktbeslutning.
  • Løsning: nytt POST /orgs {"name": ...}, autorisert med get_current_user (ikke get_authorized_org — sirkulært før org-en finnes). Ingen ny migrasjon nødvendig.
  • Selvrefererende RLS-bootstrap bekreftet å fungere (kommentaren i 002 om en "privilegert sti" var ALDRI bygget og viste seg unødvendig): generer org-ens uuid i Python FØR innsetting, sett app.current_org til nøyaktig den verdien via den eksisterende org_connection(), sett så inn organization-raden med samme id. org_self-policyens implisitte WITH CHECK (id = app_current_org()) blir da trivielt sann. teecup_app (NOSUPERUSER/NOBYPASSRLS) trenger altså INGEN egen privilegert tilkobling for å bootstrappe sin egen første organisasjonsrad.
  • Verifisert med 5 tester, inkludert en negativ kontroll som beviser mekanismen er presis, ikke et RLS-hull: forsøkte å sette inn en organisasjon med en MISMATCHENDE id (annen enn app.current_org) — avvist med insufficient_privilege, som forventet. Også bekreftet: ny org fungerer normalt med eksisterende endepunkter (GET/POST tournaments), dukker opp riktig i /auth/me, og full kryss-org-isolasjon holder mellom to uavhengig opprettede organisasjoner.

RLS-tomstreng-bug — FIKSET 2026-07-16

  • Alle RLS-policyer i 001/003 (org_isolation på 14 tabeller + org_selforganization) brukte current_setting('app.current_org', true)::uuid. Denne håndterte NULL trygt (ga ingen rader, som tiltenkt), men IKKE tomstreng — en gjenbrukt asyncpg-pool-tilkobling der en TIDLIGERE forespørsel satte GUC-en via SET LOCAL kunne lese den tilbake som '' etter at transaksjonen var ferdig, og casten kastet da en 500 i stedet for skjemaets lovede "trygg standard: se ingenting".
  • Fiks: samlet i én STABLE SQL-funksjon app_current_org() (migrasjon 005_rls_null_guard.sql) som gjør NULLIF(current_setting(...), '')::uuid — konverterer tomstreng til NULL FØR cast. Alle 15 policyer alteret (ALTER POLICY) til å bruke funksjonen i stedet for det rå uttrykket. Verifisert med 3 nye regresjonstester i test_isolation.sql (Test 10-12: tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS ikke krasj, org-bootstrap-innsetting fungerer rett etter tomstreng- tilstand) OG ved å faktisk gjenskape original-buggen mot en ekte container (pool-størrelse 1, varm opp med org_connection(), deretter kall /auth/me på samme gjenbrukte tilkobling — gikk fra 500 til 200).
  • Viktig presisering oppdaget underveis: den opprinnelige planen antok at /auth/me kunne joine organization direkte igjen når tomstreng-buggen var fikset. Det var FEIL — fiksen gjør bare at tomstreng oppfører seg som NULL (trygt: se ingenting), den endrer IKKE at org_self-policyen krever en MATCHENDE app.current_org for å vise en rad i det hele tatt (riktig RLS-oppførsel, ikke en bug). En bruker kan tilhøre flere organisasjoner samtidig, så det finnes ingen ÉN kontekst å sette for en tverr-org- spørring. /auth/me slår derfor opp hvert org-navn ETT OM GANGEN via org_connection() (N+1 spørringer, N = antall org-er brukeren tilhører, typisk 1-3) — verifisert at dette faktisk returnerer navnet korrekt.

Ønsket, men IKKE fanget før nå (fra Gemini-samtalene)

Brukerroller (utover org-medlemskap) — ADR-023 + ADR-026

  • Status: HELT FERDIG 2026-07-19 — kaptein/deltaker-delen (ADR-023) OG tilskuer-delen (ADR-026, se eget punkt lenger ned).
  • De opprinnelige samtalene beskriver: turneringsadmin, lagkaptein, spiller, tilskuer (les-only, følger live uten skriverettigheter).
  • Vi har i dag org-roller (owner/admin/member) + is_captain på roster.
  • 2026-07-19, BYGGET (ADR-023): Kaptein er nå en REELL autorisasjonsrolle, ikke bare et merke. app/team_authz.py sin nye user_is_team_captain (erstatter user_may_act_for_team) krever is_captain=true (eller org-eier/admin) for å legge til/fjerne deltakere og låse et lag (matches.py). Bevisst unntak — funnet ved å faktisk sjekke ekte produksjonsdata FØR utrulling: har laget INGEN utpekt kaptein ennå, godtas enhver rostret spiller i stedet (ellers ville «De Unge»-laget i den ekte «De Gamle er Eldst»-turneringen vært låst ute umiddelbart — 0 av 2 roster-rader er i dag merket kaptein der).
  • 2026-07-19, BYGGET (ADR-023): «Kun én kaptein per lag» håndheves nå — PATCH/POST .../roster (tournaments.py) fjerner automatisk kapteinmerket fra andre rader på samme lag når en ny kaptein settes. Nødvendig konsekvens av at kaptein nå er en autorisasjonsrolle. Ingen eksisterende lag hadde flere kapteiner (sjekket mot ekte data), så ingen opprydning av data var nødvendig.
  • 2026-07-19, BYGGET (ADR-023): Score-føring/-korrigering begrenset til matchens FAKTISKE deltakere (app/team_authz.py sin nye user_is_match_participant, brukt av scoring.py) — ikke lenger «noen på laget», og uavhengig av kapteinmerket. Erstatter «Scoring-autorisasjon»- punktet under, spørsmål (a) er dermed besvart.
  • Tilskuer — HELT FERDIG, LIVE 2026-07-19 (ADR-026), rett etter Kommunikasjon (ADR-025) som gjorde feed-synligheten (som denne skulle defineres sammen med) klar. Ingen ny rolle/tabell — «tilskuer» er ganske enkelt enhver som kan SE turneringen (tournament.visibility), nå også for LEADERBOARD, MATCH-LISTE og FULLT SCOREKORT (hull-for-hull), ikke bare info-siden/programtider som før (GET /public/tournaments/{id}/ leaderboard, .../sessions/{id}/matches, .../matches/{id}/scorecard, alle i registration.py, gjenbruker eksisterende fetch_leaderboard/fetch_matches/fetch_scorecard). Match-listen arver blind draw-skjuling (ADR-013) automatisk via own_team_ids() gjort null-sikker for en anonym leser (tom mengde = kun avslørte matcher). Scorekortet har en eksplisitt reveal-sjekk i tillegg. Ny /t/[id]/live- side (leaderboard-bar, øktliste, matcher, hull-for-hull-scorekort), lenket fra hovedsiden. Bruker valgte å bygge fullt scorekort med i denne runden (utover opprinnelig anbefaling om kun leaderboard+matchliste).
  • Se ARCHITECTURE_DECISIONS.md ADR-023 (kaptein/deltaker) og ADR-026 (tilskuer) for alle delbeslutningene, og CLAUDE.md-status for scratch-verifiseringen (15 + 16 automatiserte sjekker).

Scoring-autorisasjon: hvem fører, hvem korrigerer, hvem lukker (rejst 2026-07-16)

  • Status: delvis avgjort — direkte oppfølger av «Brukerroller» over.
  • Hvem fører score i dag (2026-07-19, ADR-023): kun matchens FAKTISKE deltakere, ELLER org-eier/admin — app/team_authz.py sin user_is_match_participant. Byttet fra «noen på laget» samme dag. Se «Brukerroller» over for full begrunnelse.
  • Hvem kan korrigere en ført score i dag: akkurat de samme — skriving er en upsert (ON CONFLICT ... DO UPDATE), så en korrigering er ikke skilt fra en førstegangs-innføring. Ingen godkjenning fra motstanderen, ingen audit-trail (forrige verdi overskrives sporløst).
  • Match-lås ved avgjørelse: FIKSET 2026-07-16. submit_hole_score og submit_hole_result (app/routers/scoring.py) avviser nå med 409 («Matchen er avgjort og kan ikke lenger endres.») FØR upserten kjøres, hvis match.points_side_a IS NOT NULL — dette er allerede et pålitelig, entydig signal siden kolonnen kun settes når compute_match_state sier matchen er avgjort. Gjelder BÅDE nye hull og korrigering av allerede talte hull. Verifisert: hull etter avgjørelse avvist, korrigering av et talt hull avvist, GET scorecard fortsatt leselig, ikke-avgjorte matcher upåvirket. Bevisst IKKE bygget: ingen manuell «avslutt match før den er avgjort»-handling (det grenser mot walkover under, fortsatt utsatt). Turnering-status kan nå settes via API — se eget punkt lenger ned.
  • Walkover/konsesjon — BYGGET OG LIVE 2026-07-19 (ADR-024). Løste det opprinnelige hullet: en side som aldri stiller nok spillere kan nå avsluttes ved at den TAPENDE siden (kaptein, eller org-admin) erklærer walkover — ensidig, ingen bekreftelse fra motparten. POST /orgs/{id}/ matches/{id}/concede (én match) og POST /orgs/{id}/tournaments/{id}/ concede (gi opp ALLE ikke-avgjorte matcher laget har, i én operasjon — v1 er låst til nøyaktig to lag, så det finnes bare én motstander). Kan erklæres uansett hvor mange hull som allerede er registrert (poengmessig teller kun seier/tap/delt, ikke marginen) — allerede registrerte hull forblir urørt i scorekortet. Se ARCHITECTURE_DECISIONS.md ADR-024. Frontend: session-scorecard.tsx (gi opp én match) og tournament-detail.tsx (gi opp resten av turneringen, per lag).
  • Turnering-status via API — BYGGET OG LIVE 2026-07-19. status lagt til i TournamentUpdate (app/routers/tournaments.py), settes via det eksisterende PATCH /orgs/{id}/tournaments/{id} (ekte PATCH-semantikk uendret — et utelatt status-felt lar verdien stå urørt). status er, ulikt de andre PATCH-bare feltene, en EKTE Postgres ENUM-type (tournament_status, migrasjon 001) — den generiske SQL-byggeren i update_tournament fikk et eksplisitt ::tournament_status-cast for akkurat dette feltet, funnet og løst FØR noe ble antatt riktig (verifisert i scratch at castet faktisk trengs). Ingen egen tilstandsmaskin/ overgangsregler — enhver org-medlem med skrivetilgang kan sette hvilken som helst av de fire verdiene, samme tillitsnivå som resten av appen. Frontend: tournament-status-badge.tsx fikk en ny redigerbar TournamentStatusPicker (dropdown, optimistisk oppdatering med rollback ved feil), koblet inn i tournament-detail.tsx sin header.
  • Avgjort 2026-07-16:
    • Match-lås ved avgjørelse: bygget — se eget punkt over. Automatisk (ikke en handling noen utfører), så «hvem får låse» ble aldri et spørsmål som trengte avklaring.
  • Fortsatt åpent: (a) skal score-føring begrenses til faktiske matchdeltakere avgjort/bygget 2026-07-19 (ADR-023, se over). (b) skal korrigering kreve motpartens godkjenning, eller er upsert-modellen god nok for v1? (c) skal turnering-status kunne settes via API avgjort/ bygget 2026-07-19 (se over).

Blind draw (skjult lagoppstilling)

  • Status: skjema (migrasjon 003, lineup_lock) + API bygget og verifisert (app/routers/matches.py: synlighetsfilter i SQL, ikke Python-filter — se ADR-013). Frontend LIVE 2026-07-18: /tournaments/[id]/sessions/[sessionId], components/session-blind-draw.tsx. Ny DELETE .../matches/{id}/ participants/{id} (kunne ikke angre et valg før låsing uten den). Fant og fikset et "skriv blindt"-hull: org-admin kunne legge til deltakere på et lag de ikke er rostret på (forrige rundes utvidelse), men own_team_ids() i app/blind_draw.py viste dem aldri tilbake før reveal — utvidet til samme owner/admin-regel som skrive-siden. Se CLAUDE.md-status for full runde, inkl. en tredje, urelatert 500-bug (manglende handicap-indeks) funnet og fikset samtidig.
  • Kapteinene låser oppstillingen skjult; matchene avsløres samtidig når begge er ferdige.
  • 2026-07-19 (ADR-023): hvem som FÅR låse et lag er nå kaptein-spesifikt (eller org-eier/admin) — se «Brukerroller» over. Med fallback for lag uten utpekt kaptein ennå.
  • Nytt 2026-07-18, BYGGET (fant og fikset rett før frontend-skjermen): match_participant.tee_id er påkrevd, men det fantes INGEN vei til å liste EN banes tee-er (selv offisielt importerte), og egendefinerte baner har ALDRI hatt noen vei til å FÅ tee-er — et hull som fantes fra før ADR-019, ikke noe den innførte. Uten dette kunne en turnering på en manuelt navngitt bane aldri få en ekte deltaker på noen match. Ny GET/POST /orgs/{id}/courses/{id}/tees (kun full_18-rating, offisielle baner avviser manuell tee-opprettelse). Presisert eksplisitt: dette er IKKE en teeoff.no-kodeendring — egendefinerte baner er per definisjon utenfor teeoffs katalog, så dette er en ren teecup-intern funksjon. Bevisst utenfor omfang: hull-/stroke-index-data for egendefinerte baner (trengs i SCORING-fasen, ikke blind draw) — egen, senere sak når scorekort-skjermen bygges.

Forenklet scoreføring (uten slagtall)

  • Status: skjema (migrasjon 003) + API bygget og verifisert (app/routers/scoring.py: hole-scores for stroke-modus, hole-results for hole_result-modus, begge mater samme compute_match_state).
  • Nytt 2026-07-18, BYGGET (forarbeid før scorekort-skjermen): GET/POST /orgs/{id}/courses/{id}/holes løser hull-/stroke-index-hullet fra tee-runden (kun stroke-modus trengte det). GET scorecard utvidet med stroke_entries/hole_result_entries (rå registrerte tall, ikke bare utledet vinn/tap/delt) — uten dette kunne ikke en gjenlastet scorekort-side vise gjeldende tilstand. Verifisert med en full stroke-runde med ekte handicap-justering på en egendefinert bane.
  • Frontend LIVE 2026-07-18: /tournaments/[id]/sessions/[sessionId]/ matches/[matchId], components/session-scorecard.tsx. Ett hull om gangen med store steppere/knapper (banebruk i sollys, ikke et regneark — matcher UX-visjonen lenger ned i denne fila). Lenket fra blind draw sin avslørte visning. Ingen nye backend-hull denne runden.

Bøtekasse (Kangaroo Court)

  • Status: 📋 planlagt
  • Meld inn overtramp med bøter («kastet kølla på hull 4»).
  • Henger sammen med Kommunikasjon: bøter deles i feeden. Sannsynligvis en egen post-type i meldingsmodellen.

Flerårig statistikk / historikk / MVP

  • Status: 📋 planlagt (datamodell støtter det allerede)
  • Seiersprosent i singelmatcher, historisk MVP, statistikk over år.
  • Datamodellen tillater dette fordi spillere lever på org-nivå og gjenbrukes på tvers av turneringer, og handicap fryses per turnering (reproduserbart).

Leaderboard (turnering-total)

  • Status: HELT FERDIG 2026-07-18 — backend + frontend LIVE (components/tournament-leaderboard.tsx, /tournaments/[id]/ leaderboard). Siste skjerm i "bygg i rekkefølgen ting brukes"-serien for match-play-flyten (oppsett → program → blind draw → scorekort → leaderboard, alle live).
  • Summerer poeng på tvers av ALLE økter/matcher, total + per-økt-delsum. Reelt korrekthetshull designet rundt før bygging: match.team_a_id/ team_b_id er ikke garantert konsistent på tvers av matcher (settes per match) — summering skjer derfor KUN på ekte lag-id, aldri på "a"/"b"- labelen. Verifisert med et scratch-scenario med en bevisst byttet om team_a/team_b i én match — riktig total og per-økt-delsum likevel.

Push-varsler

  • Status: 📋 planlagt (infrastruktur)
  • «BREAKING: X vant matchen». PWA push. Egen infrastruktur-bit.

Sanntid (WebSockets)

  • Status: HELT FERDIG, LIVE 2026-07-19 (ADR-025 + ADR-027). Koblet til BÅDE meldinger (ADR-025) OG /t/[id]/live (leaderboard/matcher/ scorekort, ADR-027). Ny, rutefri delt modul app/realtime.py (unngår sirkulær import mellom messaging.py/registration.py/scoring.py/ tournaments.py) — sender et "noe endret seg"-signal (ikke selve dataene) hver gang recompute_and_cache_match_state/apply_concession kjører, klienten henter de vanlige REST-endepunktene på nytt. Samme in-memory-per-prosess-begrensning som meldinger.

Knockout / cup-turnering (egen turneringstype)

  • Status: 💤 utsatt (egen fremtidig type, ADR-011)
  • Utslagsmatcher: 128 spillere → finale + bronsefinale. Rundene er avhengige (vinner går videre), krever bracket/progresjon, seeding, fripass. IKKE i v1.
  • v1 er låst til nøyaktig to lag (Ryder Cup-format). Match-modellen er generell, så denne typen kan legges til senere uten dataomskriving — den legger bare til et progresjons-lag.

Flere turneringsformater utover Ryder Cup (reist 2026-07-19)

  • Status: 📋 notert, IKKE designet/bygget — trenger egen ADR-runde senere.

  • Brukeren ønsker at TeeCup etter hvert skal støtte flere turneringsformater enn dagens rene to-lags Ryder Cup-modell (ADR-011). Fire konkrete eksempler gitt, med ulik arkitektonisk konsekvens (fra «passer nesten inn i dag» til «krever en helt egen datamodell») — notert ordrett under, ikke forenklet, for at reglene skal være presise når dette tas fatt på:

    «Københavner» (engelsk: trolig "Copenhagen" — direkte oversettelse, brukes noen steder i engelskspråklig golf-litteratur om nettopp dette poengsystemet, men usikkert om det er en universelt anerkjent standardbetegnelse; verdt å dobbeltsjekke før navnet ev. brukes i UI-et). 3 spillere, alle mot alle (INGEN lag). 6 poeng fordeles på hvert hull etter relativ plassering: vinner alene → 4-2-0. Vinner + de to andre deler → 4-1-1. To deler beste score, én taper → 3-3-0. Alle likt → 2-2-2. Flest poeng totalt etter runden vinner. Størst arkitektonisk avstand fra i dag: ingen to-siders match i det hele tatt, individuelt felt med poeng-per-hull-fordeling — passer ikke inn i match/team_a_id/ team_b_id-modellen slik den er nå (se merknad ved ADR-011).

    «High-low-high» (4 spillere, 2 lag à 2). Per hull: beste spiller («high») på hvert lag møter hverandre i en del-match, dårligste spiller («low») på hvert lag møter hverandre i en egen del-match — resultatet av hele hullet i hovedmatchen avgjøres av disse to del-oppgjørene til sammen. Eksempel: hull 1 — spiller A (lag 1) får 3 poeng og slår begge på lag 2 (høyest slår høyest), spiller B (lag 1) stryker og taper mot lagets low-motpart. Hullet blir da delt 1-1 siden «high» vant for lag 1 og «low» vant for lag 2. Arkitektonisk vrien del: hvem som er «high»/«low» per hull avgjøres AV SCORENE selv, etter at de er registrert — ikke satt opp på forhånd slik dagens match_participant-oppsett (fast rolle/side satt ved blind draw) forutsetter.

    «Robbins» (foursome med partnerbytte). De første 6 hullene spilles som én foursome-match, deretter bytter alle makker og spiller neste 6 hull som en ny foursome-match, og de siste 6 hullene spilles med den tredje/siste kombinasjonen — slik at alle har spilt med og mot alle i løpet av runden. Seier i en 6-hulls delmatch gir 2 poeng, uavgjort gir 1 poeng, flest poeng totalt vinner. Arkitektonisk konsekvens: én økt blir egentlig TRE sekvensielle del-matcher med roterende partnerskap innad i samme runde — dagens modell (én match = ett fast lag-oppsett for hele økten) dekker ikke dette direkte.

    «Try all» (2 lag, variant av foursome). Begge spillerne slår egen ball fra tee, men BYTTER ball til andreslaget (spiller A slår spiller B sin ball og omvendt), og paret velger deretter hvilken av de to ballene som skal spilles videre — resten av hullet spilles som ordinær foursome på den valgte ballen. Minst arkitektonisk avstand fra i dag: sannsynligvis en ren spilleregel-variant av eksisterende foursome-format (samme poengmodell, bare en annen fremgangsmåte de to første slagene) — trenger trolig ikke ny datamodell, bare en presisering i regelverket/UI-teksten.

    «Flaggturnering» (Flag tournament), reist av bruker 2026-07-25 — IKKE del av den opprinnelige fire-formater-listen over, notert som et eget femte format. Hver spiller får et fast antall slag (typisk par + handicap for hele runden) og spiller til slagene er brukt opp — den som kommer lengst rundt banen før slagene tar slutt, vinner ("planter flagget" der siste slag ble brukt). Konkret krav fra bruker, eksplisitt formulert: en visning som viser GJENSTÅENDE slag for spilleren, oppdatert etter hvert spilte hull (nedtelling, ikke bare et sluttall). Fremtidig idé, uttrykkelig betinget av at GPS er integrert i appen først (se GPS/avstandsmåling-notatet lenger opp i denne filen, ADR-033- seksjonen — samme avhengighet): bruk GPS til å markere/registrere HVOR på banen spilleren faktisk endte opp når slagene tok slutt, ikke bare hvilket hull. Ingen datamodell eller UI designet ennå for noen del av dette — rent notat.

  • Ingenting av dette er designet eller bygget ennå — kun fanget her slik at det ikke går i glemmeboken. Naturlig neste steg når dette tas fatt på: vurder de fire hver for seg (ikke som én stor runde), start med «Try all» (lavest kostnad) hvis en rask seier er ønskelig, eller med «Københavner» hvis en bredere individuell/felt-basert turneringstype uansett skal bygges først som fundament for de andre.

  • Ressurs, lagt til 2026-07-19: brukeren har lastet opp tre PDF-er til prosjektroten (spilletyper-og-spilleformer-2023.pdf, Live Tourney _ A Guide to Handicap Scoring in Golf for Tournaments.pdf, SCGA Club Digest.pdf) som til sammen skal gi en tydelig beskrivelse av hvordan HCP og mottatte/tildelte slag beregnes/fordeles — les disse FØR design av handicap-/slagfordelingslogikken for disse fire formatene, se CLAUDE.md sin "Autoritative kilder"-seksjon.

Utvidelse 2026-07-26: individuelle turneringer, flerrunde-turneringer, og Order of Merit

Reist av brukeren som svar på et spørsmål om et individuelt turnering-leaderboard — svaret avdekket at ønsket er STØRRE enn bare Københavner som ett format blant flere: TeeCup skal etter hvert kunne arrangere ekte INDIVIDUELLE turneringer (ikke bare lagturneringer), disse skal kunne gå over FLERE RUNDER, og det skal være mulig å sette opp et Order of Merit — sesong-sammenlagt poeng/rangering på tvers av flere separate arrangementer (brukerens eget eksempel: "klubbdager" som gjentas gjennom en hel sesong, med en løpende sammenlagt-tabell). Ren notat- runde, ingen kode skrevet, ingen ADR skrevet ennå — brukeren ba eksplisitt om at dette kun noteres nå.

Tre distinkte, men beslektede strukturelle spørsmål — bevisst holdt fra hverandre siden de har ulik arkitektonisk tyngde:

  1. Individuelle turneringer (ingen lag i det hele tatt). ADR-011 låser v1 til NØYAKTIG to lag — en individuell turnering (et flatt felt av spillere, som Københavner) bryter denne forutsetningen helt, ikke bare "trenger flere enn to lag". Dette er allerede notert i ADR-011 sitt eget "Merk (2026-07-19)"-avsnitt via Københavner-eksemplet, men brukerens presisering nå gjør det tydelig at individuelle turneringer er et EGET, generelt tilfelle — ikke bare én formatvariant blant fem. Trenger en egen ADR som avklarer om dette blir en helt egen turnering-TYPE (parallell til dagens to-lags-type, med sin egen match/scoring-modell) eller en utvidelse av eksisterende modell.

  2. Flerrunde-turneringer (samme arrangement, flere runder/dager, sammenlagt resultat). Dagens lagturneringer har ALLEREDE flere session-er (f.eks. en Ryder Cup-helg med økter fredag/lørdag/søndag) — men poengsummeringen er PER MATCH innad i hver økt, ikke en sammenlagt SLAGSUM på tvers av runder slik en individuell flerrunde-turnering (f.eks. en 3-dagers slagspillturnering) ville trengt. Reell strukturell kollisjon å avklare: frittstående runder (ADR-033 sin round/round_participant/round_hole, Beslutning A) er BEVISST bygget helt UTENFOR organisasjon/RLS-systemet (plain_connection(), eid av user_id, ingen organization_id i det hele tatt) — mens turneringer er strengt org-scopet og RLS- beskyttet. En org-arrangert flerrunde-individuell-turnering trenger runder som lever INNENFOR en organisasjons/turnerings-kontekst — dette er IKKE det samme systemet som de personlige rundene, selv om datamodellen (hull-for-hull-registrering) sannsynligvis ligner mye. Må avklares eksplisitt: gjenbruke round-tabellene (utvidet med en valgfri turnering-/org-kobling), eller bygge en parallell, org-scopet rundemodell? Dette er trolig den vanskeligste enkeltbeslutningen av de tre.

  3. Order of Merit (sesong-sammenlagt på tvers av FLERE separate turneringer/arrangementer). Krever et HELT NYTT overordnet konsept som ikke finnes i skjemaet i dag — noe a la en "sesong" eller "serie" som grupperer flere separate turnering-rader og akkumulerer poeng per spiller på tvers av dem, med sin egen løpende sammenlagt-rangering. Forutsetter sannsynligvis at (1) og (2) over er løst først (det er individuelle arrangementer som skal telle inn i et Order of Merit, ikke lag-baserte Ryder Cup-turneringer) — naturlig SISTE steg av de tre, ikke noe som kan designes isolert.

Ingenting av dette er designet eller bygget — kun fanget presist her slik at retningen er dokumentert før noe glemmes. Se også ADR-011 sitt eget notat (samme sak, kortere) og "Åpne spørsmål"-listen i ARCHITECTURE_DECISIONS.md.

Oppdatering 2026-07-26, samme dag: grunnstruktur for (1) og (2) AVKLART — se ADR-037

Brukeren ba om å starte ADR-runden på strukturspørsmålet direkte. Fire load-bærende beslutninger avklart eksplisitt (AskUserQuestion), full begrunnelse i ADR-037 (ARCHITECTURE_DECISIONS.md):

  • Ny, parallell org-scopet datamodell — IKKE en utvidelse av ADR-033s round-tabeller (unngår hybrid/betinget RLS, bevarer et tidligere bevisst valg).
  • Samme tournament-tabell, ny format_type-diskriminator ('team'/'individual') — gjenbruker synlighet/join-kode/status helt uendret.
  • Flerrunde fra start: ny tournament_round-tabell (økt-lignende), sammenlagt resultat summert ved lesing på ekte deltaker-id (samme prinsipp som det eksisterende lag-leaderboardet).
  • Rå slag lagres OG poeng caches per format — samme mønster som match.points_side_a/b i dag. Nye formater (Københavner m.fl.) blir dermed i hovedsak: én ny motorfunksjon i handicap_engine.py + én ny CHECK-verdi, ikke en skjemaendring.

Viktig presisering som oppsto underveis: "flight" i en formell org-turnering er KUN en tee-tid-gruppering, IKKE en leaderboard-grense (leaderboardet spenner alltid hele feltet) — ULIKT den ad hoc "flere flighter i en frittstående runde"-ideen (der leaderboardet bevisst er avgrenset til det man selv satte opp). De to holdes bevisst ADSKILT nå, ikke forent slik forrige runde antydet — se egen seksjon lenger ned i denne filen ("Frittstående runder: flere flighter...").

Fortsatt IKKE avgjort: de fem konkrete formatenes egne poengregler (punkt utenfor denne strukturrunden), og (3) Order of Merit — bekreftet som naturlig SISTE steg, ikke designet. Ingen migrasjon skrevet — ADR-037 er ren struktur-beslutning, neste steg er et konkret migrasjonsutkast til gjennomgang.


Varsler: push til telefon + in-app varslingssenter — 📋 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: In-app varslingssenter BYGGET, SCRATCH-VERIFISERT OG RULLET UT LIVE 2026-07-26 (bruker bekreftet eksplisitt, se CLAUDE.md sin statuslogg for utrullingsdetalj — denne linjen var utdatert helt til den ble rettet i en senere gjennomgang samme dag). V0 kjørte prompten (zip 20), backend bygget for å matche eksakt: migrasjon 026_notifications.sql (tabell notification, plain_connection()-mønster, ingen RLS — samme som personlig profil/ runder/venner), ny app/routers/notifications.py (create_notification() delt hjelpefunksjon + fire endepunkter: GET /notifications, GET /notifications/unread-count, POST /notifications/{id}/read, POST /notifications/read-all). To trigger-punkter koblet inn i friends.py (kun venneforespørsel-hendelsene som faktisk finnes i dag, ADR-036 fase 1): POST /friends varsler mottakeren, POST /friends/{id}/ accept varsler den opprinnelige forespørreren. components/notifications.tsx + ny rute /my-notifications (IKKE /notifications — samme kollisjonsklasse unngått fra start som /rounds//friends). Bjelle-ikonet fra V0s dashbord-eksport portert inn i den LIVE dashboard.tsx sin header (ikke en full revert av filen — samme kirurgiske uttrekk-mønster som alltid), koblet til et ekte GET /notifications/unread-count-kall ved mount. Scratch-verifisert i samme testløp som rundeleaderboardet under (se den seksjonen for detaljer om selve scratch-infrastrukturen, totalt 39/39 sjekker på tvers av begge funksjonene): full forespørsel→varsel→lest-syklus begge retninger, mark-all-read, og eksplisitt kryss-bruker-isolasjon (bruker B kan ikke markere bruker A sitt varsel som lest via id-gjetting — stille no-op, ikke en feilmelding som ville lekket at id-en fantes). Ekte typesjekket produksjonsbuild kompilerte rent, /my-notifications listet blant rutene. Ikke bygget i denne runden, bevisst utenfor omfang: ekte push til telefonens OS (egen, større runde, se vurderingen over), e-post-fallback (se tillegget under, fortsatt kun foreslått). Venter på brukerens bekreftelse før migrasjon 026 kjøres mot ekte teecup_db og containerne redeployes — se CLAUDE.md for den samlede utrullingsplanen (denne runden + rundeleaderboardet under ble bygget sammen).

Tillegg 2026-07-25: e-post som fallback-kanal, betinget av samtykke

Brukeren foreslo at TeeCup i tillegg sender en e-post til mottakeren av en venneforespørsel ("du har fått en forespørsel, åpne appen for å se den") — MEN kun hvis brukeren har akseptert e-post som kommunikasjonskanal fra TeeCup.

Sjekket eksisterende kode: det finnes I DAG ingen generell kommunikasjons-/varslings-samtykke-flagg på app_user — det eneste samtykket i skjemaet er tournament_registration.consent_given_at (ADR-017), som er noe HELT ANNET (samtykke til selve turneringspåmeldingen, org-scopet, ikke en kontoinnstilling). Dette må altså bygges som et nytt, eget felt, ikke gjenbrukes.

Foreslått, ikke bekreftet:

  • Nytt app_user.notification_emails_enabled boolean NOT NULL DEFAULT false — OPT-IN, ikke opt-out (samme "trygg standard"-filosofi som resten av appen, f.eks. rundevisibilitet default private). Satt via en ny bryter i kontoinnstillinger (/account), IKKE en del av den obligatoriske profil-fullføringen (dette er valgfritt, ikke påkrevd).
  • Ny send_friend_request_email() i app/email.py — følger EKSAKT samme mønster som de fem eksisterende utsendingsfunksjonene der (nb/en-maler, _send_sync via asyncio.to_thread, driftsfeil lekker aldri til klientresponsen). Sendes fra POST /friends, KUN hvis mottakeren har notification_emails_enabled = true.
  • Fremtidig presisering, ikke et problem nå: med kun ÉN varseltype (venneforespørsel) holder én global boolean. Den dagen appen får flere varseltyper (ADR-036 fase 2s rundevisibilitet, fremtidige turnering-hendelser, osv.) bør dette trolig bli et SETT av brytere per type, ikke én global av/på — notert her for å ikke bli glemt, ikke løst nå.

PWA-installasjon: hvordan få brukere til å installere raskt — 📋 VURDERT 2026-07-25, IKKE designet i detalj

Brukeren reiste dette som en oppfølging av varsel-diskusjonen: hvordan få brukere til "nærmest umiddelbart" å installere TeeCup som app på telefonen, gitt at ADR-028 allerede har bygget selve PWA-fundamentet (manifest, service worker, ikoner, "Legg til på Hjemskjerm"-metadata) — men ingenting proaktivt OPPFORDRER til installasjon i dag.

Plattformvirkeligheten, avgjør hele designet:

  • Android/Chrome-familien: nettleseren fyrer selv av et beforeinstallprompt-event når siden kvalifiserer (manifest+service worker+https — alt allerede på plass). Fanges opp med event.preventDefault() + lagres, og kan trigges SENERE fra en egen knapp via event.prompt() — full kontroll på NÅR spørsmålet stilles, ikke bare nettleserens egen timing.
  • iOS Safari: INGEN programmatisk vei finnes i det hele tatt. Apple har aldri implementert beforeinstallprompt. Eneste vei er den manuelle Del-ikon → "Legg til på Hjemskjerm"-flyten — appen kan KUN vise en instruksjonsoverlegg (f.eks. med skjermbilde/animasjon av hvilken knapp som skal trykkes), aldri utløse selve installasjonen. Dette er en hard Apple-begrensning, ikke noe TeeCup kan designe seg rundt.
  • Deteksjon nødvendig for begge retninger: matchMedia( "(display-mode: standalone)") (evt. navigator.standalone på eldre iOS) avslører om brukeren ALLEREDE kjører den installerte PWA-en — vis ALDRI noe installasjons-UI da. iOS-vs-Android/Chrome avgjøres med UA-sniffing (upresist, men standard og nødvendig her siden det ikke finnes noen bedre feature-deteksjon) for å velge riktig av de to variantene (ekte knapp vs. instruksjonsbanner) — andre nettlesere uten noen reell installasjonsvei bør ikke vise noe i det hele tatt.

Om "nærmest umiddelbart" — mild uenighet, med begrunnelse: et prompt FØR brukeren har vist noen interesse (f.eks. på selve innloggingsskjermen) treffer typisk dårlig og føles påtrengende, OG beforeinstallprompt har ikke alltid rukket å fyres av så tidlig uansett. Appen har derimot ALLEREDE et universelt, høy-intensjons sjekkpunkt HVER ny bruker går gjennom: den obligatoriske profil-fullføringen (ADR-031/ "obligatorisk profil-fullføring ved innlogging"-runden). Å vise installasjons-oppfordringen RETT ETTER det steget — første gang brukeren faktisk når /dashboard med en komplett profil — er trolig det beste "nærmest umiddelbart"-tidspunktet som finnes: reelt tidlig, men etter at brukeren allerede har investert litt og vist ekte intensjon, ikke et kaldt overfall på innloggingssiden.

Foreslått, ikke besluttet:

  • Vis KUN når display-mode ikke allerede er standalone.
  • Første visning: rett etter fullført profil, første gang /dashboard nås.
  • "Ikke nå"-avvisning lagres i localStorage med en avkjølingsperiode (f.eks. ikke vis på nytt før om N dager) — ingen server-side felt nødvendig, dette er et rent klient-signal.
  • To distinkte UI-varianter (Android: ekte "Installer"-knapp som kaller event.prompt(); iOS: instruksjonsbanner) — INGEN visning for andre nettlesere uten en reell installasjonsvei.

Status: kun vurdert/skissert i denne runden, ikke designet i detalj eller bygget — ingen V0-prompt skrevet ennå for dette (i motsetning til varslingssenteret over). Naturlig neste steg om bruker vil gå videre: en egen designrunde for selve UI-teksten/visuelt (særlig iOS- instruksjonsbanneret, som må vise konkrete steg) — trolig verdt et eget V0-prompt da, siden det er en egen, synlig UI-flate.


Leaderboard for runder og turneringer — 🧠 DRØFTET 2026-07-25, IKKE besluttet

Brukeren ba om et leaderboard for pågående og ferdige runder OG turneringer, usikker på om det bør ligge der man allerede ser rundens detaljer så langt (round-stats.tsx) eller integreres i selve detaljsiden (round-detail.tsx) — og lastet opp to skjermbilder av hvordan Golf GameBook har løst akkurat dette, som referanse. Bevisst drøftet her, ikke besluttet — brukeren ba selv om å "drodle", ikke bygge.

Referansen (Golf GameBook), oppsummert — IKKE noe TeeCup skal kopiere rett av, kun inspireres av struktur/konsept: en egen, dedikert "Leaderboards"-fane nederst (sidestilt med "Rundeinfo" og "Spill-feed", ikke en del av noen av dem), med faner ØVERST for ulike scoringsmetoder ("Slagspill NET"/"Stableford NET"), en rangert liste (#, navn, HCP, score, til par, "F" for ferdig), en blå "HCP-RUNDE"- merkelapp, og at hver rad kan TRYKKES UT til å vise spillerens fulle horisontale scorekort inline (samme hull-for-hull-tabellformat TeeCup allerede har bygget i round-scorecard.tsx) pluss sosiale handlinger (Lik/Kommentar/Statistikk).

To reelle presiseringer funnet ved å faktisk sjekke koden, ikke antatt:

  1. "Runder" kan få dette NÅ, ingen avhengighet til ADR-036 fase 3. En frittstående runde støtter allerede flere deltakere i dag (eier + gjester, round-detail.tsx sine spiller-faner) med uavhengig hull-for-hull-score hver — et rangert leaderboard PÅ TVERS av disse deltakerne er fullt buildbart nå. Fase 3 (ekte medspillere med egen konto) endrer ikke dette, det utvider bare HVEM som kan være en deltaker.
  2. "Turneringer" har allerede et leaderboard (tournament- leaderboard.tsx, live) — men det er et LAG-POENG-leaderboard for Ryder Cup-matchplay (ADR-011, to lag), strukturelt noe helt annet enn Golf GameBooks individuelle slagspill-rangering. Åpent spørsmål, ikke avklart: mener brukeren en individuell rangering INNAD i en turnering (f.eks. rangere spillere etter brutto/netto score i en økt, ved siden av det eksisterende lag-poeng-leaderboardet), eller var "turneringer" ment mer løst/generelt? Bør avklares før noe designes for turnering-siden av dette.

Plassering — min foreløpige vurdering, ikke en konklusjon: verken round-detail.tsx (allerede tett under selve spillingen, samme "for mye stablet oppå hverandre"-fare som tidligere runder denne uken allerede ryddet opp i) eller round-stats.tsx (dedikert til DYP enkelt-spiller-statistikk, ikke tvers-sammenligning) er et perfekt hjem alene. To ideer, ikke gjensidig utelukkende:

  • En KOMPAKT leaderboard-oppsummering (topp/posisjon, ikke full tabell) øverst på round-detail.tsx — det man faktisk vil sjekke RASKT mens man spiller ("hvem leder nå").
  • Gjenbruk EKSISTERENDE infrastruktur for full detalj i stedet for å duplisere Golf GameBooks "trykk ut for fullt scorekort inline": round-stats.tsx har ALLEREDE en spillervelger (pill-rad) bygget for flere-deltakere-runder — en leaderboard-rad kan trolig bare LENKE dit/bytte valgt spiller, i stedet for å bygge en helt ny inline- scorekort-mekanisme på nytt.
  • Flere scoringsmetode-visninger (Slagspill NET vs. Stableford NET) — TeeCup regner allerede Stableford klientside i round-stats.tsx, men har ingen tilsvarende "netto slagspill"-rangeringsvisning i dag. Verdt å designe eksplisitt om dette ønskes, ikke noe som følger gratis av det som allerede finnes.

Status: ingen kode, ingen V0-prompt ennå — venter på at retning (og særlig turnering-spørsmålet over) avklares før noe designes ferdig.

Oppdatering 2026-07-26: RUNDE-leaderboardet HELT FERDIG (håndkodet, ikke V0 -- se under)

Brukeren ba eksplisitt om å få leaderboardet for frittstående runder på plass (kun runder, ikke turneringer — turnering-spørsmålet over fortsatt ikke avklart). Bygget som ny GET /rounds/{round_id}/leaderboard (app/routers/rounds.py), samme plain_connection()/eier-only- autorisasjon som resten av rundene (_get_owned_round_or_404).

Datakontrakt:

GET /rounds/{round_id}/leaderboard
{
  "holes_planned": 9 | 18,
  "completed": boolean,
  "entries": [
    {
      "participant_id": string,
      "display_name": string,
      "is_owner": boolean,
      "holes_played": number,        // "thru"
      "total_score": number | null,  // null = ingen hull registrert ennå
      "score_to_par": number | null,
      "net_score_to_par": number | null  // null hvis ingen course handicap
                                          // (f.eks. gjest uten HCP oppgitt)
    },
    ...
  ]
}

Sortert server-side stigende på score_to_par (lavest/best først, ingen registrerte hull sist). Samme allokeringsalgoritme (allocate_strokes_by_index) som resten av appen for netto — uavhengig kryssjekket mot handicap_engine.py direkte i scratch, ikke bare "kjørte uten feil".

Scratch-verifisert (39/39 sjekker, to separate testløp): varsel- trigger-punktene fra samme runde (se "Varsler"-seksjonen), pluss full leaderboard-runde (3 deltakere — eier ferdig 9 hull til par, gjest 9 hull +1/hull, gjest kun 3 hull -1/hull — riktig rangering/thru/to-par for alle tre), kryss-bruker-autorisasjon (403 for en annen bruker), ukjent runde-id (404), OG en dedikert netto-kryssjekk (avvikende SI-rekkefølge, kun 9 av 18 hull spilt, resultatet sammenlignet mot en UAVHENGIG beregning via handicap_engine.allocate_strokes_by_index direkte — stemte eksakt).

V0-prompt sendt til bruker 2026-07-26, ikke kjørt i v0.app ennå:

Design et "Leaderboard"-visning for en enkelt frittstående golfrunde i TeeCup (Next.js + Tailwind + shadcn/ui, mobil-først, eksisterende merkevarefarger — grønn primær, oransje sekundær). Runden kan ha ALLE typer deltakerantall — fra kun eieren alene til flere flighter samtidig (opptil 13+ spillere er reelt mulig), så designet må skalere pent fra 1 til 15+ rader UTEN å bli en endeløs, monoton liste.

Data kommer fra et allerede bygget API-endepunkt som returnerer, for runden: om den er fullført eller pågår, planlagt hullantall (9/18), og en liste med én rad per deltaker: navn, om det er rundens eier, antall hull spilt ("thru"), total score, score til par (brutto), og score til par netto (kan være fraværende — vises da ikke for den spilleren, IKKE som "0" eller en feil).

Ranger deltakerne etter brutto score til par (lavest/best først). Gi et tydelig, men ikke overveldende, rangeringstall (#1, #2, ...) — delt plassering (likt resultat) skal vises tydelig som delt (f.eks. "T-2"), ikke to forskjellige tall for samme resultat.

Topp-plassering fortjener litt ekstra visuell vekt (f.eks. en diskret kant/bakgrunnstone eller et lite ikon) — men ALDRI kun farge for å skille ledere fra resten (tilgjengelighetskrav, se under).

For en pågående runde: vis "thru X" (f.eks. "thru 5") for spillere som ikke har fullført alle planlagte hull ennå, i stedet for en ferdig-markering. For en FULLFØRT runde: vis heller en tydelig "Ferdig"-markering per spiller i stedet for "thru X av X".

Score-til-par-tall skal formateres på golfvis: "E" for jevnt med par (0), "+N" over, "N" (ekte minustegn) under — ALDRI bare "0"/"-3" uten fortegn. Bruk FORM i tillegg til farge der du fremhever over/ under par (f.eks. en liten sirkel/firkant-indikator, ikke bare tekstfarge) — samme "aldri kun farge"-prinsipp som resten av TeeCup.

Gi brukeren en brutto/netto-veksling (to faner eller en enkel switch øverst) som bytter både HVILKET tall som vises OG selve rangeringsrekkefølgen mellom de to. Spillere uten et netto-tall (ingen HCP registrert) skal vises tydelig nederst/uten rangering i netto-visningen, ikke skjules eller krasje.

To distinkte merker, ikke ett: et "Eier"-merke for rundens eier (kommer direkte fra API-et sitt is_owner-felt), OG uavhengig av det et "Deg"-merke for raden som tilhører DEN som ser på leaderboardet akkurat nå (siden runden også kan sees av lenkede medspillere, ikke bare eieren -- komponenten mottar hvilken participant_id som er "meg" som en egen prop utenfra, ikke fra selve leaderboard-dataen). De to kan gjelde samme rad (eieren ser sin egen runde) eller ulike rader (en medspiller ser både sin egen "Deg"-rad og eierens "Eier"-rad) -- design for begge.

Design også en kompakt "mini-leaderboard"-variant (topp 3 + evt. "og N til") egnet til å vises øverst på selve rundens detaljside — et raskt "hvem leder nå"-blikk uten å måtte navigere til hele leaderboardet.

Tilgjengelighet er et ufravikelig krav: god kontrast, stor nok skrift, store trykkflater (min. 44px), aldri kun farge for å formidle informasjon (rangering/over-under par), lesbar uten briller.

Rettet i selve prompten 2026-07-26, FØR den ble kjørt i v0.app: den opprinnelige "Deg"-merke-instruksen antok feilaktig at kun eieren ser sin egen runde -- ADR-036 fase 3 (bygget samme dag) gjør nå at en lenket medspiller også kan se leaderboardet. Byttet til to uavhengige merker ("Eier" fra is_owner, "Deg" fra en egen participant_id-prop komponenten mottar utenfra) -- se prompten over.

Oppdatering, samme dag: HÅNDKODET i stedet for kjørt i v0.app. Brukeren gikk tom for V0-credits (samme situasjon som spillerliste- redesignet rett over) og ba meg bygge direkte etter nøyaktig samme designspesifikasjon som prompten over. Ny components/round-leaderboard.tsx

  • rute /my-rounds/[id]/leaderboard, pluss en RoundLeaderboardMini (topp 3 + "og N til", lenket fra selve rundens detaljside) og en ny "Se leaderboard"-lenke på hvert rundekort i /my-rounds-listen (krevde å gjøre om round-card.tsx sin ytre <Link> til en <div> med to separate lenker -- unngår en nestet <a>). Gjenbrukte bevisst etablerte mønstre (ScoreMarks form+farge-språk fra round-scorecard.tsx, WS-sanntid- mønsteret fra round-stats.tsx) fremfor å finne opp nye. To oppfølgingspunkter samme dag, begge bygget: (1) leaderboardet skulle vise hullscorer per spiller -- løst som en utvidbar rad (klikk for å vise en horisontal strip med hull-for-hull brutto-merker), krevde en liten backend-utvidelse (LeaderboardHoleOut/holes-felt, data var allerede hentet, bare ikke eksponert). (2) et tredje "Poeng" (stableford)-modus lagt til ved siden av Brutto/Netto (rangert synkende, siden høyere poengsum er bedre) -- krevde strokes_received per hull + total_points på hver leaderboard-rad (også backend, samme utvidelsesmønster). Netto/poeng vises nå også som en rolig sekundærlinje RETT under det fremhevede brutto-hullmerket (inspirert av et referansebilde fra en konkurrentapp bruker delte, bevisst IKKE en kopi av fargevalg/layout). Full verifiserings- og utrullingsdetalj i CLAUDE.md sin statuslogg (2026-07-26) -- ikke gjentatt her for å unngå duplisering.

Spillerliste-redesign (vertikal, utslag/HCP/rediger inline) — HELT FERDIG, håndkodet (V0 tom for credits), live 2026-07-26

Samme dag som HCP-i-søk/rediger-utslag-per-deltaker (se ADR-033-loggen i CLAUDE.md), rett etter at den runden var rullet ut live. Brukeren viste et skjermbilde av "+ Medspiller"-skjermen og pekte på to ting: (1) utslag+HCP (nettopp bygget samme dag) bør flyttes OPP i selve spillerknappen i stedet for å ligge som en egen rad under statistikknivå-velgeren, med Navn/ Utslag/Hcp/Rediger inni knappen, og spillerne listet VERTIKALT i stedet for dagens horisontale scroll-rad. For en "midlertidig spiller" (gjest uten konto) skal Rediger-flyten også dekke Navn/Kjønn/E-post. (2) Under selve hull-registreringen bør det vises hvor mange mottatte slag aktiv spiller har på det hullet, eksempel "Hull 7 - Par 4 - Hcp 5 - -1".

Punkt 2 er ALLEREDE bygget og live — ren frontend-tilføyelse i round-detail.tsx sin hull-header, bruker data (strokes_received) som allerede var hentet fra GET .../holes fra før (samme felt ScoreSoFar/ leaderboardet bruker til netto), bare aldri vist FØR scoring. Golfvis fortegn (ekte minustegn), vist kun når spilleren faktisk mottar minst ett slag på hullet (Hull 7 · Par 4 · Hcp 5 · 1).

Punkt 1 krevde ny backend, bygget og scratch-verifisert (22/22 sjekker + full regresjon av samme dags 27+35-punkts testsuiter) samme dag: ny migrasjon 029_round_participant_guest_email.sql (round_participant. guest_email, nullable). ParticipantCreate (POST .../participants) fikk et valgfritt guest_email: EmailStr | None (avvist sammen med user_id — en lenket bruker har allerede sin egen konto-e-post). ParticipantUpdate (PATCH .../participants/{id}) fikk guest_name/ gender/guest_email — men KUN gyldig for en gjest (user_id IS NULL); forsøk på en lenket deltaker avvises tydelig (400). gender-endring er nå en del av samme "rating_changed"-bunt som utslag/HCP (påvirker hvilken utslags-rating som er gyldig) — regnes om, avvist (400) hvis den nye kombinasjonen (nytt/uendret utslag × nytt kjønn) mangler rating, og bevisst avvist etter fullføring (409, samme presedens som utslag/HCP). guest_name-endring alene har INGEN rating-implikasjon og forblir derfor tillatt selv etter fullføring (ren metadata, samme som RoundUpdate sitt navnefelt).

V0-prompt sendt til bruker 2026-07-26, ikke kjørt i v0.app ennå:

Design om spillerlisten på en frittstående golfrundes hull-for-hull- registreringsside i TeeCup (Next.js + Tailwind + shadcn/ui, mobil-først, eksisterende merkevarefarger — grønn primær, oransje sekundær). I dag er spillerne en horisontal scroll-rad av små faner øverst på siden; dette skal bli en VERTIKAL liste av spillerkort i stedet. Runden kan ha alt fra kun eieren alene til mange spillere samtidig, så listen må skalere pent til 10+ rader uten å bli en endeløs, monoton vegg.

Hvert spillerkort viser: navn, utslagssted, HCP (eller "ikke satt" hvis fraværende), og en tydelig "Rediger"-knapp/lenke. Kortet er OGSÅ selve trykkflaten for å VELGE spilleren som aktiv for hull-registrering (samme funksjon som dagens fane-klikk) — velg en tydelig, men ikke overveldende, måte å vise "dette kortet betyr både 'velg meg' og 'rediger meg'" på (f.eks. hele kortet velger, en distinkt undersone/ knapp for rediger) uten at de to handlingene blandes sammen ved et uhell.

To distinkte merker på kortet, kan begge gjelde samme kort: "Deg" (spilleren SOM SER PÅ siden akkurat nå — komponenten mottar egen deltaker-id som prop, siden både eieren og en lenket medspiller kan se og bruke siden) og "Eier" (rundens eier, fra et eget boolsk felt på spilleren).

Rediger-flyten (åpnes fra kortet, f.eks. en utvidbar seksjon eller et ark/modal — din vurdering) inneholder:

  • Utslagssted: en nedtrekksliste hentet fra et eget endepunkt (banens faktiske utslag på DENNE runden), filtrert til utslag som har en rating for spillerens (gjeldende, eller nylig valgte — se under) kjønn.
  • HCP: et tallfelt, valgfritt (kan stå tomt/fjernes).
  • Statistikknivå for spilleren: tre valg ("Kun slag" / "Slag og putter" / "All statistikk") — samme tre-valgs-mønster som resten av appen bruker for dette (segmentert knapperad).
  • KUN hvis spilleren er en "midlertidig spiller" (gjest uten TeeCup- konto — komponenten vet dette fra at spilleren mangler en konto-id): TRE EKSTRA felt — Navn (fritekst), Kjønn (mann/kvinne/annet), E-post (valgfritt, e-postformat). Endring av kjønn her skal oppdatere hvilke utslag som er valgbare i utslag-nedtrekkslisten (samme filter som over, reaktivt).
  • En tydelig "gjelder kun denne runden, endrer ikke [spillerens] profil"-forklaring for utslag/HCP-feltene (gjelder IKKE navn/kjønn/ e-post for en gjest, siden en gjest ikke har noen egen profil å bevare uendret).

For en spiller MED egen TeeCup-konto (ikke en gjest) skal Rediger- flyten KUN vise utslag/HCP/statistikknivå — ALDRI navn/kjønn/e-post- feltene (gir ingen mening, personen har sin egen konto).

"Legg til medspiller"-knappen/flyten (søk-som-du-skriver mot ekte brukere, med et "legg til uten konto"-alternativ for gjester) beholdes konseptuelt som i dag, men tilpasses visuelt til den nye vertikale listen. Gjesteskjemaet i "legg til uten konto" bør få det samme valgfrie e-post-feltet som Rediger-flyten nå støtter (kan sette e-post allerede ved opprettelse, ikke bare i etterkant).

Skjul redigering/tilføyelse/fjerning helt når komponenten får beskjed om at brukeren IKKE har lov til å forvalte runden (en prop, f.eks. canManage) — en medspiller uten forvaltningsrett skal fortsatt kunne VELGE et kort for å registrere score, bare ikke redigere/legge til/ fjerne noen. Skjul ALL redigering (også for eieren) når runden er fullført (en readOnly-prop) — vis da utslag/HCP som ren tekst, ingen Rediger-knapp.

Tilgjengelighet er et ufravikelig krav: god kontrast, stor nok skrift, store trykkflater (min. 44px), aldri kun farge for å formidle status (Deg/Eier-merkene trenger tekst, ikke bare farge), lesbar uten briller.

Data komponenten skal jobbe mot (allerede live i produksjon):

type Participant = {
  id: string
  user_id: string | null       // null = gjest ("midlertidig spiller")
  guest_name: string | null
  guest_email: string | null   // kun meningsfullt når user_id er null
  display_name: string         // alltid utfylt, bruk denne til visning
  is_owner: boolean
  gender: "m" | "f" | "x"
  tee_name_snapshot: string
  handicap_index_snapshot: number | null
  course_handicap_snapshot: number | null
  stat_level: "strokes_only" | "strokes_and_putts" | "full"
}

GET /rounds/{round_id}/tee-options
→ [{ name: string, genders: ("m"|"f")[] }]

PATCH /rounds/{round_id}/participants/{id}
body (alle valgfrie, kun de som faktisk endres sendes):
  stat_level, tee_name, handicap_index (kan settes til null),
  guest_name, gender, guest_email (kan settes til null)
  -- guest_name/gender/guest_email avvises (400) hvis participant.user_id
     er satt (ikke en gjest)
  -- avvist (409) hvis runden er fullført OG tee_name/handicap_index/
     gender er blant feltene som sendes

POST /rounds/{round_id}/participants
body: ENTEN { user_id, tee_name?, stat_level? }
      ELLER { guest_name, gender, handicap_index?, guest_email?, tee_name?, stat_level? }

Oppdatering 2026-07-26: bygget HÅNDKODET i stedet for via V0. Brukeren gikk tom for V0-credits rett etter at prompten over ble skrevet, og valgte eksplisitt (spurt via AskUserQuestion) at jeg bygger det direkte fremfor å vente på fornyede credits. Implementert etter nøyaktig samme prompt/ data-kontrakt som over, i samme Tailwind/shadcn-stil som resten av round-detail.tsx: ny PlayerList-komponent (erstatter PlayerTabs) — vertikal liste, hvert kort en stor <button> for "velg som aktiv spiller"

  • separate Rediger/fjern-knapper (bevisst IKKE nestede interaktive elementer), "Deg"/"Eier" som Badge-komponenter (shadcn, gjenbrukt fra components/ui/badge.tsx som fantes men ikke var i bruk i denne filen fra før). EditParticipantPanel utvidet med statistikknivå (samme tre-valg som den fjernede frittstående StatLevelPicker, nå død kode og fjernet) og — kun for gjester (player.userId === null) — Navn/Kjønn/E-post, kjønnsendring filtrerer utslagslisten reaktivt. "+ Medspiller"-flyten (søk/gjesteskjema) flyttet fra en fast plassering utenfor spillerlisten til å rendres INNI PlayerList selv, nederst i den vertikale listen. Rediger-flyten ble en inline-utvidelse av kortet (ikke et eget ark/modal) -- enklest å implementere korrekt uten et nytt UI-primitiv, og konsistent med EditRoundPanels eksisterende inline-mønster i samme fil. Ny Player-type-detalj funnet nødvendig under bygging: player.name er allerede viewer-relativt ("Deg" for egen rad, se playerLabel()) -- måtte legge til et eget rawName (urørt display_name) for å forhåndsutfylle gjeste-navnefeltet korrekt, siden "Deg" åpenbart ikke skal havne i et redigerbart tekstfelt. Verifisert: ekte typesjekket produksjonsbuild (samme Dockerfile-steg som deployes) kompilerte rent. Satte i tillegg opp en engangs next dev- container mot en scratch-backend med en ekte runde (gjest med avvikende utslag+kjønn+e-post, aktiv spiller med et registrert slag på et hull med mottatte slag) og hentet siden sin server-rendrede HTML med en ekte sesjonscookie -- bekreftet 200 (ikke en Next.js-feilside/digest), ruten matcher riktig. Viktig begrensning, ærlig flagget: siden er en klient-komponent (all spillerdata hentes via fetch i useEffect ETTER hydrering) -- den server-rendrede HTML-en viser derfor kun last-skjelettet (spinner), ikke selve spillerkortene/Rediger-panelet. Ingen ekte nettleser-basert interaksjonstest (klikk "Rediger", bytt kjønn og se utslagslisten filtrere reaktivt, lagre og se kortet oppdatere) er utført -- intet nettleserverktøy tilgjengelig i denne økten. Bruker bør selv klikke gjennom flyten (spesielt gjeste-kjønnsendringens reaktive utslagsfilter og "velg vs. rediger"-trykkflatene) før full tillit.

Oppfølging samme dag: tildelte slag + jevn korthøyde, HÅNDKODET OG LIVE. Brukeren rapporterte to ting rett etter forrige rullings: (1) spillerkortet manglet "tildelte slag" (course handicap for runden, ikke selve HCP- indeksen) når det spilles med HCP, (2) kortene burde ha lik høyde og ikke være høyere enn nødvendig, slik at man havner under score-tastaturet uten unødvendig scrolling. Begge rettet i PlayerList: Player-typen fikk courseHandicap: number | null (fra course_handicap_snapshot, allerede i API-et), lagt til i info-linjen som "Tildelte slag: N" -- KUN vist når verdien faktisk finnes (samme betingelse som "spilles med HCP"). Kort- knappen fikk en fast min-h-[68px] (var variabel min-h-16 + fri wrapping) med BEGGE tekstlinjer (navn+merker, utslag/HCP/tildelte slag) trunkert til én linje hver (truncate, ikke wrap) -- alle kort blir dermed like høye uansett antall merker eller lengde på navn/utslag, samtidig som selve listen tar minst mulig vertikal plass.

Oppfølging samme dag: rundeleaderboardet HÅNDKODET OG LIVE. Brukeren spurte eksplisitt om jeg, "med designerbrillene på", trodde jeg kunne få det til å se like profesjonelt ut som V0 -- svarte ja (gjenbruk av etablerte mønstre, ikke fri visuell utforskning) og bygget det. Ny components/round-leaderboard.tsx + rute /my-rounds/[id]/leaderboard. Gjenbrukte bevisst EKSISTERENDE etablerte mønstre fremfor å finne opp nye: signedToPar-formateringen og "form + farge, aldri kun farge"-språket fra ScoreMark i round-scorecard.tsx (sirkel = under par, firkant = over par, her som et ToParMark for et rundetotal-tall i stedet for ett hull), samme WS-sanntid-/viewerId-oppkoblingsmønster som round-stats.tsx/ round-scorecard.tsx (refreshKey bumpet av /ws/rounds/{id}/live), Badge-komponenten fra spillerliste-redesignet over for "Deg"/"Eier". Rangeringslogikk (delt plassering vist som "T-N", riktig "hopp over" rangeringstall ved tie, ulik rangering brutto vs. netto, uspilte/uten netto-tall sortert samlet nederst uten rangeringstall) og formatToPar-formateringen VERIFISERT UAVHENGIG i et frittstående Node-script (samme "test beregningen i Node"-mønster som tidligere brukt for round-stats.tsx sin computeStats()) -- 19/19 sjekker, inkl. et scenario med to spillere tidd for ledelsen på brutto men IKKE tidd på netto, og et scenario med en spiller som ikke har startet ennå. Ny RoundLeaderboardMini-variant (topp 3 + "og N til", hele kortet en lenke til full side) lagt inn i round-detail.tsx rett under headeren -- viser seg ikke i det hele tatt for en solo-runde (komponenten returnerer null når runden har færre enn 2 deltakere). Verifisert: ekte typesjekket produksjonsbuild kompilerte rent (ny rute /my-rounds/[id]/leaderboard listet). Satte opp en fersk scratch-runde med tre deltakere via ekte API-kall (to tidd på brutto til par 0, ulik netto pga. ulik HCP, én som ikke har spilt ennå) og bekreftet leaderboard-JSON-en matchet forventningen presist. Samme engangs next dev-container-sjekk som spillerliste-redesignet -- begge rutene (/my-rounds/[id] med den nye mini-varianten, /my-rounds/[id]/leaderboard) ga 200, ingen Next.js- feilside. Samme ærlige begrensning som over: ingen ekte nettleser- interaksjonstest (brutto/netto-veksling, faktisk visuell høyde/form-språk) er utført.

Oppfølging samme dag: scoringsflyten redesignet til en skjermovertagende veiviser (v1 avvist av bruker, v2 erstattet den helt) — LIVE 2026-07-26. Brukeren delte en skjermopptaksvideo av en konkurrentapp (Golf GameBook) sin scoreregistrering. Første forsøk (v1) la kun til golf-term-taltastatur + en "Neste spiller"-knapp på den EKSISTERENDE lange inline-siden — brukeren testet den live og avviste den eksplisitt ("ingen forbedring i det hele tatt", "visuelt like overveldende og rotete"). v2 bygget samme dag: en ekte skjermovertagende ScoringWizard (tre steg maks, drevet av stat_level), hovedsiden erstattet med én kompakt spillerliste (navn, akkumulert til-par-så-langt, en stor rund knapp som åpner veiviseren). Krevde også å laste ALLE deltakeres hull med det samme (ikke lenger lat lasting), slik at akkumulert score kan vises for alle samtidig. Reell driftsfeil funnet OG rettet samme dag: den gamle "Så langt i runden"-boksen (ScoreSoFar) ble ved en feil IKKE flyttet i v2-bygget — lå fortsatt øverst, uendret, og fikk brukeren til å rapportere "ingen synlig endring i det hele tatt" (bekreftet presist ved å hente den faktisk kjørende JS-bunten i produksjon og søke i den — ikke en cache-feil, en ekte plasseringsfeil). Rettet ved å flytte boksen til under scoringsseksjonen. Full beslutnings-/begrunnelsesdetalj i ARCHITECTURE_DECISIONS.md (ADR-033-oppdatering 2026-07-26), full verifiserings-/utrullingsdetalj i CLAUDE.md sin statuslogg — ikke duplisert her.

Oppfølging 2026-07-27 — teecup-scorekort-og-entry-spec.md (nytt, brukeropplastet PRESKRIPTIVT motstykke til DESIGN_SYSTEM.md, basert på en 5-app-sammenligning) evaluert, §3 "Compliance-pass" delvis BYGGET SAMME DAG — LIVE: "Deg Deg"-badge-duplikat fjernet (playerLabel() viser nå alltid ekte navn), scoringslistens score-knapp gjort om til å følge §Golfscore-språket (gjenbruker ScoreMarks fargespråk fra round-scorecard.tsx), pluss et system­atisk tabular-nums/44px- trykkgulv-avvik-audit fikset (13 steder).

Spec-dokumentets §1 (scorekort som fullt grid) krevde et bevisst "JA" fra brukeren først (egen informasjonsarkitektur enn ett-hull-om-gangen-listen) — bekreftet SAMME dag, og BYGGET OG RULLET UT LIVE 2026-07-27 rett etter compliance-passet: erstattet med en scrollbar ScorecardGrid (spillere som rader, hull som kolonner, sticky navnekolonne + Ut/Inn/Sum), celler følger §Golfscore-språket UBRYTELIG (gjenbrukt eksakt fra round-scorecard.tsx). Tapp en celle/navnerad/hull-overskrift åpner samme ScoringWizard som før, nå adressert direkte fra gridet.

Reell alvorlig rendering-bug funnet OG fikset SAMME DAG, rett etter utrullingen, via ekte nettleser-testing (første gang en Chrome DevTools MCP var tilgjengelig denne økten) — position: sticky på tabellceller kombinert med sticky venstre+høyre kolonner samtidig rendret fullstendig ødelagt/overlappende i Chrome. Fikset ved å droppe sticky-posisjonering på Ut/Inn/Sum-kolonnene (scroller nå med resten av hullene i normal flyt) og beholde kun den velprøvde sticky venstre navnekolonnen. Verifisert med ekte skjermbilder + scroll-simulering mot ekte produksjonsdata.

Samme dag, oppfølging: full nettleser-gjennomgang av alle 22 skjermer (brukeren spurte hvilke visninger som finnes, ba deretter om at alle ikke-browsersjekkede ble sjekket). Fant OG fikset en ny, ekte "til par"- bug i round-scorecard.tsx (regnet mot hele rundens par i stedet for kun spilte hulls par — ga en absurd "53 til par" midt i en runde). Resten av de 22 skjermene bekreftet uten krasj/konsoll-feil. Full detalj i CLAUDE.md sin statuslogg (2026-07-27) — ikke duplisert her.


En tredje (informasjons-)farge til designet — BYGGET OG LIVE 2026-07-28

Brukeren spurte om det ville vært en idé å introdusere én (eller kanskje to) nye farger til TeeCups design — trolig utløst av Golf GameBook- skjermbildene over, som bruker flere fargenyanser for score-mot-par- indikasjon og en egen blå "HCP-RUNDE"-merkelapp.

Sjekket faktisk palett i globals.css FØR noe ble foreslått: TeeCup har i dag --primary (grønn, hue ~130, ADR-016 — bevisst avledet fra Teeoffs egen logo, se ADR-009/016 sin begrunnelse for hvorfor dette IKKE er en tilfeldig fargevalg), --brand-orange (hue ~36.5, samme opprinnelse), og --destructive (rød, hue ~27, reservert for slette-/ feil-handlinger). I TILLEGG finnes allerede en --chart-1…6- datavisualiseringsskala (lagt til under rundestatistikk-arbeidet) — --chart-3 er ALLEREDE en blåtone (hue 210), med egne, ferdig avstemte verdier for BÅDE lyst og mørkt tema.

Anbefaling, forankret i dataviz-prinsippet om at statusfarger skal være RESERVERTE (god/advarsel/alvorlig/kritisk) og aldri gjenbrukt som "serie 4": i stedet for å finne på en helt ny fargetone, LØFT den allerede eksisterende --chart-3-blåtonen til en egen, navngitt kjerne-designtoken (f.eks. --info/--info-foreground, samme mønster som --brand-orange/--destructive allerede er egne tokens utover selve chart-skalaen) — gjenbruker allerede validerte OKLCH-verdier for begge temaer, i stedet for å øke det totale fargeantallet i appen. Denne "informasjons"-fargen kunne dekke akkurat den typen behov Golf GameBook løser med blått: en nøytral status mellom "bra" (grønt) og "trenger oppmerksomhet" (oransje) — f.eks. en "teller for HCP"-merkelapp, eller en mellomste score-til-par-kategori (par ↔ bogey ↔ dobbel bogey, hvis TeeCup noen gang vil fargekode scorekortceller mer finmasket enn i dag).

Anbefaler IKKE en fjerde/femte helt ny nyanse i tillegg — grønn (positiv/merkevare), oransje (merkevare/oppmerksomhet), en løftet blå (nøytral/informasjon), og rød (destruktiv, reservert) dekker allerede de fire klassiske statuskategoriene godt (god/informasjon/advarsel/ kritisk-destruktiv). Flere farger enn det risikerer å utvanne betydningen uten en konkret, begrunnet bruk å vise til ennå.

Oppdatering 2026-07-28: brukeren ba eksplisitt om TO nye farger (ikke bare den anbefalte ene). Løftet BEGGE --chart-3 (blå → --info) OG --chart-4 (gul/gull → --gold, ikke drøftet over, men samme gjenbruk-fremfor-ny-nyanse-logikk) til egne kjernetoken i globals.css. Konkret førstebruk: --info på et nytt "Hcp spilt til X"-merke på rundekort, --gold på et nytt "Personlig rekord"-merke (laveste til-par blant minst to fullførte runder). Full detalj i CLAUDE.md sin statuslogg (2026-07-28) — ikke duplisert her.


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). FIKSET 2026-07-26 (i en egen "fiks alle kjente små bugs"-runde, lenge etter oppdagelsen over): et stroke-modus scoreinnsending på en bane UTEN registrerte hull (hole-tabellen tom) krasjet 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. Fikset akkurat som foreslått da bugen ble oppdaget: eksplisitt sjekk (len(all_18_si) != 18) rett før allocate_over_played_holes kalles, ikke en try/except rundt symptomet — samme mønster som den allerede kjente/fikset manglende-handicap-indeks-krasjen fra blind draw-runden (2026-07-18). Scratch-verifisert presist (5/5 sjekker): en bane UTEN hull gir nå ren 400 VALIDATION_FAILED ved hull-scoreinnsending i stedet for 500, OG en regresjonssjekk bekreftet at en NORMAL bane (med alle 18 hull) fortsatt scorer helt uendret (begge sider, full scorekort-henting). test_isolation.sql 12/12 uendret (ren Python-logikk-fiks, ingen migrasjon). Rullet ut sammen med dashbord-hilsen-fiksen under, se CLAUDE.md-status.

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) — ALLE FIRE FIKSET (siste 2026-07-21)

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.

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

  2. 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 (venner-kjernen) FERDIG, deler av FASE 3 (søk+legg til medspiller, skriv for hele flighten) BYGGET 2026-07-26, fase 2 (rundevisibilitet) 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. DENNE DELEN BYGGET 2026-07-26 (se egen "Fase 3 (delvis)"- underseksjon under Fase 1) — søk+legg til + "skriv for hele flighten" er live. list_rounds/RoundOut sine viewer-relative my_*-felt (se under) er del av denne leveransen. Sletting av runden og bane-/utslagsbytte forblir eier-eksklusivt (bekreftet, se ADR-036).

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) — HELT FERDIG, backend+frontend rullet ut live 2026-07-25

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.


Fase 3 (delvis): søk+legg til medspiller, skriv for hele flighten — BYGGET OG SCRATCH-VERIFISERT 2026-07-26

Utløst av at brukeren rapporterte at "+ Gjest"-skjemaet på en frittstående runde ikke søkte etter spillere — bekreftet reelt (rent tekstfelt, ingen søk koblet på ennå). Bygget den etterspurte delen av fase 3 (søk-og- legg-til + full skrivetilgang for medspillere), IKKE rundevisibilitet (fase 2, fortsatt egen, senere runde).

Backend (app/routers/rounds.py): POST /rounds/{id}/participants tar nå ENTEN user_id (funnet via /people/search, samme tiered algoritme som vennesøket) ELLER guest_name (uendret fallback for spillere uten konto) — kjønn/HCP hentes automatisk fra den valgte personens EGEN profil ved user_id, ikke tastet manuelt. Ny migrasjon 027_round_participant_user_unique.sql (partiell unik indeks, hindrer dobbel-lenking av samme bruker). Ny _get_accessible_round_or_404 (eier ELLER lenket medspiller) brukt for lesing/hull-scoring/fullføring — _get_owned_round_or_404 (strengt eier-only) beholdt for rediger/ slett/legg til/fjern/endre-stat_level. RoundOut sine owner_*-felt omdøpt til my_* og gjort VIEWER-relative (bekreftet nødvendig allerede 2026-07-25). Ny display_nameRoundParticipantOut (levende oppslått, ikke snapshot) — fanget og fikset en reell latent bug i leaderboard-endepunktet (bygget samme dag) som ville vist blankt navn for enhver lenket medspiller.

Frontend (round-detail.tsx + samme fiks portert til round-stats.tsx/ round-scorecard.tsx): "+ Gjest" omdøpt til "+ Medspiller", nytt søk-som-du-skriver-felt (debounce, avatar-initialer, hjemmeklubb) med en "Legg til uten konto"-fallback til det gamle tekstfeltet. Viewer-relativ "Deg"-visning (krevde en /auth/me-utvidelse for å kjenne den innloggede brukerens egen id) — en lenket medspiller ser nå seg selv som "Deg" og eieren under sitt eget navn, ikke omvendt. Rediger/slett-runde og legg til/fjern-medspiller-knappene skjules nå for en ikke-eier-viewer (backend avviser uansett, men UI-et bør ikke vise handlinger som bare feiler).

Scratch-verifisert grundig, 39/39 sjekker i to testløp: søk-og- legg-til (kjønn/HCP auto-fylt fra profil), avvist duplikat/selv-tillegg/ ukjent bruker/ufullstendig profil (mangler kjønn), lenket medspiller kan lese runden + registrere score for BÅDE egen OG andres rad (whole- flight-regelen), men nektes å forvalte runden (rediger/slett/legge til/fjerne/endre stat_level — alle 403), lenket medspiller KAN fullføre runden, /rounds-listen viser nå runden for en lenket medspiller (med DERES egen fremdrift, ikke eierens), en helt urelatert bruker fortsatt 403/ikke i listen, leaderboardets navn stemmer for alle tre deltakertyper. test_isolation.sql 12/12 uendret. Ekte typesjekket produksjonsbuild kompilerte rent.

Oppfølging samme dag: sanntid — BYGGET OG SCRATCH-VERIFISERT 2026-07-26

Brukeren spurte om spillere ser i sanntid at en annen registrerer en score — svaret var nei, og brukeren ba om at det bygges. Gjenbrukte det etablerte WebSocket-mønsteret fra ADR-027 ("Følg live" for turneringer): et rent "noe endret seg"-signal, klienten reagerer med de vanlige REST- kallene. Nytt /ws/rounds/{id}/live i rounds.py, ALDRI anonym tilgang (krever eier eller lenket medspiller), kringkasting lagt inn i alle skrivende rundeendepunkter. Se ARCHITECTURE_DECISIONS.md/CLAUDE.md for full detalj, inkl. et reelt Starlette-funn (alle avvisningskoder kollapser til HTTP 403 i selve håndtrykket — sikkerheten er upåvirket). Scratch-verifisert med en EKTE WebSocket-klient (10/10 sjekker) — ekte kringkasting bekreftet begge veier, ikke bare REST-svar.


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 OG PRESISERT 2026-07-25/26, IKKE besluttet eller 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 sessionmatch-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.

Presisert 2026-07-26 (fortsatt IKKE besluttet, kun tydeligere)

Brukeren presiserte tre konkrete ting ved oppfølging:

  1. Oppsettflyten er ÉN handling utført av ÉN person. "Når jeg setter opp en vanlig runde kan jeg sette opp for bare meg, for inntil 3 andre i samme flight, eller for flere flighter." Det er brukeren selv som tar ansvar for å sette opp ALLE flightene (også vennenes, i eksemplet) — ikke at hver flight settes opp uavhengig av sine egne deltakere. Speiler dagens gjest-mønster (eieren legger til gjester), bare utvidet til å dekke flere adskilte flight-grupper i samme handling.
  2. Fremtidig, ikke motstridende: "det er selvfølgelig de i flightene bak som må føre sin egen score" — bekrefter at ansvaret for å SETTE OPP og ansvaret for å REGISTRERE SCORE er to forskjellige ting, og at det andre (scoreregistrering per flight) forventes løst av ekte medspillere med konto (ADR-036 fase 3), ikke av oppsetteren manuelt.
  3. Leaderboard-omfanget er PRESIST det som ble satt opp SAMMEN, ikke "alle som spilte samme bane samme dag": brukerens eget eksempel — setter Alice opp en flight med fire, og vennene deres setter opp en HELT ANNEN flight med fire (uavhengig av Alices oppsett), skal disse to leaderboardene IKKE slås sammen. Kun flightene som ble satt opp SAMMEN i én handling deler leaderboard.

Konsekvens for de to retningene: punkt 3 er et sterkt signal FOR retning 1 (løs gruppering av separate round-rader, bundet sammen av et tynt "satt opp sammen"-konsept som samtidig er leaderboardets naturlige omfang) — retning 2 (én round som beholder) ville krevd en ekstra mekanisme for AKKURAT denne avgrensningen uansett, siden "alle som spilte samme bane samme dag" aldri var riktig omfang i utgangspunktet. Fortsatt IKKE en endelig beslutning — brukeren utforsket/presiserte forståelse, bekreftet ikke eksplisitt en byggeretning.

Brukerens egen observasjon, viktig å fange presist: "jeg ser veldig godt at jeg opererer i grenseland mellom frittstående runde og turneringer her." Dette pekte først mot en mulig forening med "Individuelle turneringer, flerrunde-turneringer og Order of Merit" — men presisert og IKKE lenger antatt samme konsept, se ADR-037 (2026-07-26): i en formell org-turnering er "flight" KUN en tee-tid-gruppering, og leaderboardet spenner alltid HELE feltet uavhengig av hvem som spilte sammen. Her, i den ad hoc frittstående runden, skal leaderboardet AVGRENSES til nøyaktig det som ble satt opp sammen. De to "flight"-begrepene ligner i UI (flere grupper spiller samme dag), men er strukturelt ulike leaderboard-omfang — holdes derfor bevisst ADSKILT (to separate, mindre systemer), ikke forent til ett. Denne seksjonens egen retning (løs gruppering av separate round-rader, se over) står fortsatt som anbefaling, uendret av presiseringen.


Frittstående rundeføring + detaljert statistikk (uten turnering/organisasjon) — BACKEND + FRONTEND LIVE, løpende oppfølging t.o.m. 2026-07-28 (se ADR-033/ADR-038 for full detalj per dag)

Oppdatering 2026-07-28 (ADR-038): en full gjennomgang av eksisterende funksjonalitet mot .md-filene avdekket at hele WHS-indeksmotoren i handicap_engine.py (bygget/testet under ADR-033) aldri ble kalt fra noe API-endepunkt — score-differensialer ble lagret per runde, men app_user.handicap_index endret seg kun manuelt. Bygget, scratch- verifisert (111/111) og rullet ut live: to atskilte HCP-tall (manuelt vs. faktisk/beregnet), en eksplisitt "overfør til manuelt HCP"-handling, en manuell eksklusjons-toggel per deltaker (kontrollert av HVER innlogget deltaker for sin egen rad), og selvdeklarert spilleform (slagspill/matchspill) med en anbefalt-men-overstyrbar eksklusjon for matchspill. Se CLAUDE.md-status 2026-07-28 for full detalj (migrasjon 030, backend/frontend-endringer, verifisering). Fortsatt IKKE tettet, notert samme runde: offline-kø kun for turnering-scorekortet (ikke frittstående runder), ingen Stableford, rundedeling/visibility (ADR-036 fase 2) fortsatt ikke bygget.

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 (<1m8m+), 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 rå 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 på 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 på slag, og (b) å kunne få opplyst avstand til forskjellige steder på 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 på 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 nå 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 på 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 på 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 må 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 — MÅ 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 på 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 nå.

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 på hullet (allerede dekket av eksisterende hole_score-form)
  • Antall putter
  • Antall chip
  • Antall bunkerslag
  • Antall straffeslag
  • Lengde på 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 på alle domenetabeller, håndhevet av RLS." En frittstående runde har per definisjon INGEN organisasjon å henge organization_id på — dagens RLS-modell (org_isolation-policyer som alle stoler på 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 på 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 på 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 må fortsatt kunne velge en bane)?
  • Skal frittstående runder vises i "Mine runder" på 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.


Frittstående runder: ekte spillformer (slagspill/match/skins/par-lag) — BACKEND BYGGET OG SCRATCH-VERIFISERT (ADR-039), IKKE ENNÅ rullet ut mot ekte systemer, frontend fortsatt ikke bygget

Oppdatering 2026-07-28 (ADR-039): design + backend er ferdig samme dag som punktet ble reist. Se CLAUDE.md-status 2026-07-28 for full detalj (migrasjon 031, nye endepunkter, 96/96 scratch-sjekker). Kort: alle fire load-bærende spørsmål (sider/gruppering, omfang av par-/lag- underformater, skins-regler, HCP-tellestatus for delt-ball) avklart med bruker, hele match-play-motoren fra org-turneringer PORTERT uendret (ingen ny regnelogikk for match/fourball/foursome/greensome/scramble), kun skins fikk ekte ny motorkode. Gjenstår: frontend (ingen skjerm for sideoppsett/skins-konfig/matchstatus-visning ennå) og selve utrullingen mot ekte teecup_db/teecup_api — begge egne, separate neste steg.

Opprinnelig reist av brukeren rett etter at ADR-038 (faktisk HCP) ble rullet ut: "Når en singlerunde settes opp: Er dette en slagspillsrunde, en match mellom to spillere, skins, eller en par- eller lag-konkurranse. Avhengig av svaret så må hcp beregnes forskjellig, og også scorekortet vil se annerledes ut."

Viktig presisering av hva som FINNES i dag, for å unngå forveksling: round.play_format ('stroke'/'match', ADR-038) er i dag KUN en selvdeklarert ETIKETT som styrer én ting — et forslag om å ekskludere runden fra faktisk-HCP-grunnlaget. Den endrer INGENTING ved selve scoringsmodellen eller scorekortet — en "matchspill"-runde i dag registreres og vises identisk med en slagspill-runde (rå slag per hull per spiller). Dette nye punktet ber om noe vesentlig større: at spilleform faktisk STYRER både HCP-beregningen og scorekort- presentasjonen, for fire distinkte typer.

De fire spillformene, og hva som mangler for hver:

  1. Slagspill — dagens modell, uendret. Allerede fullt bygget (Score Differential/AGS, ADR-033 Beslutning G).
  2. Match (to spillere) — trenger match-play-slagfordeling (match_play_strokes i handicap_engine.py, allerede bygget/testet for turnering-matcher) i stedet for allocate_strokes_by_index, og et scorekort som viser løpende matchstatus (hull for hull vunnet/tapt/ delt + "X UP"/"AS", samme presentasjonsspråk som session-scorecard.tsx allerede har for turnering-matcher) i stedet for rå slagsummer. Reelt skjemahull: round_participant har i dag INGEN "hvem spiller mot hvem"-kobling — en runde med 3+ deltakere har ingen måte å si at akkurat to av dem utgjør matchen.
  3. Skins — hull-for-hull-konkurranse der laveste (netto eller brutto) score på hullet vinner en "skin", uavgjort hull ruller premien videre til neste hull. Ingen eksisterende motorstøtte i det hele tatt — verken en beregningsfunksjon eller noen skjema-plass for "skins vunnet"/gjeldende premieverdi. Må designes fra bunnen (inkl. avklaring: netto eller brutto skins, rullerer uavgjort-verdien videre eller deles, minst 3 spillere).
  4. Par- eller lag-konkurranse — fourball/foursome/greensome/scramble- type spill blant rundens deltakere. Motoren for AKKURAT dette (Format-enum, AllowanceStrategy-familien, unit_playing_handicap) er allerede bygget og testet — men KUN brukt av den org-scopede turnering-modellen (match/match_participant under session). Reelt skjemahull, samme klasse som punkt 2: frittstående round_participant-rader er i dag rene individer uten noe par-/lag-konsept — ingen kobling for "disse to er makkere denne runden."

Sentral arkitektur-spenning, verdt å legge merke til FØR design starter: punkt 2 og 4 er strukturelt nesten IDENTISKE med det app/routers/matches.py/scoring.py allerede gjør for org-scopede turneringer (samme Format-enum, samme match-play-motor) — bare uten en organisasjon rundt. Dette overlapper direkte med det ennå ubesluttede spørsmålet i ADR-037 ("individuelle turneringer") og det tidligere presiserte "flere flighter i én frittstående runde"-spørsmålet (se egen seksjon over) — tre beslektede, men foreløpig separat behandlede problemstillinger som alle til slutt lander på "hvordan grupperer/ parer vi deltakere, og hvilken motor regner poeng fra rå slag." Bør trolig avklares SAMMEN, ikke som tre uavhengige design-runder, for å unngå tre parallelle, litt ulike implementasjoner av i bunn og grunn samme idé.

Åpne spørsmål, ingen besvart ennå:

  • Skal round.play_format utvides med 'skins'/'pair_team' (ny migrasjon, ny CHECK-verdi), eller er dette en helt egen entitet parallelt med round?
  • Match/par-lag: hvordan velges/lagres hvem som spiller mot/med hvem — ved oppsett (som blind draw for turneringer), eller fritt valgt av eieren i etterkant?
  • Skins: netto eller brutto, og hvordan behandles uavgjorte hull (rullerer premien, eller deles)?
  • Skal scorekortets NYE presentasjoner (matchstatus, skins-tavle, side-score) bygges som varianter av eksisterende komponenter (round-scorecard.tsx/round-detail.tsx), eller gjenbruke turnering- sidens session-scorecard.tsx-språk direkte?
  • Hvordan påvirker dette allerede byggede ADR-038 (faktisk HCP)? Match/ skins/par-lag-runder trenger sannsynligvis EGNE regler for om/hvordan de teller mot faktisk HCP (samme "matchspill telles vanligvis ikke"- resonnement som allerede finnes for 'match', men skins/par-lag er ikke vurdert i det hele tatt ennå).

Ingen kode skrevet — dette er bevisst kun fanget/dokumentert nå, på brukerens eksplisitte instruks. Trenger en egen, dedikert design-/ ADR-runde (samme skala som ADR-033/036/037) før noe bygges, gitt at punkt 2-4 hver krever nytt skjema, ny motorlogikk (for skins) eller gjenbruk av eksisterende turnering-motor (for match/par-lag), og en egen scorekort-presentasjon per format.


É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 på 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 på dashbordet. Etter bekreftelse dukker inviterte turneringer på 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.) må 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) må kjøres for HVER av kontoens verifiserte adresser — naturlig utløst rett etter en ny adresse er bekreftet, og sannsynligvis også trygt å kjøre på 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 må 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 på dashbordet, så 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 nå 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 på 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 nå 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 på punktet over.
  • Offline-first (ADR-006) — BYGGET 2026-07-19 (ADR-028), se eget punkt under. Scoreregistrering (hole-scores/hole-results) fungerer nå offline med automatisk synk.
  • PWA: manifest, service worker, «Legg til på 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 på 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 lå 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 windows online-event, pluss manuell "Synkroniser nå"-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).