Bruker rapporterte at en runde delt med kategorien "Make" ikke ble synlig for vedkommendes ektefelle. Diagnostisert direkte mot ekte teecup_db (skrivebeskyttede spørringer): eieren hadde kategorisert ektefellen under tre kategorier, mens runden kun delte to av dem. _can_view_round krevde at ALLE en venns kategorier måtte være i rundens synlige sett -- en presisering fra 2026-07-29 som kun ble dokumentert i kode-kommentarer, aldri i denne ADR-en, og som strammet inn utover ADR-036 Beslutning B sin opprinnelige "minst én kategori"-regel fra 2026-07-25. Reversert til den opprinnelige regelen i alle fire duplikate SQL-steder (_can_view_round, _friends_who_can_see_round, list_friends_on_course i rounds.py; feed-listingen i round_messages.py) -- fra "NOT EXISTS en kategori utenfor synlig sett" til "EXISTS en kategori innenfor synlig sett". Scratch-verifisert i begge retninger: venn med overlappende kategori slipper nå inn, venn med ingen overlappende kategori fortsatt korrekt avvist. Se CHANGELOG.md punkt 44 for full verifiseringsdetalj og bekreftelse mot de ekte, virkelige dataene etter utrulling.
274 KiB
TeeCup — Arkitektur-beslutningslogg (ADR)
Dette dokumentet er den autoritative kilden til hva som er bestemt og hvorfor. Det leses av mennesker og av AI-modeller (Claude, Gemini) i starten av hver økt. Endre aldri en beslutning uten å legge til en ny ADR som erstatter den — historikken skal bevares.
Status: Levende dokument Sist oppdatert: 2026-07-22
Kontekst
TeeCup er en kommersiell (SaaS) webapplikasjon for å opprette, administrere og
gjennomføre golfturneringer i «Ryder Cup»-format. Den driftes på samme VPS som
teeoff.no, men skal fungere som et isolert økosystem med eget subdomene
(teecup.teeoff.no) og egen database (teecup_db).
Målgruppe: klubber, bedrifter og vennegjenger som arrangerer turneringer over tid.
ADR-001 — Tenant = Organisasjon
Beslutning: Isolasjonsenheten («tenant») er en organisasjon, ikke en turnering.
En organisasjon er et bevisst nøytralt begrep som dekker klubb, bedrift og vennegjeng. Ved å ikke kalle den «klubb» i datamodellen unngår vi refaktorering den dagen første bedriftskunde kommer.
Hierarki:
Organisasjon (tenant — det som isoleres og faktureres)
├── Medlemmer/spillere (gjenbrukbare på tvers av turneringer)
├── Egendefinerte baner
└── Turneringer
├── Lag
└── Matcher
└── Scores
Begrunnelse: En turnering er en hendelse med start og slutt, ikke en kunde. Gjenbrukbare ting (spillere, baner, historikk) må leve over turneringens levetid. Turnering er derfor en entitet inne i en organisasjon.
Konsekvens: tenant_id i alle skjemaer heter organization_id.
ADR-002 — Bruker og organisasjonsmedlemskap er adskilt
Beslutning: Identitet (innlogging) og medlemskap i en organisasjon er to
forskjellige ting. Én user kan ha flere organization_memberships.
Begrunnelse: Samme person spiller ofte både klubbturneringen og jobbturneringen. En bruker må kunne krysse organisasjoner med samme innlogging. Dette er lett å bygge inn fra start og smertefullt å legge til senere.
Konsekvens: Utelukker «database-per-tenant» (se ADR-003), fordi en bruker som krysser organisasjoner da måtte eksistere i flere databaser samtidig.
ADR-003 — Isolasjonsstrategi: Shared schema + Row-Level Security
Beslutning: Én database (teecup_db), delte tabeller, isolert på
organization_id-kolonne, håndhevet av PostgreSQL Row-Level Security (RLS).
Vurderte alternativer:
| Strategi | For | Mot | Valgt |
|---|---|---|---|
Shared schema + organization_id + RLS |
Enkel drift, billig, skalerer til mange org. | Krever disiplin; RLS må settes riktig | Ja |
| Database/schema per tenant | Sterk isolasjon | Tung migrering; bryter med ADR-002 | Nei |
Begrunnelse: RLS flytter isolasjonen fra applikasjonskoden (der én glemt
WHERE organization_id = ... lekker data mellom kunder) ned til databasen, som
håndhever den uansett hva koden gjør. Kombinert med ADR-002 er dette det eneste
praktiske valget.
Konsekvens / oppgave: Hver økt/tilkobling må sette SET app.current_org (eller
tilsvarende) slik at RLS-policyen kan filtrere. Dette må inn i tilkoblingslaget
tidlig, ikke ettermonteres.
ADR-004 — Banedata fra teeoff_db via enveis, lesende API-kontrakt
Beslutning: TeeCup henter offisielle banedata fra hovedplattformen gjennom et veldefinert, lesende API — ikke via direkte databasekobling på tvers.
Begrunnelse: En direkte kobling ville låst TeeCup til hovedplattformens
skjemaendringer, og et brudd ett sted ville tatt ned begge produktene.
teeoff_db er «master» for offisielle banedata; alt brukergenerert innhold
(egendefinerte baner, turneringer, brukere) forblir strengt i teecup_db.
Konsekvens: API-kontrakten mot TeeOff må versjoneres og behandles som en ekstern avhengighet, selv om den kjører på samme server.
ADR-005 — Handicap-motoren er et frittstående, testet bibliotek
Beslutning: All handicap- og score-logikk bygges som en ren Python-modul uten avhengigheter til database, API eller web-rammeverk. Den testes grundig i isolasjon.
Begrunnelse: Dette er produktets hjerte og den mest risikofylte biten. Feil her koster mest troverdighet. Ved å isolere den kan reglene enhetstestes mot kjente fasitverdier uavhengig av resten av systemet.
Konsekvens / viktig presisering: Prosentbaserte «allowances» (f.eks. 75 %, 90 %, 3/4) er konfigurasjon, ikke hardkodet logikk. Motoren vet ikke at «fourball = 90 %»; den mottar tildelingen som parameter. De konkrete standardprosentene per format må verifiseres mot gjeldende WHS/lokale regler før produksjon — de skal ikke antas fra hukommelse.
ADR-006 — Teknologistack
Beslutning:
- Backend: Python + FastAPI (gjenbruker stacken fra TeeOff → enklere vedlikehold).
- Database: PostgreSQL (+ PostGIS der banegeometri trengs).
- Frontend: React (Vite) som PWA, offline-first (Service Workers + IndexedDB).
Begrunnelse: Offline-first er kjernefunksjonalitet, ikke luksus — golfbaner har ofte dårlig mobildekning, og score må kunne registreres uten nett og synkes senere.
ADR-007 — Miksede formater via økter, og spiller-pool
Beslutning: En turnering er en ordnet sekvens av økter (sessions), ikke ett enkelt format. Hver økt bærer format, hullomfang og allowance. Ryder Cup = foursome/18 → fourball/18 → singles/18 er tre økter.
Hvert lag har en pool (team_roster) som kan være større enn antall
matchplasser. Deltakelse avgjøres per match via match_participant. En reserve
er en spiller i poolen uten deltaker-rad i en gitt økt; en spiller kan spille
kun enkelte økter (f.eks. bare singelen).
Begrunnelse: Dette er selve Ryder Cup-strukturen. Å modellere format på turneringsnivå ville gjort miksede formater umulig; å modellere deltakelse på turneringsnivå ville gjort reserver og delvis deltakelse umulig.
Konsekvens: Handicap-snapshot fryses per turnering i team_roster
(handicap_index_snapshot) for reproduserbare resultater.
ADR-008 — 9-hulls-slag: "slagene som faller på 18-hulls-kortet"
Beslutning: For 9-hulls-økter (front/back) brukes spillerens fulle 18-hulls-tildeling, og slagene fordeles over hele 18-hulls stroke index — deretter tas kun de spilte hullene ut. Spilleren mottar slag på de spilte hullene der SI ≤ mottatte slag.
Vurdert alternativ: WHS' formelle 9-hulls course handicap (eget 9-hulls rating, grovt sagt halvparten). Kan gi et litt annet totaltall. Ikke valgt som standard fordi ikke alle baner publiserer 9-hulls-ratinger, og den valgte metoden er den vanlige i vennegjeng-/klubbmatch-spill.
Kritisk implementasjonsdetalj: Man må IKKE sende bare de ni spilte hullene inn
i slagfordelingen med et 18-hulls slagtall — det gir feil ved høye slagtall
(12 slag på back-9 blir da 10 i stedet for riktige 6). Motoren har derfor
allocate_over_played_holes(...) som fordeler over alle 18 og så tar ut de spilte.
Dekket av test test_nine_hole_twelve_strokes_is_six_not_ten.
Konsekvens: tee_rating beholder likevel front/back-omfang, slik at WHS'
alternativ kan tilbys senere som konfig per turnering uten skjemaendring.
ADR-009 — Egen innlogging, uavhengig av teeoff
Beslutning: TeeCup har egne brukerkontoer (app_user), uavhengig av teeoffs
innlogging.
Begrunnelse: TeeCup selges kommersielt til klubber/bedrifter som kanskje aldri har hørt om teeoff.no. Delt innlogging ville vært forvirrende for dem og koblet de to systemenes auth tettere sammen enn ellers ønskelig. Styrker isolasjonen fra ADR-004.
Konsekvens: Egen auth (registrering, Google/magic-link e.l.), egne secrets (jf. åpent spørsmål 1), egen sesjonshåndtering. Kaptein- og tilskuer-roller defineres innenfor TeeCups eget auth-lag.
ADR-010 — Feed krever innlogging (ingen anonym lesesti i v1)
Beslutning: Den «offentlige» runde-feeden er felles for begge lag i turneringen, men bak innlogging. Ingen verdenssynlig lenke i v1.
Begrunnelse: «Offentlig» betyr her «alle innloggede deltakere i turneringen», ikke «hvem som helst med en lenke». Dette fjerner den uautentiserte lesestien og den tunge modereringen (som ellers kreves når innhold er synlig for verden) fra v1.
Konsekvens: Synlighet modelleres som kanal-scope (lag / turnering). En ekte verdenssynlig delingslenke kan legges til senere som egen bryter, med moderering, uten å bygge om.
ADR-011 — Turneringsstruktur: lagformat i v1, bracket som egen fremtidig type
Beslutning: v1 bygges for Ryder Cup-lagformatet (økter → uavhengige matcher → summerte poeng), og låses til nøyaktig to lag.
Begrunnelse: En match/flight er alltid to-sidig. Tre lag ville krevd kryss-oppgjør (A–B, A–C, B–C), nok spillere til å møte to motstandere samtidig, balansert oppsett, og tre-veis blind draw — mye kompleksitet for lite gevinst. To lag er både enklere og riktigere for formatet. Et knockout-bracket (f.eks. 128 spillere, utslagsmatcher, finale + bronsefinale) er en helt egen turneringstype: rundene er avhengige (vinner av match A møter vinner av match B), og krever progresjon mellom matcher, seeding, fripass og bronsegren — noe dagens modell ikke har.
Konsekvens: To-lags-grensen håndheves i app-laget (validering ved turneringsoppsett), ikke med DB-trigger. Match-modellen holdes generell (to sider, vilkårlige lag-referanser), så flere lag eller en knockout-type kan komme senere uten dataomskriving — kjernen (spillere, motor, hull-score, RLS) er typeuavhengig. Knockout er fanget som egen type i FEATURE_BACKLOG.md.
Merk (2026-07-19): brukeren har reist ønske om flere turneringsformater UTOVER Ryder Cup-lagformatet (f.eks. «Københavner» — se FEATURE_BACKLOG.md sitt eget punkt for alle fire eksemplene). Minst ett av disse («Københavner»: alle-mot-alle poengfordeling i et felt av spillere, ikke to lag i det hele tatt) passer IKKE inn i denne ADR-ens to-lags-modell — en fremtidig, egen ADR trengs når/hvis dette tas fatt på, samme mønster som knockout-punktet over. Kun notert her ennå, ikke designet eller bygget.
Merk (2026-07-26), presisert og utvidet: brukeren bekreftet at ønsket er STØRRE enn Københavner som ett format blant flere — TeeCup skal etter hvert støtte ekte INDIVIDUELLE turneringer (helt uten lag) som eget generelt tilfelle, disse skal kunne gå over FLERE RUNDER, og det skal være mulig å sette opp et Order of Merit (sesong-sammenlagt på tvers av flere separate arrangementer). Full analyse, inkl. en reell strukturell kollisjon med ADR-033 sitt bevisst org-uavhengige rundesystem, i FEATURE_BACKLOG.md ("Utvidelse 2026-07-26: individuelle turneringer, flerrunde-turneringer, og Order of Merit"). Ren notat-runde, ingen ADR skrevet ennå.
ADR-012 — To scoring-moduser per økt
Beslutning: Hver økt har en scoring_mode: stroke (spillere taster slag per
hull) eller hole_result (registrer bare hvem som vant hullet / delt).
Begrunnelse: Ønsket fra start: kunne føre score enten detaljert (slag) eller raskt (tapp «lag rød vant hullet»). De to modusene har ulik datainngang.
Konsekvens: stroke skriver til hole_score (brutto, motor utleder netto og
hull-resultat). hole_result skriver til ny tabell match_hole_result
(vinnende side per hull, NULL = delt). Begge mater samme
compute_match_state i motoren, så matchstatus beregnes likt uansett modus.
ADR-013 — Blind draw via lås per lag per økt
Beslutning: Lagoppstilling settes skjult; matchene avsløres når BEGGE lag har
låst. Modellert med tabell lineup_lock (én rad per lag per økt).
Begrunnelse: Kjerneønske i Ryder Cup-formatet — kapteinene låser i blinde, og oppgjørene avsløres samtidig.
Konsekvens: match_participant-radene (oppstillingen) opprettes per lag, men
motstanderens side skjules i app-laget til det finnes en lineup_lock for begge
lag. Etter lås kan egen oppstilling ikke endres. Dette er finkornet synlighet
(lag-nivå) og håndheves i app-/spørrelaget, ikke RLS — begge lag ligger i samme
organisasjon (jf. ADR-010).
ADR-014 — Konfigurerbar handicap-pipeline: fire uavhengige brytere
Beslutning: Handicap-anvendelsen i en økt er FIRE uavhengige, valgfrie steg — ikke bare én allowance-prosent:
- Bruk handicap (av/på) — helt av gir scratch-spill.
- Bruk course handicap (av/på) — om slope/rating-justeringen
(
course_handicap_raw) påføres, eller om råhandicap_indexbrukes direkte. - HCP-prosent (allowance-strategien, allerede dekket av ADR-005 /
session.allowance_override). - Bruk matchplay-handicap (av/på) — om resultatet konverteres til relative
slag (
match_play_strokes: beste enhet spiller «av 0», resten får differansen), eller brukes som absolutt Playing Handicap.
Begrunnelse: Skjermbilder fra Golf GameBook (referanseprodukt, delt
2026-07-16) viser nøyaktig denne firedelte bryter-strukturen per runde/format i
deres oppsettsdialog. Deres standard-prosenter (foursome 50 %, better ball/
fourball 90 %, singles 100 %) stemmer eksakt med DEFAULT_MATCHPLAY_ALLOWANCES
i handicap_engine.py — uavhengig bekreftelse på at defaultene er riktige,
slik ADR-005 krever verifisert. Motoren støtter allerede alle fire som atskilte,
komponerbare funksjonskall (course_handicap_raw, en AllowanceStrategy,
match_play_strokes) — hver kan hoppes over uavhengig av de andre — men
API-et/skjemaet eksponerer dem ikke som brytere ennå.
Konsekvens: session.allowance_override (jsonb) skal utvides til å bære
alle fire bryterne, ikke bare prosent, når motor-/scoring-integrasjonen bygges
(se FEATURE_BACKLOG.md). Ingen skjemaendring nødvendig (feltet er allerede
jsonb), men API-lagets validering og motor-kall må håndtere: bruk_handicap = false → hopp over hele kjeden (brutto = netto), bruk_course_handicap = false
→ bruk handicap_index/handicap_index_snapshot direkte uten
slope/rating-justering, bruk_matchplay_handicap = false → bruk absolutt
Playing Handicap i stedet for å kalle match_play_strokes.
ADR-015 — Utledet tee-time og maskinlesbar feilkode-kontrakt
Beslutning A — klokkeslett er utledet, ikke lagret per match: session
får scheduled_at (starttid for FØRSTE match) + tee_interval_minutes
(minutter mellom hver "flight"). En matchs tee_time er
scheduled_at + (sequence-1) * tee_interval_minutes, beregnet i Python ved
lesing (app/routers/matches.py), ALDRI skrevet til match-raden. Unntak:
match.tee_time_override (nullable timestamptz) for enkeltmatcher som må
justeres uavhengig (forsinkelser) — vinner alltid over den utledede verdien
når satt. session.start_hole er eksplisitt lagret (ikke utledet av
hole_config).
Begrunnelse: Brukeren beskrev selv mønsteret rett fra domenet: "man sier at førstematchen starter på hull X klokka Y, og det er Z minutter mellom hver flight" — det er slik tee-tider faktisk fungerer i golf, og å lagre ett tidspunkt per match ville vært duplisert, avledet data som kunne komme ut av synk med intervallet. Et lite unntak (override) dekker det ene tilfellet der den avledede regelen ikke holder.
Konsekvens: Alle tre nye tidsfelter er nullable/additive (migrasjon
006_scheduling_and_locale.sql) — en økt uten planlagt klokkeslett gir
tee_time: null i API-et, ikke en feil. Det finnes ennå intet
PATCH-endepunkt for tee_time_override (satt direkte i databasen ved
verifisering); bygges når frontend faktisk trenger å justere enkeltmatcher.
Beslutning B — én uniform feilrespons på tvers av HELE API-et:
{"detail": {"code": "...", "message": "..."}} på ALLE stedene som kaster
HTTPException (39 steder ved innføring), via en liten factory
app_error(status_code, code, message) i app/errors.py — ingen egen
exception-klasse, ingen global exception handler. Kodene er en liten,
gjenbrukt taksonomi (~15 koder, IKKE én unik kode per kastested) —
f.eks. NOT_FOUND, DUPLICATE, LIMIT_REACHED, NOT_ROSTERED_ON_TEAM,
NOT_AUTHENTICATED. Pydantic sine egne 422-valideringsfeil er bevisst
UTENFOR denne kontrakten og beholder FastAPI sin standard {"detail": [...]}.
Begrunnelse: Hardkodet norsk prosa i detail (slik det var før denne
runden) kan ikke brukes til noe annet enn "vis strengen" av en frontend —
den kan ikke skille en 409 pga. dupliserte rader fra en 409 pga. et
forretningstak, og kan ikke oversettes (se i18n under). En liten, gjenbrukt
kode-taksonomi lar frontend bygge stabil logikk (f.eks. "vis en spesifikk
inline-feil for LIMIT_REACHED, en generisk toast for alt annet") uten å
måtte parse norsk tekst. Skulle vært gjort FØR frontend startet — dyrt å
ettermontere når klientkode allerede har begynt å parse tekststrenger.
Konsekvens: Enhver ny HTTPException i fremtidig API-kode SKAL bruke
app_error(), aldri en rå HTTPException(status_code, detail="...") — se
app/errors.py sin docstring. Nye forretningsbetydninger som ikke passer
noen eksisterende kode får en ny kode i taksonomien, ikke gjenbruk av en
semantisk feil kode.
Beslutning C — locale er klient-oppgitt, ikke server-gjettet:
app_user.preferred_locale og magic_link_token.locale (kun nb/en
foreløpig, håndhevet med CHECK) settes fra en locale-verdi klienten
sender eksplisitt ved POST /auth/request-link — ALDRI gjettet server-side
(f.eks. fra Accept-Language). En NY bruker får preferred_locale satt fra
forespørselens locale ved førstegangsopprettelse; en EKSISTERENDE bruker som
ber om en ny lenke på et annet språk får IKKE sin lagrede preferanse
overskrevet — kun selve e-posten sendes på forespørselens språk. Ingen
endepunkt for å ENDRE preferred_locale på en eksisterende bruker ennå
(egen, senere sak — profilinnstillinger).
Begrunnelse: Frontend vet sitt eget gjeldende visningsspråk (brukerens valg i UI-et) — det er en bedre kilde enn å gjette fra headere eller IP. At en påfølgende innlogging på et annet språk (f.eks. en gjest som låner en enhet) IKKE skal endre den lagrede preferansen er bevisst: en innloggingshandling bør ikke ha den overraskende bivirkningen å endre brukerens varige profilinnstilling.
ADR-016 — Frontend som eneste offentlige overflate, API-et server-side proxyet
Beslutning: teecup.teeoff.no peker (Caddy) på Next.js-frontenden
(teecup_frontend), IKKE direkte på FastAPI-et. Frontenden proxyer selv
kjente API-sti-prefikser (/auth/*, /orgs/*, /health — som til sammen
dekker HELE dagens API-overflate, verifisert med et grep av samtlige
rutedefinisjoner) videre til teecup_api:8000 server-side, via Next.js sin
egen rewrites()-mekanisme (frontend/next.config.mjs). API-et er ikke
lenger separat Caddy-rutet eller offentlig eksponert under eget navn.
Begrunnelse: Alt kjører dermed under samme opprinnelse (origin) sett fra
nettleseren — ingen CORS-konfigurasjon trengs, og HttpOnly-sesjonscookien
(ADR-009) fungerer helt uendret enten kallet "egentlig" går til frontend
eller API. Alternativet (eget subdomene for API-et, SameSite=None-cookie
eller CORS-hull) ville svekket cookie-sikkerhetsmodellen som allerede var
bevisst bygget stram. Samme mønster generaliserer til alle fremtidige
skjermer uten videre Caddy-endringer — nye API-ruter under /auth, /orgs
trenger ingen ny proxy-regel, kun nye Next.js-sider som kaller dem med
relative URL-er.
Reell fallgruve funnet og fikset ved bygging (2026-07-17): Next.js sin
rewrites() løses ved BUILD-tid for output: "standalone" (bakes inn i
server-bunten), ikke ved container-oppstart. En docker run -e TEECUP_API_ORIGIN=... ved kjøretid ble derfor stille ignorert (falt tilbake
til default localhost:8000, som ikke fantes i containeren — proxy-kall
feilet med ECONNREFUSED). Løst med en Docker build-time ARG TEECUP_API_ORIGIN (default http://teecup_api:8000, matcher alltid det
delte nettverkets tjenestenavn) i frontend/Dockerfile, satt via
docker-compose.yml sin build.args. Generell lærdom for fremtidige
Next.js/Docker-oppsett i dette prosjektet: alt som brukes inne i
next.config.mjs er en BUILD-tids verdi, ikke en runtime-verdi, med mindre
det eksplisitt leses på nytt et sted som faktisk kjører per request (en
route handler, ikke selve config-filen).
Konsekvens: Enhver fremtidig ny API-sti-prefiks (utenfor /auth,
/orgs, /health) MÅ legges til i frontend/next.config.mjs sin
rewrites()-liste, ellers blir den utilgjengelig fra nettleseren selv om
API-et selv fungerer (kun nåbar internt på Docker-nettverket). teecup_api
sin port er ikke lenger tenkt nåbar direkte utenfra i prod.
ADR-017 — Selvregistrering og utvidet spillerprofil
Kontekst: Reist av brukeren rett etter at lag/roster-skjermen var live. Dagens modell antar at organisator kjenner og legger inn alle spillere selv — i praksis vet organisator ofte ikke hvem som faktisk blir med før de melder seg på selv.
Beslutning A — Påmelding er offentlig, krever IKKE innlogging. En
delbar lenke (/register/{tournament_id} — turneringens UUID er allerede
uforutsigelig nok, ingen ny token-mekanisme) viser et minimalt skjema. Ingen
magic-link, ingen konto kreves for å melde seg på.
Begrunnelse: Å kreve innlogging FØR man kan melde seg på er unødvendig friksjon for "jeg blir med lørdag"-bruksmønsteret. Kontosammenkobling skjer gratis senere (Beslutning B), ikke som et eget steg i selve påmeldingen.
Beslutning B — E-post er sammenkoblingsnøkkelen mellom en organisator-
forhåndsopprettet player-rad og en spiller som senere melder seg selv på
eller logger inn. Finnes det en player-rad i org-en med samme e-post ved
påmelding, fylles manglende felt inn på DEN raden i stedet for å opprette en
duplikat. player.user_id kobles først når noen med matchende e-post
faktisk logger inn via magic-link — verify_magic_link utvides til også å
slå opp org-scopede player-rader på e-post, ikke bare app_user.
Konsekvens: player.email er bevisst IKKE en UNIQUE-constraint —
familier deler av og til e-post (forelder melder på barn), en hard unik-
regel ville krasje akkurat den vanlige situasjonen. Matching er et mykt,
applikasjonslags-oppslag.
Beslutning C — Påmelding er et eget, lettvekts steg, atskilt fra
team_roster, med konfigurerbar godkjenning, kapasitet og samtykke. Ny
tabell tournament_registration (status: pending/confirmed/
waitlisted/declined/withdrawn). Tre nye felt på tournament:
registration_capacity (nullable — organisators valg om det i det hele
tatt skal være en grense), registration_overflow_policy
(waitlist/closed, kun relevant når kapasitet er satt),
registration_requires_approval (boolean). Rekkefølge ved en ny
påmelding: (1) er fristen passert → avvis; (2) er kapasitet nådd →
waitlisted eller avvis, avhengig av policy; (3) ellers pending eller
confirmed, avhengig av godkjenningsbryteren.
Begrunnelse: team_roster betyr i dag "committed til et bestemt lag".
Å blande "vil kanskje spille" med "spiller garantert" i samme tabell ville
gjort det umulig å skille en påmeldt-men-ikke-plukket spiller fra en som
aldri var interessert. Kapasitet og godkjenning er to reelle, uavhengige
organisator-beslutninger — å låse ett svar for alle turneringer ville vært
feil for minst noen av dem (brukeren bekreftet eksplisitt: begge skal være
konfigurerbare valg, ikke faste regler).
Beslutning D — Utvidet spillerprofil + samtykke. Nye felt på player:
mobile, email, birth_date (IKKE alder — alder blir feil neste år,
fødselsdato er ikke det), nickname, country, club,
club_member_number. Samtykke er obligatorisk ved påmelding — API-et
avviser innsending (400) uten consent: true, og
tournament_registration.consent_given_at er beviset på at det faktisk ble
gitt, ikke bare antatt. Samtykket lever på REGISTRERINGEN, ikke på
player, fordi det er selve påmeldingshandlingen for DENNE turneringen
samtykket knytter seg til.
Ikke et nytt felt: "utslagssted for anledningen" er sannsynligvis
allerede dekket av match_participant.tee_id (per match, ikke per
spillerprofil, siden det kan variere fra runde til runde).
Beslutning E — public_tournament_org(): den eneste broen fra en
uautentisert forespørsel til riktig RLS-kontekst. Et offentlig
påmeldingskall kjenner en turnering-id, men ikke organisasjonen den hører
til — og uten app.current_org satt slipper RLS ingen rader gjennom
(heller ikke selve oppslaget for å FINNE riktig org). Løst med en snever
SECURITY DEFINER-SQL-funksjon (p_tournament_id -> organization_id, kjørt
med skaperens BYPASSRLS-rettigheter, SET search_path = public mot
kapring) — samme "løs kontekst-problemet FØR RLS kan håndheve noe"-mønster
som den selvrefererende org-bootstrapen (app/routers/organizations.py),
nå for et lese-oppslag i stedet for en innsetting.
Konsekvens: App-laget slår opp org-id via denne funksjonen FØRST, åpner
deretter en vanlig org_connection(org_id) og fortsetter med normal
RLS-håndhevelse for alt det faktiske arbeidet. Funksjonen eksponerer KUN en
uuid->uuid-kobling, ingenting annet fra tournament-raden — et bevisst
smalt unntak, ikke en generell RLS-omgåelse. Ethvert fremtidig offentlig
(uautentisert) endepunkt som trenger å slå opp org-kontekst fra en kjent
ressurs-id bør gjenbruke akkurat dette mønsteret, ikke finne opp et nytt.
Migrasjon: 007_registration_and_player_fields.sql. Kontosammenkoblingen
i Beslutning B (e-post → player.user_id ved innlogging) fikk sin egen
SECURITY DEFINER-bro (link_player_by_email(), samme mønster som
Beslutning E) i en oppfølgende migrasjon, 008_link_player_by_email.sql.
ADR-018 — Landingssider: synlighet, org-profil, delbart innhold
Kontekst: Reist av brukeren rett etter at registrerings-API-et (ADR-017) var live — turneringer og organisasjoner bør ha egne, delbare landingssider (hero, tekst, program, sponsorer, påmelding for turnering; klubbprofil + turneringsliste for organisasjon). Brukeren krevde eksplisitt at synlighet må være et VALG: offentlig, kun org-medlemmer, eller kun turnering-deltakere.
Beslutning A — Trenivås synlighet, ett felt per nivå.
tournament.visibility ('public'/'org'/'participants', default
'org') og organization.public_profile (boolean, default false).
Trygg standard: ingen eksisterende eller nyopprettet turnering/org blir
offentlig av seg selv. "Kun deltakere" gir ikke mening på org-nivå (en
organisasjon har ikke "deltakere"), derfor en enklere bryter der, ikke
samme trenivå-enum som turnering.
Beslutning B — RLS beskytter TENANT-grenser, ikke INNHOLDS-synlighet.
org_isolation-policyen håndhever kun at org A aldri ser org B sin data —
den sier ingenting om hvem INNENFOR riktig org-kontekst som får se hva.
Det viste seg at det eksisterende GET /public/tournaments/{id} (ADR-017)
allerede leser fullt turneringsinnhold uten noen synlighetssjekk i det
hele tatt, fordi RLS er fornøyd så snart org-konteksten er riktig satt —
uansett hvem (eller om noen) som spør.
Konsekvens: visibility MÅ håndheves eksplisitt i applikasjonslaget på
hvert offentlig lese-endepunkt (og på registrering, se Beslutning D), ikke
forventes løst av RLS.
Beslutning C — Ny autorisasjonsvei: "deltaker". For
visibility='participants' må leseren enten være org-medlem, ELLER
innlogget med en player-rad (koblet via player.user_id, ADR-017
Beslutning B) som har en tournament_registration- eller
team_roster-rad for NØYAKTIG denne turneringen. Ny
get_current_user_optional-avhengighet i app/auth.py — som
get_current_user, men returnerer None i stedet for å kaste 401, siden
et offentlig endepunkt skal fungere for en anonym leser også (bare med
visibility='public'-tilgang).
Beslutning D — Registrering følger SAMME synlighetsgrense som
landingssiden. Kan du ikke se turneringen, kan du heller ikke melde deg
på den — ingen særbehandling av /register-endepunktet.
Beslutning E — Turnering-landingsside: innhold nå, bilder som inerte
felt. Nye felt: tournament.description (presenterende tekst),
tournament.hero_image_key (inert til MinIO-runden — kun kolonnen, ingen
opplastingslogikk, sparer en fremtidig migrasjon for null kostnad nå). Ny
tabell tournament_sponsor (navn+lenke aktivt, logo_key inert av samme
grunn). Program vises via GET /public/tournaments/{id}/sessions, som
gjenbruker _fetch_sessions() (nytt uttrekk fra tournaments.py sin
list_sessions, delt mellom den innloggede og den offentlige ruten) —
blind draw-hemmelighold (ADR-013) arves automatisk, ikke reimplementert.
Bevisst utenfor omfang denne runden: lag-/roster-visning på
landingssiden (må også respektere blind draw-lås) — egen, senere sak.
Beslutning F — Org-landingsside: slug + tredje SECURITY DEFINER-bro.
organization.slug (unik når satt, CHECK (NOT public_profile OR slug IS NOT NULL)). GET /public/orgs/{slug} løser org via
public_org_by_slug(slug) — samme mønster som public_tournament_org()/
link_player_by_email() (007/008), nå tredje instans. Returnerer NULL
for BÅDE "finnes ikke" og "finnes, men er privat" — samme
anti-enumerering som magic-link (ADR-009), ikke en tilfeldighet at
mønsteret gjentas.
Bevisst utsatt til egen, senere runde: hero-/sponsorbilder (krever MinIO — arkitektur-invarianten finnes fra før, se CLAUDE.md, men er aldri satt opp i praksis).
Migrasjon: 009_landing_pages_and_visibility.sql.
ADR-019 — Offisiell banedata: import (kopi), ikke live oppslag
Kontekst: ADR-004 vedtok prinsippet (lesende API, ikke delt database) helt
i starten av prosjektet, men ble aldri bygget — kun source='custom'-baner
fantes (organisator taster inn banen selv). Brukeren påpekte dette rett etter
program-skjerm-runden (2026-07-18): det er en reell mangel at en organisator
må taste inn en bane manuelt når banen faktisk allerede finnes i teeoff.
Kartlagt før noe ble besluttet (lest /opt/teeoff/backend/main.py, kun
lesing): GET /api/facilities/{slug} er offentlig, uten auth/API-nøkkel, og
returnerer allerede courses[] med holes[] (hole_number, par,
hcp_index) og tees[] (name, cr_men/slope_men, cr_women/
slope_women) — nøyaktig det hole/tee/tee_rating-tabellene i
teecup_db (migrasjon 001) allerede modellerer. GET /api/facilities? view=search gir en lettvekts liste (id, slug, name, city, county,
…) egnet for søk. Par/hcp_index kan være NULL i teeoffs skjema (ufullstendig
baneregistrering) — teecups hole-tabell krever begge NOT NULL.
Beslutning A — Import ved organisators eksplisitte valg, ikke live
oppslag ved hver bruk. Organisator søker blant teeoff sine baner, velger
én, og teecup kopierer da bane+hull+tee+tee_rating INN i teecup_db som en
vanlig course-rad med source='official' og
external_course_ref = '{facility_slug}:{teeoff_course_id}'. Etter import
er raden en helt vanlig lokal course-rad — økt-/match-/handicap-koden
(som allerede kun kjenner course_id som fremmednøkkel) trenger INGEN
endring.
Begrunnelse: Handicap-motoren joiner tee_rating/hole direkte i SQL
midt i en database-transaksjon (app/handicap.py) — et live HTTP-kall til
teeoff derfra ville krevd at hver matchberegning avhenger av teeoffs
oppetid, og ville ikke vært transaksjonssikkert. Import fryser dataene på
importtidspunktet — samme reproduserbarhets-prinsipp som handicap-
snapshotten i ADR-007 (team_roster.handicap_index_snapshot): endrer
teeoff en rating i etterkant, skal ikke en allerede opprettet turnering
plutselig regne annerledes. Dette er også det som gjør ADR-004s opprinnelige
"et brudd ett sted skal ikke ta ned begge produktene"-begrunnelse reell: en
teeoff-nedetid blokkerer kun NYE importer, ikke bruk av allerede importerte
baner.
Beslutning B — Server-til-server, internt Docker-nettverk, ingen ny
hemmelighet. teecup_api kaller http://teeoff_api:8000 direkte (samme
teeoff_default-nettverk begge allerede deler) — ikke via Caddy/det
offentlige domenet. Teeoff sitt API har ingen auth-mekanisme på disse
endepunktene i det hele tatt, så ingen ny credential trengs. CORS-listen på
teeoff-siden (ekskluderer teecups origin) er irrelevant — CORS gjelder kun
nettleser-fetch, ikke et backend-til-backend-kall. Ny avhengighet: httpx
(async HTTP-klient), lagt til i app/requirements.txt.
Beslutning C — Ufullstendige teeoff-data feiler importen tydelig, importen
skjer aldri delvis. Mangler et hull par/hcp_index, eller mangler en
tee både cr_men OG cr_women, avvises HELE importen med en klar feil
(code: EXTERNAL_DATA_INCOMPLETE) FØR noe skrives — ikke en delvis
importert bane med hull som senere feiler i handicap-beregning. Hele
importen kjører i én DB-transaksjon (org_connection sin eksisterende
conn.transaction()), så en feil midtveis ruller automatisk tilbake.
Beslutning D — Kun full_18-rating importeres. Teeoff har ingen egen
front9/back9-rating i sitt skjema (kun én CR/slope per tee, kjønnsdelt).
Samme valg som ADR-008 allerede gjorde bevisst for egendefinerte baner: den
formelle WHS 9-hulls-metoden er ikke i bruk, 18-hulls-tildelingen med uttak
av spilte hull dekker front/back-økter.
Beslutning E — Reimport av samme teeoff-bane er en feil, ikke en
duplikat-rad. Ny partiell unik indeks (organization_id, external_course_ref) WHERE external_course_ref IS NOT NULL (migrasjon
010) — importerer en organisator samme teeoff-bane to ganger, avvises det
med den eksisterende DUPLICATE-koden (409), ikke en ny rad med samme
banedata.
Konsekvens: app/teeoff_client.py (ny, ren HTTP-klient, ingen
db/RLS-avhengighet — samme isolasjonsprinsipp som handicap_engine.py).
app/routers/courses.py utvides med GET .../courses/official-search og
POST .../courses/official-import. Migrasjon 010_official_course_unique_ ref.sql.
Tillegg 2026-08-03 (research, ikke bygget): samme import-ved-eksplisitt-
valg-prinsipp er identifisert som riktig mal for en fremtidig internasjonal
banekilde (golfapi.io, for baner utenfor Norge/teeoff_dbs dekning) — en
tredje course.source-verdi + app/golfapi_client.py ved siden av
app/teeoff_client.py, ikke en ny arkitektur. Ingen ADR-runde startet ennå,
kun notert. Full detalj i FEATURE_BACKLOG.md, «Internasjonale baner».
ADR-020 — Invitasjonskode (oppdagelse), matchledelse og projisert stilling
Kontekst: Reist av brukeren 2026-07-19, rett etter at "bygg i rekkefølgen ting brukes"-serien var ferdig. Tre relaterte, men separate mangler:
- Den eneste veien inn til en turnering er en direkte lenke (
/t/[id]) eller org-ens klubbside (kunpublic-synlige turneringer). En spiller som bare har fått muntlig beskjed ("du spiller lørdag") har i dag ingen vei inn i det hele tatt — dette var reelt ikke gjennomtenkt tidligere. - Leaderboardet viser kun FAKTISK opptjente poeng. Ingen visning av hva stillingen ville blitt om pågående, ikke-avgjorte matcher holder seg som de står nå (vanlig i profesjonell golf-TV-dekning av Ryder Cup).
- Ingen visuell indikasjon i matchlister på hvem som leder en pågående match — brukeren viste et skjermbilde av referanseproduktets fargekodede matchrader (rød/beige etter ledende side) som ønsket retning.
Beslutning A — Kort, menneske-skrivbar invitasjonskode per turnering, som
OVERSTYRER tournament.visibility. Ny tournament.join_code (6 tegn, fra
et alfabet uten forvekslingsbare tegn — ABCDEFGHJKLMNPQRSTUVWXYZ23456789,
altså uten 0/O/1/I), generert automatisk ved opprettelse, globalt unik
(kodeoppslag skjer FØR org-kontekst er kjent, samme problem som
public_tournament_org() løste for ADR-017). Login-skjermet får et eget
kode-felt (fungerer FØR innlogging) som løser koden til riktig turnering og
sender brukeren til /t/{id}?code=....
Bevisst valgt fremfor å la koden respektere synlighet: en kode gitt
muntlig eller på en lapp ER selve invitasjonen — å likevel kreve org-
medlemskap eller deltakerstatus for en org/participants-synlig turnering
ville gjort koden verdiløs for akkurat den situasjonen den er ment å løse.
Koden er ikke hemmelig i sikkerhetsforstand (den er MENT å deles), men
6 tegn fra et 33-tegns alfabet (≈1,3 milliarder kombinasjoner) gjør blind
gjetting upraktisk uten separat rate-limiting — akseptabelt for et
tillitsbasert klubb-/vennegjeng-verktøy (samme trusselmodell-resonnement som
team_authz.py, se «Brukerroller» i FEATURE_BACKLOG.md).
Konsekvens: GET /public/tournaments/{id} og
POST /public/tournaments/{id}/register godtar en valgfri code-parameter;
matcher den turneringens join_code (case-insensitivt), hoppes den vanlige
_check_visibility()-sjekken helt over. Ny SECURITY DEFINER-bro
public_tournament_by_code(code) RETURNS uuid (tournament_id) — fjerde
instans av samme mønster som public_tournament_org() (007),
link_player_by_email() (008), public_org_by_slug() (009). Ingen
kode-regenerering bygget denne runden (organisator kan i dag ikke bytte ut
en lekket kode) — egen, senere sak i FEATURE_BACKLOG.md om det blir
etterspurt.
Beslutning B — Projisert stilling: pågående matchers nåværende leder får
full poengsum, uavgjort/ikke-startet splittes likt. For hver IKKE avgjort
match brukes match.leading_side (se Beslutning C) til å tildele hele
øktens points_per_match til den ledende siden i den PROJISERTE summen —
"AS" (all square) eller en match som ennå ikke har noen registrerte hull
splittes 0,5/0,5, samme regel som en faktisk halvert match. Avgjorte
matcher bidrar likt til både faktisk og projisert sum (de er jo allerede
det de blir). Speiler hvordan TV-dekning av Ryder Cup vanligvis viser
"hvis det sluttet nå"-tavler.
Beslutning C — Ny cachet kolonne match.leading_side, samme mønster som
status_text/points_side_a/b. recompute_and_cache_match_state()
(app/routers/scoring.py) beregner den allerede tilgjengelige
MatchState.lead-verdien (fortegn = ledende side) ved HVER hull-innsending
uansett om matchen er avgjort ennå — lagres nå også i en egen kolonne i
stedet for kun å ligge innbakt i den menneskelesbare status_text-strengen
("2 UP (A)"), slik at frontend kan style etter et strukturert felt
("a" | "b" | null) i stedet for å parse norsk/engelsk tekst. Brukes til
BÅDE projisert stilling (Beslutning B) og fargekoding av matchlister
(Beslutning D).
Beslutning D — Fargekoding er ren frontend-presentasjon, ingen ny
backend-modell. Matchlister (blind draw sin avslørte visning, ev. flere
steder senere) farger den ledende sidens kant/bakgrunn med lagets EKSISTERENDE
team.color når leading_side er satt og matchen ikke er avgjort — samme
fargekilde som resten av appen (leaderboard, roster) allerede bruker, ingen
ny fargemodell innført.
Migrasjon: 011_join_code_and_leading_side.sql.
ADR-021 — Passord (valgfritt tillegg) + valgfri 2FA (TOTP eller e-post)
Kontekst: Reist av brukeren 2026-07-19, sammen med et konkret sesjons-
problem: dagens magic-link-cookie (ADR-009, 30 dager) fungerte visstnok ikke
som tiltenkt — brukeren måtte be om ny innloggingskode ved HVERT besøk til
teecup.teeoff.no. Diagnostisert og FIKSET samme dag (ikke en del av
ADR-021s videre omfang, men verdt å nevne her siden det var starten på denne
tråden): kodegjennomgangen av app/auth.py/app/routers/auth.py fant ingen
feil i selve cookie-settingen, og brukerens ekte, ferske cookie (hentet fra
nettleseren på forespørsel) bekreftet 30 dagers levetid, Secure/HttpOnly/
SameSite=Lax alt korrekt. Rot-årsaken var derfor IKKE en cookie-/backend-
bug, men en manglende sjekk i frontend: app/page.tsx (rot-siden) viste
ALLTID innloggingsskjemaet uten noensinne å sjekke om en gyldig sesjon
allerede fantes — Dashboard sjekket /auth/me og sendte til / ved
MANGLENDE sesjon, men ingenting gjorde det motsatte. Fikset med en
server-side sesjonssjekk (leser cookien via next/headers, kaller
/auth/me direkte mot TEECUP_API_ORIGIN server-til-server, samme mønster
som generateMetadata i app/t/[id]/page.tsx) som sender en allerede
innlogget bruker rett til /dashboard. Verifisert med brukerens ekte cookie:
uten cookie → 200 (skjema), med gyldig cookie → 307 til /dashboard. Rullet
ut live, kun teecup_frontend, ingen migrasjon, teeoff.no upåvirket.
Utover selve bugen ønsket brukeren eksplisitt: e-post/brukernavn+passord (må håndtere spesialtegn og mellomrom korrekt) SOM ET TILLEGG til (ikke erstatning for) den passordløse innloggingen, samt topartsautentisering.
Beslutning A — Passord er et valgfritt, sidestilt alternativ, ikke påkrevd. Login-skjermet får en tredje modus (ved siden av e-post-magic-link og invitasjonskode, ADR-020) for e-post+passord. En bruker setter selv et passord når hen ønsker det (egen "sett passord"-handling, krever en allerede gyldig sesjon — samme "du må bevise identitet FØRST" som andre sensitive endringer) — INGEN eksisterende eller ny bruker tvinges til å sette passord. Magic-link fortsetter å fungere uendret for alle, uavhengig av om et passord i tillegg er satt.
Beslutning B — Passord-hashing: Argon2id, ikke bcrypt. Bcrypt trunkerer
stille ved 72 BYTES (et kjent fallgruve-mønster — to ulike passord som deler
de første 72 bytene hasher likt) og har historiske NUL-byte-kvirker i enkelte
implementasjoner. Siden brukeren eksplisitt ber om korrekt håndtering av
spesialtegn/mellomrom (dvs. lengre, mer varierte passord/passfraser er
forventet brukt), velges Argon2id (argon2-cffi) — minnehardt, OWASPs
anbefalte standard i dag, ingen lengde-fallgruve. Lagres i ny
app_user.password_hash (nullable — NULL betyr "ikke satt", faller da
tilbake til kun magic-link).
Beslutning C — 2FA: brukeren velger metode selv, TOTP ELLER e-post-
engangskode. app_user.two_factor_method (nullable, 'totp'/'email').
- TOTP:
app_user.totp_secret(bare satt når metoden er'totp'),pyotpfor generering/verifisering (RFC 6238-standard, virker med Google Authenticator/Authy/1Password etc. uten videre). QR-kode for oppsett generert server-side (qrcode-biblioteket + eksisterende Pillow- avhengighet, samme mønster som AVIF-konverteringen) — ingen ny ekstern tjeneste, ingen løpende kostnad. - E-post-engangskode: gjenbruker eksisterende SMTP-oppsett (
app/email.py, ADR-009) — ny, kort 6-sifret kode, samme hash-og-utløp-mønster sommagic_link_token(ny tabelltwo_factor_code, 5 minutters gyldighet). Svakere som eneste faktor (e-post-kontoen blir da reelt sett den ENESTE hemmeligheten), men krever ingen ny infrastruktur og er brukerens eget, informerte valg mellom de to metodene. - Bevisst UTENFOR omfang: SMS-2FA — krever en betalt tredjeparts
SMS-leverandør (f.eks. Twilio), løpende kostnad per melding, ny ekstern
avhengighet. Ikke bygget denne runden; kan legges til som en tredje metode
senere uten å røre TOTP/e-post-sporene, siden
two_factor_methodallerede er et åpent tekstfelt, ikke en hardkodet to-verdi-enum i skjemaet.
Beslutning D — 2FA er PÅKREVD for org-eier/admin, valgfritt ellers.
Ved innlogging (uansett om via magic-link eller passord): har brukeren
organization_membership.role IN ('owner','admin') i MINST én organisasjon
OG two_factor_method IS NULL, gis IKKE en full sesjon — brukeren tvinges
inn i et "sett opp 2FA nå"-steg først. En vanlig member-rolle (eller en
bruker uten noe org-medlemskap ennå) kan fortsette å bruke appen helt uten
2FA om hen ønsker det. Begrunnelse: organisator-roller har skrivetilgang
til andre spilleres personopplysninger (ADR-017) og kontroll over hele
turneringer — et kompromittert organisator-passord/magic-link er en langt
alvorligere hendelse enn en kompromittert spillerkonto. Håndheves ved HVER
innlogging (ikke bare første gang), siden en bruker kan BLI eier av en ny
org (POST /orgs, ADR ingen restriksjon — se avklaringen under) etter at
kontoen allerede eksisterer uten 2FA.
Beslutning E — To-stegs innlogging via en stage-claim i sesjons-JWT-en,
ikke en egen tabell for "pågående innlogging". Når 2FA kreves (enten fordi
brukeren selv har slått det på, eller fordi Beslutning D tvinger det),
utsteder primær-autentisering (magic-link-verifisering ELLER passord-innlogging)
et KORTLEVD (5 min) JWT med stage: "pending_2fa" i stedet for en full
30-dagers sesjon. En ny avhengighet (get_pending_2fa_user, speiler
get_current_user) godtar KUN denne mellomtilstanden, og eksponeres bare på
2FA-verifiserings-/oppsett-endepunktene — get_current_user (brukt av ALLE
andre endepunkter) avviser eksplisitt en pending_2fa-claim som ugyldig,
slik at en ufullstendig innlogging ALDRI gir reell tilgang til noe. Først når
riktig TOTP-/e-post-kode verifiseres, byttes denne inn mot den vanlige fulle
sesjonscookien (samme 30-dagers levetid som i dag).
Avklaring underveis, ikke en ny beslutning: brukeren spurte samtidig om
«kan en bruker eie flere organisasjoner» er tenkt gjennom. Bekreftet: JA,
dette var alltid en del av modellen (ADR-002 — bruker og org-medlemskap er
bevisst atskilt nettopp for at én bruker skal kunne krysse flere
organisasjoner). POST /orgs (app/routers/organizations.py) har INGEN
begrensning på hvor mange organisasjoner én bruker kan opprette/eie —
hver ny org gir automatisk role='owner' for oppretteren, uavhengig av
eksisterende medlemskap. Allerede testet i praksis: dashbordets org-bytter,
kryss-org-isolasjonstestene, og /auth/me sin N+1-oppslagsstrategi
(«N = antall organisasjoner brukeren tilhører») forutsetter alle nettopp
dette. Ingen kodeendring nødvendig — kun bekreftelse.
Status: ✅ BYGGET OG LIVE 2026-07-19. Migrasjon 012_password_2fa_and_ org_invitations.sql kjørt mot ekte teecup_db. Se CHANGELOG.md for
full byggerunde (backend/frontend-detaljer, tre reelle bugs funnet og
fikset under scratch-testing).
ADR-022 — Organisasjonseierskap: dele, invitere, frasi seg, superadmin
Kontekst: Reist av brukeren 2026-07-19, rett etter ADR-021. Et reelt,
mer FUNDAMENTALT hull ble synlig under gjennomgangen: organization_ membership har i dag INGEN vei til å legge til et nytt medlem i det hele
tatt etter at organisasjonen er opprettet — den ENESTE raden som noensinne
settes inn er grunnleggerens egen owner-rad (POST /orgs,
app/routers/organizations.py). Det finnes ingen invitasjon, ingen
rollestyring, ingen måte å fjerne noen på. «Del eierskap»-ønsket er derfor
bare den mest synlige kanten av et bredere manglende felt: hele
medlemskaps-livssyklusen etter opprettelse.
Beslutning A — Flere eiere er allerede støttet av SKJEMAET, kun API-et
mangler. organization_membership.role har ingen unikhetsbegrensning som
hindrer flere owner-rader for samme organisasjon — dette var aldri en
sperre, bare et ubrukt hull. Ingen skjemaendring nødvendig for selve
flereeiere-støtten, kun nye endepunkter.
Beslutning B — E-post-basert invitasjon, samme mønster som spiller-
sammenkobling (ADR-017 Beslutning B), ikke et helt nytt konsept. Ny
POST /orgs/{id}/invitations {email, role} oppretter en
organization_invitation-rad (e-post, rolle, token-hash, utløper, hvem som
inviterte) og sender en e-post via eksisterende SMTP-infrastruktur
(app/email.py). Den inviterte trenger IKKE ha en konto fra før — lenken
tar dem til innlogging (magic-link, evt. passord etter ADR-021), og
verify_magic_link utvides til å sjekke ventende invitasjoner på e-posten
sin (samme "kjør på hver innlogging, idempotent" mønster som
link_player_by_email) og sette inn organization_membership-raden da.
Rolle-grense ved invitasjon: en owner kan invitere til ENHVER rolle
(owner/admin/member); en admin kan KUN invitere til member — å la en
admin invitere en ny eier ville vært en reell privilegie-eskaleringsvei
(en admin gir seg selv/en alliert eierskap). Dette er en bevisst, ikke
åpen, avgrensning — minste-privilegium-prinsippet, samme resonnement som
team_authz.py sin eksisterende owner/admin-splitt.
Beslutning C — Rollestyring og fjerning, med et «siste eier»-vern.
Ny PATCH /orgs/{id}/memberships/{membership_id} {role} (kun owner,
UNNTATT at en bruker alltid kan senke SIN EGEN rolle selv — «frasi seg
eierskapet» er en selvbetjent handling, ikke noe som må be en annen eier om
lov) og DELETE /orgs/{id}/memberships/{membership_id} (kun owner, eller
selv for å forlate organisasjonen). Begge avviser handlingen med en klar
409 (LAST_OWNER) hvis den ville latt organisasjonen stå igjen med NULL
eiere — samme «TOCTOU-trygg med FOR UPDATE»-mønster som ADR-011s
to-lags-grense. En enslig eier må altså enten forfremme noen andre til
eier FØRST, eller be en superadmin om hjelp (Beslutning D) hvis
organisasjonen skal forlates helt.
Beslutning D — Superadmin er et manuelt tildelt, ikke selvbetjent, flagg.
Ny app_user.is_super_admin (boolean, default false). INGEN API-endepunkt
lar noen sette dette flagget på seg selv ELLER andre — det settes kun
direkte i databasen av en driftsansvarlig (samme tillitsnivå som å kjøre en
migrasjon), bevisst utenfor appens eget autorisasjonssystem. Årsak: et
selvbetjent «bli superadmin»-endepunkt ville vært selve
sikkerhetshullet det er ment å ikke være. Med flagget satt får brukeren en
NY autorisasjonssti (get_current_user_or_superadmin, parallell til
get_authorized_org — sjekker is_super_admin FØR det vanlige
org-medlemskaps-kravet) som lar dem kalle de samme rolle-/medlemskaps-
endepunktene på ENHVER organisasjon, ikke bare de de selv er medlem av.
Bevisst avgrenset: superadmin-stien dekker KUN medlemskap/rolle-
styring i denne runden (nøyaktig det brukeren spurte om — «sette hvem som
helst som eiere av hvilken som helst organisasjon»), ikke generell
skriveadgang til turnering-/spiller-data i andres organisasjoner. En
bredere «support/drift kan se alt»-rolle er en egen, senere beslutning om
den blir etterspurt.
Beslutning E — Fjernet/frasigende eiers roster-/spillerdata forblir
URØRT. Bekreftet av bruker 2026-07-19. Kun organization_membership-raden
endres/fjernes ved rollestyring eller fjerning — team_roster/player-rader
(deltakelse-historikk) røres aldri av disse handlingene. Organisasjons-
STYRING og DELTAKELSE er allerede modellert som separate ting, og forblir
det.
Status: ✅ BYGGET OG LIVE 2026-07-19, samme runde som ADR-021 (samme
migrasjon 012). Se CHANGELOG.md for full byggerunde.
ADR-023: Brukerroller — kaptein som reell autorisasjon, deltaker-avgrenset scoring
Reist 2026-07-19, direkte oppfølging av det lenge åpne «Brukerroller»-punktet i FEATURE_BACKLOG.md (der siden prosjektets start beskrevet som turnerings- admin/lagkaptein/spiller/tilskuer, men aldri fullt ut avgjort). Fire delspørsmål, alle avklart eksplisitt med bruker før bygging.
Beslutning A — Kaptein gir eksklusive rettigheter til å sette opp/låse
laget, ikke lenger «hvem som helst rostret». app/team_authz.py sin
user_is_team_captain (erstatter user_may_act_for_team) krever
team_roster.is_captain = true for brukeren (eller org-eier/admin, uendret
fra 2026-07-18-runden) — brukt av matches.py sin add_participant/
remove_participant/lock_lineup. Ny feilkode NOT_TEAM_CAPTAIN (403,
erstatter NOT_ROSTERED_ON_TEAM for disse tre stedene).
Bevisst unntak, funnet ved å faktisk sjekke ekte produksjonsdata FØR
utrulling, ikke antatt: har et lag INGEN utpekt kaptein i det hele tatt,
godtas enhver rostret spiller i stedet — uten dette ville en kaptein-only-
regel umiddelbart LÅST ute et helt lag fra å sette opp seg selv. Sjekket mot
ekte teecup_db: laget «De Unge» i «De Gamle er Eldst» har i dag 0 av 2
roster-rader merket kaptein — dette er altså ikke et hypotetisk
kantscenario, det ville rammet en reell, allerede opprettet turnering med
en gang. Har laget FØRST fått en kaptein, gjelder utelukkende den.
Beslutning B — «Kun én kaptein per lag» håndheves nå. PATCH .../ roster/{id} og POST .../roster (app/routers/tournaments.py) fjerner
automatisk kapteinmerket fra enhver annen roster-rad på SAMME lag når en ny
kaptein settes, i samme transaksjon. Nødvendig konsekvens av Beslutning A —
uten dette ville det vært uklart hvem som faktisk har myndighet hvis flere
er merket. Sjekket mot ekte data: ingen eksisterende lag hadde flere
kapteiner (kun opprydningsbehovet «0 kapteiner» fantes reelt), så ingen
data-migrering var nødvendig.
Beslutning C — Score-føring/-korrigering begrenses til matchens faktiske
deltakere. app/team_authz.py sin nye user_is_match_participant krever
en match_participant-rad for brukeren i AKKURAT den matchen (valgfritt
begrenset til én side via team_side — brukt for stroke-modus sin
side-spesifikke innsending, None/hvilken som helst side for hole_result-
modus siden begge sider kan rapportere). Erstatter den brede
«rostret på laget»-sjekken i app/routers/scoring.py sin
submit_hole_score/submit_hole_result. UAVHENGIG av kapteinmerket — å
være kaptein gir ikke i seg selv rett til å føre score for en match man
selv ikke spiller; org-eier/admin har samme unntak som før. Ny feilkode
NOT_MATCH_PARTICIPANT (403, erstatter NOT_ROSTERED_ON_TEAM her).
Beslutning D — «Tilskuer»-rollen utsettes bevisst. Ingen kode denne
runden. Offentlig/deltaker-lesetilgang finnes allerede
(tournament.visibility + get_current_user_optional, ADR-018) — et
formelt tilskuer-begrep bør defineres sammen med det ennå ubesluttede
synlighetsspørsmålet for «Banter Board»-feeden (FEATURE_BACKLOG), ikke
isolert her, for å unngå å bygge to overlappende synlighetsmodeller.
Status: ✅ BYGGET OG LIVE 2026-07-19. Ingen migrasjon (ren autorisasjonslogikk, ingen skjemaendring). Se CHANGELOG.md for scratch-verifisering og utrulling.
ADR-024: Walkover/konsesjon
Reist 2026-07-19, direkte oppfølging av det tidligere åpne punktet i FEATURE_BACKLOG.md: en side som aldri stiller nok spillere får ALDRI beregnet handicap, og matchen kan derfor ALDRI avgjøres — den henger uendelig. Ventet tidligere på Brukerroller (ADR-023), som nå er avgjort. Fire delspørsmål, alle avklart eksplisitt med bruker før bygging.
Beslutning A — Kun den TAPENDE siden (eller org-eier/admin) kan erklære,
til fordel for motstanderen. Speiler ekte golf-etikette: du gir bort DITT
EGET tap, du krever ikke seier på motstanderens vegne. Bruker
app/team_authz.py sin eksisterende user_is_team_captain (ADR-023) på det
KONSEDERENDE laget spesifikt — ingen ny autorisasjonsfunksjon nødvendig. Rene
no-show-tilfeller (den tapende siden har ingen innlogget/rostret spiller i
det hele tatt) dekkes av org-admin-fallbacken som allerede finnes i
user_is_team_captain.
Beslutning B — Ensidig erklæring, ingen bekreftelse fra motparten. Samme tillitsnivå som all annen scoring i appen (allerede en upsert uten godkjenning). Ingen ny "venter på bekreftelse"-tilstand eller varslingsmekanisme bygget.
Beslutning C — Både match- og turnering-nivå, i samme runde.
POST /orgs/{id}/matches/{id}/concede (app/routers/scoring.py) for én
match. POST /orgs/{id}/tournaments/{id}/concede
(app/routers/tournaments.py) for å gi opp ALLE ikke-avgjorte matcher laget
har i turneringen, i én operasjon — v1 er låst til nøyaktig to lag
(ADR-011), så det finnes bare ÉN motstander uansett hvor mange
sesjoner/matcher turneringen har, og turnering-nivå-konsesjon er derfor
bare match-nivå-logikken (apply_concession) kjørt per ikke-avgjorte match.
Hull-nivå konsesjon er bevisst IKKE en egen mekanisme — hole_result-modus
dekker det allerede (rapporter bare hvem som vant hullet).
Beslutning D — Kan erklæres uansett hvor mange hull som allerede er
registrert. I match-play teller kun seier/tap/delt for poeng, ikke
marginen — det er derfor ingen reell forskjell, poengmessig, på "ga opp
etter 5 hull" og "ga opp før start". apply_concession overstyrer status/
poeng direkte, uavhengig av _compute_hole_results; allerede registrerte
hull forblir urørt i hole_score/match_hole_result (kun matchens
avgjørelses-felt endres), så scorekortet fortsatt viser nøyaktig hva som ble
spilt før konsesjonen.
Gjenbruk, ikke duplisering: apply_concession skriver til nøyaktig de
samme fire kolonnene (status_text/points_side_a/b/leading_side) som
recompute_and_cache_match_state — samme "matchen er avgjort"-signal
(points_side_a IS NOT NULL) som allerede stopper videre hull-innsending
(submit_hole_score/submit_hole_result), ingen ny låsemekanisme
nødvendig. Status-teksten "Walkover (A)"/"Walkover (B)" følger samme
"(bokstav)"-visningskonvensjon som compute_match_state.describe() sine
egne strenger (f.eks. "9&7 (A)").
Status: ✅ BYGGET OG LIVE 2026-07-19. Ingen migrasjon (ren applogikk, ingen skjemaendring). Se CHANGELOG.md for scratch-verifisering og utrulling.
ADR-025: Kommunikasjon — lag-chat + offentlig runde-feed
Reist 2026-07-19, rett etter turnering-status-runden. Dekker to ganske ulike ting under samme paraply: lag-intern chat («det hemmelige rommet») og en offentlig runde-feed («Banter Board»). Fire beslutninger avklart eksplisitt med bruker (AskUserQuestion) før bygging.
Beslutning A — Begge bygges i samme runde, ikke lag-chat først som egen runde. Deler mye infrastruktur (meldingsmodell, sanntid-levering), så designes/bygges sammen selv om feeden isolert sett hadde flere åpne spørsmål.
Beslutning B — Sanntid via WebSockets, ikke polling. Løser samtidig det
tidligere åpne "sanntid vs. polling"-spørsmålet i FEATURE_BACKLOG.md
generelt (samme mekanisme kan gjenbrukes for leaderboard/andre skjermer
senere, selv om denne runden kun kobler den til meldinger).
Viktig driftsbegrensning, videreført fra et allerede kjent åpent
arkitekturspørsmål (se «Åpne spørsmål» punkt 2 i dette dokumentet):
tilkoblingsregisteret er en in-memory Python-struktur i teecup_api-
prosessen. Med kun én container/prosess (dagens oppsett) er dette trygt;
skaleres API-et til flere prosesser/containere senere, må broadcast flyttes
til noe delt (Redis pub/sub e.l.) — samme klasse begrensning som den
allerede aksepterte in-memory-cachen.
Beslutning C — Lag-chat er EKTE privat: kun rostrede spillere på laget,
INGEN unntak for org-eier/admin. Et bevisst avvik fra appens ellers
gjennomgående mønster (kaptein-/deltaker-sjekkene i team_authz.py har
alltid en org-admin-fallback, se ADR-023). Ny, egen autorisasjonsfunksjon
user_is_rostered_on_team (uten fallback) brukt KUN her — de eksisterende
funksjonene med org-admin-unntak røres ikke, siden de fortsatt er riktige
for sine egne bruksområder (oppsett/scoring, der en organisator uten
dette ville stått fast tidlig i en turnering).
Beslutning D — Bilder med fra start, ikke utsatt. Gjenbruker
app/storage.py sin allerede byggede og bevist MinIO+AVIF-konverterings-
pipeline (samme mønster som turnering-hero-bilder/sponsorlogoer, ADR-018) —
ingen ny opplastingsinfrastruktur trengs, kun en ny prefix="messages".
Datamodell: delt message-tabell (migrasjon 013_messaging.sql) med
en scope-diskriminator (team/tournament_feed) i stedet for to separate
tabeller — meldingsformen (tekst + valgfritt bilde) er identisk, kun
synlighet/autorisasjon skiller dem. author_display_name FRYSES ved
skrivetidspunkt (samme prinsipp som handicap_index_snapshot, ADR-007) —
utledet server-side fra avsenderens player-rad i org-en (hvis den finnes),
ellers e-postens lokaldel som fallback (en org-ansatt uten egen spillerprofil
kan fortsatt poste i den offentlige feeden).
Offentlig feed — synlighet og posterett, to atskilte spørsmål:
LESING gjenbruker registration.py sitt eksisterende trenivå-mønster
(tournament.visibility + get_current_user_optional + deltaker-sjekk,
ADR-018) uendret — ingen ny synlighetsmekanisme. POSTING er derimot
STRENGERE enn lesing: en anonym leser på en public-synlig turnering kan
lese feeden, men må logge inn OG være enten org-medlem eller faktisk
deltaker/registrert i NØYAKTIG denne turneringen for å få poste — hindrer at
en helt urelatert innlogget bruker (konto et helt annet sted i systemet) kan
poste på en fremmed offentlig turnering-side bare fordi den er synlig.
Moderering: forfatteren selv, ELLER org-eier/admin, kan slette et
feed-innlegg. Lag-chat har INGEN moderering utover forfatteren selv (rommet
er privat, org-admin har uansett ikke lesetilgang og kan derfor ikke
moderere det).
Status: ✅ BYGGET OG LIVE 2026-07-19. Se CHANGELOG.md for scratch-
verifisering og utrulling, inkl. en egen Caddy-rute (/ws/*) for
WebSocket-trafikk direkte til teecup_api (Next.js sin rewrites()
proxyer ikke WebSocket-oppgraderinger pålitelig — samme klasse
infrastrukturvalg som media-ruten i MinIO-runden, ADR-018).
ADR-026: Tilskuer-rolle — offentlig leaderboard, matcher, scorekort
Reist 2026-07-19, rett etter Kommunikasjon (ADR-025), som gjorde det mulig å definere "tilskuer" skikkelig (samme begrunnelse ble notert allerede i FEATURE_BACKLOG.md fra starten av: bør avgjøres sammen med feed-synligheten, som nå finnes).
Kjernebeslutning: ingen ny rolle, ingen ny mekanisme. "Tilskuer" er
IKKE en egen kontotype eller databasetabell — det er ganske enkelt: enhver
som kan SE en turnering (per tournament.visibility, ADR-018) kan nå også
følge den LIVE, ikke bare lese info-siden og programtidene. Samme
trenivå-visibility + get_current_user_optional-mønster som all annen
offentlig lesing, gjenbrukt helt uendret.
Hva var det egentlige hullet: GET /orgs/.../leaderboard,
GET /orgs/.../sessions/{id}/matches og GET /orgs/.../matches/{id}/ scorecard fantes allerede (bygget for organisatorer/spillere), men var
KUN tilgjengelige med org-medlemskap — en spectator med kun /t/[id]-
lenken (eller anonym på en public-synlig turnering) kunne aldri se dem.
Løst ved å ekstrahere den delte kjernelogikken til gjenbrukbare funksjoner
(fetch_leaderboard i tournaments.py, fetch_matches i matches.py,
fetch_scorecard i scoring.py — samme "gjort delt for gjenbruk"-mønster
som recompute_and_cache_match_state/apply_concession tidligere), og la
tre nye offentlige endepunkter i registration.py kalle dem, etter egen
visibility-sjekk.
Omfang, valgt av bruker utover anbefalingen: BÅDE leaderboard+ matchliste OG fullt hull-for-hull-scorekort per match, i samme runde (ikke kun leaderboard+matchliste som opprinnelig anbefalt).
own_team_ids() (blind_draw.py) gjort null-sikker: tar nå
user_id: str | None — en anonym/ikke-tilknyttet leser har per definisjon
ingen egne lag, og skal derfor (korrekt, ikke en feil) kun se AVSLØRTE
matcher, akkurat som en tilfeldig org-medlem uten roster. Ingen ny
synlighetslogikk, bare eksisterende logikk gjort tilgjengelig for en
None-bruker.
To NYE sikkerhetssjekker lagt til, funnet under design, ikke i etterkant:
session_id/match_idi URL-en må eksplisitt verifiseres å høre til NØYAKTIGtournament_idi samme URL —org_connection()setter kun TENANT-grensen (RLS), ikke at stiens id-er faktisk henger sammen. Uten denne sjekken kunne noen med tilgang til én offentlig turnering i en organisasjon lest en HVILKEN SOM HELST økt/match i SAMME organisasjon (inkl. en helt privat en) ved å gjette/prøve seg frem på id-er — nøyaktig samme klasse hull som ADR-018 Beslutning B advarte om generelt ("RLS beskytter kun tenant-grenser, ikke innholds-synlighet").- Det offentlige scorekort-endepunktet krever eksplisitt at BEGGE lag har
låst oppstillingen for økten (blind draw, ADR-013) — leaderboard/
matchliste arver reveal-skjuling automatisk via
own_team_ids(), men scorekortet har ingen tilsvarende innebygd sjekk (kan i prinsippet inneholde registrerte hull før reveal, selv om det ikke er normal flyt).
Status: ✅ BYGGET OG LIVE 2026-07-19. Ingen migrasjon (kun nye endepunkter + refaktorering av eksisterende spørringer til delte funksjoner). Se CHANGELOG.md for scratch-verifisering og utrulling.
ADR-027: Sanntid for "Følg live"-siden
Reist 2026-07-19, rett etter ADR-026 (tilskuer-rolle) — brukeren valgte min
egen anbefaling: /t/[id]/live krevde omlasting for å se nye resultater,
litt selvmotsigende for en side som heter "Følg live". Løser samtidig det
tidligere åpne "koble leaderboard til sanntid"-punktet i FEATURE_BACKLOG.md.
Gjenbruker WebSocket-mekanismen fra ADR-025 (meldinger), men "noe endret seg, hent på nytt"-signal i stedet for å sende selve dataene — å bygge/ sende hele leaderboard+matchliste+scorekort-formen over WS ville duplisert betydelig beregningslogikk (leaderboardets projeksjons-regnestykke, blind draw-filtrering osv.). Klienten reagerer på signalet ved å kalle de samme REST-endepunktene på nytt (ADR-026), akkurat som ved førstegangslasting — kun for det som faktisk er synlig/åpent på skjermen (leaderboard alltid, en økts matcher kun hvis økten er utvidet, et scorekort kun hvis det er åpnet).
Ny, RUTEFRI modul app/realtime.py for selve tilkoblingsregisteret og
kringkastingsfunksjonen — verken i messaging.py, scoring.py eller
tournaments.py. Årsak: registration.py (som eier selve
/public/...-endepunktene) importerer allerede fra scoring.py/
tournaments.py/matches.py, og messaging.py importerer fra
registration.py — å plassere kringkastingsfunksjonen i noen av routerne
ville skapt en sirkulær import. app/realtime.py ligger bevisst BAK alle
routere i importgrafen.
Kringkastingen er lagt INN I de delte funksjonene selv
(recompute_and_cache_match_state, apply_concession), ikke som noe
kallerne må huske å gjøre etterpå — samme selv-ansvarlig-mønster som andre
sentrale funksjoner i prosjektet. apply_concession fikk en ny påkrevd
tournament_id-parameter kun for dette formålet.
Samme kjente in-memory-per-prosess-begrensning som ADR-025 (se «Åpne
spørsmål» under) — trygt med dagens ene teecup_api-container.
Status: ✅ BYGGET OG LIVE 2026-07-19. Ingen migrasjon, ingen ny
Caddy-rute (gjenbruker /ws/*-ruten fra ADR-025 uendret). Se CLAUDE.md-
status for scratch-verifisering og utrulling.
ADR-028: PWA — installasjon + offline scoreregistrering
Reist 2026-07-19, brukeren valgte å ta fatt på PWA (stod som "neste steg" i CLAUDE.md siden ADR-006 vedtok prinsippet helt i starten av prosjektet — aldri bygget). To beslutninger avklart eksplisitt med bruker (AskUserQuestion) før bygging.
Beslutning A — Full offline scoreregistrering i v1, ikke bare installasjon. Bruker valgte det mest ambisiøse alternativet: BÅDE manifest/ikoner/service worker (installerbar app) OG at scorekort-skjermen skal fungere uten nett — skriv til en lokal IndexedDB-kø, synk automatisk når nettet er tilbake. Matcher ADR-006s opprinnelige formulering ordrett ("score må kunne registreres uten nett og synkes senere").
Beslutning B — Omfang av selve offline-skrivingen: kun hole-scores/
hole-results, ingenting annet. De to eksisterende scoreregistrerings-
endepunktene (ADR-012) er de eneste som køes. Bevisst UTENFOR omfang, ikke
glemt:
- Walkover/konsesjon (ADR-024) — sjeldnere handling, kan kreve nett.
- Chat/feed-posting (ADR-025) — bilder gjør en offline-kø vesentlig mer komplisert (blob-lagring i IndexedDB), og er ikke "score"-handlingen ADR-006 faktisk siktet til.
- Alle andre skrive-endepunkter (oppsett, roster, økter osv.) — forutsetter nett i v1, uendret.
Beslutning C — Køen lever i klientkoden (IndexedDB, lib/offline- queue.ts), IKKE i service workeren, og bruker window sitt online-
event, IKKE Background Sync API. To bevisste forenklinger:
- Å gi umiddelbar, presis UI-tilbakemelding ("lagret lokalt, venter på
synk", pending-antall) er enklere og mer testbart fra selve
React-komponenten enn fra en service worker sin
fetch-handler. - Background Sync API (som ville gitt synk selv om appen er lukket) støttes
IKKE av iOS Safari i det hele tatt — en stor andel av klubb-/
vennegjeng-brukerne er trolig på iPhone, og et rent
Background-Sync-avhengig design ville derfor vært brutt for dem. Et
window.addEventListener("online", ...)-mønster (pluss en manuell "Synkroniser nå"-knapp i UI-et som reserve) fungerer overalt, på bekostning av at synk krever at appen faktisk er åpen når nettet kommer tilbake — akseptabelt for v1.
submitStroke/submitHoleResult (components/session-scorecard.tsx)
sjekker navigator.onLine FØRST (unngår en unødvendig ventetid på et
nettverkskall som uansett vil feile), og fanger ellers en EKTE nettverksfeil
i fetch-kallet separat fra et avvist HTTP-svar (res.ok === false, f.eks.
409 "matchen er avgjort") — kun den førstnevnte køordner, sistnevnte viser
fortsatt den vanlige feilteksten uendret. Et lokalt overlay
(pendingStrokes/pendingResults, ikke persistert i selve
scorecard-staten) viser køede verdier umiddelbart i UI-et, merket
"Lagret lokalt · venter på synk", inkludert i hull-navigasjonens
registrert-markering.
Konfliktmodell: samme tillitsnivå som resten av appen, ingen ny
mekanisme. Server-siden er allerede en upsert per hull (ON CONFLICT ... DO UPDATE) — siste innsending vinner, ingen audit-trail (kjent, tidligere
dokumentert åpent spørsmål, se «Scoring-autorisasjon» i FEATURE_BACKLOG.md).
Køen sender i rekkefølge (aldri parallelt) for å respektere lokal
innsendingsrekkefølge ved flere endringer av samme hull offline. Et
definitivt HTTP-avvist forsøk ved synk (typisk: matchen ble avgjort på en
ANNEN enhet mens denne var offline) fjernes fra køen og vises som en
feilmelding — blir ALDRI hengende for alltid.
Service worker-strategi: nettverk først, cache som fallback — bevisst
IKKE stale-while-revalidate. public/sw.js cacher (a) side-navigasjon og
(b) GET-kall under /orgs/*. Begge prøver ekte nettverk FØRST og faller
kun tilbake til cache ved reell feil. En stale-while-revalidate-strategi ble
vurdert og avvist: scorekortet gjør et refetchScorecard()-kall RETT ETTER
hver innsending, og må da alltid få fersk data — en umiddelbart utdatert
cache ville vist feil matchstatus/hull-tall rett etter en vellykket
innsending. /auth/* og /public/* caches bevisst ikke — omfanget er
begrenset til akkurat det scoreregistrerings-flyten trenger.
Kjent, akseptert begrensning (ikke løst i v1): Cache Storage er nøklet
på URL, ikke på innlogget bruker — deler flere kontoer samme enhet/
nettleser, kan en offline-fallback teoretisk vise data cachet av en
TIDLIGERE innlogget bruker på samme enhet. Ingen cache-tømming ved
utlogging bygget. Samme tillitsnivå/trusselmodell som appen ellers opererer
med (tillitsbasert klubb-/vennegjeng-verktøy, jf. team_authz.py sin
begrunnelse i ADR-023/025).
Ikoner: enkelt, midlertidig sett generert programmatisk (grønt
golf-flagg), ikke endelig design. Bruker valgte å generere nå fremfor å
vente på ekte design, MED eksplisitt beskjed om at disse skal erstattes
senere — notert i FEATURE_BACKLOG.md. Erstattet samtidig den gamle
public/apple-icon.png (v0.app sin generiske plassholder-logo, ikke
TeeCup-merkevare i det hele tatt) med samme nye ikon, av konsistens.
Ikke testet i ekte nettleser (viktig, ikke bare en formalitet):
verifisert med typesjekket produksjonsbuild (samme Dockerfile som
deployes) og en kort container-boot med curl (manifest/service worker/
ikoner/offline.html svarer riktig), men INGEN faktisk browser-basert
offline-test (DevTools "Offline"-modus, "Legg til på hjemskjerm") er
gjennomført denne runden — ingen nettleserverktøy tilgjengelig i denne
økten. Anbefales sterkt at brukeren selv tester scorekort-siden med Chrome
DevTools sin Offline-bryter før tillit legges til flyten i skarp bruk.
Status: ✅ BYGGET OG LIVE 2026-07-19. Bruker bekreftet eksplisitt, ingen
migrasjon, kun teecup_frontend (og en ren, uendret gjenoppbygging av
teecup_api som en bivirkning av docker compose up --build sin
avhengighets-oppløsning — ingen backend-kode rørt denne runden). Verifisert:
/health/dashboard → 200, /manifest.webmanifest/sw.js/ikoner alle
200 over ekte https, teeoff.no upåvirket. Se CHANGELOG.md for full
byggerunde.
Gjenstående, IKKE en del av "ferdig"-vurderingen over: faktisk nettleser-basert offline-test (Chrome DevTools Offline-modus, ekte "Legg til på hjemskjerm") er fortsatt ikke gjort — ingen nettleserverktøy tilgjengelig i byggeøkten. Brukeren har bedt om at dette noteres eksplisitt for oppfølging, se FEATURE_BACKLOG.md sin PWA-tabell og CLAUDE.md sin "Neste steg"-liste.
ADR-029: Kjønn hører til tee-RATINGEN, ikke selve utslaget
Reist av brukeren 2026-07-19, som del av en runde med fire rapporterte UX-/korrekthetshull fra faktisk testing av blind draw og scorekort. Startet som en antatt frontend-fiks ("tee-valget bør følge spillerens kjønn automatisk"), men brukeren presiserte at premisset mitt 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 (noen klubber sloper bevisst ikke det lengste utslaget for damer).
Bekreftet problem, ikke antatt: tee.gender (migrasjon 001) la kjønn på
selve utslaget, ikke ratingen. Sjekket mot ekte, importert produksjonsdata
(Tjøme Golfklubb, ADR-019): alle fire utslagene lå som RENE navnepar --
"32"/m + "32"/f, "44"/m + "44"/f, osv. -- to separate tee-rader for
akkurat samme fysiske utslag. Blind draw-skjermen viste dette som "velg
Dame- eller Herre-tee", som om det var to ulike steder å slå fra.
Beslutning A -- gender flyttes fra tee til tee_rating. Ett fysisk
utslag (tee, identifisert kun ved navn) kan ha 0, 1 eller 2
kjønnsspesifikke ratinger. tee_rating sin unikhet endres fra
(tee_id, scope) til (tee_id, scope, gender). gender på tee_rating
er NOT NULL CHECK IN ('m','f') -- 'x' (gyldig på PLAYER-nivå) gir ikke
mening for en WHS-rating, som alltid er for ett bestemt kjønn.
Beslutning B -- tee-valget blir helt automatisk, ingen manuell
kjønnsvelger (bekreftet med bruker, valgte det anbefalte alternativet).
Organisator/kaptein velger KUN fysisk utslag i blind draw
(session-blind-draw.tsx, ingen "H"/"D"-suffiks lenger). Riktig
kjønnsspesifikk rating løses AUTOMATISK server-side fra spillerens
registrerte player.gender -- app/handicap.py sin
compute_and_store_side_handicaps joiner nå tee_rating på
(tee_id, scope='full_18', gender = player.gender) i tillegg til den
tidligere ADR-028-fiksen (alltid full_18, uavhengig av hole_config).
Beslutning C -- manglende rating/kjønn feiler tydelig FØR innsetting,
ingen stille fallback (bekreftet med bruker, valgte det anbefalte
alternativet). matches.py sin add_participant validerer, når
config.use_handicap er sann: (1) spilleren har et registrert kjønn --
avvist med VALIDATION_FAILED hvis ikke ("sett kjønn på spilleren
først"), (2) valgt utslag har faktisk en tee_rating for NØYAKTIG det
kjønnet -- avvist med VALIDATION_FAILED hvis ikke ("dette utslaget har
ingen dame/herre-rating -- velg et annet utslag"). Samme "fail loudly"-
prinsipp som ADR-019 sin importvalidering og den eksisterende
handicap_index-sjekken (2026-07-18) rett ved siden av. _remap_course
(bane-bytte midt i en økt) fikk samme presisering: matcher fortsatt på
tee-navn, men sjekker nå at den NYE tee-en har en rating for hver berørte
spillers kjønn, ikke bare at navnet finnes.
Konsekvens for import/opprettelse: ADR-019 sin teeoff-import
(import_official_course) lager nå ÉN tee-rad per fysisk teeoff-utslag
(tidligere: to, én per kjønn), med inntil to tee_rating-rader under.
Manuell tee-opprettelse (POST .../courses/{id}/tees) redesignet
tilsvarende: TeeCreate.ratings er nå en liste (1-2 elementer, distinkte
kjønn) i stedet for ett flatt kjønn+rating-sett -- ingen reell
bruker-/produksjonsdata brukte dette endepunktet fra før (kun Tjøme, som
er offisielt importert), så ingen bakoverkompatibilitet var nødvendig.
Migrasjon 014_tee_gender_to_rating.sql: flytter gender til
tee_rating, slår deretter sammen eksisterende kjønns-par-tee-rader til
én fysisk tee-rad per (bane, navn) -- velger laveste id som "beholder",
flytter alle tee_rating- og match_participant.tee_id-referanser dit,
sletter duplikatene. Bekreftet TRYGT å kjøre mot ekte data FØR skriving
(read-only sjekk): alle 8 eksisterende tee-rader (Tjøme) er rene m/f-par,
ingen gender='x'-rader, ingen gruppe med mer enn to rader.
Scratch-verifisert grundig, flere runder: (1) selve
fletting-migrasjonen kjørt mot syntetisk data som gjenskaper Tjøme-
mønsteret nøyaktig, inkl. én match_participant-rad som pekte til
DUPLIKATEN (ikke beholderen) -- bekreftet korrekt reparert til beholderens
id etterpå, og et utslag med KUN én rating (ingen duplikat) forblir
urørt. (2) Manuell tee-opprettelse: to ratinger på ett utslag, kun én
rating (simulerer "klubben har ikke slopet dette kjønnet"), duplikat
kjønn i samme innsending avvist. (3) GET tees viser riktig sammenslått
struktur (2 fysiske utslag, ikke 3 rader). (4) add_participant: kvinne
- utslag med dame-rating lykkes; mann + utslag som KUN har dame-rating
avvist tydelig; spiller uten registrert kjønn avvist tydelig; en full
kjønnsblandet singel-match scoret korrekt end-to-end. (5) Offisiell
import kjørt mot EKTE
teeoff_api(Borregaard Golfklubb) -- bekreftet 4 fysiske utslag importert med begge kjønnsratinger hver, ikke 8 doble rader. (6)_remap_course: bane-bytte til en bane UTEN matchende kjønnsrating avvist tydelig, bane-bytte til en bane MED matchende rating lykket.
Status: ✅ BYGGET OG LIVE 2026-07-19. Migrasjon 014 kjørt mot ekte
teecup_db, bruker bekreftet eksplisitt: Tjømes 8 tee-rader slått sammen
til 4 fysiske utslag, alle 8 tee_rating-rader fikk riktig gender,
match_participant-referansene forble gyldige (0 brutte fremmednøkler
etter migrasjonen, bekreftet med en direkte spørring). Begge containere
(teecup_api, teecup_frontend) bygget og redeployet, /health/
/dashboard → 200, teeoff.no upåvirket.
ADR-030: Dashbord-datoer utledes fra øktene, ikke et separat felt
Reist av brukeren 2026-07-19/20, direkte oppfølging av det tidligere dokumenterte UI-hullet ("Dashboard-turneringskortet viser 'Ingen datoer satt'", notert 2026-07-19). Brukeren viste et faktisk skjermbilde: en turnering med en økt tydelig planlagt til "lør. 11. juli, 10:50" på Program-fanen, mens dashbord-kortet fortsatt viste "Ingen datoer satt".
Root cause, bekreftet ved kodegjennomgang (samme konklusjon som forrige
runde, nå faktisk fikset): dashbord-kortet leser tournament.start_date/
end_date — et eget felt på selve turneringen (ADR-015) — som INGEN UI
noensinne har hatt en vei til å sette. Fullstendig atskilt fra
session.scheduled_at (som Program-fanen korrekt viser).
Beslutning: utled datospennet fra øktenes scheduled_at i
list_tournaments, ikke bygg et manuelt datofelt-skjema. To kilder til
sannhet (et manuelt "turnering-dato"-felt OG faktiske økt-tidspunkter) ville
uunngåelig kommet ut av synk — turneringens reelle datoer ER ganske enkelt
når rundene faktisk er satt til å spilles. GET /orgs/{id}/tournaments
gjør nå COALESCE(t.start_date, MIN(økt.scheduled_at)) /
COALESCE(t.end_date, MAX(økt.scheduled_at)) — et eksplisitt satt
start_date/end_date (om det noensinne blir gitt en skrivevei senere)
vinner fortsatt over det utledede spennet, men i praksis er det alltid det
utledede spennet som vises i dag. En turnering uten noen tidsplanlagte
økter viser fortsatt riktig "Ingen datoer satt" (ikke en feil).
Konsekvens: kun list_tournaments sin spørring endret (ikke
_TOURNAMENT_COLUMNS, som fortsatt brukes uendret av opprett-/
PATCH-endepunktene sine RETURNING-klausuler — de trenger ikke
sesjons-utledningen rett etter en skriveoperasjon). Ingen migrasjon.
Scratch-verifisert: turnering uten økter → null/null (ikke feil);
turnering med to økter (11./12. juli) → riktig utledet spenn; en turnering
med et EKSPLISITT satt start_date/end_date ved opprettelse beholder
fortsatt sin egen verdi selv med økter senere lagt til (bekrefter
COALESCE-prioriteringen).
Status: ✅ BYGGET OG LIVE 2026-07-20. Bruker bekreftet eksplisitt, ingen migrasjon. Verifisert mot ekte data: "De Gamle er Eldst" viser nå korrekt 11. juli 2026 (utledet fra dens økt) i stedet for "Ingen datoer satt".
ADR-031: Personlig landingsside for enhver registrert bruker + personlig profil
Reist av brukeren 2026-07-20, eksplisitt som en "tenk igjennom og foreslå"- instruks, deretter et klart "gjør det" med et utvidet omfang (personlig profil-CRUD: profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb).
Bekreftet, reelt hull: app/page.tsx sendte enhver innlogget bruker til
/dashboard, som viste "opprett organisasjon" så snart brukeren ikke var
org-medlem — også for en bruker som KUN er spiller (koblet via
player.user_id, ADR-017 B), aldri organisator.
Beslutning A — ETT samlet dashboard, ikke to atskilte ruter. /dashboard
viser nå en ny "Mine runder"-seksjon øverst (turneringer brukeren er ROSTRET
i, på tvers av organisasjoner) når den finnes, med organisasjonsseksjonen
uendret under. En bruker som er BÅDE organisator og spiller ser begge deler
— ingen tvungen valg mellom to identiteter.
Beslutning B — personlig profil er ETT sett PER KONTO (app_user), atskilt
fra org-scopede player-rader. Ny migrasjon 015_user_profile.sql:
app_user får first_name/last_name/birth_date/gender/
handicap_index/home_club/avatar_key. Bevisst IKKE forsøkt slått sammen
med player-radene (som fortsatt er per-organisasjon, eid av organisator,
brukt til roster/handicap-snapshot) — en person kan ha flere player-rader
i ulike klubber (ulikt hjemmeklubb-medlemsnummer, potensielt ulik registrert
HCP per klubb i den virkelige golfverdenen), mens KONTOENS egen profil er
brukerens EGEN, selvstyrte fremstilling av seg selv — to bevisst atskilte
konsepter, ikke ett duplisert.
Konsekvens: PATCH /auth/profile (vanlig exclude_unset-PATCH-mønster
— et felt sendt eksplisitt som null sletter det, et utelatt felt endres
ikke), POST/DELETE /auth/profile/avatar (samme ekte multipart→AVIF-
opplastingsmønster som tournament.hero_image_key, ADR-018 MinIO-runden —
se app/storage.py). Ny seksjon i /account (account-settings.tsx).
Beslutning C — "Mine runder" krever et NYTT tverr-org-oppslag, samme
mønster som fire tidligere bygde bruksområder. player er RLS-beskyttet
per organisasjon; å finne "hvilke org-er har jeg en spiller-rad i" krever
samme smale SECURITY DEFINER-bro som public_tournament_org() (007)/
link_player_by_email() (008)/public_org_by_slug() (009)/
public_tournament_by_code() (011) — ny player_organizations_for_user()
(migrasjon 015), eksponerer KUN en uuid-liste. /auth/me utvidet med
my_tournaments (samme N+1-over-org_connection()-mønster som allerede
brukes for organizations).
Beslutning D — ny sikkerhetsutvidelse funnet UNDER design, ikke antatt på
forhånd: en faktisk deltaker skal aldri stenges ute av sin EGEN turnering,
uansett synlighetsnivå. Under bygging av "Mine runder" ble det klart at
check_visibility() (ADR-018 Beslutning C) kun ga deltaker-tilgang for
visibility='participants' — IKKE for 'org' (som er DEFAULT for enhver
NY turnering). En ren spiller (rostret, men uten organisasjonsmedlemskap)
ville dermed vært stengt ute fra sin egen, helt vanlige turnering (default
'org'-synlighet) — nøyaktig den brukergruppen "Mine runder" er bygget
for. Utvidet: deltaker-sjekken gjelder nå for BEGGE ikke-offentlige tiere,
ikke bare 'participants'. Begrunnelse: visibility styrer eksponering
mot UTENFORSTÅENDE, aldri mot folk som faktisk spiller i turneringen — det
finnes intet scenario der en organisator ønsker å skjule en turnering for
sin EGEN spiller. Verifisert presist: en faktisk deltaker (ikke org-medlem)
FÅR nå tilgang til en 'org'-synlig turnering, mens en helt ubeslektet
FREMMED (innlogget, men ikke deltaker) og en ANONYM leser fortsatt begge
avvises som før (403 NOT_VISIBLE) — ren utvidelse, ingen innstramming.
Bevisst UTENFOR omfang denne runden, kjent gjenstående begrensning:
"Mine runder" lenker til den offentlige turnering-siden (/t/{id}), IKKE
til lagets private chat eller det organisator-vendte scorekortet — disse
krever fortsatt get_authorized_org (ekte organisasjonsmedlemskap), en
strengere sperre enn deltaker-status alene, brukt av dusinvis av
endepunkter på tvers av hele appen. Å utvide DENNE sperren trygt til også å
godta "faktisk deltaker" er en egen, større og mer risikofylt endring
(påvirker autorisasjonsarkitekturen bredt) — bevisst IKKE gjort i denne
runden, notert i FEATURE_BACKLOG.md som naturlig neste steg.
Scratch-verifisert, 15 sjekker: profil-CRUD (sett alle felt, delvis
PATCH lar andre felt stå urørt, eksplisitt null sletter et felt, tomt
PATCH avvist, avatar lastet opp med ekte AVIF-URL, avatar slettet, ugyldig
filtype avvist), "Mine runder" for en EKTE ren spiller (null organisasjons-
medlemskap, men my_tournaments viser riktig turnering+lag), OG den
kritiske sikkerhetssjekken: samme rene spiller FÅR nå se sin 'org'-
synlige turnering, mens en fremmed innlogget bruker og en anonym begge
fortsatt avvises. Ekte typesjekket produksjonsbuild kjørt og bekreftet.
Status: ✅ BYGGET OG LIVE 2026-07-20. Migrasjon 015 kjørt mot ekte
teecup_db, bruker bekreftet eksplisitt. Begge containere redeployet,
/health//dashboard//account → 200, teeoff.no upåvirket. Se
CHANGELOG.md for full byggerunde.
ADR-032: Verifisert e-postbytte + mobil på personlig profil
Reist av brukeren 2026-07-20, samme dag og rett etter ADR-031: "identifikatoren" (e-post) manglet i den nye personlige profilen, og mobil (med landsnummer) burde være en opsjon.
Beslutning A — mobil er et rent, enkelt tillegg til ProfileUpdate
(samme PATCH som resten av profilen), splittet i to felt.
mobile_country_code (f.eks. "+47") og mobile_number er separate
kolonner på app_user (migrasjon 016_profile_contact.sql) — ikke én
sammensatt streng — slik at frontend kan tilby en egen landsnummer-
velger uten å måtte parse en fritekststreng i etterkant.
Beslutning B — e-post er IKKE en del av den vanlige profil-PATCH-en, og
kan det aldri bli. E-post er innloggings-identifikatoren (magic-link-
mål) — en enkel PATCH (som de andre feltene) ville latt en skrivefeil
ELLER en kapret sesjon stjele kontoen for godt, ingen verifisering av at
den NYE adressen faktisk eies av noen. Løst med et eget, to-stegs
bekreftelsesløp, samme token_hash+expires_at+consumed_at-mønster
som magic_link_token (004), gjenbrukt for et nytt formål: ny tabell
email_change_token (bruker_id, ny e-post, token-hash, utløp).
POST /auth/profile/email (krever gyldig sesjon — du må bevise at du
ER kontoen i dag) genererer tokenet og sender en bekreftelseslenke til
DEN NYE adressen (ikke den gamle — beviser eierskap av MÅLET, ikke bare
at avsenderen fortsatt er innlogget). POST /auth/profile/email/confirm
(ingen sesjon påkrevd — samme mønster som selve magic-link-verifiseringen,
siden lenken kan åpnes på en annen enhet/nettleser enn den som ba om
byttet) forbruker tokenet atomisk og gjennomfører selve byttet. E-posten
endres IKKE før lenken faktisk åpnes.
Konsekvens: duplikat-sjekk (er den ønskede adressen allerede en annen
kontos?) gjøres TO ganger — én gang ved forespørsel (rask
tilbakemelding), én gang igjen rett før selve UPDATE-en ved bekreftelse
(kan ha blitt tatt av noen andre i mellomtiden) — pluss den eksisterende
unike indeksen (app_user_email_unique, migrasjon 004) som siste
bakstopper via translate_db_errors().
Scratch-verifisert, 10 sjekker: mobil satt via vanlig PATCH; vanlig
profil-PATCH endrer aldri e-post; bytte til en allerede brukt adresse
avvist (409); e-post FORBLIR uendret helt til lenken bekreftes; ugyldig
bekreftelseskode avvist; gyldig kode fullfører byttet; SAMME kode kan
ikke brukes to ganger; en helt ny innlogging med den GAMLE adressen
oppretter nå en fersk, tom konto (beviser byttet er reelt og fullstendig,
ikke kosmetisk). Ekte typesjekket produksjonsbuild kjørt og bekreftet
(ny /verify-email-rute listet).
Status: ✅ BYGGET OG LIVE 2026-07-20. Migrasjon 016 kjørt mot ekte
teecup_db, bruker bekreftet eksplisitt. Begge containere redeployet,
/health//dashboard//account//verify-email → 200, teeoff.no
upåvirket.
ADR-033: Frittstående rundeføring med detaljert statistikk
Reist av brukeren 2026-07-21 (se FEATURE_BACKLOG.md), utdypet 2026-07-22 med konkrete statistikk-felt og en eksplisitt ambisjon: dette skal bli appens hovedfokus — når rundeføring med detaljert statistikk er på plass, skal turneringsoppsett deretter gjøres ekstremt enkelt. Dette er den største enkeltbeslutningen i prosjektet siden ADR-001, fordi den direkte utfordrer tenant-invarianten ("organisasjon er isolasjonsenheten") CLAUDE.md hittil har krevd en ny ADR for å bryte.
Fire load-bærende delbeslutninger ble avklart eksplisitt med bruker (AskUserQuestion) FØR resten av denne ADR-en ble skrevet. De tre HCP-PDF-ene (se referanse i CLAUDE.md) ble lest i sin helhet som forberedelse til Beslutning F/G, i tråd med prosjektets stående instruks om å ikke anta HCP-regler fra hukommelse.
Beslutning A — Eierskapsmønster: nytt, parallelt, IKKE en skjult organisasjon
En frittstående runde eies av en BRUKER (app_user.id), ikke en
organisasjon. Ingen ny RLS-policy-familie trengs for dette — prosjektet
har ALLEREDE et etablert, bevist mønster for nøyaktig denne typen data:
personlig profil (migrasjon 015), sekundær e-post (017) og
HCP-historikk (018) bruker alle plain_connection() (ingen
app.current_org satt) med eksplisitt WHERE user_id = $1-filtrering i
hver spørring, i stedet for RLS. Frittstående runder gjenbruker dette
mønsteret uendret — nye tabeller (round, round_participant,
round_hole_stat, se Beslutning B) har en owner_user_id-kolonne, INGEN
organization_id, og INGEN RLS-policy — autorisasjon håndheves i
app-laget (eier ser/redigerer egne runder; en deltaker uten konto har
ingen egen tilgang, se Beslutning D).
Begrunnelse for å avvise "usynlig personlig organisasjon"-alternativet: en skjult organisasjon måtte for alltid filtreres bort fra ENHVER org-listing/-bytter/-medlemsside/fremtidig fakturering — en varig lekkasjerisiko som vokser for hver ny org-scopet skjerm som bygges fremover. Det parallelle mønsteret er ikke bare konseptuelt riktigere, det er også ALLEREDE bygget og bevist for personlig data — dette er mindre nytt arbeid enn først antatt i brainstorm-runden.
Beslutning B — Statistikk-datamodell: fast sett navngitte felt, ikke fri slag-for-slag-logg
Per hull, i tillegg til slagtall (som i dag): kølle brukt ved utslag, utslags-resultat (fairway/høyre/venstre — kun relevant på par 4/5), innspill-resultat (traff/lang/kort/høyre/venstre), antall putter, lengde på første putt, antall chip, antall bunkerslag, antall straffeslag.
Presis definisjon av "innspillsslaget" (nødvendig for at GIR skal kunne beregnes automatisk, uavhengig av hullets par): SISTE slag før første putt. På en par 3 er dette utslaget selv; på en par 4 normalt 2. slag; på en par 5 kan det være 2. ELLER 3. slag (f.eks. ved en layup) — modellen trenger ikke vite hvilket slagnummer det var, kun at det faktisk er det siste FØR putting startet.
GIR er DERIVERT, ikke tastet inn direkte: Green In Regulation = sant hvis innspill-resultat er "traff" OG antall slag brukt til da er ≤ (hullets par − 2). Utslag-resultat og innspill-resultat er derimot OBSERVERTE felt spilleren selv taster inn — appen har ingen GPS og kan ikke oppdage dette selv.
UX-prinsipp, ikke bare et skjema-valg: ALLE detalj-felt er valgfrie per hull. Rask "bare slagtall"-registrering skal alltid fungere uendret — dette er bevisst for å unngå at "hovedfokus" i praksis blir for tungvint til daglig bruk (samme klasse avveining som gjorde at detaljerte felt ble utsatt fra scorekort-rundene tidligere i prosjektet).
Avvist: fri slag-for-slag-logging (hvert slag = egen rad). Ville dekket samme behov, men med vesentlig tyngre registrering og et mer komplekst skjema fra dag én, uten at brukerens beskrevne behov faktisk krever det.
Beslutning C — Banedata for frittstående runder: LIVE oppslag mot teeoff, bekreftet av bruker
Custom-baner er i dag org-scopet (course.organization_id) — dette
fungerer ikke uten organisasjon. Bekreftet med bruker 2026-07-22:
offisielle baner slås opp LIVE mot teeoff sitt API ved behov (samme
lesende API som ADR-019, IKKE en importert/kopiert kopi som i
turnering-flyten). ADR-019 sin "importer, ikke slå opp live"-beslutning
var begrunnet i turneringers behov for reproduserbarhet (et resultat skal
ikke endre seg retroaktivt hvis banedata oppdateres) — en frittstående
statistikk-runde har ikke samme behov, og live oppslag unngår i tillegg
dagens duplisering (hver org som importerer "Tjøme Golfklubb" i dag lager
sin egen kopi). Egendefinerte baner som ikke finnes i teeoff: global
banekatalog uten organisasjonstilknytning, søk-før-opprett for å begrense
duplikater (denne delen — selve den globale katalogen for CUSTOM baner —
er fortsatt kun min anbefaling, ikke eksplisitt bekreftet punkt for
punkt, men følger naturlig av at live-oppslag-beslutningen er tatt).
Beslutning D — Deltakere i flighten uten TeeCup-konto: gjenbruk eksisterende mønster
round_participant får en nullable user_id (ekte TeeCup-bruker som
spiller med) og et guest_name-fritekstfelt (ingen konto). Samme
konsept som player uten user_id i org-sammenheng. E-post-basert
kobling i etterkant (samme mønster som link_player_by_email,
migrasjon 008) er en naturlig, men IKKE besluttet, senere utvidelse —
notert som åpent punkt under, ikke bygget i første omgang.
Beslutning E — Hullantall og starthull: ingen tvang, kun standardvalg
18/første ni/siste ni er standardvalg i UI-et, ikke en database-
begrensning. Spilleren velger starthull fritt (gjenbruk av samme
start_hole-konsept som session.start_hole, ADR-015). En runde kan
avsluttes etter et hvilket som helst antall hull uten et forhånds-
deklarert mål — statistikk og par-sum regnes alltid fra hullene FAKTISK
spilt.
Beslutning F — HCP-tellende runde: presist kildebelagt fra WHS Rules of Handicapping 2024
Brukeren lastet opp den offisielle kilden 2026-07-22 ("WHS Rules of Handicapping 2024", USGA/R&A — IKKE en tredjeparts-blogg som de to andre PDF-ene). Dette erstatter den tidligere, mer omtrentlige "minst 9 hull"-antakelsen med presise regler (Rule 2.2):
- 18-hulls-type runde: minimum 10 av 18 hull må spilles for at scoren skal være akseptabel. De resterende (inntil 8) uspilte hullene fylles med en "expected score" (se under), IKKE net par (net par- metoden ble erstattet av expected-score-metoden i 2024-revisjonen — et prinsipielt skifte fra tidligere WHS-versjoner, verdt å merke seg siden eldre kilder/hukommelse fortsatt kan referere net par).
- 9-hulls-type runde: ALLE 9 hull i det spesifikke, ratede 9-hulls- settet (front ELLER back, de eneste to som normalt har egen Course/ Slope Rating) må spilles. Færre enn 9 hull totalt → scoren er IKKE akseptabel for HCP-formål i det hele tatt, uansett grunn.
- Konsekvens for Beslutning E (fritt valgt starthull): den frie starthull-friheten gjelder fullt ut for CASUAL, ikke-HCP-tellende logging. For at en runde skal telle mot HCP, må de spilte hullene derimot samsvare med enten (a) et sammenhengende 10-18-hulls-utsnitt av banens 18-hulls-rating, eller (b) nøyaktig banens ratede front-9 eller back-9 — en vilkårlig 9-hulls-strekning (f.eks. hull 5-13) har ingen egen Course/Slope Rating og kan derfor aldri bli HCP-tellende. Dette må kommuniseres tydelig i UI-et, ikke bare håndheves stille i motoren.
Beslutning G — HCP-indeksberegning bygges NÅ, presist kildebelagt (WHS Rules of Handicapping 2024)
Full kjede, alle tall/formler hentet direkte fra kilden (Rule 3/5/6), ikke hukommelse:
- Net Double Bogey (maks hull-score for HCP-formål) = hullets par +
2 + spillerens handicapslag på det hullet (Rule 3.1b) — allerede
dekket av eksisterende
allocate_strokes_by_index/allocate_over_played_holes, ingen endring. - Uspilte hull — LØST 2026-07-22 med et bevisst, kildebelagt avvik fra "Expected Score": WHS sin offisielle "Expected Score"-mekanisme (Rule 3.2b) er eksplisitt beskrevet som automatisk beregnet av sertifisert WHS-programvare, UTEN at selve formelen er publisert i regelboken (samme mønster som PCC) — kan derfor ikke bygges presist. Brukeren instruerte eksplisitt å bruke WHS sin egen, presist DEFINERTE "Net Par"-term i stedet (Rule 3.2b/2 — normalt reservert for spesielle godkjente tilfeller, men her vedtatt som TeeCups generelle policy): for hvert uspilt hull antas spilleren å ha skåret sin Net Par = hullets par + mottatte handicapslag på det hullet (samme formel som Net Double Bogey, uten +2-leddet) — tilsvarer 2 Stableford-poeng per uspilt hull. Summeres inn i Adjusted Gross Score FØR standard 18-hulls Score Differential-formelen brukes. Viktig konsekvens: dette gjør at en 9-hulls-runde nå KAN telle fullt mot HCP-indeksen (de resterende 9 hullene fylles med Net Par, hele runden går gjennom SAMME 18-hulls- formel) — det tidligere spørsmålet om en egen 9-hulls-differensial- formel (Rule 5.1b) er dermed ikke lenger nødvendig å bygge separat. Fortsatt gyldig kun når minimumsantallet (Beslutning F) er oppfylt.
- Ikke fullført hull (spilleren plukker opp) (Rule 3.3): laveste av "most likely score" (allerede tatte slag + sannsynlig antall til fullføring, tabell basert på ballens avstand fra hullet + eventuelle straffeslag) eller net double bogey.
- 18-hulls Score Differential (Rule 5.1a) =
(113 ÷ Slope Rating) × (Adjusted Gross Score − Course Rating − PCC-justering), avrundet til nærmeste tidel (,5 rundes opp). - (Rule 5.1b, WHS sin egen 9-hulls-differensial-formel) — IKKE brukt. Erstattet av Net-Par-tilnærmingen i punkt 2: en 9-hulls-runde regnes nå som en 18-hulls-runde med 9 Net-Par-fylte hull, gjennom SAMME formel som punkt 4. Nevnt her kun for å dokumentere at det bevisst er valgt bort, ikke oversett.
- Handicap Index (Rule 5.2) = gjennomsnitt av de beste 8 av de siste 20 Score Differentials, avrundet til nærmeste tidel. For færre enn 20 runder i historikken brukes en egen opptrappingstabell (f.eks. 3 runder → laveste 1 med justering −2,0; 9-11 runder → snitt av laveste 3, ingen justering; osv. — full tabell i Rule 5.2a, IKKE en enkel "gjennomsnitt av alt"-tilnærming for nye spillere).
- Low Handicap Index (Rule 5.7): laveste indeks siste 365 dager — nødvendig referansepunkt for cap-mekanismen under, må lagres/spores per bruker.
- Soft cap / hard cap (Rule 5.8): øker en oppdatert indeks mer enn 3,0 slag over Low Handicap Index, begrenses overskytende beløp til 50 %; mer enn 5,0 slag over Low Handicap Index er et absolutt tak. Ingen nedre grense på hvor mye indeksen kan SYNKE.
- Maksimal indeks (Rule 5.3) = 54,0 — samsvarer med det allerede
satte
le=54-taket iProfileUpdatefra profil-fullførings-runden (2026-07-22), god konsistens-bekreftelse. - 9-hulls Course Handicap (Rule 6.1b) — NY, presis detalj,
AVVIKER fra 18-hulls-formelen:
(Index ÷ 2, avrundet til nærmeste tidel) × (9-hulls Slope ÷ 113) + (9-hulls Course Rating − 9-hulls Par). Dette er IKKE det samme som å bruke full indeks mot en 9-hulls rating — indeksen halveres først. Denne formelen gjelder frittstående 9-hulls-RUNDER (denne ADR-en), IKKE det eksisterende front_9/back_9-øktoppsettet i turnering-flyten (ADR-008/2026-07-19- fiksen, som bevisst bruker full_18-rating for HELT ANDRE grunner — slagfordeling innad i en turneringsmatch, ikke offisiell HCP- runde-innsending. De to må IKKE forveksles eller slås sammen uten en egen vurdering.) - 18-hulls Course Handicap (Rule 6.1a) =
Index × (Slope ÷ 113) + (Course Rating − Par)— allerede korrekt implementert (course_handicap_raw, verifisert ved grep), ingen endring. - Playing Handicap (Rule 6.2) — uendret, allerede korrekt dekket av eksisterende allowance-strategier (ADR-014).
Dette er i hovedsak en HELT NY komponent i handicap_engine.py
(punktene 4-9), ikke en utvidelse av det eksisterende — dagens motor tar
alltid indeksen som et KJENT input; den har aldri regnet UT en indeks fra
en historie av runder. Punkt 10 er en presisering/utvidelse av
eksisterende kode for 9-hulls-tilfellet. Holdes ren og testet isolert,
som resten av handicap_engine.py (ADR-005).
Bevisst UTENFOR omfang i første byggerunde, til tross for at kilden nå finnes (for å holde v1 håndterbar — presist avgrenset, ikke bare utsatt i vage vendinger):
- Playing Conditions Calculation (PCC) (Rule 5.6) — full prosedyre funnet og forstått (statistisk sammenligning av dagens faktiske scorer mot forventet, justering −1,0 til +3,0, krever minst 8 aksepterte scorer på banen samme dag blant spillere med indeks ≤36,0). Ikke bygget nå — krever et helt annet datagrunnlag (ALLE spilte runder på en bane en gitt dag på tvers av ALLE brukere) enn det en enkelt frittstående runde naturlig gir. Runder telles inn UTEN PCC-justering til dette tas som egen, senere runde.
- Exceptional Score-reduksjon (Rule 5.9) — funnet og forstått (en differensial 7,0-9,9 slag bedre enn gjeldende indeks gir automatisk −1,0 på de siste 20 differensialene, 10,0+ gir −2,0). Ikke bygget i v1, notert for senere presisjon.
- Initial indeks fra færre enn 3 runder, Handicap Committee-skjønn (Rule 5.2a) — TeeCup har ingen "Handicap Committee"-rolle; en forenklet, automatisk variant av opptrappingstabellen brukes i stedet, uten menneskelig overstyring i v1.
Åpent, ikke besluttet: når en reell WHS-indeks kan beregnes fra
runder, skal manuell redigering av app_user.handicap_index
(eksisterende PATCH /auth/profile, ADR-031) fortsatt tillates ved
siden av (f.eks. for en spiller uten noen TeeCup-runder ennå), eller skal
feltet bli read-only/auto-beregnet så snart minst én HCP-tellende runde
finnes? Påvirker om handicap_history (018) skal gjenbrukes uendret
eller trenger en ny kolonne som skiller "manuelt satt" fra "beregnet fra
runde".
Skjemaet (migrasjon 020_personal_rounds.sql) er ✅ SKREVET OG
SCRATCH-VERIFISERT 2026-07-22, som andre byggesteg (etter motoren).
Sju nye tabeller: round (header, owner_user_id-eid, ingen RLS),
round_participant (deltakere — lenket bruker ELLER gjestenavn, XOR-
håndhevet via CHECK; maks én markert eier per runde via partiell unik
indeks), round_hole (rating-SNAPSHOT + statistikk per deltaker per
hull — GIR er bevisst IKKE en egen kolonne, kun deriverbar ved lesing:
approach_result='hit' AND (score-putts) <= par-2, verifisert eksakt
mot ekte testdata), pluss fire tabeller for den globale banekatalogen
(personal_course/_hole/_tee/_tee_rating — sistnevnte BEVISST uten
scope-kolonne, ulikt org-tabellen, siden Net-Par-tilnærmingen gjør
9-hulls-spesifikk rating overflødig). round/personal_course_* har
INGEN RLS (Beslutning A) — autorisasjon i app-laget.
Scratch-verifisert, 9 sjekker (kjørt som teecup_app_scratch, ikke
superbruker): duplikat kjønn på samme utslag avvist, teeoff+custom-felt
samtidig avvist (CHECK), verken/begge user_id+guest_name avvist (XOR-
CHECK), to markerte eiere på samme runde avvist, duplikat hullnummer per
deltaker avvist, ugyldig approach_result-verdi avvist, kaskade-sletting
av en runde fjerner alle dens deltakere+hull men lar ANDRE runder stå
urørt. test_isolation.sql fortsatt 12/12 (ingen RLS-regresjon på
eksisterende tabeller). Rullet ut mot ekte teecup_db 2026-07-22,
bruker bekreftet eksplisitt: alle sju tabeller bekreftet opprettet,
test_isolation.sql fortsatt 12/12 mot ekte database. Gjenstår:
API-lag og frontend — ingen av disse er startet.
API-laget er ✅ BYGGET, SCRATCH-VERIFISERT OG RULLET UT LIVE 2026-07-22,
som tredje byggesteg. Ny app/routers/rounds.py (registrert i main.py):
POST/GET /rounds (opprett/list egne runder), GET/DELETE /rounds/{id},
POST/DELETE /rounds/{id}/participants (kun gjester i v1, se moduldoc),
PATCH /rounds/{id}/participants/{pid}/holes/{n} (hull-for-hull-
registrering), POST /rounds/{id}/complete (kjører hele motor-kjeden:
Adjusted Gross Score → Score Differential → counts_for_handicap via
round_counts_for_handicap), pluss GET/POST /personal-courses for den
globale banekatalogen. Ny motor-funksjon lagt til underveis:
round_counts_for_handicap(played_holes_count, holes_planned) — to
distinkte terskler (Rule 2.2a: min 10/18 ved 18-hulls-intensjon; Rule
2.2b: ALLE 9 ved 9-hulls-intensjon, ikke "minst 9"), testet (2 nye
tester, 43/43 totalt i handicap_engine.py).
Reelt hull funnet OG fikset FØR API-et kunne fullføres:
round_participant manglet kolonner for selve rating-tallene (Course
Rating/Slope Rating/Par) brukt til å beregne Course Handicap — kun
round.tee_name_snapshot (navn) fantes, ikke tallene. Ny migrasjon
021_round_participant_rating_snapshot.sql (tre nye nullable kolonner)
skrevet, scratch-verifisert sammen med resten, og rullet ut.
Scratch-verifisert grundig, 26 sjekker (isolert scratch-rolle+MinIO+
engangs API-container): full livssyklus for en custom-bane-runde
(course_handicap_snapshot regnet riktig — Index 15/Slope 128/Rating
71.5/Par 72 → 16, verifisert for hånd), en gjest UTEN HCP (ingen
snapshot/differensial, teller aldri), 18/18 spilt → tellende med korrekt
differensial (16.3, verifisert for hånd), 9 av 18 spilt ved 18-hulls-
intensjon → IKKE tellende, 9 av 9 spilt ved 9-hulls-intensjon → TELLENDE
(Net Par fyller resten av de 18, se Beslutning G punkt 2), full
autorisasjons-isolasjon (en fremmed bruker avvist 403 fra både lesing og
hull-oppdatering, egen runde-liste tom), kan ikke fjerne eieren, slett-
runde-kaskade, ufullstendig profil avvist fra å opprette runde. Egen,
separat verifisering av teeoff-LIVE-oppslaget (Beslutning C) mot den
ekte kjørende teeoff_api-containeren (Borregaard Golfklubb, samme
anlegg som ADR-019s opprinnelige verifisering) — bekreftet at INGEN
course/hole/tee-rad skrives noe sted, kun et navn-snapshot
("Borregaard Golfklubb – Hovedbanen") og et rating-snapshot; en andre
deltaker lagt til samme runde utløste et FERSK, uavhengig live-oppslag
mot teeoff (ikke gjenbruk av cachet data). test_isolation.sql fortsatt
12/12.
Rullet ut mot ekte systemer 2026-07-22: migrasjon 021 kjørt mot ekte
teecup_db (kolonner bekreftet, test_isolation.sql fortsatt 12/12),
docker compose up -d --build teecup_api (kun backend — ingen frontend-
skjerm bygget for dette ennå), boot-et rent, /health//dashboard → 200,
teeoff.no upåvirket. frontend/next.config.mjs sin rewrites() fikk
/rounds/* og /personal-courses/* lagt til proaktivt (samme lærdom som
ADR-016/medlemsside-hendelsen — enhver ny API-prefiks MÅ inn her FØR en
frontend-side bygges) — denne ENDRINGEN ligger IKKE deployet ennå (ingen
frontend-kode bruker den), tas med i neste frontend-runde.
Frontend BYGGET OG RULLET UT 2026-07-23 — fjerde og siste lag
(engine → skjema → API → frontend). Tre nødvendige tillegg til
app/routers/rounds.py funnet og bygget UNDER frontend-designet, ikke
antatt på forhånd: GET /rounds/official-search/{slug} (samme
teeoff-søkemønster som courses.py, men uten org-kontekst — frittstående
runder har ingen), GET /personal-courses/{id} (detalj med
utslag+kjønn — søk-endepunktet returnerte kun navn), og
GET .../participants/{id}/holes (et reelt hull: RoundOut bar aldri
hull-nivå-data, så ingen skjerm kunne vise gjeldende tilstand ved
gjenlasting). Hull-PATCH endret til å returnere hele den oppdaterte raden
i stedet for {"ok": true}.
Reelt kontraktsfunn, bekreftet i scratch FØR frontend stolte på det:
hull-PATCH-endepunktet er IKKE et ekte delvis-PATCH — det skriver ALLE
felt ved hvert kall (arvet fra hvordan HoleUpdate-modellen alltid har
defaultverdier for utelatte felt). Et PATCH som kun sender score ville
derfor stille NULLSTILT putts og alle andre allerede lagrede felt.
Løst ved at round-detail.tsx alltid slår sammen med gjeldende
hull-data før hver PATCH, aldri sender et isolert feltnavn alene —
verifisert eksplisitt i en egen scratch-test som FØRST beviste
nullstillings-oppførselen uten merge, DERETTER beviste at
merge-mønsteret unngår den.
Nye sider: /rounds (liste over egne runder), /rounds/new
(bane-kilde teeoff vs. egen — for egen bane: søk-før-opprett, samme idé
som org-banenes gjenbrukbare katalog; utslag filtrert til kun de som har
rating for brukerens registrerte kjønn, ADR-029s automatikk-prinsipp
gjenbrukt her selv om HCP-motoren er en helt annen), /rounds/[id]
(deltaker-faner — eier + gjester, ingen ekte kontokobling i v1 per
Beslutning D; hull-navigasjon fra runde-ens starthull; slag/putt-
tallvelgere i samme visuelle stil som session-scorecard.tsx sin
StrokePicker; kølle/retning/innspill/chip/bunker/straffeslag/
putt-avstand bak en «flere detaljer»-utvidelse; GIR utledet og vist
KLIENTSIDE ved lesing, aldri lagret — nøyaktig Beslutning B sitt prinsipp;
fullfør-runde med HCP-differensial-sammendrag, låser videre redigering).
Lenket fra dashbordet som «Egne runder» (header-lenke + en egen kort på
forsiden) — bevisst adskilt navn fra det eksisterende «Mine runder»
(ADR-031, turnering-deltakelse) for å unngå at de to konseptene blandes
sammen i UI-et, selv om begge bokstavelig talt handler om "runder".
Scratch-verifisert grundig (isolert teecup_app_scratch-rolle +
isolert scratch-MinIO + engangs API-container, samme mønster som resten
av ADR-033): 22 automatiserte sjekker (egendefinert-bane-opprettelse med
to utslag/to kjønn, søk, detalj, PATCH-kontraktsbeviset over,
GIR-derivering for et konstruert par4/score3/putt1-tilfelle, gjest-
fjerning, kryss-bruker-autorisasjon 403, full fullføring med differensial)
PLUSS en separat, egen test av HELE teeoff-baserte opprettelsesløpet mot
den ekte kjørende teeoff_api-containeren (Borregaard Golfklubb, samme
anlegg som tidligere ADR-019/033-verifiseringer) som bekreftet
course_handicap_snapshot ble beregnet riktig fra live-hentet
rating. Ekte typesjekket PRODUKSJONSBUILD kjørt via
docker build --target builder (nøyaktig samme steg Dockerfile bruker
i prod, ikke next dev) — kompilerte rent, alle nye ruter listet.
Rullet ut live 2026-07-23, bruker bekreftet eksplisitt: ingen
migrasjon i denne del-runden, docker compose up -d --build teecup_api teecup_frontend, begge containere boot-et rent, /health//dashboard/
/rounds → 200 over ekte https, teeoff.no upåvirket.
Bevisst utenfor omfang, ikke bygget denne runden: ekte kontokobling
for gjeste-deltakere (Beslutning D), automatisk oppdatering av
app_user.handicap_index/handicap_history ved fullført tellende runde
(åpent spørsmål i Beslutning G, fortsatt ubesvart), shotgun-start
(egen, separat ADR-034), GPS/avstandsmåling (se eget notat i
FEATURE_BACKLOG.md, krever data ingen kilde har i dag).
Frontend ERSTATTET med V0-designet versjon 2026-07-23, samme dag som
den hånd-bygde frontend-en over ble rullet ut: brukeren påpekte
(berettiget) at de tre nye skjermene var hånd-kodet av meg direkte i
stedet for designet i V0 -- et avvik fra prosjektets etablerte mønster
gjennom HELE resten av appen. Bekreftet med bruker at arbeidsmåten
fortsatt skal være: JEG skriver V0-prompten, BRUKEREN kjører den i
v0.app og sender koden tilbake, JEG integrerer (samme flyt som alltid,
ikke endret). Skrev tre detaljerte prompter (liste/opprett/hull-
registrering, inkl. eksplisitt tilgjengelighetskrav i hver) -- bruker
lastet opp tre zip-eksporter (tee-cup-login-screen (10/11/12).zip).
Samme "full re-eksport hver gang"-mønster som ALLE tidligere V0-runder:
diffet mot levende tre FØR noe ble tatt inn -- kun fire filer var reelt
nye/relevante (round-card.tsx, own-rounds.tsx, new-round.tsx,
round-detail.tsx + tre app/rounds*/page.tsx-ruter), resten
(login-form, dashboard, config, ui/*) var forventede full-reverts og ble
IKKE tatt inn. Samme kjente V0-feil dukket opp igjen, hoppet bevisst
over: app/clubs/[id]/page.tsx med feilnavngitt [id]-parameter
(egentlig en slug) -- identisk feil som ble rettet i klubbside-runden
under ADR-018, V0 gjenskaper den tydeligvis når prosjektet re-genereres.
Mine tre hånd-bygde komponenter (personal-rounds.tsx, round-new.tsx,
og min opprinnelige round-detail.tsx) er ERSTATTET, ikke supplert --
V0s presentasjon beholdt, datalaget skrevet om fra mock til ekte fetch
(samme "V0 leverte mock, jeg kabler ekte data"-mønster som enhver annen
skjerm i appen). Reelle tilpasninger utover ren om-kabling:
- V0s teeoff-søkemodell antok hele bane+utslag-lista lå ferdig i
søkeresultatet -- det ekte API-et er et to-stegs oppslag (søk →
facility-detalj). Løst med en
resolving-tilstand iOfficialSearchStep(eneste strukturelle tillegg til V0s JSX). - La til et tredje kjønnsvalg "Annet" i gjeste-skjemaets
ChoiceRow(V0 hadde kun Mann/Kvinne) -- matcher appens ellers etablerte Herre/Dame/Annet-konvensjon (account-settings.tsxm.fl.), og API-etsGuestParticipantCreate.genderstøtter alleredex. - Beholdt merge-før-PATCH-sikringen fra forrige rundes kontraktsfunn
(hull-PATCH skriver alle felt, ikke bare det endrede) -- V0 kjente
naturligvis ikke til denne kontraktsdetaljen, lagt inn i
updateStat()uendret i prinsipp fra min opprinnelige versjon. - Fjernet V0s dev-only forhåndsvisningskontroller (
ReviewToggleiown-rounds.tsx, den nederste "Forhåndsvis: Pågår/Fullført"-linjen iround-detail.tsx) -- samme opprydningsmønster som ALLE tidligere V0-runder i prosjektet. Ingen backend-endring i denne del-runden (samme API-kontrakt som allerede var scratch-verifisert). Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen ny interaktiv nettleser-test utført (intet slikt verktøy tilgjengelig i denne økten) -- kun kodegjennomgang + typesjekk, flagget eksplisitt til bruker. Rullet ut live 2026-07-23, bruker bekreftet eksplisitt:docker compose up -d --build teecup_frontend(gjenskapte ogsåteecup_apisom vanlig compose-bivirkning, ingen backend-kode rørt), begge containere boot-et rent,/health//dashboard//rounds//rounds/new→ 200 over ekte https,teeoff.noupåvirket. De tre V0-zip-ene slettet fra prosjektroten etter fullført integrering.
Reell produksjonsbug funnet OG FIKSET 2026-07-23, rapportert av bruker
med to skjermbilder (/rounds ga "Klarte ikke å hente rundene dine",
/rounds/new sitt siste steg ga "Klarte ikke å opprette runden"):
rot-årsak var en EKSAKT navnekollisjon mellom frontend-sidens toppnivå-
prefiks (/rounds) og backend-APIets ressursprefiks (samme /rounds,
app/routers/rounds.py) -- en verre variant av den allerede dokumenterte
"afterFiles"-fellen fra ADR-016s medlemsside-hendelse, denne gangen
rammende BEGGE retninger samtidig:
/rounds(eksakt sti): en STATISK frontend-side. Statiske sider sjekkes FØR rewrites, så siden vant presedens -- klientensfetch("/rounds")/POST /roundstraff ALDRI backend, fikk Next sin egen HTML tilbake i stedet for JSON (stilleres.json()-parsefeil, fanget av try/catch, viste den generiske feilteksten)./rounds/[id](DYNAMISK side): her sjekkes rewrites FØR dynamiske sider, så rewrite-regelen vant presedens i stedet -- selve rundedetalj-SIDEN var dermed fullstendig UOPPNÅELIG (ville vist rå backend-JSON i stedet for UI-et), bekreftet direkte medcurlFØR fiksen (anonymtGET /rounds/00000000-...ga ekte backend-JSON i stedet for Next sin HTML). Det andre skjermbildets utslagsnavn "55/50/44/32" ble UNDERSØKT OG BEKREFTET Å IKKE VÆRE EN BUG -- lest direkte fra ekteteeoff_api(GET tjome-golfklubb): Tjøme Golfklubb sine faktiske utslagsnavn i teeoff ER bokstavelig talt disse tallene (lengde i hundremeter, ikke fargenavn) -- data gjengitt korrekt, ingen kode-endring nødvendig for dette punktet. Fikset ved samme prinsipp som medlemsside-hendelsen: flytt siden, ikke APIet. Alle tre frontend-rutene flyttet til et helt nytt, ikke- overlappende toppnivå-prefiks/my-rounds/*(app/rounds/→app/my-rounds/), API-et (/rounds/*) uendret. Åtte interne navigasjonsreferanser oppdatert på tvers avround-card.tsx/own-rounds.tsx/new-round.tsx/round-detail.tsx/dashboard.tsx-- ektefetch()-kall til API-et (samme filer) bevisst latt urørt, kun<Link href>/router.push/router.replaceendret. Utvidetnext.config.mjssin allerede eksisterende advarselskommentar med denne nye, verre varianten av samme fellesklasse, som en fremtidig påminnelse: et rewrite-prefiks og en frontend-sides toppnivå-segment må ALDRI være identisk streng. Verifisert presist FØR og ETTER utrulling (ikke bare "bygget uten feil"): etcurlmot ekte produksjon FØR fiksen bekreftet nøyaktig mekanismen i begge retninger (se over). Ekte typesjekket produksjonsbuild etterpå viste selv at rutetreet nå lister/my-rounds,/my-rounds/[id],/my-rounds/newi stedet for de gamle/rounds- rutene. Rullet ut live 2026-07-23, bruker bekreftet eksplisitt: kunteecup_frontend(gjenskapteteecup_apisom vanlig bivirkning, ingen backend-kode rørt). Verifisert ETTERPÅ med et nytt settcurl-kall mot ekte https: anonymtGET /roundsga nå korrekt backend-JSON (NOT_AUTHENTICATED, IKKE Next sin HTML som før), anonymtGET /rounds/<uuid>fortsatt korrekt backend-JSON (uendret, som forventet), og -- den avgjørende nye sjekken -- anonymtGET /my-rounds/<uuid>ga nå faktisktext/html(selve React-siden, ikke lenger uoppnåelig)./dashboard//my-rounds//my-rounds/new→ 200,teeoff.noupåvirket.
Nok en runde brukerrapporterte punkter, ALLE BYGGET OG LIVE 2026-07-24,
samme dag: (1) Utslagstidspunkt (round.started_at, migrasjon
023_round_start_time.sql, valgfritt) + tidsbruk beregnet klientside
som completed_at − started_at når runden fullføres. Bekreftet
eksplisitt med bruker: "Ferdig"-tidspunktet er den ALLEREDE eksisterende
"Fullfør runde"-knappen, ingen ny handling/kolonne. (2) "Idx" i hull-
overskriften byttet til "Hcp". (3) Slag-tastaturets numpad merker nå
knappen som tilsvarer hullets par med en liten "par"-bildetekst.
(4) Hull-navigasjonens "Forrige"/"Neste" respekterte tidligere ALLTID
18 hull uansett holes_planned -- en 9-hulls runde 10-18 hoppet feilaktig
til hull 9 ved "Forrige" fra hull 10. Fikset: navigasjonsrekkefølgen
bygges nå med holes_planned som lengde, ikke hardkodet 18.
(5) Putter/Chip/Bunker/Straffeslag/Anywayslag kan nå aldri velges høyere
enn antall registrerte slag på hullet (NumberPicker fikk en
maxValue-prop, Stepper en max-prop) -- ingen vits i å tilby et
selvmotsigende tall. (6) "Slett runde" bygget i UI-et (backendens
DELETE /rounds/{id} fantes fra før, men hadde aldri fått en
frontend-knapp) -- bekreftelsesdialog, sletter for alle (kaskade
fjerner automatisk alle deltakere/hull, guest-spillere har uansett ingen
egen konto å bevare noe for).
(7) Ny PATCH /rounds/{id} -- retter opp feil bane/utslag eller
feil antall hull ETTER opprettelse, uten å røre allerede registrerte
slag/putter/etc. Speiler samme filosofi som turnering-øktenes
_remap_course (ADR-tidligere runde), men enklere: ingen tee-navn-
matching på tvers av kjønn siden hver deltaker valideres eksplisitt mot
den NYE banens rating for sitt eget kjønn FØR noe skrives (hele byttet
avvises 400 hvis ÉN deltaker ville mistet HCP-sporing). Bevisst
AVVIST (409) etter at runden er fullført -- ulikt turnering-øktenes
bane-bytte, som bevisst tillater dette selv etter avgjørelse; her er
omfanget mindre (ingen re-beregning av differensial bygget for dette
tilfellet). Frontend: ny "Rediger runde"-seksjon på rundesiden (antall
hull som enkelt 9/18-valg, "Bytt bane" som en kompakt søke-flyt --
teeoff-søk ELLER egen-bane-søk, samme mønster som ved opprettelse, bare
kondensert). Etter et vellykket bytte hentes hull-data på nytt for
ALLE allerede lastede deltakere (par/stroke-index kan ha endret seg for
alle, ikke bare aktiv spiller).
Scratch-verifisert grundig (isolert teecup_app_scratch-rolle +
isolert scratch-MinIO + engangs API-container): 22 sjekker for
rediger/slett-rundene (bane-bytte med FAKTISK ulike par/stroke-index-
verdier mellom to egendefinerte baner, bekreftet at allerede registrerte
slag på hull 1-3 var UENDRET etter byttet mens par/stroke-index OG
course_handicap_snapshot var oppdatert; avvist bytte til en bane uten
rating for deltakerens kjønn, bekreftet at INGENTING ble endret ved
avvisning; avvist bane/hull-endring etter fullføring, 409; slett-runde
- idempotent 404 + kryss-bruker-autorisasjon 403) pluss 8 sjekker for
utslagstid/tidsbruk.
test_isolation.sql12/12. Ekte typesjekket produksjonsbuild kompilerte rent begge ganger. Rullet ut mot ekte systemer 2026-07-24, bruker bekreftet eksplisitt: migrasjon 023 kjørt mot ekteteecup_db(kolonne bekreftet,test_isolation.sqlfortsatt 12/12), deretterdocker compose up -d --build teecup_api teecup_frontend, begge containere boot-et rent,/health//dashboard//my-rounds→ 200 over ekte https,teeoff.noupåvirket.
To til brukerpunkter, ALLE BYGGET OG LIVE 2026-07-25, samme dag:
(1) PATCH /rounds/{id} utvidet med start_hole/started_at/
completed_at -- start_hole kan rettes uansett (ren metadata, aldri
sperret av fullført-status), started_at kan justeres når som helst,
completed_at kan KUN justeres på en runde som allerede ER fullført
(avvist 400 ellers -- denne PATCH-en fullfører aldri runden selv, kun
korrigerer et allerede satt tidspunkt fra "Fullfør runde"). Ny
validering: completed_at må være etter started_at, ellers 400. Løser
brukerens konkrete case: glemmer å trykke "Fullfør runde" i flere timer,
vil rette opp tidsbruken i etterkant. EditRoundPanel sin "Bane/antall
hull"-blokk forblir sperret post-fullføring (uendret fra forrige runde),
mens et nytt tidspunkt-skjema (Utslagstid alltid, Fullført-tidspunkt kun
hvis fullført) nå vises uansett fullført-status.
(2) Nærmeste offisielle baner i "Ny runde"-flyten -- nytt
GET /rounds/official-search/nearby?lat=&lng=&limit= (Haversine-formel,
sortert stigende, MÅ registreres FØR /rounds/official-search/{slug} i
routeren, samme presedens-lærdom som ADR-020s "by-code"). Henter ALLE
174 teeoff-anleggenes lat/lng via samme search_facilities("")-kall som
allerede finnes (ingen ny teeoff-avhengighet), beregner avstand i Python
per forespørsel (ingen cache -- datasettet er lite nok). Frontend: ny
NearbyClubs-komponent i OfficialSearchStep, ber om
navigator.geolocation ved mount, viser inntil 5 nærmeste med
nærmeste tydelig merket + avstand (m under 1 km, ellers km) -- rett
FØR søkefeltet, som bedt om. Avslått/manglende posisjon feiler helt
stille (ingen feilmelding), søket fungerer uendret som fallback.
Scratch-verifisert: 14 sjekker (start_hole-endring, tidspunkt-
korreksjon i begge retninger inkl. de to nye valideringsreglene,
holes_planned fortsatt sperret post-fullføring uendret, OG et ekte
nearby-kall mot den kjørende teeoff_api-containeren fra Tjømes egne
koordinater som korrekt fant Tjøme selv som nærmeste/nest-nærmeste
treff). test_isolation.sql 12/12 (ingen skjemaendring). Ekte
typesjekket produksjonsbuild kompilerte rent.
Rullet ut live 2026-07-25, bruker bekreftet eksplisitt: ingen
migrasjon, docker compose up -d --build teecup_api teecup_frontend,
begge containere boot-et rent, /health//dashboard//my-rounds/new
→ 200, teeoff.no upåvirket.
Reell UX-bug funnet OG fikset, samme dag 2026-07-25, rapportert av
bruker med skjermbilder av en konkurrerende golf-app (Golf Game Book)
som referanse: brukeren rapporterte at "All statistikk" ikke viste
noe utover slag/putter under selve registreringen. Bekreftet direkte
mot ekte teecup_db (kun lesing) at dette IKKE var en datafeil --
brukerens faktiske runde hadde stat_level='full' lagret korrekt. Rot-
årsaken var et REELT presentasjonsproblem: alle detaljfeltene (kølle,
utslag, innspill, chip, bunker, straffeslag, putt-avstand, anywayslag)
lå bak en "Score"/"Statistikk"-fane (V0-runden 2026-07-24s bevisste
designvalg) som brukeren aldri oppdaget -- "aktivert full statistikk"
ga i praksis ingen synlig endring uten et ekstra, ikke-annonsert
tastetrykk. Fikset ved å fjerne fane-løsningen helt: alle feltene
vises nå ALLTID samlet under slag/putter i én sammenhengende scroll når
statLevel==="full" (samme prinsipp brukeren opprinnelig ba om FØR
V0-rundens sveip/fane-vurdering) -- fjerner enhver tvetydighet om
hvorvidt statistikken faktisk er slått på. PanelTabs-komponenten og
panelTab-state fjernet som død kode.
Samtidig bygget: "Så langt i runden"-oversikt, direkte etterspurt
("jeg trenger et grensesnitt som viser scoren min så langt") med
referansebildene som inspirasjon for FUNKSJONEN (ikke kopiert
utseendemessig). Ny komponent ScoreSoFar i round-detail.tsx: en
kompakt alltid-synlig linje ("Så langt: X hull · Y slag · +Z til par")
rett under spillerfanene, pluss en "Vis full oversikt"-knapp (samme
etablerte mønster som session-scorecard.tsx sin HoleSummaryTable
for turnering-scoring) som åpner en tabell: hull, par, score, netto,
løpende sum. Ny backend-beregning for å muliggjøre netto-kolonnen:
RoundHoleOut fikk et nytt felt strokes_received (list_holes i
app/routers/rounds.py) -- utledet fra deltakerens
course_handicap_snapshot + hullenes stroke_index via den
ALLEREDE eksisterende allocate_strokes_by_index() fra
handicap_engine.py (samme allokeringsalgoritme som brukes overalt
ellers i appen, ingen ny logikk) -- None for en deltaker uten
beregnet HCP (f.eks. en gjest uten oppgitt handicap), aldri lagret,
kun beregnet ved lesing.
Scratch-verifisert grundig (isolert teecup_app_scratch-rolle +
isolert scratch-MinIO + engangs API-container): 11 sjekker, inkl. et
presist talleksempel (course rating 72.0/slope 113/HCP 10.0 → course
handicap nøyaktig 10, bekreftet at de 10 laveste stroke-indeksene fikk
nøyaktig 1 slag hver og de resterende 8 fikk 0, sum(strokes_received)
== course_handicap), et registrert hull som ga korrekt netto (score 5 −
1 mottatt slag = 4), og en gjest UTEN HCP som korrekt fikk
strokes_received: null på alle 18 hull. test_isolation.sql 12/12
(ingen skjemaendring). Ekte typesjekket produksjonsbuild kompilerte
rent.
Rullet ut live 2026-07-25, bruker bekreftet eksplisitt: ingen
migrasjon, kun teecup_api+teecup_frontend redeployet.
/health//dashboard → 200, teeoff.no upåvirket.
Reell produksjonsregresjon rapportert AV BRUKEREN samme dag, rett
etter utrulling over, funnet og fikset umiddelbart: "Ingenting er
klikkbart i den avanserte statistikken." Første antagelse (frontend-
CSS/event-håndtering) ble IKKE bekreftet ved kodegjennomgang -- all
onClick-kabling i DirectionCross/Stepper/ChoiceRow/NumberPicker
var korrekt. Root cause funnet ved å faktisk GJENSKAPE brukerens
klikk-sekvens mot en fersk, isolert scratch-container (samme mønster
som resten av runden) i stedet for å gjette videre: update_hole
(PATCH-endepunktet for hull-registrering) konstruerte fortsatt
RoundHoleOut(**dict(row)) UTEN det nye påkrevde
strokes_received-feltet fra runden rett over -- en Pydantic
ValidationError (500) på HVER ENESTE hull-lagring, ikke bare de
"avanserte" feltene. Frontend sin updateStat() svelger !res.ok
stille uten feilmelding, så symptomet fremsto nøyaktig som "ingenting
skjer når jeg trykker" for ALLE felt (Slag/Putter inkludert) -- brukeren
merket det trolig først på de avanserte feltene siden Slag/Putter fra
TIDLIGERE runder allerede hadde lagrede verdier som så riktige ut ved
åpning.
Fikset: update_hole beregner nå strokes_received for akkurat det
oppdaterte hullet (henter deltakerens course_handicap_snapshot +
ALLE 18 sine stroke_index -- samme allokeringsalgoritme som
list_holes, siden fordelingen avhenger av hele rundens
stroke-indeks-rekkefølge, ikke bare ett hull) og sender den med i
responsen.
Scratch-verifisert på nytt, presist mot akkurat denne regresjonen:
14 sjekker som gjenskaper brukerens EKSAKTE klikk-rekkefølge (Slag →
kølle → Utslag-retning → Innspill-retning → Chip → første putt-bøtte →
Anywayslag, pluss et klikk på et avansert felt FØR Slag i det hele tatt
er satt, og en gjest uten HCP) -- alle 200 med riktig ekko, GET etterpå
bekrefter faktisk lagring, gjest uten HCP gir korrekt strokes_received: null uten å krasje. test_isolation.sql uendret (ingen skjemaendring,
ren Python-fiks). Rullet ut live 2026-07-25, bruker bekreftet
eksplisitt: kun teecup_api redeployet, /health//dashboard → 200,
teeoff.no upåvirket.
Rediger/Fullfør/Slett tonet ned og flyttet til toppen + auto-scroll ved
hull-bytte, LIVE samme dag (2026-07-25): brukeren rapporterte at
"Fullfør runde"/"Slett runde" lå som store, fremtredende knapper RETT
under "Neste hull"-navigasjonen nederst i hull-panelet -- altfor lett å
trykke feil ved et uhell mens man bare skulle bla mellom hull. Flyttet
alle tre (Rediger/Fullfør/Slett) til en samlet rad ØVERST på siden,
tonet ned til samme nøytrale "trigger"-stil (ikke lenger store,
fargede knapper) -- brukeren må nå aktivt scrolle OPP forbi hele
hull-registreringen for å nå dem. Samtidig rapportert, i samme runde:
"Neste hull"/"Forrige" lastet nytt innhold, men brukeren ble stående
scrollet nede der knappene er, med det nye hullets Slag-felt utenfor
skjermen. Løst med holePanelRef + scrollIntoView({block:"start"})
kalt fra goPrev/goNext OG fra hull-navigasjonens direkte hull-valg,
med scroll-mt-28 på panelet for å unngå at den sticky headeren dekker
toppen. Ren frontend-endring, ingen backend/migrasjon. Ekte typesjekket
build kjørt og bekreftet (alle 19 ruter), rullet ut, teeoff.no
upåvirket.
"Så langt i runden" utvidet med netto/stableford-sum, putt-/kølle-
statistikk-totaler og grafisk fairway-/innspill-fordeling, LIVE samme
dag (2026-07-25): brukeren etterspurte flere detaljer i
rundeoversikten: netto- og stableford-sum, hvor man kan se totalt antall
putter/chip/bunker/straffeslag/anywayslag for runden, grafisk fremstilt
prosentvis fordeling av fairwaytreff og innspillstreff, og gjennomsnittlig
score til par (to desimaler) splittet på fairwaytreff-vs-bom og
innspill-treff-vs-bom.
Ingen backend-endring nødvendig -- alt beregnes klientside fra data
GET .../holes allerede returnerer (score/par/strokes_received/putts/
chip_count/bunker_shot_count/penalty_strokes/tee_shot_result/
approach_result/anyway_strokes). ScoreSoFar-komponenten
(round-detail.tsx) utvidet med: en StatPill-rutenett (Slag/Til par/
Netto/Stableford/Putt/Chip/Bunker/Straffeslag/Anywayslag -- hver vist
kun hvis det faktisk finnes registrert data for akkurat det feltet), en
ny Stableford-kolonne i hull-for-hull-tabellen (kun når netto er
beregnbart), og to nye DistributionBar-seksjoner (Fairwaytreff/
Innspill) -- generalisert N-kategori-variant av samme visuelle idé som
SegmentedBar i tournament-leaderboard.tsx (ett fargesegmentert
rektangel + prosent-legend), ikke noe chart-bibliotek lagt til.
Presisert i kode-kommentar, bevisst valg: appen har ingen egen
"spilleform"-innstilling for frittstående runder -- stableford beregnes
derfor alltid ut fra netto score når det er mulig (krever registrert
HCP, samme forutsetning som netto), uavhengig av om brukeren
"egentlig" spiller slagspill eller stableford. Standard stableford-
poengtabell brukt (netto par = 2 poeng, ett poeng mer/mindre per slag
bedre/dårligere enn par, gulv på 0).
Gjennomsnittlig score-til-par (fairway/innspill, treff-vs-bom) bruker
BRUTTO score (ikke netto) relativt til par, formatert med fortegn og to
desimaler (formatSignedAvg), matcher brukerens eksplisitte
spesifikasjon. Fairwaytreff ekskluderer par-3-hull (samme regel som
registrerings-skjemaet, som aldri viser Utslag-retning der).
Logikken verifisert manuelt mot et regnet eksempel (4 hull, blandet
par 3/4/5, ulike utslag-/innspillresultater) FØR utrulling -- stableford-
sum, netto-sum og begge gjennomsnitts-splittene stemte med
håndregning. Ekte typesjekket produksjonsbuild kjørt og bekreftet (alle
19 ruter). Rullet ut live 2026-07-25, bruker bekreftet eksplisitt:
ren frontend-endring, ingen migrasjon, /health//my-rounds → 200,
teeoff.no upåvirket.
Rundestatistikk-skjerm (dypdykk), inspirert av en konkurrent-video, BYGGET OG LIVE samme dag (2026-07-25): brukeren lastet opp en 26 sekunders skjermopptaksvideo av en KONKURRENT-apps statistikkskjerm og ba eksplisitt om et V0-prompt inspirert av innholdet, IKKE et plagiat. Video analysert bilde for bilde (ffmpeg kjørt i en engangs Docker- container, ikke installert på verten -- ryddet opp etterpå). Innhold identifisert: score-fordeling (par/bogey/dobbel bogey/verre), snitt-til-par per hulltype, fairwayfordeling + score-splitt, GIR-donut + per-hulltype + kryss med fairway + score-splitt, bom-retning på green som et kompass-diagram, putt-fordeling (1/2/3-putt) + snitt per hulltype
- med/uten GIR, én-putt% etter puttlengde + lengdefordeling hit/miss,
chip-fordeling, scrambling%/sand save%, bunker/straffeslag per runde +
score-splitt.
Bevisste valg for å unngå plagiat, skrevet inn i selve V0-promptet:
egen visuell identitet (TeeCups presise grønn/oransje, ikke konkurrentens
fargekoding), fritt valg av chart-type/layout/rekkefølge til V0 selv,
INGEN kopiering av konkurrentens eksakte ordlyd/fargekoding/ikonografi.
Puttlengde-bøttene i promptet er TeeCups EGNE seks bøtter
(<1m/<2m/<3m/<5m/<8m/8m+, samme som ADR-033 allerede lagrer) -- bevisst
ANDRE enn videoens fem bøtter (<1m/1-2/2-4/4-8/+), både fordi det unngår
en direkte kopi og fordi det er det datamodellen faktisk allerede
produserer. "Lengste drive" (krever GPS/avstandsmåling appen ikke har)
utelatt fra promptet.
Zip 14 mottatt og integrert samme dag: diffet mot live-treet FØR noe
ble tatt inn (samme rutine som alltid) -- kun to reelt nye filer
(
components/round-stats.tsx, en nyRoundStats-skjerm bygget med kollapsbareStatCard-seksjoner, conic-gradient-donuter med sentertall, avviks-stolper fra en null-linje, og et kompass-rutenett for bom-retning -- en tydelig ANNEN visuell løsning enn konkurrentens skjermbilder, ikke en klone). Resten av eksporten var V0s vanlige uvitende reverts, inkl. sin egen/rounds/[id]-ruteversjon fra FØR/my-rounds-omdøpingen (se lenger opp, "Reell produksjonsbug...") -- korrekt hoppet over. Ny rute lagt til somapp/my-rounds/[id]/stats/page.tsx(ikkeapp/rounds/[id]/statssom eksporten foreslo), samme kollisjon-unngåelse.app/globals.csssin nye--chart-1..6data-viz-fargeskala (god->dårlig-skala, brukt sammen med tekst-/tall-etiketter) slått sammen inn i alle tre temablokkene; V0s egne, mer omtrentlige--primary/--ring/--brand-orange-verdier i SAMME diff ble bevisst IKKE tatt inn -- beholdt de presise OKLCH- verdiene utregnet i ADR-016. Datalag skrevet fullstendig om fra mock: nycomputeStats()- funksjon iround-stats.tsxregner ut alt fra råGET .../holes-data (samme endepunkt round-detail.tsx allerede bruker -- ingen ny backend). Hver seksjon skjules helt når det ikke finnes nok data for den (samme "vis kun det som faktisk finnes"-prinsipp somScoreSoFar). Ny enkel spillervelger (pill-rad) lagt til når runden har flere deltakere, default til eieren.CompletedBanneri round-detail.tsx fikk en "Se full rundestatistikk"-lenke (fantes i V0s egen, ellers reverterte fil -- portert manuelt inn i vår live versjon i stedet for å ta hele filen). Matematikken verifisert FØR utrulling, ikke bare "kompilerer":computeStats()-logikken portert til et frittstående Node-script og kjørt mot et hånd-etterregnet 6-hulls syntetisk datasett (blandet par 3/4/5, fairwaytreff/-bom, GIR-treff/-bom, bunkerslag) -- alle utledede tall (score-kategorier, snitt-til-par per hulltype, fairway-splitt, GIR%/kryss-fairway/score-splitt, bom-retning, putt-fordeling, scrambling%, sand save%) stemte med manuell utregning. Ekte typesjekket produksjonsbuild kjørt og bekreftet (/my-rounds/[id]/statslistet som ny rute). Rullet ut live 2026-07-25, ren frontend-endring, ingen migrasjon,/health//my-rounds→ 200,teeoff.noupåvirket.
Rundeliste-info + scorekort-redesign, BYGGET OG LIVE samme dag
(2026-07-25): brukeren delte en ny skjermopptaksvideo (video 2, 18
frames analysert ved 1,5 fps) av to problemer: "Egne runder"-listen
manglet ALL score-informasjon (kun tee/hull/dato/spillerantall/status),
og selve scorekort-registreringen (rapportert "veldig dårlig
designet") stablet fem ulike kontrolltyper (numpad, retningskors,
steppere, pill-rutenett, pill-rad) uten hierarki, pluss at "Runde
fullført"-siden gjentok NØYAKTIG samme tall tre ganger (CompletedBanner-
differensial, "Så langt"-oppsummeringslinjen, og StatPill-rutenettet
under). Bedt om å reflektere kort og eventuelt skrive V0-prompt(er).
Reflektert og handlet i samme runde (ikke bare skrevet ned): pekte ut
at score/til-par mangler helt fra listekortet som det klareste,
konkrete hullet; anbefalte å samle alt utover Slag/Putter bak én
kollapsbar "Flere detaljer"-seksjon som den viktigste enkeltfiksen for
scorekort-rotet; og fikset duplikat-problemet PÅ "Runde fullført"-siden
DIREKTE (ikke via V0) siden det bare er snakk om å SKJULE innhold, ikke
designe noe nytt: ScoreSoFar skjules nå helt når runden er fullført
({!completed && <ScoreSoFar .../>}) -- CompletedBanner sin "Se full
rundestatistikk"-lenke dekker akkurat det samme, langt grundigere.
Ny backend-beregning FØR V0-prompten (slik at prompten kunne
referere til ekte, tilgjengelig data): _load_round_out
(app/routers/rounds.py) kjører nå en liten aggregatspørring mot
round_hole for eierens egen deltaker-rad ved hver lasting --
owner_holes_played/owner_total_score/owner_score_to_par (null
til minst ett hull er registrert). Verifisert med 10 scratch-sjekker,
inkl. at en gjests egen score IKKE lekker inn i eierens aggregat.
To V0-prompter skrevet (round-card-redesign med prominent
score-flis/hull-fremdrift-bar/differensial; scorekort-redesign med
Slag+Putter+Avstand-første-putt alltid synlig og alt annet bak "Flere
detaljer", pluss en bevisst ikke-bygget reservasjon av layout-plass for
en fremtidig avstandsmåling-funksjon brukeren bekreftet er planlagt).
Zip 15 og 16 mottatt samme dag (samme v0.app-prosjekt, kontinuerlig
-- round-card.tsx/own-rounds.tsx byte-for-byte identiske i begge
zip-ene; zip 16s round-detail.tsx var den nyeste med selve
kollaps-redesignet, zip 15s tilsvarende fil manglet det og ble derfor
forkastet til fordel for zip 16).
round-card.tsx: ny ScoreTile -- prominent resultat+til-par-flis
(fargekodet under/over par uten å stole på farge alene, tekst+tall
alltid med) for fullførte/scorede runder, en hull-fremdrift-progressbar
("6/18 hull spilt") for runder som fortsatt pågår, pluss en HCP-
differensial-chip i metadata-raden når runden faktisk telte. Fullt
aria-label på hele kortet for skjermlesere. own-rounds.tsx sitt
datalag skrevet om fra mock til ekte fetch, kobler de nye owner_*-
feltene fra backend + eierens score_differential (kun vist når
counts_for_handicap).
round-detail.tsx: ny kollapsbar "Flere detaljer"-seksjon (lukket som
default, ChevronDown-rotasjon på toggle) som nå rommer kølle/utslag-
retning/innspill-retning/chip-bunker-straffeslag/anywayslag -- Slag,
Putter og Avstand første putt forblir alltid synlig over kollapsen,
uendret rekkefølge fra den forrige runden. Viktig integrasjonsdisiplin:
V0s eksport hadde reversert til en MYE eldre mock-baseline (manglet
StatLevel-gating, Anywayslag, bøtte-basert puttlengde, kølle-bag fra
profil, maxValue-capping, "Hullet er spilt"-avkrysningen som bevisst BLE
FJERNET en tidligere runde, "Tid brukt" i CompletedBanner) -- kun selve
kollaps-mekanismen og plasseringen av "Flere detaljer" ble hentet ut og
lagt oppå den fullt oppdaterte, LIVE koden. Ingen av de tidligere
byggede funksjonene gikk tapt. Lagt til en kort kode-kommentar (ikke
noe bygget UI) som reserverer plass ved siden av hull-headeren til en
fremtidig avstandsmåling-indikator.
Verifisert: ekte typesjekket produksjonsbuild kjørt og bekreftet
(alle 19 ruter). Ingen ny scratch-backend-runde nødvendig utover
owner-score-testen over (ingen ny domenelogikk i selve rundeliste-/
scorekort-visningen, kun lesing av allerede-testede felt).
Rullet ut live 2026-07-25, ren frontend-endring + den lille
backend-tilføyelsen over, ingen migrasjon, /health//my-rounds →
200, teeoff.no upåvirket.
Manglende scorekort på statistikk-siden, funnet og fikset SAMME dag
(2026-07-25): brukeren spurte rett etter forrige runde: "Hvor er
scorekortet? (Gjerne også med statistikk?)" -- et ekte, uforutsett hull
i forrige rundes endring. Da ScoreSoFar ("Så langt i runden") ble
skjult for fullførte runder (for å fjerne dobbel informasjon, se over),
forsvant OGSÅ det eneste stedet den rå hull-for-hull-tabellen (Hull/
Par/Score/Netto/Sum) fantes -- ingen tilsvarende tabell ble noensinne
lagt til på den nye round-stats.tsx-siden, som kun inneholdt UTLEDET/
aggregert statistikk (donuter, kategori-stolper, avviks-visualiseringer),
aldri de faktiske rå tallene per hull. Med andre ord: brukeren fikk
riktignok fjernet duplikatet, men mistet samtidig tilgang til selve
scorekortet -- en reell regresjon, ikke bare en presentasjonsdetalj.
Fikset ved å legge til en ny "Scorekort"-seksjon FØRST på
round-stats.tsx (før "Scorer"-kategoriseksjonen), åpen som default
(i motsetning til resten av seksjonene som starter kollapsbare men
åpne -- denne er det brukeren eksplisitt spurte etter, så den skal ikke
kreve et ekstra trykk). Samme tabellmønster som ScoreSoFar sin
tidligere tabell hadde (Hull/Par/Score/Netto/Stableford/Sum, Stableford-
kolonnen vist kun når netto er beregnbart). To type-tilføyelser var
nødvendig: start_hole: number lagt til round-stats.tsx sin
ApiRound-type (manglet fra før, siden ingen tidligere seksjon på
denne siden trengte rundens faktiske start-/rekkefølge) og
strokes_received: number | null lagt til ApiHole-typen (feltet kom
allerede fra backend -- lagt til i forrige runde for list_holes --
men ble aldri lest av denne siden før nå). Ny holeOrder/
orderedHoles-beregning i RoundStats gjenbruker EKSAKT samme
sirkulære start_hole-formel som round-detail.tsx, slik at en 9-hulls
runde som starter på hull 10 vises i riktig spillerekkefølge (10-18),
ikke bare rå hullnummer 1-9.
Lærdom notert for fremtidige runder: når et helt panel flyttes/
skjules for å fjerne duplisert informasjon, må hver del av det gamle
panelet spores til et nytt hjem FØR det fjernes -- ikke bare de delene
som åpenbart var "statistikk". Denne runden ble oppdaget kun fordi
brukeren faktisk lette etter scorekortet rett etterpå, ikke ved egen
verifisering før utrulling.
Ekte typesjekket produksjonsbuild kjørt og bekreftet. Rullet ut live
2026-07-25, ren frontend-endring, ingen backend/migrasjon,
teeoff.no upåvirket.
Scorekort-visning: ny presentasjonsregel + V0-prompt skrevet, IKKE
bygget ennå (2026-07-25): brukeren delte et referansebilde av et
tradisjonelt fysisk golf-scorekort (horisontal layout, hull 1-9/10-18
som KOLONNER, Slope/Par/Score/Net som rader, "Ut"/"Inn"-sum-kolonne
YTTERST TIL HØYRE for hver ni-hulls-halvdel) og formulerte en generell
presentasjonsregel: listes hullene horisontalt (som kolonner), skal
summeringen stå TIL HØYRE; listes hullene vertikalt (som rader), skal
summeringen stå UNDER. Den nylig byggede "Scorekort"-seksjonen på
round-stats.tsx (se punktet rett over) lister hull VERTIKALT (én rad
per hull) med en løpende sum-KOLONNE innimellom hver rad -- ikke i tråd
med regelen (en vertikal liste burde hatt en avsluttende sumrad
UNDERST, ikke en kolonne). Bedt om å tenke gjennom dette og skrive et
V0-prompt for et "fabelaktig" scorekort inspirert av (ikke et plagiat
av) referansebildet.
Bevisste avvik fra referansebildet for å unngå plagiat, skrevet inn i
selve promptet: egen visuell identitet (TeeCups grønn/oransje, ikke
bildets rød-sirkel/blå-firkant-fargekoding for birdie/bogey), egen
term ("Hcp" for hullets slag-fordelings-rangering -- IKKE "Slope", som
i golf-terminologi betyr banens/tee-ens helhetlige vanskelighetsgrad,
et helt annet tall enn det bildet faktisk viser per hull), en ekstra
Stableford-rad (finnes ikke i referansen, men vi beregner det allerede
andre steder), og INGEN kopiering av bildets spiller-header-komposisjon
(navn/HCP/posisjon-boksen) -- kun selve tabell-strukturen er
inspirasjonskilden.
Presist håndtert i promptet, IKKE triviell: appen støtter allerede
vilkårlig start_hole + holes_planned (9 ELLER 18, sirkulær
rekkefølge, se "Manglende scorekort"-punktet over) -- en STIV
fysisk "hull 1-9 er alltid Ut" ville vært feil for en 9-hulls runde som
starter på hull 10. Promptet ber derfor om ÉN 9-kolonners blokk når
holes_planned=9, TO blokker (første/andre halvdel AV SPILLEREKKEFØLGEN,
ikke nødvendigvis fysisk hull 1-9/10-18) når holes_planned=18 -- den
faktiske hull-til-blokk-tildelingen løses av meg i datalaget ved
integrering, som med alle tidligere V0-runder.
Venter på V0-eksport før noe bygges. Når den kommer, erstatter den
den eksisterende vertikale "Scorekort"-tabellen på round-stats.tsx
(bygget rett over samme dag) -- ikke en ny, separat skjerm.
Notert som en generell prinsipp-lærdom, relevant utover akkurat dette
scorekortet: samme horisontal-til-høyre/vertikal-til-under-regel bør
vurderes senere for turnering-scorekortet (session-scorecard.tsx,
match-play) den dagen det scorekortet også skal pusses -- ikke i
omfang nå, bare notert for konsistens.
Presisering av promptet samme dag, FØR noe ble sendt til V0: brukeren spurte eksplisitt om et "if REALLY necessary"-unntak for horisontal scroll (opprinnelig formulering) faktisk fanget opp målet om ALDRI å måtte scrolle. Vurdert og svart nei -- reell breddekonflikt, ikke bare en formulering-detalj: 9 hull + 1 sum-kolonne (+ en label-kolonne) får ikke plass på en telefonskjerm med normal "lesbar uten briller"-tekststørrelse, og et unntak formulert som en myk fallback ville sannsynligvis latt V0 falle tilbake til scroll uansett, siden tilgjengelighetskravet rett under ga et påskudd. Løst ved å gjøre "ingen scroll" til et HARDT krav i promptet, OG eksplisitt fortelle V0 hvordan det oppnås (kompakte, fete, høykontrast-siffer i selve rutenettet -- samme konvensjon som et fysisk scorekort bruker, også synlig i brukerens eget referansebilde -- mens "lesbar uten briller"-kravet eksplisitt avgrenses til labels/knapper/løpetekst, ikke hvert enkelt rutenett-siffer). Smale forkortede rad-labels ("Hcp", "Par", "Score", "Netto") i en trang venstre-gutter i stedet for en bred tekstkolonne, for å frigjøre bredde til de 9 hull-kolonnene.
Zip 17 mottatt, horisontalt scorekort BYGGET OG LIVE samme dag
(2026-07-25): V0 leverte akkurat det det reviderte promptet ba om --
en EKTE HTML <table> med <colgroup> (fast w-9-label-kolonne, auto
for hver hull-kolonne, w-[11%] sum-kolonne) og table-fixed, ingen
scroll-container noe sted. Score-cellene bruker FORM (sirkel = under
par, firkant = over par) + fylt/ufylt (fylt = 2+ slag av) i stedet for
farge alene, pluss en egen liten symbolforklaring under rundesammendraget
-- tilfredsstiller "aldri stole på farge alene" uten å kopiere
referansebildets rød-sirkel/blå-firkant-konvensjon.
Egen, ny dedikert side /my-rounds/[id]/scorecard
(components/round-scorecard.tsx, ny app/my-rounds/[id]/scorecard/ page.tsx) -- IKKE slått sammen inn i round-stats.tsx, siden V0
designet komponenten med sin egen fulle side-chrome (sticky header,
rundesammendrag-strip), ikke som et innebygd tabell-fragment. Dette gir
en klar arbeidsdeling: round-scorecard.tsx = det rå scorekortet,
round-stats.tsx = utledet/aggregert statistikk -- samme prinsipp som
allerede etablert for ScoreSoFar vs. round-stats.tsx tidligere denne
økten.
Bevisst forkastet fra V0s eksport: V0s egen strokesReceived()-
funksjon var en generisk Math.floor(hcp/18) + (index <= hcp%18 ? 1 : 0)-modulo-formel -- byttet ut med backend sin ALLEREDE beregnede
strokes_received per hull (samme allocate_strokes_by_index() som
resten av appen bruker), for å unngå to parallelle, potensielt
avvikende implementasjoner av HCP-slagfordeling i samme app. Samme
sirkulære start_hole-rekkefølge som round-detail.tsx/
round-stats.tsx bruker for Ut/Inn-blokkene. Ny spillervelger
(pill-rad) lagt til for runder med flere deltakere, samme mønster som
round-stats.tsx.
Opprydning i round-stats.tsx: den midlertidige vertikale
"Scorekort"-tabellen (bygget tidligere samme dag som en rask fiks for
"Hvor er scorekortet?") er FJERNET og erstattet med en enkel, prominent
"Se scorekort"-lenke til den nye siden, rett under rundesammendraget --
round-stats.tsx er dermed nå rendyrket aggregert/utledet statistikk,
det rå scorekortet finnes kun ett sted. stablefordPoints()-
hjelpefunksjonen og holeOrder/orderedHoles-beregningen i
round-stats.tsx, som kun eksisterte for den fjernede tabellen, fjernet
som dødt kode.
V0 la selv til en to-knappers layout i CompletedBanner
(round-detail.tsx) -- "Se scorekort" (primær, fylt) ved siden av den
eksisterende "Se full rundestatistikk" (sekundær, omrisset) -- tatt inn
uendret bortsett fra å rette /rounds/...-hrefs til /my-rounds/....
Samtidig, urelatert, rapportert av bruker midt i integreringen:
Anywayslag manglet helt fra round-stats.tsx sin "Chip, bunker og
straffeslag"-seksjon (verken anyway_strokes i ApiHole-typen eller
noe utledet total fantes). Lagt til anyway_strokes i typen, ny
anywayPerRound-aggregat i computeStats().
Feilrettet SAMME dag, rett etterpå: min første fiks slo sammen
Anywayslag-tallet INN I den eksisterende "Chip, bunker og
straffeslag"-seksjonen og omdøpte HELE seksjonen til "Annet" -- feil,
påpekt av bruker. Rettet: "Chip, bunker og straffeslag" beholder sitt
opprinnelige navn og innhold UENDRET (Bunkerslag/Straffeslag-flisene
tilbake til to, ikke tre). Ny, EGEN "Annet"-seksjon lagt til RETT ETTER
den (ikke slått sammen), med kun Anywayslag-statistikk: total per runde
- andel hull med minst ett anywayslag (samme "andel hull med..."-mønster
som straffeslag-statistikken). Ny
pctHolesWithAnyway-beregning icomputeStats(). Den midlertidigeStatTileRow-komponenten (bygget for den feilaktig sammenslåtte tre-flis-varianten) fjernet igjen som dødt kode --StatTilePair(fast to fliser) er tilstrekkelig for begge seksjonene nå. Notert for senere: brukeren ser for seg at et fritekst-notatfelt havner i "Annet"-seksjonen etter hvert. Verifisert: ekte typesjekket produksjonsbuild kjørt og bekreftet, ny rute/my-rounds/[id]/scorecardlistet. Ingen ny backend-endring (anyway_strokes/strokes_receivedfantes allerede iRoundHoleOut). Rullet ut live 2026-07-25 (to runder, feilrettingen rullet ut rett etter originalen), ren frontend-endring, ingen migrasjon,teeoff.noupåvirket.
Motor-komponenten (punkt 1-11) er ✅ BYGGET OG TESTET 2026-07-22,
som første, isolerte byggesteg (ren Python, ingen DB/API/frontend ennå —
matcher ADR-005s "test i isolasjon FØR resten"). Nye funksjoner i
handicap_engine.py: net_par, max_hole_score_for_handicap,
adjusted_gross_score, round_half_up_decimal, score_differential,
handicap_index_from_differentials, low_handicap_index,
apply_index_caps, course_handicap_9_raw/course_handicap_9.
Bevisst avvik fra opprinnelig plan, instruert av bruker 2026-07-22:
"Expected Score" (Rule 3.2b) sin upubliserte formel erstattes gjennomgående
av WHS sin egen, presist definerte "Net Par"-term (par + mottatte
handicapslag = 2 Stableford-poeng) for uspilte hull — gjelder BÅDE
ufullstendige 18-hulls-runder og konvertering av en 9-hulls-runde til
18-hulls-ekvivalent (Rule 5.1b sin egen separate 9-hulls-differensial-
formel er dermed bevisst IKKE implementert, se punkt 5 over).
41/41 tester bestått (test_handicap_engine.py, kjørt uten pytest —
ikke installert i miljøet, kun den innebygde selvsjekk-runneren), 17 nye
i tillegg til de 24 eksisterende. Flere verifisert mot regelbokens EGNE
tallregneeksempler, ikke bare intern konsistens: Rule 5.2a sine to
initial-indeks-eksempler (13,2 og 34,1, samt oppfølgingen til 37,4),
Rule 5.1c sine tre avrundingseksempler, Diagram 5.8 sin soft-/hard-cap-
oppførsel, og Diagram 3.1b sin Net-Double-Bogey-capping (der front-9 ble
verifisert eksakt mot diagrammet, mens back-9 sine åtte ikke-annoterte
scorer bevisst ble egenkomponert pga. usikker bilde-lesing av akkurat de
sifrene — se testens egen kommentar for full transparens om hva som er
kildebelagt og hva som ikke er det).
Gjenstår: migrasjon (nye tabeller for runde/deltaker/statistikk),
API-lag, frontend — ingen av disse er startet.
Beslutning H — Shotgun- vs. fortløpende start: EGEN, separat ADR (ADR-034)
Bekreftet med bruker: dette er et turnering/økt-konsept (start_hole per match i stedet for per økt, samme klokkeslett for alle grupper), uten reell avhengighet til rundeførings-arkitekturen over. Ikke behandlet videre her.
Ikke besluttet, gjenstår før bygging kan starte
- Den globale banekatalogen for EGENDEFINERTE (ikke-teeoff) baner (del av Beslutning C) — naturlig konsekvens av live-oppslag- beslutningen, men ikke bekreftet punkt for punkt ennå.
- Gjest-e-post-kobling (Beslutning D) — notert, ikke designet i detalj.
- Manuell vs. auto-beregnet HCP-indeks etter at motoren finnes (Beslutning G) — notert, ikke avgjort.
- Skal turnering-scoring til slutt bruke SAMME statistikk-modell (reist i brainstorm-runden 2026-07-22, FEATURE_BACKLOG.md) — ikke avgjort, ingen konsekvens for denne ADR-ens omfang uansett.
- Eksakte tabellnavn/skjema (
round/round_participant/round_hole_stater arbeidsnavn i denne ADR-en, ikke endelig fastlagt) — avgjøres ved migrasjonsskriving.
Status: 🔨 ADR skrevet OG kildebelagt 2026-07-22, BYGGING PÅBEGYNT.
Alle tre store åpne punktene fra første utkast (banedata, WHS 9-hulls-
regel, full HCP-indeksformel) er enten eksplisitt bekreftet med bruker
(Beslutning C) eller presist kildebelagt fra den offisielle WHS Rules of
Handicapping 2024 (Beslutning F/G) — ikke lenger antatt eller tilnærmet.
Tre byggesteg ferdig samme dag, alle rullet ut mot ekte systemer:
(1) hele HCP-indeks-motor-komponenten (Beslutning G, punkt 1-11) bygget
og testet i handicap_engine.py (43/43 tester), (2) full
databasemigrasjon (020_personal_rounds.sql + rettefiksen 021_round_ participant_rating_snapshot.sql, åtte tabeller/utvidelser) kjørt mot
ekte teecup_db, (3) fullt API-lag (app/routers/rounds.py) bygget,
scratch-verifisert (26 sjekker + egen teeoff-live-oppslag-test) og
redeployet (teecup_api). Gjenstår: HELE frontend-en — ingenting
bygget ennå. Notat fra bruker 2026-07-22 (IKKE designet): planer om
slaglengde-måling + avstand-til-punkter-på-banen (golf-GPS/rangefinder),
krever geografiske data ingen kilde har i dag — se FEATURE_BACKLOG.md.
Sju punkter fra faktisk bruk, BYGGET OG LIVE 2026-07-24 (rapportert av
bruker som selv testet scorekort-skjermen): (1) currentHole respekterte
aldri round.start_hole (useState<number>(1) + prev || start_hole —
1 er truthy i JS, så || ble en no-op), fikset ved å bruke null som
"ikke satt ennå"-tilstand og en sirkulær 18-hulls navigasjonsrekkefølge
fra starthullet. Trolig rot-årsak til det samtidig rapporterte GIR-
avviket: selve formelen (slag − putter ≤ par − 2) var allerede
matematisk identisk med regelen brukeren beskrev, men feil hull i fokus
ga feil par inn i en ellers korrekt formel. (2) Kølle-bag på personlig
profil — 28 faste kølletyper (BAG_CLUBS i app/routers/auth.py,
speilet i frontend), maks 14 (den ekte golfregelen, håndhevet i både
Pydantic og en CHECK-constraint), brukt som knapp-utvalg for "kølle
brukt ved utslaget" for runde-eieren (gjester har ingen profil, beholder
fritekst). (3) Nytt statistikkfelt "Anywayslag", siste punkt i "Flere
detaljer", samme tallvelger-stil som slag/putter (ikke en liten
stepper). (4) Valgfritt statistikknivå per deltaker
(strokes_only/strokes_and_putts/full, ny kolonne round_participant. stat_level, default strokes_only — brukerens egen presisering: kun
slag er strengt tatt nødvendig for resultat/HCP). Nytt PATCH-endepunkt
/rounds/{id}/participants/{id} for å endre nivået underveis. (5) Putt-
avstand endret fra fritekst-tall til seks faste bøtter (<1m…8m+) —
round_hole.first_putt_distance_m (numeric) erstattet med
first_putt_distance_bucket (text+CHECK) i migrasjon
022_round_stats_and_bag.sql, ingen produksjonsdata å bevare (0 rader
hadde verdi). (6) "Hullet er spilt"-avkrysningen FJERNET helt — reelt
overflødig, played settes allerede automatisk når et slagtall velges.
(7) Direkte spørsmål om avkrysningens funksjon avdekket at brukeren
egentlig ville vite om played (bevisst enkel avledning, uendret) — i
samme svar reiste brukeren "plukket opp"-behovet for et fremtidig
Stableford-format (fanget i FEATURE_BACKLOG.md, IKKE bygget — krever en
helt egen scoring-format-designrunde).
Punkt 6 (numpad-layout + retningskors + sveip-vurdering) BYGGET OG
LIVE 2026-07-24, samme dag — V0-prompten (se FEATURE_BACKLOG.md)
kjørt av bruker, zip 13 mottatt og integrert. V0s egen designbeslutning
på sveip-spørsmålet: IKKE et sveip-panel, men trykk-baserte
"Score"/"Statistikk"-faner (PanelTabs) inni samme kort — begrunnet
med at skjermen allerede har to horisontalt scrollende rader
(spillerfaner, hull-navigasjon), så en tredje sveiperetning ville vært
forvirrende. Vurdert som et godt, veloverveid valg, beholdt uendret.
Retningskors (DirectionCross/DirButton, piler + Target-ikon)
brukt for Utslag (venstre/senter/høyre) og Innspill (fullt 5-veis
kors), tekstlabel beholdt på hver knapp (ikke ikon-only). Tallvelgerne
fikk ny numpad-layout (3 kolonner, h-16-knapper, fyller bredden).
Reelt integreringsarbeid, ikke ren om-kabling: siden V0 ikke kjente
til dagens datalag (bygget i en tidligere, separat runde samme dag),
måtte PanelTabs/DirectionCross flettes inn i EKSISTERENDE, allerede
fungerende kode — ikke erstatte den. Konkret: PanelTabs vises kun når
statLevel==="full" (ingen "Statistikk"-fane å bytte til ellers),
"Score"-innholdet (Slag, evt. Putter) vises direkte uten fane-UI når
nivået er lavere. DirectionCross erstattet kun de to ChoiceRow-
kallene for Utslag/Innspill — ChoiceRow selv beholdt uendret til
resten (Kjønn, Statistikknivå, putt-avstand-bøtter). Kølle-bag-picker,
anywayslag, putt-bøtter, stat_level-gating, merge-før-PATCH,
starthull-fiksen og alle /my-rounds-lenker fra tidligere samme dag
ALLE bevart uendret -- kun presentasjonslaget for tallvelgere/retning
byttet ut. V0s egen PlayedToggle (som den ikke visste var fjernet)
ble bevisst IKKE tatt inn igjen. Ekte typesjekket produksjonsbuild
kompilerte rent. Rullet ut live, kun teecup_frontend (+ vanlig
teecup_api-bivirkning), ingen migrasjon, teeoff.no upåvirket.
Zip 13 slettet fra prosjektroten.
Scratch-verifisert grundig (isolert teecup_app_scratch-rolle +
isolert scratch-MinIO + engangs API-container): 18 sjekker — kølle-bag
lagret/hentet riktig, ukjent kølletype avvist (422), mer enn 14 køller
avvist (422), stat_level-default og eksplisitt verdi ved
opprettelse/gjest-tilføyelse, PATCH av stat_level vedvarer etter
refetch, anyway_strokes/first_putt_distance_bucket lagret riktig,
ugyldig bøtte-verdi avvist (422), OG en bevisst re-bekreftelse av
merge-før-PATCH-kontrakten (et PATCH uten anyway_strokes nuller den
fortsatt — samme grunnkontrakt som forrige runde, ikke endret av denne
utvidelsen). test_isolation.sql fortsatt 12/12. Ekte typesjekket
produksjonsbuild kompilerte rent.
Rullet ut mot ekte systemer 2026-07-24, bruker bekreftet eksplisitt:
migrasjon 022 kjørt mot ekte teecup_db (alle nye kolonner/CHECK-er
bekreftet, test_isolation.sql fortsatt 12/12), deretter docker compose up -d --build teecup_api teecup_frontend, begge containere
boot-et rent, /health//dashboard//my-rounds//account → 200 over
ekte https, teeoff.no upåvirket.
Tre nye brukerpunkter, ALLE BYGGET, SCRATCH-VERIFISERT OG LIVE 2026-07-25:
- Enkeltbane-anlegg dropper det overflødige banenavnet. Brukeren
påpekte at Tjøme Golfklubb (og de aller fleste andre norske anlegg —
kartlagt ved å skanne alle 174 teeoff-anlegg: kun 15 har mer enn én
bane) fikk et unødvendig sammensatt navn ("Tjøme Golfklubb –
Hovedbanen") siden anlegget uansett bare har ÉN bane. Fikset i BEGGE
stedene navnet bygges (
app/routers/courses.pysinimport_official_course,app/routers/rounds.pysin_resolve_teeoff_course): harfacility["courses"]nøyaktig ett element, brukes kun anleggsnavnet; har det flere (bekreftet fortsatt riktig for f.eks. Ålesund Golfklubb, som har "Solnør Gaard" og "Moa Golfsenter"), beholdes det kombinerte "Anlegg – Bane"-navnet uendret. Ingen migrasjon (ren navnelogikk, ingen lagret data endret av seg selv — kun FREMTIDIGE importer/runde-opprettelser får det nye navnet). - Runder kan nå navngis (
round.name, migrasjon024_round_name.sql, nullable). Valgfritt felt lagt tilRoundCreate/RoundUpdate/RoundOutiapp/routers/rounds.py— PATCH- kontrakten følger samme "tom streng = fjern, utelatt = ikke rør"-mønster som resten avRoundUpdatesine felt (ikke et ekteexclude_unset, konsistent med hvordanholes_planned/start_holeallerede håndteres i samme endepunkt). Vises på tvers avround-card.tsx(rundeliste),round-detail.tsx(header + redigerbart i "Rediger runde"-panelet),round-scorecard.tsxoground-stats.tsx— alle fire faller tilbake tilcourse_name_snapshotnår navn ikke er satt, og viser banenavnet som sekundær informasjon når et eget navn ER satt (ikke bare erstattet det, siden banen fortsatt er relevant informasjon). - Land+hjemmeklubb i profilen, redesignet (ADR-031/032s
ProfileOnboarding/ProfileSection). Brukeren ba om at Land skal stå FØR Hjemmeklubb, og at Hjemmeklubb skal være en nedtrekksliste fra teeoff, filtrert på valgt land. Avklart eksplisitt med bruker (AskUserQuestion, begge anbefalte valg): "Land" er nå en ekte<select>(COUNTRIES = ["Norge"], ett element foreløpig — bevisst klargjøring for fremtidig flerspråklighet, ingen backend-endring trengs når flere land legges til, kun listen utvides), og "Hjemmeklubb" er en søkbar kombinasjons-boks (HomeClubField, debounce 250ms) som gjenbruker det ALLEREDE EKSISTERENDE/rounds/official-search- endepunktet (org-uavhengig, tilgjengelig for enhver innlogget bruker siden ADR-033) — ingen ny backend-kode i det hele tatt. Teeoff filtrerer allerede bort upubliserte/nedlagte anlegg server-side (is_published), så "nedlagte og ikke operative klubber skal ikke inkluderes" er dekket uten egen filtrering på teecup-siden. Begge komponentene delt mellomProfileOnboarding(obligatorisk profil-fullføring) ogProfileSection(kontoinnstillinger) iaccount-settings.tsx, ingen duplisert implementasjon. Bevisst IKKE bygget: hard validering som avviser fritekst utenfor søkeresultatene — eksisterende, allerede lagredehome_club-verdier (fritekst fra før denne runden) forblir gyldige og redigerbare, feltet oppfører seg som "skriv for å filtrere, klikk for å velge" fremfor en strengt låst<select>, for å unngå å gjøre eksisterende profiler utilgjengelige. Scratch-verifisert grundig (isolertteecup_app_scratch-rolle + isolert scratch-MinIO + engangs API-container, samt et direkte engangs-oppslag mot ekteteeoff_apifor å kartlegge hvilke anlegg som faktisk har mer enn én bane): 22 sjekker — enkeltbane-navn uten suffiks (Tjøme), flerbane-navn med suffiks uendret (Ålesund), rundenavn lagret ved opprettelse, rundenavn med i rundeliste, PATCH omdøper, PATCH med tom streng fjerner navnet, PATCH uten navn-felt lar det urørt,official-search-endepunktet (brukt av det nye profilfeltet) fortsatt fungerer identisk.test_isolation.sqlfortsatt 12/12. Ekte typesjekket produksjonsbuild kompilerte rent, alle 21 ruter listet. Rullet ut mot ekte systemer 2026-07-25, bruker bekreftet eksplisitt: migrasjon 024 kjørt mot ekteteecup_db(kolonne bekreftet,test_isolation.sqlfortsatt 12/12), deretterdocker compose up -d --build teecup_api teecup_frontend, begge containere boot-et rent,/health//dashboard//my-rounds//account→ 200 over ekte https,teeoff.noupåvirket.
Oppdatering 2026-07-26: utslag/HCP per deltaker, gjeste-kontaktinfo, og scoringsflyten redesignet (v2)
Tre beslutninger denne dagen utvider ADR-033 uten å endre grunn- arkitekturen (Beslutning A-G står uendret).
1. Utslagssted er nå PER DELTAKER, ikke per runde. Reelt hull
oppdaget ved bruk: alle deltakere delte tidligere round. tee_name_snapshot uansett kjønn/faktisk valg — en medspiller kunne
aldri spille fra et annet utslag enn eieren. Migrasjon 028_round_ participant_tee.sql gir round_participant sitt eget tee_name_ snapshot, backfylt fra rundens eksisterende utslag for eksisterende
rader. ParticipantCreate/ParticipantUpdate fikk et valgfritt
tee_name; et bane-bytte (eksisterende RoundUpdate-mekanisme)
nullstiller nå eksplisitt ALLE deltakeres per-deltaker-utslag til det
nye standardutslaget (et bane-bytte gjør individuelle valg fra den
gamle banen meningsløse). HCP kan nå også redigeres per deltaker for
KUN denne runden (samme reproduserbarhets-unntak som resten av
round_participant — endrer aldri spillerens faktiske profil),
avvist etter at runden er fullført (samme presedens som bane-bytte).
2. Gjester (user_id IS NULL) fikk et valgfritt kontaktfelt +
redigerbart navn/kjønn. Migrasjon 029_round_participant_guest_ email.sql (guest_email, nullable). ParticipantUpdate fikk guest_ name/gender/guest_email — eksplisitt avvist (400) for en LENKET
deltaker (disse feltene gir ingen mening for noen med egen konto).
gender-endring inngår i samme rating-rekalkulering som utslag/HCP
(påvirker hvilken rating som er gyldig).
3. Scoringsflyten redesignet til en skjermovertagende veiviser --
to iterasjoner, andre erstattet den første samme dag. Brukeren delte
en skjermopptaksvideo av en konkurrentapp (Golf GameBook) sin
scoreregistrering og spurte om prinsippene kunne forbedre TeeCup.
Videoen ble analysert bilde for bilde (ffmpeg i en engangs Docker-
container) og avdekket et "samlebånd"-mønster: taltastatur med
kontekstuelle golf-termer (Eagle/Birdie/Par/Bogey ut fra hullets par),
og at fullført registrering for én spiller automatisk åpner NESTE
spillers registrering for samme hull.
Første forsøk (v1), BEVISST avvist fra en full modal: vurdert som
unødvendig risikofylt å bygge en skjermovertagende steg-for-steg-flyt
korrekt uten visuell testing (intet nettleserverktøy tilgjengelig i
denne økten). I stedet: golf-term-taltastatur lagt til der det allerede
var (NumberPickers nye showGolfTerms-modus), pluss en "Neste:
{navn}"/"Neste hull"-knapp nederst på den EKSISTERENDE, lange
inline-siden.
Brukeren testet v1 live og avviste den eksplisitt: "ingen forbedring
i det hele tatt", "visuelt like overveldende og rotete" — den
forsiktige, risikoreduserte tilnærmingen var utilstrekkelig; en ekte
skjermovertagende veiviser var det som faktisk krevdes, sammen med et
nytt, eksplisitt krav: akkumulert score-så-langt for RUNDEN synlig for
HVER spiller samtidig (ikke bare én valgt), matchende konkurrentappens
vedvarende "E"/"+1"-visning ved siden av hvert navn.
v2 bygget samme dag, ERSTATTER v1 helt: ny ScoringWizard-komponent
(fullskjerm, egen DOM-stack utenfor <main>), tre steg maks drevet av
stat_level ("strokes_only" → kun Slag; "strokes_and_putts" → + Putter/
avstand første putt; "full" → + ett samlet detalj-steg). Bevisst FÆRRE,
grovere steg enn konkurrentens egne 5-6 skjermer — en risikoreduksjon
uten visuell testing, og TeeCups stat_level-modell gjør en finere
oppdeling mindre naturlig uansett. Alle spillerne vises som en fast,
ikke-trykkbar kontekst-rad øverst i veiviseren (speiler konkurrent-
appens "mist aldri oversikten"-prinsipp — bevisst IKKE trykkbar, siden
GameBooks egen video heller ikke viste spillerbytte midt i et steg).
Hovedsiden forenklet radikalt: det gamle Slag/Putter/"flere detaljer"-
skjemaet er fjernet, erstattet med én kompakt liste (navn, "HCP X ·
{til-par} så langt (N hull)", en stor rund knapp som åpner veiviseren).
Datalasting utvidet til å hente ALLE deltakeres hull med det samme
(ikke lenger lat lasting kun for aktiv spiller), nødvendig for at
akkumulert score kan vises for alle samtidig.
Reell driftsfeil funnet OG rettet SAMME dag, rapportert av bruker som
testet live: den gamle ScoreSoFar("Så langt i runden")-boksen ble
ved en feil IKKE fjernet/flyttet i selve v2-bygget — den lå fortsatt
øverst, uendret, og fikk brukeren til å rapportere "ingen synlig
endring i det hele tatt" (toppen av siden var reelt uendret). Bekreftet
presist ved å hente den FAKTISK kjørende JS-bunten i produksjon og
søke i den — begge de gamle OG de nye tekst-strengene lå i samme bunt,
som beviste dette var en ekte plasseringsfeil, ikke en cache-/
utrullingsfeil. Rettet ved å flytte ScoreSoFar til UNDER scorings-
seksjonen (fortsatt tilgjengelig, bare ikke lenger i veien for
hovedoppgaven) — samme klasse feil hele denne rundens redesign forsøkte
å løse.
Bevisst utenfor omfang, IKKE bygget: de mest granulære trekkene fra
konkurrentappen (separate skjermer per detalj-felt, en illustrert
"bommet green"-grafikk, spillerbytte midt i et steg) — vurdert som for
stor risiko å bygge presist uten visuell testing, og TeeCups egen
stat_level-gruppering gjør en så fin oppdeling mindre naturlig uansett.
Ingen ekte nettleser-interaksjonstest utført av noen av iterasjonene
(intet slikt verktøy tilgjengelig denne økten) — kun logikk (Node-
simulering av stegmaskinen, rangering, akkumulert-score-beregning) og
et ekte API-rundtur-script (nøyaktig samme feltkombinasjon som
veiviseren sender) er verifisert. Full detalj i CLAUDE.md sin
statuslogg (2026-07-26).
ADR-035: Dashbord-redesign — organisasjon blir implisitt
Direkte oppfølging av en refleksjonsrunde 2026-07-25: brukeren spurte eksplisitt hvorfor organisasjon i det hele tatt trengs, gitt at en enkelt bruker som vil sette opp ÉN turnering for en vennegjeng ikke får noe igjen for opprettelses-seremonien. To alternativer ble veid opp mot hverandre:
- A — bruker-eide turneringer (samme mønster som ADR-033 sine
frittstående runder,
user_id-eierskap, ingen RLS): AVVIST. Praktisk talt HELE turnering-apparatet (lag/roster/blind draw/scoring/chat/ leaderboard/offentlig side, se CLAUDE.md sin arkitektur-invariant omorganization_idpå alle domenetabeller) er bygget rundt RLS og org-medlemskap. Å gjøre turneringer bruker-eide ville krevd enten en full duplisering av dette apparatet i en parallell, RLS-fri variant, eller å gjøreorganization_idvalgfri overalt — et brudd på invarianten CLAUDE.md eksplisitt krever en ny ADR for, med en reverseringskostnad som går GALT begge veier (migrere eksisterende bruker-eide turneringer inn i organisasjoner senere, eller gjenoppfinne medadministrasjon fra bunnen om den viser seg nødvendig uansett). - B — organisasjon beholdes, men opprettelsen gjøres usynlig/automatisk: VALGT. Rører verken skjema, RLS eller noen av de ni routerne som allerede er bygget rundt organisasjon — alt fortsetter å virke uendret. Kun UX-seremonien fjernes.
Beslutning A — Hva "usynlig organisasjon" betyr konkret
POST /orgs (app/routers/organizations.py) krever i dag KUN name —
verifisert direkte i koden: slug/public_profile settes separat via
PATCH /orgs/{id} (ADR-018), ikke ved opprettelse. Dette betyr at
"usynlig opprettelse" krever null backend-endring — kun en endret
frontend-orkestrering:
- Trykker en bruker "Ny turnering" og har PRESIS ÉN organisasjon fra før: den gjenbrukes direkte, ingen synlig org-steg.
- Har brukeren INGEN organisasjon ennå:
POST /orgskalles automatisk med et generert navn ("{fornavn} {etternavn}s turneringer", eller"Mine turneringer"som fallback hvis navn mangler) FØR turnering- opprettelsen vises — brukeren ser aldri et eget "opprett organisasjon"- skjema. - Har brukeren FLERE organisasjoner (f.eks. fordi de også er invitert inn
i en ekte klubb, ADR-022): et lett org-valg vises FØRST da — samme
OrganizationView-mønster som i dag, men kun synlig i dette ene tilfellet, ikke som standard. - Alt annet er UENDRET: en bruker som ønsker en ekte klubb-identitet (navn, slug, offentlig side, medadministratorer) kan fortsatt navngi orgen sin og bygge den ut senere — B fjerner kun ceremonien ved FØRSTE opprettelse, ikke noen av de eksisterende org-funksjonene.
Reversibilitet
B kan reverseres på en ettermiddag: legg navnesteget tilbake i UI-flyten FØR "Ny turnering" fullføres. Ingen datamigrering — eksisterende org-rader er identiske uansett om de ble navngitt av brukeren eller auto-generert.
Status: 📋 DESIGNET 2026-07-25, IKKE BYGGET. Se "Dashbord: tom-tilstand"-seksjonen i FEATURE_BACKLOG.md for det fulle blokk-forslaget og V0-prompten som ble skrevet i samme runde.
ADR-036: Venner, kategorisert deling av runder, og tiered personsøk for medspillere
Reist av brukeren 2026-07-25, samme runde som ADR-035. To sammenhengende behov: (1) når man legger til en medspiller på en frittstående runde, skal det søkes opp EKTE personer — venner FØRST, deretter samme hjemmeklubb, deretter samme land, til slutt globalt, uavhengig av om for- eller etternavn skrives først, i samme UI-mønster som bane-/klubbsøket; (2) appen skal ha et fullverdig vennekonsept, der venner (med mindre runden er satt til "Privat") kan se livescoren din, og der DU kan kategorisere hver venn i én eller flere av et fast sett grupper — uten at vennen selv vet hvilke grupper du har puttet dem i.
Direkte oppfølging av et allerede notert, men avvist punkt: ADR-033
Beslutning D satte round_participant.user_id i skjemaet, men avgrenset
bevisst til KUN gjeste-deltakere i v1 ("en deltaker med user_id satt får
IKKE egen tilgang til runden"), med e-post-kobling notert som en mulig,
ikke-designet senere utvidelse. Denne ADR-en erstatter e-post-kobling-
ideen med noe som passer bedre til det brukeren faktisk ba om: ekte
personsøk (samme mønster som bane/klubb), ikke en e-postadresse man må
kjenne på forhånd.
Beslutning A — Vennskap er gjensidig; kategorisering er privat og ensidig
Et vennskap krever en forespørsel + aksept (samme grunnmønster som organisasjon-invitasjoner, ADR-022) — "venner ser livescoren din" gir kun mening som et GJENSIDIG, bekreftet forhold, ikke en ensidig følging. Kategoriseringen ("Make", "Golfvenner" osv.) er derimot et HELT SEPARAT, privat attributt EIET av den som kategoriserer — A kan sette B i "Golfvenner" uten at B noensinne får vite det, og B kategoriserer A helt uavhengig (kan sette A i en annen gruppe, eller ingen). Dette er bevisst samme idé som Facebooks "nære venner"-lister: vennskapet er symmetrisk, men grupperingen er det ikke.
Datamodell (arbeidsnavn, avgjøres ved migrasjonsskriving):
friendship:requester_user_id,addressee_user_id,status(pending/accepted/declined), tidsstempler. Et UNIK uttrykks-indeks på(LEAST(requester_user_id, addressee_user_id), GREATEST(...))hindrer duplikate forespørsler i begge retninger samtidig.- Kategori er IKKE en egen tabell — et fast, CHECK-constrained sett
(samme mønster som
BAG_CLUBS/gender/stat_levelellers i appen), ikke brukerdefinerbart:spouse(Make),close_family(Nær familie),extended_family(Storfamilie),close_friends(Nære venner),golf_friends(Golfvenner),colleagues(Kollegaer),business(Forretningsforbindelser),classmates(Studiekamerater),acquaintances(Perifere bekjente),other(Ymse). friend_categorization:owner_user_id,friend_user_id,category— én rad PER kategori en venn er satt i (en venn kan ha flere rader, "de skal kunne være tilknyttet forskjellige kategorier"). Håndheves i app-laget (skriving krever enaccepted-vennskapsrad mellom de to), ikke en direkte FK tilfriendship(unngår å måtte holde styr på requester/addressee-retning to steder).
Beslutning B — Rundevisibilitet: tre nivåer, samme struktur som turnering, men egen mekanisme
round får visibility_mode (public/private/friends, default
private — trygg standard, samme filosofi som RLS-policyenes "se
ingenting" ved manglende kontekst). Når friends er valgt: hvilke
KATEGORIER som får se runden velges eksplisitt (round_visible_category,
round_id+category) — ikke "alle venner", siden brukeren eksplisitt ba
om å velge blant gruppene sine ved rundestart.
Viktig presisering, unngår en reell forvekslingsfelle: dette er
ADSKILT fra om noen er lagt til som faktisk MEDSPILLER (Beslutning C
under). En medspiller du har lagt til ser ALLTID runden dere spiller
sammen, uavhengig av visibility_mode — visibility-nivået styrer kun
TREDJEPARTS innsyn (venner/offentligheten), ikke de som faktisk er med i
flighten.
Autorisasjonssjekk (app-lag, plain_connection()-mønsteret fra
ADR-033 Beslutning A — INGEN RLS, samme begrunnelse som der):
viewer == round.owner_user_id→ alltid tilgang.viewerer en lenket medspiller (round_participant.user_id == viewer) → alltid tilgang til DEN runden.visibility_mode == 'public'→ alle, også anonyme (samme mønster som turneringers offentlige side).visibility_mode == 'private'→ kun 1+2.visibility_mode == 'friends'→ krever innlogget bruker MED enaccepted-vennskapsrad til eieren OG minst énfriend_categorization-rad (owner_user_id=eier, friend_user_id= viewer) hvor kategorien finnes iround_visible_categoryfor akkurat denne runden. Legg merke til retningen: det er EIERENS kategorisering av VIEWER som brukes, ikke omvendt — riktig, siden det er eieren som begrenser hvem som får se, basert på eierens egen gruppering.
"Livescore" — forstått som sanntidsoppdatering mens runden pågår,
samme idé som turnering sin /t/[id]/live (ADR-027). Gjenbruker samme
kringkastingsmønster (app/realtime.py, broadcast_live_update) — en
tilsvarende funksjon for runder, kringkastet ved hver hull-PATCH når
visibility_mode != 'private', med et WS-endepunkt gated av samme
autorisasjonssjekk som over.
Beslutning C — Tiered personsøk: delt mellom "finn venn" og "legg til medspiller"
Samme underliggende endepunkt brukes til BEGGE formål (finn en venn å
sende forespørsel til, OG søk opp en medspiller å legge til på en runde)
— begge er i essens "finn en person", kun hva som skjer ETTER valg
skiller dem. Foreslått: GET /people/search?q=....
Navnerekkefølge-uavhengig matching: spørringen splittes i tokens
(mellomrom-separert). For HVER token må minst ett av first_name/
last_name matche (ILIKE token||'%') — ALLE tokens må matche (AND på
tvers av tokens, OR på tvers av feltene per token). Dette gjør at "Erol
Haagenrud" og "Haagenrud Erol" gir identisk treff, uten noen spesiell
"gjett rekkefølgen"-logikk — det faller naturlig ut av at hvert ord kan
matche HVILKET SOM HELST av de to feltene.
Tiered rangering, én spørring: en beregnet prioritet per kandidat —
0 hvis venn (uavhengig av kategorisering — ALLE venner, ikke bare de i
en bestemt gruppe, siden dette er søk-for-å-legge-til, ikke visibility-
sjekken over), 1 hvis samme home_club som søkeren (nå en pålitelig
eksakt streng siden ADR-025-runden 2026-07-25 gjorde Hjemmeklubb til en
ekte dropdown-verdi i stedet for fritekst — retroaktivt en god
begrunnelse for den endringen), 2 hvis samme country, 3 ellers.
ORDER BY prioritet, fornavn, etternavn LIMIT 20 gir venner-først uten
behov for separate spørringer per nivå.
Personvernhensyn, bevisst innebygd i designet, ikke tilføyd i etterkant:
- Svaret inneholder KUN navn, avatar, hjemmeklubb — ALDRI e-post/mobil/ fødselsdato.
- Foreslått minimum 2 tegn i søket før noe returneres i det hele tatt (hindrer triviell enumerering av alle brukere via ett enkelt bokstavsøk) — MIN anbefaling, ikke bekreftet med bruker.
- Kun innloggede brukere kan søke (
get_current_user, ikke anonymt).
Beslutning D — Byggerekkefølge (foreslått, IKKE bekreftet)
Gitt omfanget (nytt vennekonsept + ny rundevisibilitet + nytt delt søkeendepunkt + sanntidsutvidelse) foreslås tre uavhengig leverbare faser, samme "bygg i rekkefølgen ting brukes"-prinsipp som resten av prosjektet:
- Venner-kjernen:
friendship+friend_categorization, forespørsel/ aksept/fjern-endepunkter,/people/search, en ny/friends-side (søk, forespørsler, kategoriser). Leverbar og nyttig helt alene. - Rundevisibilitet:
round.visibility_mode+round_visible_category, valg ved rundestart,can_view_round-gatede lese-endepunkter for ikke-eiere, sanntidsutvidelse for live-visning. Avhenger av fase 1. - Ekte medspillere (ikke bare gjester): utvid
POST .../participantstil å godta et søktuser_idi tillegg tilguest_name. Avhenger av fase 1 (samme søk). Avklart med bruker 2026-07-25: JA — en lagt-til ekte medspiller skal se runden i SIN EGEN "Egne runder"-liste, ikke bare eieren. Konkret teknisk konsekvens, presisert her siden det ikke er opplagt:list_roundsfiltrerer i dag KUN påowner_user_id, ogRoundOutsineowner_holes_played/owner_total_score/owner_score_to_par-felt (lagt til 2026-07-25, se status) er alltid utledet fra EIERENSround_participant-rad. For en medspiller som ser runden i SIN liste må disse tallene i stedet vise DERES EGEN score i runden, ikke eierens — feltene må derfor bli "viewer-relative" (utledet fra HVILKEN SOM HELST deltaker-rad som matcher innlogget bruker, entenis_ownereller lenketuser_id), ikke hardkodet tilis_owner=true-raden.list_rounds-spørringen må utvides tilWHERE owner_user_id = $1 OR id IN (SELECT round_id FROM round_participant WHERE user_id = $1). Ingen endring i selve eierskapet (round.owner_user_ider fortsatt entydig én person). Skrivetilgang avklart med bruker 2026-07-25: en medspiller skal kunne registrere score for ALLE i flighten, ikke bare sin egen rad — samme skrivetilgang som eieren allerede har i dag (via_get_owned_round_or_404), nå utvidet til å gjelde ENHVER lenket ekte deltaker (round_participant.user_id), ikke kun eieren. Praktisk presisering: dette gjelder KUN hull-registrering (PATCH .../participants/{id}/holes/{hole}) og legg-til-deltaker — sletting av selve runden og bane-/utslagsbytte (DELETE /rounds/{id},PATCH /rounds/{id}sine bane-felt) forblir eier-eksklusivt, siden disse er destruktive/strukturelle handlinger uavhengig av hvem som fører score. Autorisasjonssjekken for hull-PATCH blir dermed:viewer == owner_user_id OR viewer IN (SELECT user_id FROM round_participant WHERE round_id = $1 AND user_id IS NOT NULL).
Status: 🔨 FASE 1 (venner-kjernen) BACKEND + FRONTEND BYGGET
2026-07-25. Backend: migrasjon 025_friends.sql + app/routers/ friends.py (GET /people/search, POST /friends, POST /friends/{id}/ accept, DELETE /friends/{id}, GET /friends, PUT /friends/ {friend_user_id}/categories) — 31 scratch-sjekker bestått, inkl.
presist bevist tiered rangering (venn→klubb→land→globalt) og at
kategorisering faktisk er PRIVAT (B ser aldri kategoriene A har satt B
i). test_isolation.sql fortsatt 12/12. Rullet ut mot ekte teecup_db/
teecup_api samme dag, bruker bekreftet eksplisitt.
Frontend: V0-zip (zip 19) integrert som components/friends.tsx + ny
rute app/my-friends/page.tsx (IKKE /friends, som V0 selv foreslo —
det er API-prefikset, samme kollisjonsklasse som /rounds unngått fra
start denne gangen). Dashbordets "Venner"-blokk koblet til ekte data i
samme runde. Ekte typesjekket produksjonsbuild kompilerte rent. Rullet
ut live 2026-07-25, bruker bekreftet eksplisitt.
Fase 3 (ekte medspillere på en frittstående runde) BYGGET OG SCRATCH-
VERIFISERT 2026-07-26, utløst av at brukeren rapporterte at "+ Gjest"-
skjemaet ikke søkte etter spillere i det hele tatt (rent tekstfelt,
/people/search var aldri koblet på). Bygget den delen av fase 3 som
faktisk var etterspurt: søk-og-legg-til en ekte bruker (gjenbruker
tiered /people/search, samme endepunkt som vennesøket) PLUSS den
tidligere avklarte "skriv for hele flighten"-regelen (bekreftet av
brukeren 2026-07-26 — "full tilgang nå", ikke bare rask utfylling).
IKKE bygget i denne runden: selve rundevisibilitet (fase 2,
offentlig/privat/delt-med-venner-i-grupper) — det er fortsatt et
separat, senere steg.
Konkret: round_participant.user_id (fantes i skjemaet siden ADR-033,
aldri eksponert via API) er nå skrivbar via POST /rounds/{id}/ participants (kjønn/HCP hentes automatisk fra den valgte personens
egen profil, ikke tastet manuelt). Ny migrasjon 027_round_participant_ user_unique.sql (partiell unik indeks, hindrer dobbel-lenking). Rundens
tilgang delt i to nivåer: _get_accessible_round_or_404 (eier ELLER en
lenket medspiller — lesing, hull-scoring for HELE flighten, fullføring)
vs. _get_owned_round_or_404 (fortsatt strengt eier-only — rediger/
slett runde, legg til/fjern medspillere). RoundOut sine owner_*-
statistikkfelt omdøpt til my_* og gjort VIEWER-relative (regnes nå fra
den spørrende brukerens egen deltaker-rad, ikke alltid eierens — løser
et teknisk konsekvens-punkt notert allerede 2026-07-25 da "medspiller
ser runden i egen liste" ble bekreftet). RoundParticipantOut fikk et
nytt, alltid utfylt display_name-felt (levende oppslått, ikke
snapshot) — fanget OG fikset en reell latent bug i samme runde:
leaderboard-endepunktet (bygget 2026-07-26, samme dag) ville vist et
tomt navn for enhver lenket medspiller, siden dets display_name-
utledning den gang kun sjekket is_owner.
Scratch-verifisert grundig (39/39 sjekker i to testløp): søk-og-
legg-til (kjønn/HCP auto-fylt, avvist duplikat/selv/ukjent bruker/
ufullstendig profil), lenket medspiller kan se runden+registrere score
for BÅDE egen OG andres rad (whole-flight), men KAN IKKE 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 (eier/medspiller/gjest). test_isolation.sql
12/12 uendret. Ekte typesjekket produksjonsbuild av frontend (ny
søk-UI i round-detail.tsx, "Gjest" omdøpt til "Medspiller" gjennomgående,
samme viewer-relative "Deg"-fiks portert til round-stats.tsx/
round-scorecard.tsx, som hadde samme latente bug).
Beslutning E — Kategori-match ved rundevisibilitet: MINST ÉN, ikke ALLE
Beslutning B punkt 5 (over) spesifiserte fra start "minst én
friend_categorization-rad ... hvor kategorien finnes i
round_visible_category". En senere presisering 2026-07-29 (kun
dokumentert i kode-kommentarer, aldri i denne ADR-en — selve
dokumentasjonshullet som gjorde denne avviket vanskelig å spore) strammet
dette til at ALLE en venns kategorier måtte være i rundens synlige sett,
ikke bare én.
Reversert til original Beslutning B-regel 2026-08-08, etter at brukeren rapporterte at en runde delt med kategorien "Make" ikke ble synlig for vedkommendes ektefelle. Årsak: ektefellen var i tillegg kategorisert "Storfamilie" (Beslutning A tillater flere kategorier per venn), og siden "Storfamilie" ikke var huket av på DENNE runden, blokkerte den strenge ALLE-regelen hele synligheten -- til tross for at den relevante kategorien ("Make") faktisk var valgt. I praksis gjorde dette det nærmest umulig å dele pålitelig med noen som var tagget i mer enn én kategori, uten å huke av samtlige av dem hver gang.
Ny/gjeninnført regel: en venn ser runden hvis MINST ÉN av kategoriene eieren har satt dem i, er i rundens synlige sett -- flere kategorier på samme venn er bare flere sjanser til å matche, aldri en ekstra begrensning. En venn med INGEN kategorier vises fortsatt aldri (uendret).
Rettet i fire duplikate SQL-steder (app/routers/rounds.py:
_can_view_round, _friends_who_can_see_round,
list_friends_on_course; app/routers/round_messages.py: feed-
listingen) -- samme sted-for-sted-duplisering som opprinnelig omtalt i
Beslutning B, nå alle konsistent på MINST ÉN-varianten.
ADR-037: Individuelle turneringer, flerrunde-turneringer — grunnstruktur
Kontekst: reist 2026-07-26, som følge av en avklaringsrunde om et ønsket turnering-leaderboard (se FEATURE_BACKLOG.md sin "Utvidelse 2026-07-26"-seksjon for hele forhistorien). TeeCup skal etter hvert kunne arrangere turneringer for INDIVIDUELLE spillere (f.eks. «Københavner» — se FEATURE_BACKLOG.md sitt eget punkt om flere turneringsformater), ikke bare dagens Ryder Cup-lagformat (ADR-011), og disse skal kunne gå over FLERE RUNDER med sammenlagt resultat. Et fremtidig Order of Merit (sesong-sammenlagt på tvers av flere turneringer, brukerens eksempel: "klubbdager") er identifisert som et beslektet, men separat, SISTE steg — se eget avsnitt nederst, ikke designet i denne runden.
Denne ADR-en dekker KUN grunnstrukturen (datamodell-plassering, turnering-type, flerrunde-støtte, poengmodell). Selve de fem konkrete formatene (Københavner, High-low-high, Robbins, Try all, Flaggturnering) fra FEATURE_BACKLOG.md designes hver for seg OVENPÅ denne grunnstrukturen, ikke i denne runden.
Viktig presisering underveis, som endrer en tidligere antakelse (2026-07-25-runden om "flere flighter i én frittstående runde"): i en formell, org-arrangert individuell turnering er "flight" KUN en tee-tid-/spilletempo-gruppering (som i dag for lag-turneringer) — IKKE en leaderboard-grense. Leaderboardet spenner alltid HELE feltet, uavhengig av hvem som spilte sammen. Dette er strukturelt ulikt den ad hoc "flere flighter i en frittstående runde"-ideen (der leaderboardet bevisst skal avgrenses til det man selv satte opp) — de to "flight"-begrepene ligner i UI, men er IKKE samme konsept. Holdes bevisst adskilt, ikke forent, selv om forrige runde antydet det motsatte.
Beslutning A — Ny, parallell org-scopet datamodell (IKKE gjenbruk av round)
En individuell/flerrunde-turnering får sin EGEN, RLS-beskyttede
tabellstruktur under organization_id — IKKE en utvidelse av de
frittstående rundetabellene (round/round_participant/round_hole,
ADR-033).
Begrunnelse: ADR-033 Beslutning A var et BEVISST valg om at
frittstående runder er 100 % org-uavhengige (plain_connection(),
ingen RLS i det hele tatt — autorisasjon håndheves med
WHERE owner_user_id = $1 i app-laget). Å gi round en valgfri
organization_id/tournament_id ville krevd HYBRID RLS (håndhevet kun
når organisasjon er satt) — en helt ny klasse sikkerhetslogikk som
IKKE finnes noe sted ellers i systemet i dag (organization_id på ALLE domenetabeller, håndhevet av RLS er en ufravikelig invariant i
CLAUDE.md — en betinget/nullbar variant ville vært det FØRSTE
unntaket). Dette prosjektet har allerede én dokumentert hendelse
(RLS-tomstreng-bugen, se CHANGELOG.md 2026-07-16) som kom av
nettopp denne typen RLS-finesse — vi unngår bevisst å introdusere en
ny variant av samme risikoklasse. Noe skjema dupliseres (hull-for-hull-
registrering ligner mye på round_hole), men den delte regnelogikken
(handicap_engine.py) er allerede rammeverk-uavhengig og gjenbrukes
uendret uansett hvilken tabell dataene ligger i.
Beslutning B — Samme tournament-tabell, ny type-diskriminator
tournament får en ny kolonne, f.eks. format_type ('team' |
'individual', default 'team' for bakoverkompatibilitet med alle
eksisterende rader).
Begrunnelse: en individuell turnering trenger fortsatt navn, status,
datoer, synlighet (ADR-018), invitasjonskode (ADR-020), org-eierskap —
ALT dette er allerede bygget og fungerer uendret på tournament-raden,
uavhengig av format. Å lage en helt ny toppnivå-entitet ville betydd å
gjenoppbygge synlighet/join-kode/landingssider fra bunnen for et andre
system — ren duplisering uten reell gevinst. Lag-tabellene
(team/team_roster) blir ganske enkelt ubrukte for
format_type = 'individual' — håndheves i app-laget (samme mønster som
ADR-011s to-lags-grense), ikke med en tung DB-constraint på tvers av
tabeller.
Beslutning C — Flerrunde via en ny, økt-lignende tabell
Individuelle turneringer kan ha FLERE runder fra start, via en ny
tournament_round-tabell (org+tournament-scopet, samme rolle som
session har for lag-turneringer i dag — dato/bane/hullomfang per
runde). Sammenlagt resultat på tvers av rundene i turneringen summeres
ved lesing, SAMME "summer på ekte deltaker-id, aldri på en løs
side-label"-prinsipp som det eksisterende lag-leaderboardet allerede
bruker (fetch_leaderboard, app/routers/tournaments.py) for å unngå
å blande sammen feil rader.
Bane-referanse: tournament_round.course_id peker til den
EKSISTERENDE org-scopede course-tabellen (samme som session bruker
i dag) — INGEN snapshot-mekanisme som i frittstående runder
(ADR-033s course_name_snapshot). Organisasjonens egen banedata er
allerede stabil, admin-kontrollert data; snapshot-behovet i ADR-033 kom
av at frittstående runder leser LIVE fra en ekstern, ikke-org-kontrollert
kilde (teeoff) — samme begrunnelse gjelder ikke her.
Deltaker-identitet: en ny tournament_participant-tabell (org+
turnering-scopet, samme rolle som team_roster — refererer til
EKSISTERENDE player-tabellen, fryser handicap_index_snapshot ved
uttak, samme reproduserbarhetsprinsipp som ADR-007) — IKKE en ny,
frittstående gjeste-modell slik round_participant har. En turnering
har alltid en organisator som administrerer en kjent spillerpool
(samme som lag-turneringer i dag), ulikt en privatpersons frittstående
runde.
Beslutning D — Rå slag lagres, poeng caches per format (samme mønster som match-play)
Hver tournament_round-deltaker sin hull-for-hull-registrering lagrer
RÅ BRUTTO SLAGTALL per hull (samme grunnform som round_hole.score) —
scorekortet trenger dette uansett, og det gir handlefrihet til å vise
flere ulike visninger (brutto/netto/stableford/Københavner-poeng) av
SAMME underliggende data.
I TILLEGG caches et FERDIG UTREGNET poengtall per format — samme
etablerte mønster som match.status_text/points_side_a/b i dag:
motoren (handicap_engine.py, ny funksjon per scoringsmetode) regner
poeng ved HVER hull-innsending, resultatet lagres i en egen kolonne
(f.eks. på en ny tournament_round_score-rad, én per deltaker per
runde) — rask leaderboard-lesing uten å måtte regne ut alt på nytt for
hele feltet ved hver visning, konsistent med hvordan matchstatus
allerede caches og regnes på nytt ved hver innsending
(recompute_and_cache_match_state).
tournament.scoring_method (eller ev. per tournament_round, hvis et
fremtidig format trenger å blande metoder — ikke avklart, default: fast
per turnering) avgjør HVILKEN motorfunksjon som brukes til å regne
poeng fra de rå slagene — ny CHECK-verdi + ny ren funksjon i
handicap_engine.py per format som legges til (stroke_play_gross,
stroke_play_net, stableford, copenhagen_points, ...). Hvert nytt
format fra FEATURE_BACKLOG.md sin liste blir dermed i hovedsak: én ny
motorfunksjon (testet isolert, samme "test i isolasjon FØR resten"-
prinsipp som ADR-005) + én ny CHECK-verdi, ikke en skjemaendring.
Konsekvens — hva denne ADR-en IKKE avgjør ennå
- De fem konkrete formatene (Københavner, High-low-high, Robbins, Try all, Flaggturnering) — hver trenger sin egen, mindre design-runde oppå denne grunnstrukturen (poengformel, evt. spesielle paringsregler for High-low-high/Robbins som IKKE er rent individuelle poeng-per-hull).
- Order of Merit (sesong-sammenlagt på tvers av FLERE separate
turneringer). Bekreftet av brukeren som et beslektet, men SISTE steg
— forutsetter at individuelle turneringer med et poengresultat
finnes først. Krever sannsynligvis et helt nytt overordnet konsept
(en "sesong"/"serie", org-scopet, RLS som ellers) som grupperer flere
tournament-rader og akkumulerer poeng — IKKE designet i denne runden. - Migrasjon/kode — ✅ BYGGET OG SCRATCH-VERIFISERT 2026-07-30, IKKE ENNÅ
RULLET UT. Migrasjon
040_individual_tournaments.sql(alle fem nye tabeller +tournament.format_type/scoring_method), nye motorfunksjoner ihandicap_engine.py(stroke_play_gross_total/stroke_play_net_total/stableford_points_for_hole/stableford_total, 8 nye tester), og et fullt CRUD-API (app/routers/individual_tournaments.py: runder/ turnering-deltakere/rundedeltakere/hull-for-hull-scoring/leaderboard). Se FEATURE_BACKLOG.md ("Utvidelse 2026-07-26"-seksjonens oppdatering 2026-07-30) for full detalj om design, funn under bygging og verifisering (52 API-sjekker + 63 motor-tester). Frontend er bevisst IKKE bygget i denne runden — neste steg i samme "motor → skjema → API → frontend"-rekkefølge som ADR-033/038/039.
ADR-038: Faktisk (beregnet) HCP vs. manuelt satt HCP
Kontekst: reist 2026-07-28, rett etter en gjennomgang av frittstående
rundeføring (ADR-033) som avdekket et reelt, ubesluttet hull: hele
WHS-indeksmotoren (handicap_index_from_differentials/
low_handicap_index/apply_index_caps i handicap_engine.py, testet
41/41) kalles ALDRI fra noe API-endepunkt — round_participant. score_differential regnes og lagres per runde, men app_user. handicap_index endres kun manuelt (PATCH /auth/profile). Bekreftet
eksplisitt i rounds.py sin egen moduldoc som "eksplisitt uavklart punkt
i ADR-033".
Brukeren avklarte at TeeCup skal operere med TO tall, ikke ett:
Beslutning A — To atskilte HCP-verdier
- Manuelt satt HCP (eksisterende
app_user.handicap_index, ADR-031) — UENDRET betydning og bruk: dette er tallet som brukes til Course Handicap-beregning i enhver runde/deltaker med mindre eksplisitt overstyrt per runde (allerede slik i dag viahandicap_index_snapshot). Redigeres fritt av brukeren selv, som i dag. - Faktisk HCP (ny
app_user.computed_handicap_index+computed_handicap_index_updated_at) — WHS Handicap Index (Rule 5.2), regnet automatisk fra beste 8 av de ≤20 nyeste TELLENDE Score Differential-ene, med den offisielle opptrappingstabellen for færre enn 20 runder (_INDEX_TABLE_UNDER_20, allerede implementert og testet — ingen ny motorkode trengs, kun at den faktisk kalles). Aldri direkte redigerbar — kun avledet.
Beslutning B — Uttrykkelig "overfør til manuelt HCP"-handling
Golferen får en egen, eksplisitt knapp ("Bruk som mitt HCP") i
/account som kopierer gjeldende computed_handicap_index inn i
handicap_index (samme skrivevei/historikk-logging som en vanlig
manuell PATCH). INGEN automatisk synkronisering noen vei — de to
tallene lever bevisst uavhengig av hverandre helt til brukeren selv
trykker knappen.
Beslutning C — Eksplisitt eksklusjon per deltaker, kontrollert av HVER innlogget deltaker selv
Ny round_participant.exclude_from_handicap boolean DEFAULT false —
uavhengig av counts_for_handicap (som fortsatt er den AUTOMATISKE
WHS-kvalifiseringen, minimum spilte hull). Endelig medregning i faktisk
HCP krever BEGGE: counts_for_handicap AND NOT exclude_from_handicap.
Avklart eksplisitt med bruker (AskUserQuestion), IKKE gjettet: hver
INNLOGGET deltaker (eier ELLER en lenket medspiller, ADR-036 fase 3)
styrer SIN EGEN rads eksklusjon — ikke bare runde-eieren. Autorisasjon i
PATCH .../participants/{id} utvidet presist til akkurat dette ene
feltet: en ikke-eier kan KUN sende exclude_from_handicap (ethvert
annet felt i samme kall avvises 403) og KUN på sin egen rad (rp.user_id == requester) — alle andre felt (tee/HCP/kjønn/navn/stat_level) forblir
strengt eier-only, uendret. Feltet er bevisst IKKE en del av
rating_changed-sperren i update_participant — kan endres uansett
fullført-status, siden det ikke påvirker AGS/differensial-matematikken
i seg selv, kun om resultatet TELLER i det HELE tatt.
Beslutning D — Spilleform (slagspill/matchspill), selvdeklarert
Ny round.play_format text DEFAULT 'stroke' CHECK (IN ('stroke', 'match')). Frittstående runder har i dag INGEN uavhengig måte å
"oppdage" matchspill på (ingen egen match-motor, ulikt turnering-siden)
— feltet er derfor selvdeklarert av brukeren ved oppsett (og fritt
redigerbart senere, som ren metadata, uendret av fullført-status).
WHS-kilde lest og lagt til grunn (WHS_Rules_of_Handicapping_2024. pdf, Rule 3.3 "When a Hole is Started But Player Does Not Hole Out"):
matchspill-scorer ER teknisk et gyldig HCP-grunnlag under WHS (Rule
2.1a), MEN et hull som avgjøres/konsederes før utspilt krever en
subjektiv "most likely score" (avgrenset til netto dobbel bogey) — noe
TeeCups rene tallregistrering ikke har noen UI-vei til å representere
presist. Dette begrunner brukerens instinkt ("man bruker vanligvis ikke
matchspill til å beregne hcp") uten å hardkode et forbud: WHS tillater
det teknisk, men TeeCup kan ikke garantere et pålitelig grunnlag for
det i dag.
Derfor: spør, ikke tving. Når play_format = 'match' velges (ved
opprettelse, ELLER ved senere tillegg av en lenket medspiller), foreslår
frontend exclude_from_handicap = true som FORHÅNDSVALGT verdi med en
forklarende tekst — brukeren kan uansett overstyre til false (f.eks.
en fullstendig utspilt vennskapelig 18-hulls "matchspill" der alle hull
faktisk ble spilt ut). Ingen server-side tvang — play_format og
exclude_from_handicap er to uavhengige felt i skjemaet; kun
frontendens forhåndsutfylling kobler dem sammen.
Beslutning E — Datagrunnlag: designet for BEGGE kilder, bygget for én
Avklart eksplisitt med bruker: "faktisk HCP" skal på sikt kunne
telle scorer fra BÅDE frittstående runder OG organisasjons-/
turneringsscoring, men v1 bygger KUN kilden for frittstående runder.
Løst med ett tynt, bevisst uabstrahert skjøtepunkt — IKKE en ny,
generell "scoring record"-tabell (ville vært for tidlig abstraksjon for
en kilde som ikke finnes ennå, se CLAUDE.md sin regel mot design for
hypotetiske fremtidige krav): en enkelt funksjon
_gather_qualifying_differentials(conn, user_id) i rounds.py henter
i dag KUN fra round_participant JOIN round, med en kommentar som
peker ut nøyaktig dette som skjøtepunktet for en fremtidig UNION mot
turnering-siden (som uansett trenger sin egen ADR-037-baserte
poeng-/differensial-motor FØRST, se ADR-037 Beslutning D).
Beslutning F — Low Handicap Index / cap-historikk gjenbruker handicap_history, ikke en ny tabell
Rule 5.7 (Low Handicap Index, laveste indeks i 365 dager) og Rule 5.8
(soft/hard cap) krever en historikk av TIDLIGERE beregnede indeksverdier
å sammenligne mot — samme grunnform som den eksisterende
handicap_history-tabellen (ADR-031-oppfølging, migrasjon 018), men
den logger i dag KUN manuelle profilendringer. Løst med én ny kolonne
handicap_history.source text DEFAULT 'manual' CHECK (IN ('manual', 'computed')) fremfor en parallell tabell — samme tabell, to kilder,
low_handicap_index() filtrerer på source = 'computed'. Det aller
første beregnede indekstallet for en bruker har ingen tidligere
computed-historikk å låses mot — capping hoppes bevisst over da
(Rule 5.8 sitt "Low HI" er per definisjon udefinert før en første indeks
er etablert), ikke behandlet som en feil.
Konsekvens — hva denne ADR-en IKKE dekker
- Turnering-/organisasjonsscoring teller fortsatt IKKE mot faktisk HCP (Beslutning E) — venter på ADR-037s videre arbeid.
- Ingen automatisk periodisk "aging" av gamle differensialer utover de 20 nyeste (Rule 5.5) — løst implisitt ved at spørringen alltid henter KUN de 20 nyeste, ikke en egen bakgrunnsjobb.
- Stableford-format for frittstående runder (se FEATURE_BACKLOG.md) er fortsatt ikke bygget — påvirker ikke denne ADR-en, siden Score Differential regnes fra Adjusted Gross Score uavhengig av poengformat.
ADR-039: Ekte spillformer for frittstående runder (match/skins/par-lag)
Kontekst: reist 2026-07-28, rett etter ADR-038: "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: round.play_format (ADR-038) styrer i dag KUN et
HCP-eksklusjonsforslag — ikke selve scorings- eller scorekort-modellen.
Dette utvider play_format til å faktisk STYRE begge deler.
Fire load-bærende beslutninger avklart eksplisitt med bruker (AskUserQuestion, to runder) før denne ADR-en ble skrevet.
Beslutning A — To sider per runde, gjenbruker ADR-011s antakelse
Match/fourball/foursome/greensome/scramble trenger et «hvem spiller
mot hvem»-konsept round_participant ikke har i dag (en flat liste
uten gruppering). Løst med nøyaktig TO SIDER per runde (ny
round_side-tabell), håndhevet i app-laget — samme mønster som
ADR-011s to-lags-grense for organisasjons-turneringer, ikke en
DB-constraint. En side er 1 spiller (match) eller 2+ (fourball/
foursome/greensome/scramble_2/scramble_4). round_participant. round_side_id (nullable — kun satt for to-sidede formater) knytter
deltakeren til sin side.
Beslutning B — Alle fire par-/lag-underformater fra start, samme motor som turneringer
Bruker valgte bredt omfang: fourball, foursome, greensome OG begge
scramble-variantene (scramble_2/scramble_4) — ikke bare fourball
(som alene ville passet uendret inn i dagens skjema).
Gjenbruker den EKSISTERENDE, testede match-play-motoren fullstendig
uendret (handicap_engine.py sin Format-enum,
AllowanceStrategy-familien, match_play_strokes,
allocate_over_played_holes, compute_match_state, HoleResult) —
ingen ny motorkode for disse fire formatene. Selve API-laget
(app/routers/rounds.py) porterer det allerede bevisste,
produksjonskjørte mønsteret fra app/handicap.py/app/routers/ scoring.py (compute_and_store_side_handicaps →
relative_strokes_for_match → _side_net/_compute_hole_results →
recompute_and_cache_match_state), tilpasset round/round_side i
stedet for match/team_side. Samme _SIDE_IS_UNIT/FORMAT_UNIT_SIZE-
tabeller (hvor mange spillere som kreves per side før handicap kan
beregnes) gjenbrukes identisk.
Beslutning C — Delt-ball-formater: round_hole gjenbruker hole_scores nullable-nøkkel-mønster
Foursome/greensome/scramble har ÉN kombinert score per SIDE per hull,
ikke én per spiller — akkurat samme situasjon som org-scopet
hole_score allerede løser (match_participant_id NULLABLE, delt-ball-
rader identifisert av team_side alene, to partielle unike indekser).
round_hole.round_participant_id gjøres NULLABLE, ny
round_hole.round_side_id lagt til, med samme
(participant XOR side)-CHECK og to partielle unike indekser som
originalen. For fourball/match (individuell ball) forblir round_hole
uendret, én rad per DELTAKER per hull, akkurat som i dag.
Konsekvens for statistikk (ADR-033 Beslutning B): de detaljerte per-hull-feltene (kølle/retning/chip/bunker/straffeslag/putt-lengde) gir ingen entydig mening for en DELT ball (hvem sitt slag var det?) — for delt-ball-formater lagres derfor KUN selve slagtallet på siderraden, ingen av de andre detalj-feltene (samme "et fast sett felt, ikke fri logg"-prinsipp, bare at feltsettet er tynnere for denne raden).
Beslutning D — Skins: konfigurerbar netto/brutto OG rullerer/deles, ny motorfunksjon
Ingen eksisterende motorstøtte for skins i det hele tatt (ikke et turnering-format). Bruker valgte at DEN SOM SETTER OPP runden skal kunne velge BEGGE akser uavhengig — ikke ett fastlåst standardvalg:
round.skins_scoring:'net'(HCP-slag trekkes fra, WHS-vanlig) |'gross'.round.skins_tie_handling:'carry'(uavgjort hulls "skin" ruller til neste hull, vinneren der tar hele potten) |'split'(uavgjort hulls skin deles likt mellom de tied spillerne i stedet).
Ny, ren, testbar motorfunksjon compute_skins(scores_by_hole, tie_handling) i handicap_engine.py — tar allerede netto-ELLER-brutto-
avgjorte per-hull-scorer (kalleren bestemmer hvilket FØR kall, motoren
vet ikke selv om input er netto eller brutto), returnerer
{participant_id: antall skins vunnet} (float, siden 'split' kan gi
brøkdeler). Ingen sider trengs for skins — rent individuelt, som
slagspill.
Beslutning E — HCP-telling per format
- Slagspill, fourball, skins (individuell ball — hver spiller har en FULL, ekte brutto-score per hull): teller normalt mot faktisk HCP, samme pipeline som i dag (Score Differential fra AGS). Bekreftet eksplisitt av bruker for skins — "krever fullt utspilte hull (ingen konsedering som i match) -- samme grunnlagsproblem som gjorde match upålitelig gjelder ikke her."
- Match (uendret fra ADR-038): fortsatt "spør, anbefal ekskludering" — Rule 3.3-problemet (konsederte hull) gjelder fortsatt uendret.
- Foursome/greensome/scramble (delt ball — INGEN individuell score
finnes å telle):
counts_for_handicapforblir usann for disse deltakerne i disse rundene, alltid, uansett antall spilte hull. Bekreftet eksplisitt av bruker som riktig løsning — matcher for øvrig hvordan WHS i praksis også behandler delt-ball-formater (ikke indeks-byggende på samme måte som individuell/fourball-spill).
Beslutning F — Scorekort-presentasjon (frontend, ikke bygget i denne runden)
Notert for senere: match/fourball/foursome/greensome/scramble trenger
en løpende matchstatus-visning (gjenbruk av describe()-språket fra
MatchState, samme som session-scorecard.tsx allerede viser for
turnering-matcher) i stedet for rå slagsummer. Skins trenger en
skins-tavle (skins vunnet per spiller + hvilke hull som fortsatt er
"i potten"). Presist UI-design ikke gjort i denne runden — kun
backend/motor/skjema.
Konsekvens — hva som bygges i DENNE runden vs. senere
Bruker valgte å gå rett til migrasjon + motor (ikke bare dokumentere).
Bygget: skjema (migrasjon 031), compute_skins-motorfunksjon + tester,
og backend-API for sider/delt-ball-scoring/format-resultat. Frontend
(nye skjermer for sideoppsett og de nye scorekort-presentasjonene,
Beslutning F) er en egen, senere runde — samme lagdeling som ADR-038
(backend/motor FØR frontend).
ADR-040: «Det store grepet» — sammenhengende rundeoppsett + delt
Score/Scorekort/Leaderboard-navigasjon
Kontekst: reist 2026-08-01/02, som en direkte oppfølging av flere
visuelle redesign-runder (login-skjerm og dashbord, begge inspirert av et
eksternt design-verktøy, "Stitch"). Brukeren pekte på to strukturelle
problemer som selve fargeredesignet ikke løser: (1) rundeoppsettet er i
dag spredt over ETT langt skjema (new-round.tsx) OG en helt annen skjerm
(round-detail.tsx, etter at runden allerede er opprettet, for sider/
Money Ball-rekkefølge), (2) Score/Scorekort/Leaderboard for en frittstående
runde deler ingen fast navigasjon — hver side bygger sin egen fulle
side-chrome, ulikt Stitchs skisse.
Beslutning A — rundeoppsettet blir én sammenhengende veiviser, 5 steg: Bane & tid → Spilleform (inkl. HCP-innstillinger) → Spillere → Lag/ rekkefølge (kun to-sidede formater/Money Ball) → Deling. Erstatter dagens ett-langt-skjema-pluss-en-helt-annen-skjerm-mønster.
Beslutning B — HCP-prosent er ALLTID justerbar, Match-HCP er en egen
bryter: brukeren presiserte eksplisitt at prosenten (ADR-014s
strategy) aldri skal være låst til formatets standardverdi, og at
to-sidede formater i tillegg skal kunne skru use_matchplay_handicap
av/på — differensial-fordeling mellom sidene (laveste settes til 0
mottatte, resten mottar differansen fordelt fra laveste stroke index) i
stedet for ren brutto når det ikke er noen forskjell. Funnet ved
verifisering, ikke antatt: begge deler fantes allerede FERDIGBYGD i
motoren (match_play_strokes() i handicap_engine.py, ADR-014s "fire
brytere" i app/handicap.py) — kun brukt av org-scopede turneringer
(session.allowance_override). Frittstående runder (round-tabellen)
manglet bare selve kolonnen; ingen ny regnelogikk.
Beslutning C — "Spillere og runde" (inkl. flighter) blir værende på rundeoppsettsiden, flytter IKKE inn i den nye delte fane-raden — dette er forvaltning, ikke visning. Score/Scorekort/Leaderboard trenger i stedet en kompakt, alltid tilgjengelig vei tilbake til oppsettsiden (sammen med "Fullfør runde"/"Slett runde") — plassering (toppmeny vs. footer vs. noe annet) er bevisst overlatt til V0 å foreslå ut fra en presist beskrevet kontekst, ikke forhåndsbestemt her.
Beslutning D — Scorekort og Statistikk slås sammen til ÉN side,
statistikk RETT UNDER scorekortet — den delte fane-raden blir dermed tre
faner (Score / Scorekort / Leaderboard), ikke fire. /my-rounds/[id]/ stats forsvinner som egen rute, innholdet flytter inn i /my-rounds/ [id]/scorecard.
Beslutning E — reelt, tidligere udokumentert funn: spillere i
scorekortet var ALDRI gruppert lagvis. Brukeren rapporterte det
konkret (en ekte fourball-runde der Erol+Kåre var ett lag, Ellen
Anette+Christer det andre, men vist interleaved i rekkefølgen de ble
lagt til). Bekreftet i koden på BEGGE steder spillere vises for et
to-sidet format: ScorecardGrid (round-detail.tsx) og
MatchScorecardGrid (round-scorecard.tsx) sorterte ingen av dem på
round_side_id — ren innsettingsrekkefølge. Fikset med en liten,
presis sortering (stabil sort på side, side A samlet FØR side B) i
begge, IKKE en ny funksjon. Om celle-nivå-fargelegging (hvem som vant
hullet) alene er nok til å skille lagene visuelt, eller om det trengs
en tydeligere lag-blokk-markering, er et åpent spørsmål overlatt til V0
(samme "spør V0"-mønster som Beslutning C).
Bygget og verifisert denne runden (2026-08-02):
- Ny migrasjon
051_round_allowance_override.sql—round.allowance_ override jsonb, rent additivt. app/routers/rounds.py: ny_round_allowance_override()-hjelpe- funksjon (henter/parser friskt fraround-raden, samme mønster sommatches.py/scoring.pysinsession.allowance_override— IKKE cachet i selve config-objektet), koblet inn i_recompute_side_ handicaps/_relative_strokes_for_round.RoundCreate/RoundUpdate/RoundOutfikkallowance_override. PATCH gjenberegnerplaying_ handicapfor allerede tildelte sider ved endring (samme mønster somtournaments.pysin_recompute_session_matches).- Scratch-verifisert presist: 100%→50%-prosent ga
playing_handicap24/10 → 12/5 (nøyaktig som beregnet for hånd fra HCP 20,0/10,0 mot en slope/rating-satt egendefinert bane),use_matchplay_handicap:falsega rå 12/5 i stedet for differensial 7/0 (match_play_strokes([12,5])→ laveste=5 til 0, differanse 7). Slagspill upåvirket (regresjon bekreftet). - Lag-grupperingsfiksen (Beslutning E) browserverifisert mot en fersk
scratch-fourball-runde som gjenskapte brukerens eget rapporterte
scenario nøyaktig (fire spillere lagt til i interleaved rekkefølge
A→Rødt, B→Blått, C→Rødt, D→Blått) — bekreftet visuelt i BÅDE
ScorecardGrid(live) ogMatchScorecardGrid(post-runde) at Rødt lag nå vises samlet, deretter Blått. - Rullet ut mot ekte systemer 2026-08-02, bruker bekreftet eksplisitt:
migrasjon 051 kjørt mot ekte
teecup_db(kolonne bekreftet,test_isolation.sqlfortsatt 12/12), deretterdocker compose up -d --build teecup_api teecup_frontend. Begge containere boot-et rent,/health//dashboard→ 200, anonymPATCH /rounds/{ukjent-id}ga korrekt401(ikke en rå 404 — bekrefter ruten når FastAPI),teeoff.noupåvirket.
Steg 3 (V0-prompt 1, rundeoppsett-veiviseren) ✅ BYGGET, GRUNDIG
VERIFISERT OG LIVE (2026-08-02), samme dag: V0-prompten (Beslutning A/B)
skrevet og kjørt av bruker (zip 25), frontend/components/new-round.tsx
skrevet fullstendig om — V0s 5-stegs veiviser-skall flettet med ekte
logikk: offisiell/egen bane-søk (inkl. nearby-geolokasjon), ekte egen-
bane-opprettelse (portert fra tidligere versjon), ekte /people/search,
ny allowance_override-konstruksjon i steg 2 (samme mønster som
CreateSessionCards buildAllowanceOverride), og en helt ny
innsendingsorkestrering — runden (+ deltakere + sider/lineup) finnes
FØRST når HELE veiviseren er fullført (runde → sider → medspillere med
round_side_id satt direkte på opprettelse → eierens egen side-
tildeling via PATCH → Money Ball-rekkefølge via PATCH → naviger til
runden), ulikt den gamle "opprett runde først, legg til spillere
etterpå"-flyten.
Reelle feil funnet og rettet under integrering: V0s egen Tee-type
manglet kjønnsfelt (ville vist ufiltrerte utslag for både eier og
gjester) — lagt til genders: ApiGender[], filtrert riktig per kjent
kjønn. To TS-feil (prop-spredning som overskrev course/ownGender
tilbake til null, og en "x"-kjønn-mismatch i tee-filtreringen).
Bevisst, ærlig begrensning: en spiller funnet via /people/search
har ukjent kjønn før de faktisk er lagt til runden (samme begrensning
som PersonMatch-modellen alltid har hatt) — utslagslisten for en slik
spiller er derfor UFILTRERT i veiviseren, med en forklarende tekst;
server-validering gir en klar feil ved et faktisk uforenlig valg.
Verifisert grundig i isolert scratch: full produksjonsbuild (alle
ruter listet), og to komplette nettleser-gjennomkjøringer mot en fersk
scratch-backend — én Fourball-runde (egen bane, gjest, to sider
opprettet og tildelt interleaved, 90 % HCP-prosent) og én Slagspill-
runde (rene standardvalg). Bekreftet direkte mot databasen at
allowance_override ble bygget nøyaktig riktig i begge tilfeller
({"strategy":{"type":"per_player","percentage":0.9},...} for
Fourball — riktig per_player, ikke combined, siden fourball ikke er
en side-enhet — og null for Slagspill, ingen unødvendig payload),
sidene/tildelingene stemte, og HCP ble beregnet korrekt fra ekte
profildata (kryssjekket mot hånd-regnet Course Handicap for begge
testspillerne).
Rullet ut live 2026-08-02, bruker bekreftet eksplisitt: ingen
migrasjon (ren frontend), docker compose up -d --build teecup_frontend
(gjenskapte også teecup_api som vanlig bivirkning, ingen backend-kode
rørt). Begge containere boot-et rent, /health//dashboard/
/my-rounds/new → 200, teeoff.no upåvirket.
**Steg 4-5 (V0-prompt 2, den delte Score/Scorekort/Leaderboard-fane-raden
- integrering) ✅ BYGGET, GRUNDIG VERIFISERT OG LIVE (2026-08-02), samme
dag:** V0-prompten (Beslutning C/D) skrevet — bevisst SMALERE i omfang
enn prompt 1: kun den delte header/fane-raden, ikke en redesign av selve
innholdet på Scorekort-siden (den sammenslåingen er ren sammenstabling av
to allerede ferdige komponenter, løst direkte i kode uten V0). Kjørt av
bruker (zip 26) — leverte
components/round-header.tsx(kontekstblokk med tilbakeknapp/bane/tee/dato/tid/Matchspill-merke, tre-fanet segmentert kontroll, og en "Administrer"-knapp som åpner en bunnsheet- dialog med lenke til "Spillere og runde" + "Fullfør runde" + to-stegs "Slett runde") — tatt inn UENDRET (ren presentasjon, ingen logikk å bytte ut). Ekte datalag bygget rundt den: nycomponents/round-page-shell.tsx— henter kun de feltene headeren trenger via/rounds/{id}, byggertabHrefstil de tre ekte rutene, og implementereronFinishRound/onDeleteRoundsom egne, selvstendige handlinger (sammePOST .../complete/DELETE ...-endepunkter somround-detail.tsxsin opprinnelige logikk, men uavhengig av den siden komponenten også må fungere fra Scorekort- og Leaderboard-siden). Wiret inn i alle tre destinasjonene: Score (round-detail.tsxsin gamle egne header fjernet, erstattet medRoundPageShell— den INTERNE "Score"/"Spillere og runde"-fanevekslingen beholdt UENDRET som et bevisst lavrisiko-valg, nås nå i tillegg via headerens "Administrer"-dialog med en ny?tab=manage-URL-parameter lest ved mount), Scorekort (round-scorecard.tsx+round-stats.tsxslått sammen til ÉN side via en nyembedded-prop på begge, statistikk RETT UNDER scorekortet som spec-et i Beslutning D — gammel/stats-rute er nå en ren redirect dit), Leaderboard. Reelle feil funnet og rettet under integrering: to<main>- landemerker på samme side etter sammenslåingen av Scorekort+Statistikk (ugyldig HTML/tilgjengelighet) — rettet med enContentTag-switch (divnår embedded,mainellers) iround-stats.tsx. Fjernet nå overflødige kryss-lenker mellom de to sammenslåtte sidene, kollapset "Se scorekort"/"Se full rundestatistikk" til én "Se scorekort og statistikk"-knapp iround-detail.tsxsitt fullført-banner. Verifisert grundig i isolert scratch: full produksjonsbuild (alle ruter listet), full nettleser-gjennomkjøring gjennom hele "Administrer"- flyten — headeren konsistent på tvers av alle tre fanene, dialogens lenke til "Spillere og runde" bekreftet å faktisk åpne riktig intern fane via?tab=manage, ekte "Fullfør runde" (bekreftetcompleted_atsatt via et direkte API-kall etterpå) og ekte to-stegs "Slett runde" (bekreftet navigerte korrekt til tom-tilstand på/my-roundsetterpå). Traff en Turbopack-flakighet i selve scratch-dev-serveren underveis (stuck spinner, ingen nettverkskall i det hele tatt) — løst med en frisk containerstart, bekreftet IKKE en kodefeil (samme kjente ustabilitetsklasse som tidligere dokumentert i dette prosjektet, ikke noe nytt). Rullet ut live 2026-08-02, bruker bekreftet eksplisitt: ingen migrasjon (ren frontend),docker compose up -d --build teecup_frontend(gjenskapte ogsåteecup_apisom vanlig bivirkning, ingen backend-kode rørt). Begge containere boot-et rent,/health//dashboard→ 200,teeoff.noupåvirket.
ADR-040 («det store grepet») er dermed HELT FERDIG, alle 5 steg, backend + frontend, live:
Backend:✅allowance_override-migrasjon + koblingKode-fiks: lag-sortering✅V0-prompt 1: den samlede rundeoppsett-veiviseren✅V0-prompt 2: den delte Score/Scorekort/Leaderboard-fane-raden✅Integrering + browserverifisering + utrulling✅
Bevisst utenfor omfang, egen fremtidig vurdering om ønskelig: den
INTERNE "Score"/"Spillere og runde"-fanevekslingen på selve Score-siden
ble bevisst beholdt (ikke fjernet/erstattet av ?tab=manage-mekanismen
alene) som et lavrisiko-valg — kan forenkles videre senere om ønsket,
ikke noe brukeren har bedt om.
ADR-041: Konkurranseklasser ("Damer fra 44, Herrer fra 50+")
Kontekst: Brukeren spurte om det var mulig å sette opp runder i en
turnering slik at f.eks. damer spiller fra utslag 44 mens herrer spiller
fra 50+. Undersøkelse viste at DELER allerede virket (hvert
match_participant/round_participant har alltid hatt sitt eget
tee_id, uavhengig av de andre, med server-side validering av at
utslaget har rating for spillerens kjønn, ADR-029) — men det var et
manuelt valg per spiller hver gang, ingen gjenbrukbar "klasse"-
gruppering. Ingen "klasse"/kategori-konsept for konkurranse fantes fra
før noe sted i skjemaet (kun den urelaterte venne-kategoriseringen,
ADR-036).
Beslutning A — Klasser er fritt navngitte, ikke en fast taksonomi.
Organisator oppretter klasser med eget navn (f.eks. "Damer", "Herrer A",
"Junior") og et VALGFRITT standardutslag — ikke en kjønns- eller
alders-spesifikk datamodell. Bekreftet eksplisitt med bruker
(AskUserQuestion) framfor en fast kjønn+alder-kombinasjon: dekker
kjønn/alder/HCP eller annet uten at systemet må forstå forskjellen,
samme fleksibilitets-prinsipp som friend_categorization (ADR-036).
Beslutning B — Delt tabell, ikke duplisert per turneringstype. Ny
tournament_class-tabell (migrasjon 053) FK'et til tournament, ikke
en utvidelse av selve tournament-raden — samme sidetabell-mønster som
tournament_sponsor. Siden BÅDE lagturneringer (ADR-011) og
individuelle turneringer (ADR-037) peker til samme delte tournament-
tabell (format_type-diskriminator), dekker ÉN tabell begge uten
duplisering. Klassetilhørighet lever på team_roster.class_id
(lagturneringer) og tournament_participant.class_id (individuelle
turneringer) — begge nullable, ON DELETE SET NULL (samme prinsipp som
tournament_round_bbb_holes FK-er, migrasjon 044): sletter man en
klasse, forsvinner ikke roster-/deltaker-radene, de mister bare
merkelappen.
Beslutning C — Standardutslag er en frontend-bekvemmelighet, ikke en
ny backend-mekanisme. tee_id er ALLEREDE required og satt per
deltaker (ikke per økt/runde) ved både POST .../matches/{id}/ participants og POST .../rounds/{id}/participants — klassens
standardutslag forhåndsvelger bare dette feltet i UI-et (fortsatt fritt
overstyrbart), backend-kontrakten er uendret. Samme mønster som
new-round.tsxs eksisterende kjønnsfilter/-default for personlige
runder. Forhåndsvelgingen sjekker eksplisitt at utslaget faktisk finnes
på DEN aktuelle øktens/rundens bane før det brukes — en klasses
standardutslag kan tilhøre en annen bane enn den som er i bruk akkurat
da.
Beslutning D — Egen resultatliste KUN i individuelle turneringer,
aldri i lagturneringer. Den mest substansielle avklaringen
(AskUserQuestion, med begrunnelse presentert først): i lagturneringer
er poeng knyttet til hele KAMPER (lag mot lag, match.points_side_a/b),
ikke enkeltspillere — å dele opp dette per klasse gir ikke naturlig
mening, og brukeren bekreftet eksplisitt at klasse der KUN skal foreslå
utslag. Det finnes fra før et smalt, PER-ØKT individuelt leaderboard for
singles/fourball-slagspill (fetch_individual_leaderboard,
tournaments.py) — bevisst IKKE utvidet eller gjenbrukt av denne runden,
det er en annen, allerede eksisterende funksjon med annet formål
(sesjonsscoped, format-begrenset). I individuelle turneringer (flatt
felt, ADR-037) er splitting derimot en naturlig match: individual_ leaderboard-endepunktet fikk class_id/class_name lagt til i
SELECT+respons, men selve summerings-/sorteringslogikken er UENDRET —
frontend grupperer den allerede korrekt sorterte listen visuelt (stabil
gruppering bevarer riktig rangering per klasse), ingen ny motorlogikk i
handicap_engine.py trengtes (Copenhagen/BBB var allerede scope-
agnostiske på deltaker-mengde).
Konsekvens: 053_tournament_classes.sql. Ny klasse-CRUD i
tournaments.py (delt, siden tournament-tabellen er delt). Roster EntryUpdate (tidligere KUN is_captain: bool påkrevd) lagt om til
exclude_unset-PATCH-semantikk. Ny PATCH .../participants/{id} i
individual_tournaments.py (fantes ikke før). Frontend: "Klasser"-kort
i BÅDE tournament-detail.tsx og individual-tournament-detail.tsx,
utslag-forhåndsutfylling i session-blind-draw.tsxs AddSlotForm og
individual-tournament-detail.tsxs AssignRoundParticipantControl,
klasse-gruppert LeaderboardTab (individuelle turneringer). Se
CHANGELOG.md 2026-08-04 for full bygge-/verifiseringsdetalj (inkl. tall-
for-tall-bekreftet leaderboard-rangering per klasse i ekte nettleser).
Rullet ut live 2026-08-04, bruker bekreftet eksplisitt.
ADR-042: Delt, plattform-omfattende bane-mal-bibliotek (personal_course)
Kontekst: Brukeren oppdaget at turneringsmodulens "opprett manuell bane" i praksis var ubrukelig (kun et navnefelt, ingen vei til hull/ utslag), og ba om at BÅDE turneringsmodulen og single-runde-modulen skal tilby "bruk en eksisterende bane (TeeOff eller andres custom-bane) som mal" ved manuell baneoppretting. Et oppfølgingsspørsmål presiserte at disse custom-banene "bør lagres, og de må være offentlige" — synlige og gjenbrukbare av andre.
Beslutning A — Gjenbruk personal_course, ikke en ny tabell.
personal_course (020_personal_rounds.sql, opprinnelig bygget for
frittstående runder) er, til tross for navnet og til tross for at ingen
tidligere runde eksplisitt utnyttet det, ALLEREDE en global,
plattform-omfattende katalog — GET /personal-courses søker på tvers av
ALLE brukeres baner uten eier- eller organisasjonsfiltrering, siden
tabellen (bevisst, fra migrasjon 020) aldri har vært RLS-/org-scopet.
Fremfor å bygge en ny community_course-tabell (vurdert og forkastet)
gjenbrukes denne eksisterende, allerede-globale tabellen direkte som det
delte mal-biblioteket — eneste tilføyelse er én kolonne,
forked_from_id (migrasjon 054, selvreferende FK, ON DELETE SET NULL)
for proveniens/attribusjon. Dette er også den ENESTE farbare veien for
at single-runde-modulen (som ikke har noe organisasjons-begrep i det
hele tatt) kan dele samme bibliotek som org-turneringsmodulen.
Beslutning B — "Offentlig" betyr hele plattformen, ikke bare egen
organisasjon. Bekreftet eksplisitt med bruker (AskUserQuestion) etter
at jeg flagget spenningen: skal en publisert custom-bane være synlig for
ALLE organisasjoner + alle frittstående brukere, eller kun innad i én
organisasjon? Svaret var plattform-omfattende — organisasjonsgrensen
gjelder domenedata (turneringer, spillere, resultater), ikke dette
delte, lavsensitive bane-referansebiblioteket. En org-course-rad
(brukt til faktisk spill i en turnering) forblir like fullt org-scopet
og RLS-beskyttet som før — publisering til personal_course skjer som
en SEPARAT, samtidig innsetting i samme transaksjon (courses.py), ikke
en endring av course-tabellens egen skoping.
Beslutning C — Eierskap håndheves i applikasjonslaget, ikke RLS.
Siden personal_course bevisst aldri har vært org-scopet, finnes det
ingen app.current_org-kontekst å håndheve mot. De nye
endre-/dupliser-/slette-endepunktene (rounds.py) sjekker eksplisitt
created_by_user_id == innlogget bruker i handler-koden — samme
plain_connection()-uten-RLS-mønster som resten av personal_course/
round-familien allerede bruker (ADR-033 Beslutning A).
Beslutning D — Rediger forker automatisk hvis du ikke eier raden;
Dupliser er en egen, eksplisitt handling. PATCH /personal-courses/{id}:
eier den innloggede brukeren raden, oppdateres den i sted; eier
brukeren IKKE raden, opprettes automatisk en NY rad (eid av innlogget
bruker, forked_from_id satt til originalen) — originalen selv røres
aldri. Ett endepunkt dekker begge casene uten at frontend selv må
forgrene på eierskap. POST .../duplicate er en SEPARAT, eksplisitt
handling tilgjengelig uansett eierskap på kilden — dekker f.eks. å lage
en variant av DIN EGEN bane uten å miste originalen, noe fork-ved-
redigering alene ikke gjør (den trigges kun når raden IKKE er din).
Bekreftet eksplisitt med bruker at begge mekanismene skulle beholdes
side om side.
Beslutning E — Mal-bruk er en engangs-kopi, ingen vedvarende kobling.
Når en org-course opprettes med en personal_course (eller en
TeeOff-bane) som utgangspunkt, kopieres dataene inn i den nye raden ved
opprettelsestidspunktet — ingen fremmednøkkel eller synk-mekanisme
knytter dem sammen etterpå. Samme filosofi som offisiell TeeOff-import
allerede etablerte (ADR-019): en mal er et utgangspunkt å redigere fritt
fra, ikke en levende referanse. Konsekvens: sletting av en
personal_course-mal påvirker ALDRI en org-course-rad som tidligere
ble opprettet fra den.
Konsekvens/bevisst utenfor omfang: ingen moderasjon/vetting av offentlige baner (tillitsbasert, matcher appens øvrige lukkede-plattform-antagelser); ingen privat/kun-min-org-synlighet per bane (alt publisert via denne veien er plattform-offentlig, ingen per-rad-innstilling bygget). Se CHANGELOG.md 2026-08-04 for full bygge-/verifiseringsdetalj (34/34 håndregnede API-sjekker, ekte nettleser-verifisering på tvers av to brukere/to organisasjoner). Rullet ut live 2026-08-04, bruker bekreftet eksplisitt.
ADR-043: Order of Merit — sesong-sammenlagt rangering [OOM]
Kontekst: Brukeren ba om en vurdering av GolfBox sin "Order of
Merit"-funksjon (opplastet PDF) og om noe var verdt å adoptere. Svaret
var "ja, delvis": skillet mellom HVA som telles per turnering
(result_type) og HVORDAN det summeres over en sesong
(aggregation_mode) er et genuint godt, gjenbrukbart mønster —
GolfBox sin lag-kobling (strengmatching på lagnavn på tvers av
turneringer) ble eksplisitt flagget som noe å IKKE kopiere, siden
TeeCup allerede har ekte player_id-identitet på tvers av hele
organisasjonen.
Beslutning A — Kun individuelle turneringer kan lenkes. En OOM
(order_of_merit, migrasjon 055) lenker KUN tournament.format_type = 'individual'-turneringer (ADR-037) — ikke org-lagturneringer (ADR-011),
av samme grunn som konkurranseklassenes leaderboard-splitting (ADR-041):
poeng i en lagturnering er knyttet til hele kamper, ikke enkeltspillere.
Beslutning B — To uavhengige akser, fem resultattyper. result_type
(poeng-etter-plassering/Stableford-sum/brutto/netto/pengeliste) utleder
HVA som telles fra en lenket turnerings EGEN, allerede beregnede
leaderboard (gjenbruker _compute_individual_standings, faktorisert ut
av individual_tournaments.py sin individual_leaderboard 2026-08-04 —
ingen duplisering av posisjon-/til-par-beregningen).
aggregation_mode (sum/snitt/eclectic) avgjør HVORDAN dette summeres
over flere turneringer, uavhengig av result_type. Poeng/pengeliste
trenger en per-lenke konfigurasjon (order_of_merit_link.points_table/
money_payout_table, JSONB) siden ulike turneringer i samme OOM kan gi
ulik poengverdi for samme plassering.
Beslutning C — "Lag" i en OOM er et sesong-par satt opp DIREKTE i
OOM-en, ikke hentet fra noen turnering. Avklart eksplisitt med bruker
etter at jeg flagget en strukturell floke (individuelle turneringer har
per definisjon ingen lag, ADR-037): order_of_merit_team/
_team_member (migrasjon 055) er en REN gruppering av spillere oppå
deres allerede beregnede individuelle OOM-resultater — ingen
strengmatching, ingen ny turnering-struktur. Bevisst IKKE bygget i denne
runden (se FEATURE_BACKLOG.md).
Beslutning D — kind og result_type låses etter opprettelse.
Matcher GolfBox sin egen tilsvarende låsing, strukket til også å dekke
result_type her: å endre den i etterkant ville gjort eksisterende
lenkers poeng-/pengetabell-konfig meningsløs uten en egen
migreringssti. PATCH-endepunktet eksponerer bevisst ikke disse to
feltene i det hele tatt (ikke bare app-lags-validering) — opprett en ny
OOM i stedet.
Beslutning E — "Regn ut ved lesing", ingen cache-tabell. Samme
filosofi som Nassau/High-low-high/individual_leaderboard selv —
OOM-leaderboardet beregnes helt på nytt ved hvert GET-kall (henter
alle lenkede turneringers standings, utleder bidrag per spiller, filtrerer
på aldersgrense/minimum-resultater, aggregerer med
order_of_merit_aggregate, rangerer). Ingen ytelsesbekymring i praksis
gitt forventet skala (en sesong har typisk noen få til noen titalls
lenkede turneringer).
Bevisst utenfor omfang v1 (se FEATURE_BACKLOG.md for full liste):
Eclectic-aggregering (per-hull "drømmerunde" på tvers av lenkede
turneringer — krever egen datainnsamlingslogikk, ikke bare en ny
aggregeringsmodus); Lag-OOM sin faktiske leaderboard-beregning (CRUD-
skjema finnes fra migrasjon 055, men GET .../leaderboard avviser
eksplisitt kind='team' med en tydelig feilmelding inntil videre).
Motor (handicap_engine.py): order_of_merit_points_for_position
(uavgjort deler SAMME poengverdi ved sin felles plassering, ikke
gjennomsnitt) og order_of_merit_aggregate (sum/snitt, valgfri
behold-N-beste — "best" er alltid størst-er-bedre, kalleren negerer
brutto/netto-verdier før kall). 9 nye enhetstester, alle grønne.
Frontend: to nye sider (/organizations/[id]/order-of-merit liste
- opprett,
.../[oomId]detalj: lenkede turneringer → innstillinger → resultatliste, i den rekkefølgen). Håndkodet (ikke V0 — ingen V0-credits tilgjengelig denne runden), bevisst bygget til samme visuelle presisjon som appens V0-eksporter (samme tokens/kort-mønster somorg-members.tsx, samme gull-ledertrøye-mønster somstroke-play-leaderboard.tsx). Ny inngangslenke fraorg-members.tsx.
Scratch-verifisert grundig (isolert teecup_app_scratch-rolle +
isolert scratch-MinIO + engangs API-/frontend-container): 63/63
håndregnede API-sjekker (fem resultattyper, uavgjort-håndtering,
behold-N-beste, minimum-resultater, aldersgrense, låst kind/
result_type), RLS-isolasjon eksplisitt testet på de fire nye
tabellene (kryss-org-lesing gir 0 rader, kryss-org-skriving blokkeres av
WITH CHECK), test_isolation.sql 12/12 uendret, ekte
nettleser-gjennomgang (liste, opprettelse, lenke turnering inkl.
poeng-tabell-skjemaet, innstillinger, gull-ledertrøye, ikke-kvalifisert-
seksjon). Full detalj i CHANGELOG.md 2026-08-04.
Rullet ut live 2026-08-04, bruker bekreftet eksplisitt.
ADR-044: Kommentarer/bilder på frittstående runder + samlet feed
Reist av brukeren 2026-08-06: samme "Banter Board"-mulighet (bilde+tekst)
som org-turneringer allerede har (ADR-025), men for en enkelt, frittstående
runde — pluss en sentral feed-side som samler dette på tvers av runder.
Underveis avdekket research at rundevisibilitet (ADR-036 fase 2 —
round.visibility_mode/round_visible_category) allerede var bygget og
live siden 2026-07-28 (en eldre, aldri oppdatert CHANGELOG-seksjon sa
fortsatt "ikke bygget") — denne ADR-en gjenbruker den infrastrukturen
uendret, den handler kun om selve kommentarene og feed-aggregeringen.
Beslutning A — Skriverett: kan-se-kan-poste. Alle som kan SE en runde
(eier, lenket medspiller, public-synlig, eller riktig-kategorisert
friends-venn — samme _can_view_round som ADR-036) kan også POSTE et
bilde/kommentar til den. Samme "kan se = kan bidra"-modell som org-feeden,
IKKE begrenset til eier+medspillere. Avklart eksplisitt med bruker
(AskUserQuestion) — overstyrer et tidligere, snevrere "eier+medspillere"-
utkast notert i FEATURE_BACKLOG.md 2026-08-06.
Beslutning B — Moderering: forfatter ELLER rundeeier. Samme
"forfatter-eller-admin"-modell som org-feedens delete_feed_message
(ADR-025 Beslutning C) — rundens eier er her analogen til org-admin. Ulikt
lag-chattens strengere "kun forfatter, ingen unntak" (den er ekte privat,
en runde-kommentarseksjon åpen for tredjeparter trenger en reell
moderasjonsvei).
Beslutning C — Egen round_message-tabell (migrasjon 058), ikke
gjenbruk av org sin message-tabell. round har verken
organization_id eller RLS (ADR-033 Beslutning A) — en delt tabell ville
krevd enten en kunstig organisasjon rundt hver frittstående runde eller et
RLS-unntak. Samme feltform (forfatter-snapshot, valgfri tekst, valgfri
bilde-nøkkel) og samme MinIO+AVIF-pipeline (app/storage.py, ADR-018)
gjenbrukt uendret, egen prefix="round_messages". Autorisasjon håndheves
utelukkende i app-laget (app/routers/round_messages.py,
plain_connection()), ikke database-policy — samme mønster som
round/round_participant.
Beslutning D — Sanntid: ingen ny WebSocket-kanal. Gjenbruker den
eksisterende broadcast_round_update//ws/rounds/{id}/live-kringkastingen
(allerede trigget ved hver hull-endring) fremfor et nytt, payload-bærende
kanal-mønster (slik org-feeden bruker). round-detail.tsx sin eksisterende
WS-lytter fikk ett nytt tick-signal (messagesRefreshTick) som
RoundMessages reagerer på med en vanlig REST-refetch — ingen egen socket
for kommentarer. Bekreftet fungerende i ekte nettleser (se
CHANGELOG.md 2026-08-06) — MERK at dette KUN virker gjennom Caddy sin
dedikerte handle /ws/* { reverse_proxy teecup_api:8000 }-rute
(/opt/teeoff/deploy/Caddyfile), IKKE gjennom Next.js sitt eget
rewrite-lag (som ikke pålitelig proxyer WebSocket-oppgraderinger i
standalone-modus) — samme forutsetning som all annen runde-/turnering-
sanntid i appen.
Beslutning E — Ny, dedikert /my-feed-side, IKKE en dashbord-seksjon.
Avklart eksplisitt med bruker. Aggregerer på tvers av egne runder (uansett
visibility_mode) + venners synlige runder, modellert på
list_friends_on_course (GET /friends/on-course) sin reverserte retning
(viewer → venners synlige runder), men uten "spiller nå/siste 24t"-
filteret — en all-time, paginert (keyset, ?before=) liste av
round_message-rader. Nås fra dashbordets "Venner på banen"-seksjon
(SeeAllLink) og en ny lenke i /more.
KRITISK, funnet under full-stack-verifisering: rute-/rewrite-kollisjon,
samme klasse som /rounds→/my-rounds og /friends→/my-friends
(dokumentert i frontend/next.config.mjs). Frontend-siden ble først lagt
på /feed — nøyaktig samme streng som det flate API-endepunktet GET /feed. Uten en eksplisitt rewrite-regel vant Next.js sin egen side-rute
over ethvert forsøk på server-side proxy, så fetch("/feed") fra klienten
fikk appens egen HTML tilbake i stedet for JSON (stille feilet
JSON-parsing). Rettet ved (1) å flytte frontend-siden til /my-feed
(samme navnemønster som de tre presedensene over) OG (2) legge til en
manglende, eksplisitt { source: "/feed", destination: ... }-regel i
next.config.mjs (det flate mønsteret, uten :path*, siden /feed ikke
har understier) — retting nummer to var nødvendig uavhengig av
sideflyttingen, siden regelen rett og slett manglet helt fra start.
Scratch-verifisert grundig (isolert scratch-DB/rolle/MinIO/API-/
frontend-container, PLUSS en minimal scratch-Caddy satt opp spesifikt for
å faktisk bevise WS-refetchen — ikke bare anta at den ville virke i
produksjon): 30/30 håndregnede API-sjekker (hele autorisasjonsmatrisen —
eier poster/sletter egen, medspiller poster, en annen ikke-eier kan IKKE
slette andres, eier KAN moderere andres, fremmed uten innsyn avvist på
GET+POST, friends-synlighet riktig inkludert/ekskludert per kategori,
feed-aggregering korrekt per bruker, paginering uten hopp/duplikat, anonym
/feed-tilgang korrekt 401). Ekte bildeopplasting bekreftet konvertert til
AVIF og lagret riktig. Ekte nettleser: kommentar+bilde postet og vist,
sletting fungerte, /my-feed viste riktig aggregert sett (egne + andres
offentlige runder), WS-refetch bekreftet fungerende GJENNOM Caddy (en
kommentar postet i én "økt" dukket opp automatisk i en åpen fane uten
interaksjon). Ekte typesjekket produksjonsbuild av frontend kjørt og
bekreftet (fanget rute-kollisjonen over ved kjøretid, ikke ved bygging —
typesjekk alene fanger ikke denne klassen feil). test_isolation.sql
uendret 12/12. Scratch-miljøet (DB/rolle/MinIO/API-/frontend-/Caddy-
containere/images, V0-eksport-zip-er) ryddet opp fullstendig etter bruk.
Frontend bygget via V0 (brukerens eksplisitte, stående arbeidsfordeling
— Claude skriver V0-prompten, bruker limer inn selv, Claude integrerer
eksporten): to prompter (kommentarseksjon + feed-side), begge levert med
eksakte data-kontrakter/design-tokens fra DESIGN_SYSTEM.md forhåndsutfylt
i prompten. Begge eksportene (zip 32/33) diffet ordrett mot hverandre og
mot ingenting-fra-før (nye filer) før integrering — ingen utilsiktet drift.
To små, bevisste integrasjonsjusteringer utover ren copy-inn: fjernet en
ubrukt cn-import, og lot Feed-komponenten selv hente /auth/me (samme
mønster som round-detail.tsx) i stedet for å kreve currentUserId som
ekstern prop, siden alle andre sider i appen er tynne wrappere som selv
henter egen brukeridentitet.
Bevisst utenfor omfang: egen payload-bærende WS-kanal for kommentarer,
redigering av innlegg (kun slett, samme som org-feeden), liker/reaksjoner,
varsler ved ny kommentar, sanntid på selve /my-feed-siden, inline
slett-knapp på /my-feed (kun fra runde-siden).
ADR-045: Tilskuer-visning (/watch/[id]) — tre faner + "Spillere og runde", full paritet med eiersiden
Reist av brukeren 2026-08-06 (video-illustrasjon av bugen): en tilskuer som
klikker seg inn på en venns pågående runde via "Venner på banen" endte på
/my-rounds/{id} (eier-/medspiller-only, _get_accessible_round_or_404)
i stedet for /watch/{id} (den faktiske tredjepartsvisningen, ADR-036
fase 2) — rettet samme dag som en liten, isolert lenke-bug (se CHANGELOG
2026-08-06, punkt 26). I samme runde ba brukeren om at /watch/[id]
(fram til da ÉN enkelt side, kun kompakt status + leaderboard/matchstatus)
skulle få samme tre faner som eiersiden (Score/Scorekort/Leaderboard) og
vise "Spillere og runde" (deltakerliste, HCP/utslag/tildelte slag,
flight-gruppering), uten eier-kun-handlingene (Rediger/Fullfør/Slett
runde, +Medspiller).
Beslutning A — Full formatparitet via delt komponent + publicMode-
bryter, IKKE nye, forenklede tilskuer-komponenter. round-scorecard.tsx
(RoundScorecard) og round-leaderboard.tsx (RoundLeaderboard) sin
faktiske rutenett-/tavle-rendring var ALLEREDE ren, skrivefri visning —
redigering skjer et helt annet sted (Score-fanens ScoringWizard i
round-detail.tsx). Det eneste som hindret gjenbruk for tilskuere var at
alle interne data-hentinger (flere hooks: useFormatResult,
useFlightTeamStandings, useLeaderboardData, pluss selve
RoundScorecard/RoundLeaderboard sine egne fetch-effekter) hardkodet
/rounds/*-prefikset og en autentisert WebSocket. Løsning: en ny,
valgfri publicMode-prop (default false, INGEN endring i eiersidens
oppførsel) som bytter alle interne URL-er til /public/rounds/* og
WS-kanalen til /ws/public/rounds/{id}/live. Dette gir ekte full
formatparitet (alle formater, ikke et utvalg) for null ekstra
vedlikeholdshold -- samme kode, to datakilder -- fremfor å bygge og
vedlikeholde en parallell, forenklet visning. Bekreftet eksplisitt med
bruker (AskUserQuestion) at full detalj var ønsket, ikke en forenklet
fellesvisning.
Beslutning B — Ingen gjenbruk av RoundHeader/RoundPageShell for
selve fane-navigasjonen. RoundPageShell gjør sitt eget autentiserte
fetch('/rounds/{id}')-kall og hardkoder /my-rounds/{id}-hrefs.
RoundHeader er i og for seg props-styrt, men "Administrer"-tannhjulet
render ALLTID og åpner ManageRoundDialog (Rediger/Fullfør/Slett-runde)
uansett om handlere er gitt inn — ingen eksisterende prop slår av selve
admin-seksjonen. Bygget i stedet en egen, liten WatchTabs-komponent
(frontend/components/watch-tabs.tsx) uten noen admin-vei i det hele
tatt -- ingen risiko for at en eier-handling vises ved et uhell, siden
koden rett og slett ikke inneholder den.
Beslutning C — Ny, egen "Spillere og runde"-komponent
(watch-players.tsx), IKKE et forsøk på å gjøre round-detail.tsx sin
lokale PlayerList-funksjon gjenbrukbar via canManage=false.
PlayerList er ikke eksportert, tett koblet til eier-autentiserte
handlere, og selv canManage-proppen dekker ikke alt konsekvent i dag
("Fullfør runde"-knappen der er kun gated på !completed, ikke
isOwnerViewer -- en eksisterende, urelatert kvalitetsbrist, ikke
arvet inn her). Den nye komponenten inneholder ingen +Medspiller-/
Rediger-/Fullfør-/Slett-kode i det hele tatt. Viser deltakerkort
(navn, "Eier"-merke -- bevisst INGEN "Deg"-merke, siden
PublicRoundParticipantOut ikke eksponerer user_id og det uansett
ikke gir mening i tilskuer-rammen), Utslag/HCP/Tildelte slag (allerede
offentlig eksponert data), "Så langt"-progresjon (fra
/public/rounds/{id}/leaderboard), og flight-gruppering.
Beslutning D — Nytt offentlig endepunkt GET /public/rounds/{id}/ flight-group. Speiler den eksisterende autentiserte /rounds/{id}/ flight-group, men med en ny _viewable_flight_rows-variant av
_accessible_flight_rows som bruker _can_view_round (eier/medspiller/
public/riktig-kategorisert friends-venn) per SØSKEN-flight uavhengig
-- en flight-gruppe kan ha søsken med ulik visibility_mode, og en
tilskuer skal kun se de søsknene de faktisk har innsyn i, akkurat som
selve anker-runden. Avklart eksplisitt med bruker (AskUserQuestion) at
flight-gruppering skulle bygges nå, ikke utsettes.
Lukket, relatert gap: den gamle watch-round.tsx sin lokale
TWO_SIDED_FORMATS-liste dekket kun 8 av 17 formater (manglet
copenhagen/bbb/flag/shamble/money_ball/scramble_solo/scramble_solo_ match, som falt til "Ingen live-visning tilgjengelig ennå"). Løst
automatisk av Beslutning A (samme RoundLeaderboard-komponent som
eiersiden, som allerede dekker alle formater) -- ikke en egen,
ekstra oppgave.
Verifisert grundig, isolert scratch-miljø (DB/rolle/MinIO/API-/
frontend-container, pluss en minimal scratch-Caddy for ekte WS-testing,
samme mønster som ADR-044): 6/6 håndregnede sjekker for det nye
flight-group-endepunktet (anonym ser kun offentlige søsken-flighter,
en venn med riktig kategori ser i tillegg friends-synlige, eieren ser
alle, en fremmed uten vennskap ser kun offentlige, en privat anker-runde
avvises helt for anonym). tsc --noEmit rent på hele frontend-prosjektet
etter refaktoren. Ekte nettleser: regresjon FØRST -- eiersidens
/my-rounds/{id}/scorecard og /leaderboard bekreftet uendret (samme
tall, samme layout) FØR noe nytt ble testet. Deretter tilskuer-sidene:
alle tre faner, "Spillere og runde" uten eier-knapper, ETT allerede-
dekket format (stroke) OG ETT tidligere udekket format (flag,
Flaggturnering) begge bekreftet å rendre korrekt gjennom
/watch/{id}/leaderboard -- beviser formatparitet-lukkingen direkte,
ikke bare i teorien. En privat rundes tilskuer-sider (alle tre) bekreftet
korrekt avvist for en anonym leser (egen isolert nettleser-kontekst,
ikke bare et API-kall) med riktig feilmelding og "Tilbake til
dashbordet"-lenke. Scratch-miljøet ryddet opp fullstendig etter bruk.
Bevisst utenfor omfang: enhver endring i selve eiersidens
PlayerList/ManageRoundDialog-logikk (den nevnte "Fullfør runde"-
gating-kvaliteten er en egen, urelatert observasjon).
ADR-046: Emoji-reaksjoner + trådede kommentarer på innlegg
Reist av brukeren 2026-08-06, som del av samme melding som "nyeste først, over alt": "man skal kunne like (eller bruke andre emojier) og kommentere på innlegg." Avklart eksplisitt med bruker (AskUserQuestion, tre spørsmål): (1) "innlegg" = BÅDE de eksisterende meldingene (rundekommentar/ lag-chat-melding/oppslagstavle-post) OG dashbord-feedens oppføringer, (2) kommentarer skal være TRÅDET (svar på svar, flere nivåer), (3) reaksjoner følger samme omfang som kommentarer.
Nøkkelfunn fra research: dashbord-feeden (/my-feed, ADR-044
Beslutning E) er IKKE en egen entitetstype ("runde fullført"-kort) — hver
feed-oppføring ER ganske enkelt én round_message-rad, aggregert på tvers
av runder. Å bygge reaksjoner/kommentarer på round_message dekker derfor
AUTOMATISK både rundens egen kommentarseksjon og feeden — ingen egen
tredje entitetstype trengtes. De reelle "innleggs"-typene var dermed kun
to underliggende tabeller: round_message (ADR-044, INGEN
organization_id/RLS) og message (ADR-025, organization_id NOT NULL
- RLS, diskriminert på
scopefor lag-chat/oppslagstavle).
Beslutning A — To parallelle tabellpar, IKKE én polymorf tabell.
Migrasjon 059: round_message_reaction/round_message_comment (ingen
RLS, speiler round_message selv) og message_reaction/message_comment
(RLS'et via app_current_org(), speiler message). Samme presedens som
ADR-044 Beslutning C — en delt tabell på tvers av RLS/ikke-RLS-grensen
ville krevd enten en kunstig organisasjon eller et RLS-unntak, og
CLAUDE.md sin invariant ("organization_id på alle domenetabeller,
håndhevet av RLS") krever egen RLS-håndtering for org-siden uansett.
Beslutning B — Reaksjoner: fast kuratert sett, én per bruker per
innlegg. Seks emojier (👍 ❤️ 😂 😮 😢 🙏), validert i app-laget (400 ved
ukjent emoji) — IKKE et fritt emoji-utvalg. Bytte av emoji ERSTATTER
(UPSERT via ON CONFLICT (post_id, user_id) DO UPDATE), stables ikke —
matcher "like (eller bruke andre emojier)"-fraseringen (velg ÉN).
Beslutning C — Kommentarer: flat lagring, kun tekst, kaskade-sletting.
parent_comment_id selv-referanse, lagret FLATT — frontend bygger
tre-strukturen fra en kronologisk (eldst-først) liste (motsatt av resten
av appens "nyeste først"-strømmer, bevisst: en samtaletråd leses
top-til-bunn, ulikt en oppdateringsstrøm). Ingen bilde i selve
kommentaren (innlegget har allerede sitt eget). Sletting av en kommentar
med svar under KASKADE-SLETTER hele undertråden (FK ON DELETE CASCADE)
— ikke myk-slett-med-plassholder, samme "kun slett, aldri rediger"-
filosofi som round_message/message selv. Frontend viser en eksplisitt
advarsel ("Sletter du denne, forsvinner også svarene under.") før
bekreftet sletting av en kommentar med svar.
Beslutning D — Autorisasjon speiler eksisterende regler per
innleggstype, ingen ny modell. Hvem kan reagere/kommentere = hvem kan
POSTE selve innlegget (rundekommentar: _can_view_round/"kan se = kan
bidra"; lag-chat: rostret på laget; oppslagstavle: _may_post_to_feed).
Hvem kan slette en kommentar = samme regel som innleggstypen allerede
bruker for å slette selve innlegget (rundekommentar: forfatter ELLER
rundeeier; lag-chat: KUN forfatter, ingen unntak; oppslagstavle: forfatter
ELLER org-admin via is_org_admin). Egen reaksjon fjernes alltid fritt av
den som satte den.
Beslutning E — Ingen ny WebSocket-payload-kanal. Samme minimalt-
fotavtrykk-valg som ADR-044 Beslutning D: klienten refetcher reaksjons-/
kommentaroppsummeringen kun etter EGEN handling, ingen live-push til andre
samtidige seere i v1. Viktig rettelse funnet under nettleserverifisering:
de nye kommentar-endepunktene i round_messages.py kalte først
broadcast_round_update() (kopiert inn ved en inkonsekvens mot egen
plan) — dette trigget rundens EKSISTERENDE "noe endret seg"-WS-tick, som
RoundMessages reagerer på med en full refetch av HELE meldingslisten,
og med det kollapset enhver allerede-utvidet kommentartråd ANDRE STEDER
på siden ved hver eneste kommentarhandling. Fjernet fra de nye
kommentar-endepunktene (reaksjons-endepunktene hadde aldri kallet) —
oppdaget og rettet FØR utrulling, takket være ekte nettleserverifisering
i steget rett etterpå (ikke bare et API-testskript).
Backend: app/routers/round_messages.py fikk
PUT/DELETE /rounds/{id}/messages/{id}/reaction og
GET/POST /rounds/{id}/messages/{id}/comments +
DELETE .../comments/{id}, pluss reactions/comment_count-felt lagt
til på RoundMessageOut OG FeedEntryOut (grupperte batch-spørringer,
ingen N+1). app/routers/messaging.py fikk samme sett ×2 (lag-chat +
oppslagstavle), importerer ALLOWED_REACTION_EMOJIS/ReactionSummary/
ReactionIn/CommentIn fra round_messages.py for å unngå duplisert
typedefinisjon (bevisst IKKE en delt SQL-hjelpemodul på tvers av filene,
siden plain_connection() vs. org_connection() uansett gjør
spørringene ulike nok at abstraksjon over filgrensen ville vært kunstig).
Frontend: ny delt komponent post-engagement.tsx — reaksjonsrad
(kuratert sett, egen valgt fremhevet grønt) + trådet kommentarfelt (lazy-
lastet ved utvidelse, "Svar"-knapp per kommentar med innrykk per nivå,
kaskade-advarsel ved sletting), bygget via ÉN Claude-skrevet V0-prompt
(standard arbeidsfordeling) og integrert hånd-kodet i FIRE eksisterende
steder: round-messages.tsx, feed.tsx (FeedCard), team-chat.tsx,
public-tournament.tsx (TournamentFeed). To bevisste
integrasjonsjusteringer utover ren copy-inn: (1) byttet V0-eksportens
egendefinerte inline SVG-ikoner til lucide-react (MessageSquare/
Reply/Trash2) for å følge DESIGN_SYSTEM.md sin "ingen annen ikonpakke
blandet inn"-regel, (2) feed.tsx sin FeedCard var opprinnelig ÉN stor
<Link> som pakket inn HELE kortet — PostEngagement måtte flyttes UT av
selve lenken (til en søskenposisjon under) for å unngå ugyldig nøstet
interaktivt innhold og utilsiktet navigering ved klikk på en
reaksjonsknapp.
Bevisst forenklet, ikke en bug: public-tournament.tsx sin
TournamentFeed setter canModerate={false} alltid — den ekte "forfatter
ELLER org-admin"-regelen håndheves korrekt av BACKEND uansett (bekreftet i
autorisasjonsmatrisen under), men frontend mangler foreløpig et signal for
"er denne innloggede brukeren org-admin i akkurat denne organisasjonen" å
style et ekstra slett-ikon med for andres kommentarer. En org-admin som
vil moderere andres kommentar kan det (API-et tillater det), men ser ikke
en slett-knapp for det i UI-et ennå — en ren UI-fullstendighets-luke, ikke
et sikkerhetshull. Kan lukkes senere ved å tilføye ett felt (f.eks. i
tournament-info-oppslaget) uten skjemaendring.
Scratch-verifisert grundig (isolert scratch-DB/rolle fra
teecup_db-mal, egen API-container): full autorisasjonsmatrise for alle
tre innleggstyper (reager/bytt-reaksjon-som-upsert/fjern-reaksjon,
kommentér på toppnivå, svar på en kommentar, slett en kommentar med svar
under og bekreft kaskade, riktig avvisning per type sin faktiske
post-/slette-regel — lag-chat kun forfatter, oppslagstavle forfatter-
eller-org-admin via ekte org-medlemskap, rundekommentar forfatter-eller-
rundeeier). RLS-isolasjon for message_reaction bekreftet eksplisitt
(annen orgs app.current_org ser 0 rader). /feed-aggregeringen bekreftet
å vise samme reaksjons-/kommentardata som selve rundesiden (samme
round_message.id gjenbrukt). test_isolation.sql 12/12 uendret.
Ekte typesjekket produksjonsbuild av frontend kjørt og bekreftet (fanget
INGEN nye feil, ren build). Ekte nettleserverifisering (egen scratch-
Caddy for sesjon/WS): reagert, byttet reaksjon (upsert bekreftet visuelt),
postet trådet samtale (rot + svar, riktig innrykk), slettet en kommentar
med svar og bekreftet kaskaden fjernet begge fra UI-et, bekreftet
feed.tsx sin PostEngagement IKKE trigger navigering ved reaksjonsklikk
(Link-nøsting-fiksen virker), bekreftet team-chat.tsx og
public-tournament.tsx sin Oppslagstavle begge rendrer komponenten
korrekt uten konsollfeil. Fant og rettet WS-tick-regresjonen (Beslutning
E) i dette steget. Scratch-miljøet (DB/rolle/API-/frontend-/Caddy-
containere/images) ryddet opp fullstendig etter bruk.
Bevisst utenfor omfang: push-varsler ved ny kommentar/reaksjon, sanntids-oppdatering hos ANDRE samtidige seere, fritt emoji-utvalg, redigering av kommentarer, myk-slett-med-plassholder.
ADR-047: Leaderboard — lukk gapet mot en konkurrentapp-referanse, uten å bygge om fra bunnen
Brukeren viste en video av GolfGameBook sitt leaderboard (trykk-for-å- utvide rad → fullt hull-for-hull-scorekort inline + sosiale handlinger per spiller) og ba om noe minst like bra i TeeCup — eksplisitt IKKE en kopi av selve utseendet.
Nøkkelfunn fra research, som endret omfanget vesentlig fra "bygg dette
fra bunnen": det meste av kjernemekanikken referansen viser var
ALLEREDE bygget og live i round-leaderboard.tsx, fra en tidligere runde
(FEATURE_BACKLOG.md "Leaderboard for runder og turneringer", 2026-07-26)
som selv var inspirert av samme konkurrentapp — og allerede DA bevisst
designet med egen form+farge-språk (sirkel/firkant, grønn/oransje) i
stedet for å kopiere referansens fargebruk. IndividualBoard (dekker
stroke/stableford/skins/flag/bbb/copenhagen) hadde allerede utvidbare
rader (IndividualRow) med et fullt hull-for-hull-rutenett
(HoleByHole). Det som faktisk manglet: (1) ingen sosial/kommentar-
tilgang noe sted i leaderboardet, (2) ingen kompakt oppsummeringslinje
under rutenettet, (3) TwoSidedBoard/HighLowBoard (to-sidede formater)
hadde INGEN utvidelse i det hele tatt — kun en statisk hull-strip, (4)
TeamFlightBoard hadde en flat, ikke-utvidbar råscore-liste.
Avklart med bruker (AskUserQuestion, to spørsmål) FØR bygging:
- Sosial-kobling:
post-engagement.tsx(ADR-046) reagerer/ kommenterer på ETT spesifiktround_message-innlegg, ikke runden som helhet — det finnes intet "innlegg per deltaker" i skjemaet (og GolfGameBooks egen "Kommentar til spillefeeden"-knapp åpner uansett samme delte feed uansett hvilken rad man trykker på). Svar: delt tråd, lenket fra hver rad — ingen ny per-deltaker-entitet, ingen ny migrasjon, ingen backend-endring i det hele tatt denne runden. - To-sidede formater: siden disse kun har 2 "sider" (ikke en spillerliste), gir "én rad per spiller"-mønsteret ikke mening. Svar: ÉN utvidbar detaljseksjon for hele kampen, ikke et per-rad-mønster.
Bygget, alt i round-leaderboard.tsx (+ to eksisterende filer fikk en
anker-id):
HoleByHole(IndividualBoard) fikk en kompakt oppsummeringslinje (par-sum/totalscore/plassering, egen ordlyd — IKKE referansens) + en "💬 Kommentarer"-tekstlenke.TeamFlightBoardsin flate råscore-liste konvertert til samme utvidbare mønster via en nyTeamMemberRow(gjenbrukerHoleByHoleuendret, uten rangeringskolonnen — et lags medlemmer rangeres ikke seg imellom der).TwoSidedBoard/HighLowBoardfikk en ny, deltMatchDetailSection— ÉN "Vis hull-for-hull med score"-knapp som, lazy, monterer den nå eksporterteMatchScorecardGrid(round-scorecard.tsx, samme ADR-045-presedens: eksporter en allerede-godkjent, modul-privat komponent uendret, ren synlighetsendring) + samme kommentar-lenke.- Type-utvidelse, ikke ny data:
ApiRoundHeader/ApiRoundParticipanti round-leaderboard.tsx fikk flere felt lagt til (tee_name_snapshot,guest_name,display_name,is_owner,playing_handicap) — alle allerede returnert av samme${apiPrefix}/{roundId}-endepunkt (bekreftet motRoundOut/PublicRoundOutiapp/routers/rounds.py), bare ikke tidligere typet der.guest_nameer valgfritt (fraværende ipublicMode, sidenPublicRoundParticipantOutikke har feltet) — en nytoGridRound()-adapter defaulter det tilnullførMatchScorecardGridmates, trygt siden komponenten aldri leser feltet. - Anker-scroll:
round-detail.tsx/watch-round.tsxsin kommentarseksjon fikkid="kommentarer", PLUSS en litenuseEffect(() => { if (round && hash === "#kommentarer") scrollIntoView() }, [round])i begge — nettleserens EGEN anker-scroll (som skjer FØR async data er lastet) rakk ikke frem til et element som ennå ikke fantes i DOM-en; funnet og rettet under nettleserverifisering.
Ingen ny V0-runde, ingen backend-endring. Alt er gjenbruk/eksport av allerede-eksisterende, allerede godkjente komponenter/mønstre til nye steder — "liten justering av eksisterende komponenter med allerede etablerte mønstre"-unntaket fra den stående V0-arbeidsfordelingen.
Bevisst forenklet, ikke en bug: ingen kommentar-ANTALL-merke på selve leaderboard-lenken (unngår et ekstra API-kall for kun et tall på en lenke).
Scratch-verifisert (isolert DB fra teecup_db-mal, egen API-/
frontend-/Caddy-container): tsc --noEmit rent, ekte produksjonsbuild
kjørt og bekreftet ren. Ekte nettleser: regresjon FØRST —
IndividualBoards eksisterende utvidelse bekreftet identisk for
slagspill-formatet (samme tall, samme rad-innhold) FØR noe nytt ble
testet. Deretter: ny oppsummeringslinje + kommentar-lenke bekreftet på
slagspill (inkl. anker-scroll faktisk fungerende, ikke bare landet på
riktig side), ny MatchDetailSection/MatchScorecardGrid-utvidelse
bekreftet på et fourball-format med ekte 4-spiller-data (riktig
hull-for-hull-score, riktig løpende matchstatus "1 UP"/"AS"/"Dormie 1"),
samme fourball-format bekreftet identisk i publicMode (anonym
tilskuer-kontekst, egen isolert nettleser-kontekst — kommentar-lenken
riktig pekende til /watch/{id}#kommentarer i stedet for
/my-rounds/...). Ingen konsollfeil utover forventet 401 på
/auth/me for den anonyme leseren (allerede tolerert i koden).
Ikke direkte nettleser-testet: TeamFlightBoard (shamble/money_ball)
og HighLowBoard (high_low_high) — ingen eksisterende testdata for
disse formatene i scratch-DB-malen, og å håndkonstruere gyldige
runde+deltaker+hull-fixturer for dem fra bunnen ble vurdert som for
tidkrevende/feilutsatt for denne verifiseringsrunden. Vurdert lav risiko
likevel: TeamFlightBoards nye TeamMemberRow gjenbruker HoleByHole
uendret (samme funksjon som allerede ble browser-testet via
IndividualBoard), og HighLowBoards nye seksjon bruker nøyaktig
samme MatchDetailSection/toGridRound/MatchScorecardGrid-kobling
som allerede ble browser-testet via TwoSidedBoard — risikoflaten
(prop-tredding, type-adapter, komponent-montering) er identisk, ikke en
egen, utestet kodevei. Anbefales likevel spot-sjekket i ekte nettleser
neste gang en runde med disse formatene finnes eller opprettes.
Merk, funnet under research, IKKE rettet i denne runden: en
skrivefeil i eksisterende kode — useLeaderboardDatas returnerte
variabel heter viewerParticipantId men inneholder faktisk den
INNLOGGEDE BRUKERENS user_id (fra /auth/me), ikke en
deltaker-id. Kun en forvirrende variabelnavngiving, ikke en funksjonell
feil (verdien er korrekt for det den faktisk brukes til alle steder,
inkludert det nye MatchScorecardGrid-kallet, som nettopp forventer
bruker-id under navnet viewerId) — la stå urørt, utenfor omfanget av
denne runden.
Bevisst utenfor omfang: RoundLeaderboardMini (topp-3-widgeten,
skal forbli et raskt overblikk), ScrambleSoloResultView (eget format,
lite utbredt), skudd-nivå-detalj (fairway/putts/GIR) utover det
HoleByHole allerede viser (den fulle scorekort-siden dekker det),
kommentar-antall-merke på leaderboard-lenken, et separat ett-trykks
"lik hele runden", "privat melding" (ingen DM-funksjon finnes).
ADR-048: Slag-for-slag GPS-avstandsmåling
Reist av brukeren 2026-08-06: "Måle lenge på slag ... Fra der man er
ELLER fra et valgt punkt på et satellittfoto, til der man står ved siden
av ballen. Man skal også kunne si hvilken kølle man slo med. Dette kan
deles i feeden, eller beholdes i eget grensesnitt." Avklart eksplisitt med
bruker (AskUserQuestion, tre spørsmål, 2026-08-07): (1) v1 skal dekke
BÅDE individuelle runder OG lagformater (fourball/scramble/foursome m.fl.,
der hull henger av round_side_id), ikke bare individuell, (2)
delingsteksten skal være auto-generert MEN redigerbar, pluss et utsnitt
av satellittfotoet med slaget tegnet inn, (3) måling skal være tilgjengelig
både som en knapp i scoreførings-veiviseren OG som en alltid-synlig
merkelapp på hull-kortet (retroaktiv måling).
Beslutning A — Kartleverandør: Mapbox GL JS, ikke Leaflet+Esri. Esris gratis satellittlag (den vanlige gratis Leaflet-kilden) forbyr eksplisitt kommersiell bruk uten en separat, betalt ArcGIS-lisens — diskvalifiserende gitt TeeCups kommersielle retning. Mapbox: 50 000 gratis kartlastninger/ mnd, ingen kortbinding for å starte, ~$5/1000 utover, eksplisitt tillatt for kommersiell bruk.
Beslutning B — Kostnadskontroll, tre tiltak. (1) ETT kart-instans per
måle-økt (montert når "velg punkt på kart" åpnes, aldri remontert per
tap/pan — Mapbox fakturerer per initialisering, ikke per interaksjon).
(2) Kartet lastes KUN ved eksplisitt "velg punkt på kart" — velger
spilleren "min posisjon nå" for BEGGE punkt, initialiseres Mapbox GL JS
aldri (må verifiseres eksplisitt via nettverksfane under scratch-testing,
ikke bare visuelt, se verifiseringsseksjonen for ADR-048 i CHANGELOG.md
når den skrives). (3) Selve avstandsberegningen er ren klient-side
Haversine (frontend/lib/geo.ts), aldri et Mapbox-API-kall.
Beslutning C — Ny round_shot-tabell (migrasjon 060), IKKE en
gjenåpning av ADR-033s avvisning av fri slag-for-slag-logging som
hull-statistikk-DATAMODELL. Dette er en separat, valgfri måle-funksjon;
round_hole sine faste felt (club_off_tee, putts osv.) er uberørt.
round_shot kjenner kun round_hole_id — IKKE round_participant_id/
round_side_id direkte — og arver dermed eierskap (individuell ELLER
lagformat) TRANSITIVT via round_hole sin eksisterende
round_participant_id/round_side_id-XOR (migrasjon 031). Dette dekker
begge hull-typer fra v1 uten egen XOR-logikk i den nye tabellen, via
speilede API-endepunkter
(/participants/{id}/holes/{n}/shots og /sides/{id}/holes/{n}/shots,
samme dobbelte mønster som RoundHoleOut/RoundSideHoleOut allerede
bruker for selve hull-scoringen). Samme non-RLS plain_connection()-
mønster som resten av det frittstående-runde-subsystemet (ADR-033
Beslutning A). shot_number er eksplisitt og hull-tolerant (ikke ORDER BY captured_at) — unngår at GPS-tidsstempel-drift stille endrer
rekkefølgen; sletting midt i sekvensen etterlater et gap, ingen
omnummerering. Kun UPDATE (shared_round_message_id) er grantet (ett
smalt, eksplisitt unntak fra "slett og opprett på nytt, aldri rediger"-
prinsippet, se Beslutning E) — ellers ingen UPDATE, samme filosofi som
round_message.
Beslutning D — Koordinater: rene double precision lat/lng-par, ikke
PostGIS. Første funksjon i prosjektet som lagrer koordinater i det hele
tatt (60 migrasjoner uten presedens frem til nå). Volumet (maks noen
titalls rader per runde) og operasjonssettet (kun punkt-til-punkt
storsirkel-avstand, regnet klient-side) rettferdiggjør ikke en ny, tung
geo-avhengighet.
Beslutning E — Deling gjenbruker round_message (ADR-044) uendret, via
en lenke. Et nytt POST /rounds/{id}/shots/{id}/share-endepunkt
genererer satellitt-utsnittet SERVER-SIDE (Mapbox Static Images API, egen
HEMMELIG TEECUP_MAPBOX_SECRET_TOKEN — atskilt fra frontendens
OFFENTLIGE, URL-restriktere NEXT_PUBLIC_MAPBOX_TOKEN som Mapbox GL JS
bruker i nettleseren), laster bildet opp via eksisterende
storage.upload_image(), og setter INN en vanlig round_message-rad —
ingen ny feed-mekanisme. shared_round_message_id er en LENKE (ikke et
bool-flagg) slik at "delt" alltid reflekterer om meldingen faktisk
finnes (ON DELETE SET NULL hvis meldingen slettes separat i feeden).
Rekkefølgen er bevisst: slaget opprettes ALLTID først (uten deling);
deling er et frivillig, påfølgende steg — unngår at en feilet
slag-opprettelse kan etterlate en foreldreløs feed-melding. Mapbox-kallet
degraderer grasiøst til ren tekst uten satellittbilde ved feil/manglende
token (samme mønster som SMTP/push i config.py), blokkerer ikke selve
delingen.
Backend: app/routers/rounds.py fikk ShotIn/ShotOut/ShotShareIn
og seks nye endepunkter (deltaker-GET/POST, side-GET/POST, DELETE, share),
alle bygget på den eksisterende _get_accessible_round_or_404 (samme
"eier ELLER lenket medspiller kan føre for hele flighten"-regel som
update_hole/side-PATCH allerede bruker). _resolve_round_message_author_name
flyttet fra round_messages.py til rounds.py (som allerede var den
importerte, ikke-importerende parten av de to filene) for å unngå en
sirkulær import da share_shot trengte å gjenbruke den uendret.
Frontend: frontend/lib/geo.ts (ren Haversine-funksjon, ingen
avhengigheter). Eksisterende kølle-plukker-mønster i ScoringWizard
(round-detail.tsx) trukket ut til en delt ClubPicker-komponent (ren
refaktor, bekreftet typesjekk uendret) slik at den nye måle-flyten kan
gjenbruke nøyaktig samme pill-knapp-interaksjon. mapbox-gl lagt til
(pakken har egne typedefinisjoner -- den separate @types/mapbox-gl-
pakken er en utdatert stub, bevisst IKKE installert), NEXT_PUBLIC_MAPBOX_TOKEN tredd gjennom
frontend/Dockerfile (build-tid, IKKE runtime — NEXT_PUBLIC_-variabler
bakes inn i klient-bundlen ved next build, samme fallgruve-mønster som
TEECUP_API_ORIGIN allerede dokumenterer i Dockerfile-kommentarene, men
speilet KUN i builder-steget her siden bruken er ren klient-side) og
docker-compose.yml sitt teecup_frontend.build.args. Selve
ShotMeasurementSheet-komponenten (alle steg + kart-plassholder) går via
én Claude-skrevet V0-prompt (standard arbeidsfordeling) — ekte Mapbox GL
JS-integrasjon i kart-steget kobles på hånd-kodet ETTER V0-eksporten, ikke
av V0 selv (V0-prompten ber eksplisitt om en visuell plassholder, ikke et
ekte kartbibliotek, for å unngå at V0 genererer en selvstendig
kart-komponent som remonterer per interaksjon og dermed bryter
Beslutning B).
Bevisst utenfor omfang: redigering av et allerede logget slag (slett og mål på nytt i stedet), en historisk "vis alle slag på kart etter runden"-visualisering.
Disse må avklares før eller under de relevante fasene:
- Sesjons-secret: TeeOff lar
PUBLIC_SESSION_SECRETfalle tilbake påJWT_SECRET. TeeCup bør bruke separate, uavhengige secrets. (Sikkerhet) - Cache over flere prosesser: In-memory
dict-cache påapp.statedeles ikke mellom flere workers/containere. Ved skalering trengs Redis. (Skalering) - Prising: Per organisasjon (abonnement) eller per turnering? Påvirker ikke isolasjonsmodellen, men påvirker fakturerings-/kvotemodell.
- Scramble-grensesnitt — ✅ HELT FERDIG 2026-07-29. Arkitekturen skal
ta høyde for formatet; eksakt UI-løsning spesifiseres senere.
Utvidet 2026-07-25, DELVIS AVKLART OG BYGGET 2026-07-28: brukeren
ba om statistikk over hvor mange utslag hver spiller har hatt (dvs.
hvor mange ganger spillerens drive ble valgt). Bygget for FRITTSTÅENDE
RUNDER (ADR-039 sitt delt-ball-format,
round_hole.selected_ participant_id, migrasjon 036) — se FEATURE_BACKLOG.md ("Scramble: statistikk over utslag brukt per spiller") for full detalj. Fullført 2026-07-29: samme funksjon bygget for org-scopede turnering-scramble/-greensome også (hole_score.selected_ participant_id, migrasjon 039) — samme mønster, se CHANGELOG.md 2026-07-29 for full detalj (validering, deltsubmitStroke-utvidelse, nySelectedDriverSummaryisession-scorecard.tsx). Begge domener dekket, ingen kjent gjenstående forskjell mellom scramble og greensome. - Individuell-vs-delt-ball i
hole_score: Håndheves i app-laget, ikke av databasen (CHECK når ikke opp tilsession.format). Motoren/API-et må passe på at f.eks. et foursome ikke får per-spiller-scorer. - Individuelle turneringer, flerrunde-turneringer og Order of Merit
(reist 2026-07-26). Grunnstruktur for de to første AVKLART samme
dag, se ADR-037: ny, parallell org-scopet datamodell (IKKE en
utvidelse av ADR-033s
round-tabeller), sammetournament-tabell med nyformat_type-diskriminator, flerrunde via nytournament_round-tabell, rå slag lagres + poeng caches per format (samme mønster som match-play). Fortsatt åpent: de fem konkrete formatenes egne poengregler (Københavner m.fl.), og Order of Merit (sesong-sammenlagt på tvers av flere turneringer) — bekreftet som et beslektet, men SISTE steg, ikke designet ennå. Ingen migrasjon skrevet — ADR-037 er ren struktur-beslutning. - Frittstående runder: ekte spillformer (slagspill/match/skins/
par-lag-konkurranse, reist 2026-07-28) — AVKLART, BYGGET (backend +
frontend) OG LIVE SAMME DAG, se ADR-039.
round.play_formatstyrer nå faktisk både HCP-beregning (via en portert versjon av turnering- motoren) og etformat-result-lese-endepunkt for scorekort- presentasjon, med tilhørende UI for sideoppsett/skins-konfig/ matchstatus-visning — se FEATURE_BACKLOG.md/CLAUDE.md for full detalj og etterfølgende samme-dags oppfølgingsrunder. "Flere flighter i én frittstående runde" (egen seksjon i FEATURE_BACKLOG.md) forblir et adskilt, ikke-relatert spørsmål (grupperer RUNDER, ikke deltakere innad i én runde).
Utviklingsplan (rekkefølge)
- ✅ Land tenant-modell → Organisasjon (ADR-001/002/003)
- ✅ Denne beslutningsloggen (dette dokumentet)
- ✅ Handicap-motor som frittstående, testet bibliotek (ADR-005) — 24 tester, R&A-verifisert
- ✅ Databaseskjema (
001_initial_schema.sql) — RLS, miksede formater, pool - ⏩ Backend-API + regelmotor-integrasjon (neste)
- ⬜ Frontend (admin + score-registrering)
- ⬜ PWA & offline-synk