teecup/FEATURE_BACKLOG.md
Erol Haagenrud 3dd6224bfd Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro

Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting

Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere

Frontend: profil-seksjon i /account

Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx

Scratch-verifisere alt (15 sjekker bestått)

Typesjekket frontend-build

ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:

Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.

"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.

Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.

Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.

Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00

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

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


Kommunikasjon — HELT FERDIG 2026-07-19 (ADR-025)

Del Status Notat
Lag-intern chat («det hemmelige rommet») LIVE /tournaments/[id]/teams/[teamId]/chat. Ekte privat — kun rostrede spillere, INGEN unntak for org-eier/admin (bevisst avvik fra appens vanlige autorisasjonsmønster). user_is_rostered_on_team i app/team_authz.py.
Offentlig runde-feed («Banter Board») LIVE Egen seksjon nederst på /t/[id]. Lesing gjenbruker tournament.visibility-mønsteret (ADR-018) uendret, inkl. anonym tilgang for public-synlige turneringer. Posting er strengere: krever innlogging OG org-medlemskap/deltakelse.
Bilder i feed/chat LIVE Med fra start (ikke utsatt) — gjenbruker app/storage.py sin MinIO+AVIF-pipeline fra ADR-018 uendret.
Sanntid-levering LIVE WebSockets, ikke polling — løser samtidig det tidligere åpne "sanntid vs. polling"-spørsmålet generelt. In-memory tilkoblingsregister per prosess (kun trygt med dagens ene teecup_api-container, se ARCHITECTURE_DECISIONS.md "Åpne spørsmål"). Egen Caddy-rute /ws/* rett til teecup_api.
Video 💤 Fortsatt utsatt, egen ADR om/når etterspurt.
1-til-1 direktemeldinger 💤 Fortsatt utsatt, ikke etterspurt.
Moderering (offentlig feed) LIVE Forfatteren selv, ELLER org-eier/admin, kan slette et innlegg. Lag-chatten har ingen moderering utover forfatteren (rommet er privat, org-admin har uansett ikke lesetilgang).

Se ARCHITECTURE_DECISIONS.md ADR-025 for alle fire hovedbeslutningene og CLAUDE.md-status for full byggerunde (datamodell, autorisasjon, scratch- verifisering med 20 automatiserte sjekker inkl. reell WebSocket-sanntid, og utrulling).


Landingssider (turnering + organisasjon) — ADR-018 HELT FERDIG 2026-07-18

Reist av brukeren 2026-07-18, rett etter registrerings-ADR-en (ADR-017). Backend, begge frontend-skjermer, Open Graph-metadata OG MinIO-bilde- opplasting (med AVIF-konvertering) bygget og live samme dag. Kun selve dra-og-slipp-opplastingsskjermen i frontend (V0) gjenstår.

Del Status Notat
Synlighetsnivå: offentlig / kun org-medlemmer / kun turnering-deltakere tournament.visibility (default org, trygg standard) + organization.public_profile. Håndheves eksplisitt i registration.py — RLS løser IKKE dette alene (se ADR-018 Beslutning B, reell presisering funnet under bygging). Samme trenivå-modell som «Banter Board» under bør gjenbruke dette.
Registrering følger samme synlighetsgrense ADR-018 Beslutning D, bekreftet med bruker FØR bygging.
"Deltaker"-tilgang (ikke org-medlem, men rostret/registrert) Ny get_current_user_optional + _is_participant(). Testet: rostret spiller som logget inn fikk tilgang, tilfeldig fremmed ble avvist.
Turnering-landingsside: tekst, program, sponsorer, påmelding (API) description-felt, tournament_sponsor-tabell (navn+lenke), GET /public/tournaments/{id}/sessions (gjenbruker blind draw-lås fra ADR-013). Selve SKJERMEN i frontend gjenstår.
Org-landingsside: klubbprofil, liste over turneringer (API) GET /public/orgs/{slug} — kun public-synlige turneringer. Bekreftet: klubb-profil KAN være offentlig (brukerens valg).
Lesbar URL (slug) for organisasjon Fantes faktisk allerede i skjemaet siden migrasjon 001 (oversett, funnet da migrasjon 009 feilet mot scratch — se CLAUDE.md-status). Kun CHECK-constraints lagt til i 009.
Organisator kan faktisk SETTE disse feltene Implisitt hull fylt under bygging: PATCH /orgs/{id}/tournaments/{id} (visibility/description/registrering), PATCH /orgs/{id} (slug/public_profile), full sponsor-CRUD.
Bilder (hero, sponsorlogoer), backend Ny teecup-minio-tjeneste, ekte multipart-opplasting → AVIF-konvertering (Pillow) → lagring, live på teecup.teeoff.no/teecup-media/*. Selve opplastingsskjermen i frontend (V0) gjenstår.
Del-metadata (Open Graph: og:title/og:description) generateMetadata()/t/[id]+/clubs/[slug], ekte data fra API-et, verifisert mot produksjonsimaget. og:image gjenstår (MinIO).
Blind draw-skjuling på offentlig side Arves automatisk via delt _fetch_sessions()-hjelpefunksjon (ADR-018 Beslutning E) — ikke reimplementert.
Antall påmeldte / ledige plasser vist åpent confirmed_count i GET /public/tournaments/{id}.
Frontend: turnering-landingsside + påmeldingsskjema, LIVE /t/[id]. Tre bekreftelsestilstander (bekreftet/venteliste/godkjenning venter). Verifisert med ekte POST-registrering mot scratch.
Frontend: org-/klubb-landingsside, LIVE /clubs/[slug]. Gjenbruker TournamentCard (nå med valgfri orgId) for turneringslisten. Verifisert: viser kun public-synlige turneringer.

Program-skjerm (økter/tidsplan) — BYGGET OG SCRATCH-VERIFISERT 2026-07-18

Sjette V0-skjerm, første i "bygg i rekkefølgen ting brukes"-serien (etter lag/roster kommer program, så blind draw, scorekort, leaderboard). components/tournament-program.tsx, ny rute /tournaments/[id]/program. Tidslinje over turneringens økter i rekkefølge + opprett-skjema (format, hullomfang, scoringsmodus, poeng, klokkeslett+intervall, starthull, en kollapsbar "avansert"-seksjon med ADR-014s fire handicap-brytere).

Reelt blokkerende hull funnet FØR integrering, ikke etter: SessionCreate.course_id er påkrevd, men det fantes INGEN vei til å skaffe en gyldig én — ingen course-endepunkt i API-et i det hele tatt, og ADR-004s teeoff-integrasjon er fortsatt bare vedtatt (ingen utgående HTTP-klient finnes noe sted i koden). Spurte bruker eksplisitt (samme mønster som andre scope-avklaringer) — svar: bygg enkel course-CRUD nå. Lagt til app/routers/courses.py (GET/POST /orgs/{id}/courses, kun source='custom', ingen hull-/tee-/rating-detaljer denne runden — motoren bruker foreløpig kun course_id som fremmednøkkel). Program-skjemaet fikk et type-ahead-felt for bane (samme mønster som spiller-type-ahead i roster-skjermen: søk blant org-ens eksisterende baner, eller opprett ny inline).

Reell korrekthetsfeil funnet og rettet FØR den nådde V0-designet i det hele tatt hadde blitt integrert: V0-promptet mitt ba om ett generisk "Scramble"-valg, men skjemaets CHECK-constraint og handicap_engine.py sin Format-enum krever scramble_2/scramble_4 som DISTINKTE verdier (antall spillere per side er del av selve formatet) — ren "scramble" avvises med 400. Rettet i frontend-mappingen til to egne segment-knapper.

allowance_override-mapping verifisert eksakt mot motor-kontrakten: app/handicap.py sin parse_allowance_config/_strategy_from_json forventer {type: "combined"|"per_player", percentage: 0..1} — IKKE en flat prosent. Frontend velger riktig type ut fra om formatet er side-enhet (foursome/ greensome/scramble_2/scramble_4 → combined) eller spiller-enhet (fourball/ singles → per_player), og konverterer skjemaets 0100-prosentfelt til 01-brøk før sending. Full JSON-rundtur bekreftet i scratch (se under) — ikke bare antatt riktig fra å lese motorkoden.

Verifisert grundig mot fersk scratch-infrastruktur (ny teecup_scratch- database 001→009 + en ISOLERT teecup_app_scratch-rolle som arver teecup_app sine grants via GRANT teecup_app TO teecup_app_scratch — bevisst IKKE den ekte teecup_app-rollen, siden den nå er produksjonskritisk og rollen er cluster-global på tvers av teecup_db/ teecup_scratch; en tidligere plandokument sin "drop teecup_app-rolle"- opprydning er utdatert etter go-live og ble bevisst IKKE fulgt + isolert scratch-MinIO-container): courses opprettet+listet, kryss-org-isolasjon bekreftet (org 2 ser ikke org 1 sin bane), økt opprettet med klokkeslett, økt opprettet med scramble_4 + full allowance_override-rundtur, gammel "scramble"-verdi korrekt avvist (400), test_isolation.sql fortsatt 12/12. Ekte typesjekket PRODUKSJONSBUILD (samme Dockerfile/multi-stage som faktisk deployes, ikke bare en dev-server) kjørt og bekreftet — ny /tournaments/[id]/program-rute listet korrekt i build-output.

Diffet V0-eksporten mot live-treet før noe ble tatt inn (samme mønster som alle tidligere runder): kun tre reelt nye filer (tournament-program.tsx, program/page.tsx, ui/switch.tsx) — resten var forventede full-reverts av allerede tilpassede filer, ikke rørt.

Mindre justeringer utover selve V0-promptet: fjernet V0s dev-only "forhåndsvis tom/med økter"-knapperad (ikke noe en ekte organisator skal se); lagt til en fanerad ("Lag og spillere" / "Program") i BÅDE tournament-detail.tsx og den nye skjermen, siden V0 ikke visste om den andre skjermen når den ble generert i en egen prompt.

Rullet ut live 2026-07-18, bruker bekreftet eksplisitt: begge containere (teecup_api, teecup_frontend) bygget og redeployet, live sjekker OK (/health, /dashboard → 200), teeoff.no upåvirket.

Nytt 2026-07-18, HELT FERDIG (backend + frontend live): PATCH/ DELETE for økter (app/routers/tournaments.py) — kunne tidligere verken rettes eller slettes etter opprettelse. DELETE kun for tomme økter (409 hvis den har matcher). PATCH dekker enkle felt fritt, pluss en egen, forsiktig gren for bane-bytte (finner/flytter tilsvarende tee per allerede tillagt deltaker, regner om handicap+matchstatus for hele økten etterpå — også for allerede AVGJORTE matcher, bekreftet eksplisitt av bruker). Se CLAUDE.md-status for det fulle scenarioet (verifisert med et 10-hulls avgjort-match-eksempel) og et urelatert funn (front_9/back_9 + stroke-modus kan aldri få handicap i dag, siden tee_rating alltid kun lages med full_18-omfang). Rediger-/slett-UI LIVE 2026-07-19 — utvidelse av den eksisterende Program-skjermen (ingen ny rute). Fanget en reell regresjon i selve V0-eksporten før den ble tatt inn: samme runde hadde utilsiktet fjernet bane-feltet fra "Legg til økt"-skjemaet — kun de nye rediger/slett-delene ble hentet inn, det ekte opprett-skjemaet urørt. Se CLAUDE.md-status for detaljer.


Offisiell banedata fra teeoff — BYGGET OG LIVE 2026-07-18 (ADR-019)

Bevisst sidesprang fra "bygg i rekkefølgen ting brukes" rett etter program-skjermen: brukeren påpekte at ADR-004s teeoff-integrasjon fortsatt bare var vedtatt, ikke bygget. Full design i ARCHITECTURE_DECISIONS.md ADR-019 (fem delbeslutninger). Kort: organisator søker blant teeoff sine baner i program-skjemaet, velger én, og teecup KOPIERER bane+hull+tee+ tee_rating inn i teecup_db (source='official') — ikke et live oppslag ved hver bruk. Ny app/teeoff_client.py (ren HTTP-klient mot http://teeoff_api:8000, internt Docker-nettverk, ingen auth trengs — begge containere deler allerede teeoff_default). To nye endepunkter i app/routers/courses.py: GET .../courses/official-search[/{slug}] og POST .../courses/official-import. Migrasjon 010 (unik external_course_ref per org, hindrer dupliserte importer).

Bevisste avgrensninger for denne runden: kun 18-hulls baner kan importeres (teeoffs skjema har ingen egen 9-hulls-inndeling); kun full_18-rating importeres (teeoff har ingen separat front9/back9-rating, samme valg som ADR-008 allerede tok for egendefinerte baner); ufullstendige teeoff-data (manglende par/hcp_index på et hull, eller en tee uten NOEN rating) avviser hele importen tydelig (EXTERNAL_DATA_INCOMPLETE) FØR noe skrives, ikke en delvis importert bane.

Verifisert grundig, inkludert mot EKTE teeoff_api (ikke en simulert respons): søk, anlegg-/banevalg, og import kjørt reelt mot den kjørende produksjonscontaineren (kun lesing) — importerte Borregaard Golfklubb sin hovedbane, bekreftet alle 18 hull + 8 tee/tee_rating-rader riktig i databasen, og opprettet en ekte økt med den importerte banen (beviser hele veien til handicap-motoren, ikke bare selve importen). Reimport avvist (409), kryss-org-isolasjon bekreftet, ukjent teeoff-slug ga 404, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild av frontend-utvidelsen.

Rullet ut live, bruker bekreftet eksplisitt: migrasjon 010 mot ekte teecup_db, begge containere redeployet, teeoff.no upåvirket.

Reell bug funnet og fikset samme dag, av en bruker som faktisk testet funksjonen: bane-søkeboksen ("Hent bane fra teeoff") var et <form> rendret INNI det ytre økt-opprett-skjemaet — ugyldig, nestet HTML. Å klikke "Søk" submittet i praksis det ytre skjemaet som en ekte side-navigasjon og vasket bort ?org=...-parameteren fra URL-en. Fikset ved å fjerne det indre <form>-elementet (vanlig <div> + Enter-tast/knapp-klikk i stedet). Se CLAUDE.md-status for full root cause.

Nok en reell bug funnet og fikset samme uke, rapportert fra ekte bruk mot teecup.teeoff.no: import av samme teeoff-bane til flere økter (helt normalt — flere runder spilles ofte på samme bane) ga en 409-feil i stedet for å bare gjenbruke banen, og det lagrede navnet ("Hovedbanen" alene) ga ingen måte å se hvilken klubb det gjaldt. Fikset: POST .../courses/ official-import er nå idempotent (gir tilbake eksisterende rad ved reimport, sjekket FØR teeoff-kallet), og navnet lagres nå som "{anlegg} {bane}". Ekte produksjonsdata for "De Gamle er Eldst" ryddet opp (en økt hadde ved et uhell fått en tom søppel-custom-bane — se CLAUDE.md-status for full hendelse og rotårsak). Åpent, ikke løst i denne runden: de to bane-søkeflatene (øverste felt = organisasjonens egne baner + "opprett ny"-snarvei, "Hent bane fra teeoff"-knappen lenger ned = offisielt søk) er lette å forveksle — det var nettopp dette som forårsaket søppel-banen. Vurder en tydeligere UI- sammenslåing eller rekkefølge-endring i en senere runde.


Invitasjonskode, ledende side og projisert stilling — ADR-020

Reist av brukeren 2026-07-19, rett etter at kamp-play-flyten var komplett. Full design i ARCHITECTURE_DECISIONS.md ADR-020.

Del Status Notat
Kort invitasjonskode per turnering, overstyrer visibility backend tournament.join_code (migrasjon 011), genereres automatisk ved opprettelse. GET /public/tournaments/by-code/{code} + kode-bypass i registration.py. Løser "muntlig invitert spiller finner ikke turneringen"-hullet.
match.leading_side (strukturert, ikke tekst-parsing) backend Cachet i recompute_and_cache_match_state ved hver hull-innsending, eksponert på MatchOut.
Projisert stilling (hvis pågående matcher holder seg) backend TeamStanding.projected_points + SessionStanding.projected_points_by_team i leaderboard-endepunktet. Ikke-avgjorte matcher gir full poengsum til leading_side, delt 0,5/0,5 ved "AS".
Login-skjerm: kode-felt, tar deg direkte til turneringen LIVE 2026-07-19 login-form.tsx sin JoinByCode. code tres gjennom /t/[id] sin lesing OG registrering.
Turnering-detalj: vis invitasjonskode (kopier-knapp) LIVE 2026-07-19 tournament-detail.tsx sin JoinCodeChip — henter fra org-ens turneringsliste, ikke et nytt endepunkt.
Leaderboard: to stillingsbarer (faktisk + projisert) LIVE 2026-07-19 tournament-leaderboard.tsx sin SegmentedBar — fargesegmentert rektangel, ikke tall side om side. Eksisterende tall-scoreboard beholdt som detaljvisning.
Fargekoding av matchlister etter ledende lag LIVE 2026-07-19 session-blind-draw.tsx sin RevealedView/RevealSide — farget toppkant + status-chip, bruker leading_side + eksisterende team.color, ingen ny fargemodell.
Kode-regenerering (ved lekket kode) 💤 bevisst utsatt Ikke bygget denne runden — ingen organisator-vei til å bytte ut en kode ennå. Egen sak hvis etterspurt.

ADR-020 er dermed helt ferdig — backend + frontend, alle fire del-ønsker levert samme dag. Se CLAUDE.md-status for full runde inkl. en reell (og transparent håndtert) passord-eksponeringshendelse underveis.


Innlogging: sesjon-bug + passord/2FA — ADR-021 (reist 2026-07-19)

Del Status Notat
Flere organisasjoner per bruker bekreftet allerede dekket ADR-002 fra dag én. POST /orgs har ingen begrensning på antall org-er samme bruker kan eie. Ingen kodeendring — kun bekreftet ved gjennomgang 2026-07-19.
Sesjon holder seg ikke — ny magic-link-kode kreves ved hvert besøk FIKSET og LIVE 2026-07-19 Ikke en cookie-bug (brukerens ekte cookie var korrekt satt, 30 dager, Secure/HttpOnly). Root cause: app/page.tsx sjekket aldri om en gyldig sesjon allerede fantes før den viste innloggingsskjemaet. Fikset med server-side sesjonssjekk + redirect til /dashboard. Se CLAUDE.md-status for full diagnose og verifisering.
Passord som valgfritt tillegg til magic-link LIVE 2026-07-19 Argon2id-hashing (ikke bcrypt — unngår 72-byte-trunkering). Verifisert med et ekte passord med mellomrom+æøå+spesialtegn. POST /auth/login-password, /auth/set-password, /auth/remove-password. Passord er ALDRI påkrevd.
2FA: TOTP eller e-post-engangskode, brukerens eget valg LIVE 2026-07-19 SMS bevisst utenfor omfang (krever betalt leverandør). POST /auth/2fa/setup/start+/confirm, /auth/2fa/verify, /auth/2fa/disable.
2FA påkrevd for org-eier/admin, valgfritt for medlemmer LIVE 2026-07-19 Håndheves ved hver innlogging via en stage: "pending_2fa"/"must_enroll_2fa"-mellomtilstand i sesjons-JWT-en. Verifisert: en fersk org-eier uten 2FA ble korrekt tvunget inn i oppsett ved neste innlogging.
Frontend: passord-innlogging, 2FA-verifisering/-oppsett, kontoinnstillinger LIVE 2026-07-19 login-form.tsx (passord-modus), two-factor-flow.tsx (delt mellom login/verify), /account.

ADR-021 er dermed helt ferdig. Se CLAUDE.md-status for full byggerunde, inkl. tre reelle bugs funnet og fikset under scratch-testing.

Organisasjonseierskap: dele, invitere, frasi seg, superadmin — ADR-022

Reist rett etter ADR-021. Avdekket et bredere, mer fundamentalt hull enn bare "del eierskap": det fantes tidligere INGEN vei til å legge til et organisasjonsmedlem etter opprettelse i det hele tatt — kun grunnleggerens egen owner-rad ble noensinne satt inn.

Del Status Notat
Flere eiere per organisasjon LIVE Skjemaet støttet det allerede (ingen unikhetssperre); nå faktisk brukbart via API.
E-post-invitasjon (owner→hvilken som helst rolle, admin→kun member) LIVE 2026-07-19 POST/GET/DELETE /orgs/{id}/invitations. Auto-akseptert ved neste innlogging (magic-link ELLER passord), samme mønster som spiller-e-post-kobling (ADR-017). Verifisert: admin som forsøkte å invitere som eier ble korrekt avvist.
Rollestyring + frasi seg eierskap (selvbetjent) LIVE 2026-07-19 PATCH/DELETE /orgs/{id}/memberships/{id}. «Siste eier»-vern (409 LAST_OWNER) OG selv-forfremmelse-vern verifisert eksplisitt i scratch.
Superadmin (manuelt DB-tildelt, ikke selvbetjent) LIVE 2026-07-19 POST /superadmin/orgs/{id}/memberships. Verifisert: ikke-superadmin avvist (403), superadmin kan sette eierskap på en org de selv ikke er medlem av, ukjent e-post avvist (404).
Fjernet eiers roster-/spillerdata avgjort (Beslutning E) Forblir urørt — organisasjons-styring ≠ deltakelse-historikk. Bekreftet av bruker 2026-07-18.
Frontend: medlemsstyring/invitasjon LIVE 2026-07-19 org-members.tsx, /orgs/[id]/members, lenket fra dashbordet.

ADR-022 er dermed helt ferdig. Ingen dedikert superadmin-UI bygget (bevisst — brukes via API av en betrodd operatør, matcher «sjelden, manuelt tildelt makt»-designet).


Rapporterte hull, blind draw + scorekort (2026-07-19)

Fire punkter rapportert av brukeren fra faktisk testing av /tournaments/.../sessions/... (blind draw-skjermen) og scorekort-skjermen, foursome-format. Root cause funnet ved kodegjennomgang for alle fire (det fjerde, hcp, ble bekreftet mot EKTE produksjonsdata — ikke gjettet). Alle fire er nå FIKSET OG SCRATCH-VERIFISERT samme dag — punkt 2 ble omdefinert etter en presisering fra brukeren (se under), fikk en egen ADR (ADR-029) og en reell skjemamigrasjon (014_tee_gender_to_rating.sql).

  1. FIKSET 2026-07-19: valgt spiller forsvant ikke fra listen over velgbare spillere. Root cause: AddSlotForm i components/session-blind-draw.tsx merket allerede brukte roster-rader som disabled på selve <option>-elementet — et disabled HTML-<option> blir stående synlig (kun gråtonet), forsvinner ikke. Fikset: roster-listen FILTRERES nå ned til kun ledige spillere før den rendres, i stedet for å deaktivere valget.

  2. FIKSET OG LIVE 2026-07-19 (ADR-029). Brukeren presiserte at min opprinnelige forståelse var feil: en golfbane har IKKE fysisk kjønnsdelte utslag — begge kjønn kan som regel spille fra ethvert utslag. Det eneste som faktisk varierer per kjønn er om klubben har VALGT å slope (rate) et gitt utslag for det respektive kjønnet (typisk: noen klubber sloper bevisst ikke det lengste utslaget for damer). Bekreftet mot ekte produksjonsdata (Tjøme Golfklubb, importert via ADR-019): hvert fysisk utslag ("32", "44", "50", "55") lå som TO separate tee-rader med SAMME navn, én per kjønn — selve konflateringen brukeren påpekte. Fikset: gender flyttet fra tee til tee_rating (migrasjon 014_tee_gender_to_rating.sql, se ADR-029 for full detalj), tee-valg i blind draw er nå HELT automatisk (organisator velger kun fysisk utslag, riktig kjønnsrating løses fra spillerens player.gender server-side — bruker bekreftet dette fremfor et forhåndsutfylt-men- overstyrbart alternativ), manglende kjønn/rating avvises tydelig FØR innsetting (bruker bekreftet "feil høyt" fremfor stille fallback). ADR-019s teeoff-import, manuell tee-opprettelse og bane-bytte-remap (_remap_course) alle oppdatert til samme modell.

  3. FIKSET 2026-07-19: tallvelger (19 + utvidbar "10 eller flere" → 1019) i stedet for pluss/minus-steppere. Ny StrokePicker-komponent i components/session-scorecard.tsx, erstatter den gamle ±-stepperen helt (stepStroke/Minus/Plus fjernet som død kode). Ren frontend-endring.

  4. FIKSET 2026-07-19: HCP ble faktisk ALDRI beregnet for front_9/back_9-økter — bekreftet ekte kodebug, ikke bare en synlighetsmangel. Diagnostisert presist ved å lese EKTE produksjonsdata (read-only, teecup_db) for brukerens rapporterte testøkt: hole_config='front_9', format='foursome', og ALLE fire match_participant-radene hadde course_handicap/playing_handicap = NULL. Root cause: app/handicap.py sin compute_and_store_side_handicaps joinet tee_rating på ØKTENS hole_config som ratingens scope — men tee_rating-rader lages i praksis KUN med scope='full_18' (ADR-019 Beslutning D + tee- endepunktet), og ADR-008 sin allerede etablerte design tilsier nettopp dette: course handicap skal ALLTID regnes fra full_18-ratingen, uansett øktens hole_config — front/back-9-fordelingen skjer SENERE, ved selve slagtildelingen (allocate_over_played_holes), ikke ved rating- oppslaget. Joinen matchet dermed aldri noen rad for en front_9/back_9- økt, og handicap ble stille aldri beregnet — påvirket ALLE formater på front_9/back_9-økter, ikke bare foursome. Fikset: scope hardkodet til 'full_18', hole_config-parameteren fjernet helt fra compute_and_store_side_handicaps/_recompute_session_matches (var død etter fiksen) og de tre kallstedene i matches.py/tournaments.py. Bekreftet at selve UTREGNINGEN matcher brukerens egen beskrivelse nøyaktig (kombinert course handicap / 2 via CombinedPercentage(0.5), laveste side satt til 0 mottatte slag via match_play_strokes, resten fordelt fra stroke index 1 via allocate_over_played_holes) — bugen lå i at beregningen ALDRI kjørte for front_9/back_9, ikke i selve formelen. Scratch-verifisert presist: gjenskapte nøyaktig samme scenario (foursome + front_9, hcp 10/20 vs. 5/15) — course/playing handicap beregnet korrekt (10/21 vs. 5/16, kombinert 16/10), og et hull med IDENTISK bruttoscore (5-5) på begge sider ga et ikke-delt resultat ("a" vant, ikke "halved") — direkte bevis på at handicap nå faktisk brukes. Ingen migrasjon (ren Python-logikk-fiks). Ekte, urelatert bug funnet UNDER samme scratch-test, IKKE fikset ennå: et stroke-modus scoreinnsending på en bane UTEN registrerte hull (hole-tabellen tom) krasjer med en rå 500 (IndexError: list index out of range i handicap_engine.py sin allocate_over_played_holes, kalt fra scoring.py sin _compute_hole_results) i stedet for en tydelig VALIDATION_FAILED. Samme klasse feil som den allerede kjente/fikset manglende-handicap-indeks-krasjen fra blind draw-runden (2026-07-18) — bør fikses likt (eksplisitt sjekk FØR beregning, ikke en try/except rundt symptomet). Ikke fikset i denne runden, kun oppdaget og notert.

Designspørsmålene for punkt 2 er avklart (bruker valgte det anbefalte alternativet på begge, se ADR-029 Beslutning B/C): helautomatisk tee-valg (ingen manuell kjønnsvelger), og "feil høyt" ved manglende kjønn/rating (ingen stille fallback). Migrasjon 014 kjørt mot ekte teecup_db 2026-07-19, bruker bekreftet eksplisitt — Tjømes 8 tee-rader slått sammen til 4, alle referanser intakte.

Alle fire punkter rullet ut live 2026-07-19, bruker bekreftet eksplisitt: migrasjon 014 + docker compose up -d --build teecup_api teecup_frontend. Verifisert: /health/dashboard → 200, teeoff.no upåvirket, 0 brutte match_participant.tee_id-referanser etter sammenslåingen.


Rapporterte UI-/UX-hull (2026-07-19) — notert, IKKE fikset ennå

Fire punkter rapportert av brukeren fra faktisk bruk av teecup.teeoff.no. Root cause funnet ved kodegjennomgang for de tre første (ikke bare gjettet); det fjerde er et reelt manglende UI-element, ingen kodefeil. Ingen av de fire er fikset i denne runden — kun dokumentert slik at de ikke går i glemmeboken.

  1. FIKSET 2026-07-19/20 (ADR-030). Dashboard-turneringskortet viste "Ingen datoer satt" selv om øktene (rundene) hadde dato/klokkeslett satt — bekreftet av brukeren med et faktisk skjermbilde (en økt tydelig planlagt til "lør. 11. juli", men kortet viste fortsatt "Ingen datoer satt"). Root cause: tournament.start_date/end_date var et EGET, frittstående felt (ADR-015) som INGEN UI noensinne satte — helt atskilt fra session.scheduled_at. Valgte retning (b) fra de to opprinnelig skisserte alternativene: GET /orgs/{id}/tournaments utleder nå datospennet fra øktenes scheduled_at (COALESCE med et evt. eksplisitt satt start_date/end_date, som fortsatt vinner om det noensinne settes). Se ADR-030 for full detalj og verifisering.

  2. /orgs/{id}/members-siden gir 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) — det betyr at ikke-dynamiske filer/sider sjekkes FØR rewrites, men DYNAMISKE sider (som app/orgs/[id]/members/page.tsx) sjekkes ETTER. Rewrite-regelen { source: "/orgs/:path*", destination: ".../orgs/:path*" } (satt opp i ADR-016 for å proxye API-kall) fanger derfor /orgs/{id}/members FØR Next.js noensinne når frem til den faktiske siden, og sender kallet til FastAPI i stedet — som naturligvis ikke har noen GET /orgs/{id}/members- rute (kun /orgs/{id}/memberships), derav den rå FastAPI-404-formen (ikke engang appens egen app_error-kontrakt, siden ruten ikke matcher noe sted i det hele tatt). Selve siden (org-members.tsx, lenken fra dashbordet) er ellers riktig bygget — dette er en ren rewrite/dynamisk-rute-presedens-krasj, samme klasse fallgruve som ADR-016 sin opprinnelige "alt nytt API-prefiks må inn i rewrites"-lærdom, bare i motsatt retning (en frontend-SIDE ble skjult AV en rewrite). Dette er den FØRSTE frontend-siden som noensinne har blitt nestet direkte under et allerede proxyet prefiks (/orgs/*) — ingen tidligere skjerm har truffet dette. Sannsynlig fiks: flytt siden til en ikke-proxyet sti (f.eks. /organizations/[id]/members), ELLER gjør rewrites-regelen mer presis (kun kjente API-undermønstre som /orgs/:id/tournaments, /orgs/:id/memberships osv., ikke et bredt :path*).

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

Ingenting av dette er fikset ennå — kun diagnostisert og notert på brukerens eksplisitte instruks, for å ikke gå i glemmeboken mens PWA-runden prioriteres. Punkt 1 (datovisning) senere fikset, se over (ADR-030).


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

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.

Ikke rullet ut ennå — se CLAUDE.md-status.

Naturlig neste steg (ikke bygget, notert for senere)

  • Deltaker-tilgang (uten org-medlemskap) til lag-chat og scorekort — det gjenstående hullet nevnt over. Krever en egen, forsiktig gjennomgang av get_authorized_org-bruken (brukt bredt i hele appen) — ikke en rask fiks.
  • Notifikasjons-/aktivitetsfeed på "Mine runder".
  • HCP-historikk over tid (personlig profil sin handicap_index endres i dag uten noen logg).
  • "Mine runder" for RENE påmeldinger (tournament_registration uten roster ennå) — v1 viser kun rostrede lag.

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

UX / frontend (senere fase)

  • 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp (banebruk i sollys/med solbriller).
  • 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).