`, `type="submit"` → `type="button"` med eksplisitt
`onClick`-håndtering.
**Scratch-verifisert grundig** (isolert `teecup_app_scratch`-rolle +
isolert scratch-MinIO + engangs API-/frontend-container, ekte
nettleser-innlogging som TO forskjellige brukere på tvers av TO
forskjellige organisasjoner):
- API-nivå: eget Python-testskript, 34/34 håndregnede sjekker bestått
(publisering-ved-opprettelse, kryss-bruker/kryss-org synlighet,
attribusjon, fork-ved-ikke-eier-redigering med verifisert uendret
original, eier-redigering-i-sted, eksplisitt duplisering,
eierskaps-gatede 403-er, og slette-blokkert-mens-i-bruk med riktig
409/IN_USE-kode).
- Ekte nettleser: rekkefølge-fiksen bekreftet i begge turneringstyper;
"Opprett bane med hull/utslag" fra bunnen av OG fra TeeOff-mal
(ekte Bergen Golfklubb-data, redigert par på hull 1 før lagring,
bekreftet BÅDE org-`course`- og `personal_course`-raden ble
opprettet riktig i databasen); den nye offentlige banen dukket
umiddelbart opp i single-runde-modulens banesøk MED riktig
attribusjon; "bruk som mal"-broen fra der forhåndsutfylte
`OwnCreateStep` korrekt fra en ANNEN brukers bane og forket en ny,
egen-eid rad ved lagring (bekreftet i databasen: ny rad eid av
innlogget bruker, `forked_from_id` pekende på originalen, originalen
selv uendret); "Mine baner" i kontoinnstillinger bekreftet å vise
KUN egne baner, Rediger (i sted, verifisert i databasen), Dupliser
og Slett (inkl. `confirm()`-dialoghåndtering) alle fungerende.
- `test_isolation.sql` 12/12 bestått (additiv migrasjon, ingen
RLS-endring). Ekte typesjekket produksjonsbuild (`docker build`,
samme steg som `Dockerfile` faktisk bruker) kjørt flere ganger
gjennom byggerunden, null TypeScript-feil.
Scratch-miljøet (database, rolle, MinIO, containere, images,
midlertidige hemmeligheter) fullstendig ryddet opp etter
verifisering.
**Rullet ut live 2026-08-04**, bruker bekreftet eksplisitt ("Ja
takk"): migrasjon 054 kjørt mot ekte `teecup_db` (kun
`forked_from_id`-kolonnen, bekreftet tom/nullable, ingen
databrudd), `docker compose up -d --build teecup_api
teecup_frontend`, begge containere boot-et rent, `/health` og
`/dashboard` → 200 over https.
20. **Order of Merit (sesong-sammenlagt rangering på tvers av
turneringer) — BYGGET, GRUNDIG SCRATCH-VERIFISERT OG LIVE 2026-08-04
(ADR-043, migrasjon 055).**
Brukeren ba om en vurdering av GolfBox sin Order of Merit-funksjon
(opplastet PDF) og tok deretter designet til en full plan-runde (to
`AskUserQuestion`-runder + et oppfølgingsspørsmål som avklarte at
"lag" i en OOM er et sesong-par satt opp DIREKTE i OOM-en, ikke
hentet fra noen turnering -- unngår GolfBox sin egen identifiserte
svakhet, strengmatching på lagnavn, siden TeeCup allerede har ekte
`player_id`-identitet på tvers av organisasjonen).
**Skjema** (migrasjon 055): `order_of_merit`/`order_of_merit_link`/
`order_of_merit_team`/`order_of_merit_team_member`, org-isolert RLS
(samme mønster som migrasjon 040/053). To uavhengige akser --
`result_type` (poeng-etter-plassering/Stableford-sum/brutto/netto/
pengeliste, HVA som telles) og `aggregation_mode` (sum/snitt/
eclectic, HVORDAN det summeres over en sesong).
**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). 9 nye enhetstester skrevet FØR resten av
bygget, alle grønne (107/107 totalt i `test_handicap_engine.py`).
**Backend** (nytt `app/routers/order_of_merit.py`): full CRUD for
selve OOM-en og dens lenkede turneringer, pluss et "regn ut ved
lesing"-leaderboard-endepunkt (samme filosofi som Nassau/
High-low-high -- ingen cache-tabell). `_compute_individual_standings`
faktorisert ut av `individual_tournaments.py` sin `individual_
leaderboard` (som nå kun er en tynn wrapper rundt den) for gjenbruk
her uten å duplisere posisjon-/til-par-beregningen -- `LeaderboardEntry`
fikk samtidig et nytt `player_id`-felt (manglet fra før, kun
`tournament_participant_id` fantes, utilstrekkelig for å summere én
ekte spiller på tvers av FLERE turneringers deltaker-rader).
`kind`/`result_type` låses etter opprettelse (PATCH-endepunktet
eksponerer dem bevisst ikke i det hele tatt). Denne runden dekker
spiller-OOM med alle fem resultattyper, sum/snitt-aggregering, og
behold-N-beste/minimum-resultater/aldersgrense-grenser (fødselsår
hentet fra `player.birth_date`, migrasjon 007). Eclectic-aggregering
og lag-OOM sin faktiske leaderboard-beregning er bevisst IKKE bygget
denne runden (schema og CRUD for lag finnes, men `GET .../leaderboard`
avviser eksplisitt `kind='team'` med en tydelig feilmelding) -- se
FEATURE_BACKLOG.md.
**Frontend** (`order-of-merit-list.tsx`/`order-of-merit-detail.tsx`,
nye ruter under `/organizations/[id]/order-of-merit`): håndkodet, IKKE
via V0 -- brukeren gikk tom for V0-credits midt i planleggingen av
frontend-tilnærmingen og ba om at det håndkodes "så lekkert som V0
får til" i stedet. Bygget til å gjenbruke etablerte visuelle mønstre
presist (samme header-/kort-stil som `org-members.tsx`, samme
gull-ledertrøye-behandling -- `bg-gold`/`text-gold-foreground` -- som
`stroke-play-leaderboard.tsx`). Detaljsiden har fire seksjoner i en
bevisst rekkefølge (lenkede turneringer → innstillinger →
resultatliste, siden resultater ikke gir mening før turneringer er
lenket), inkl. en egen "Ikke kvalifisert"-seksjon for spillere
ekskludert av aldersgrense/minimum-resultater med forklarende tekst
per rad. Ny inngangslenke lagt til i `org-members.tsx` (ingen egen
"org-hub"-side fantes fra før -- medlemssiden er i praksis allerede
det, samme mønster som appen ellers navigerer dit fra dashbordets
org-nedtrekksmeny).
**Scratch-verifisert grundig** (isolert `teecup_app_scratch`-rolle +
isolert scratch-MinIO + engangs API-/frontend-container, ekte
nettleser-innlogging inkl. 2FA):
- API-nivå: eget Python-testskript, 63/63 håndregnede sjekker bestått
(bevisst konstruert 3-spiller/2-turnering-scenario med kjent
Stableford/brutto/poeng/penge-utfall for hver av de fem
resultattypene, inkl. en ekte uavgjort i pengeliste-testen som
bekreftet "T1"/"T1"/"3"-skip-rank-oppførselen, behold-de-1-beste,
minimum-2-resultater-ekskludering, og et eksplisitt aldersfilter-
scenario der to av tre spillere korrekt ble ekskludert).
- RLS-isolasjon eksplisitt testet på de fire nye tabellene utover
`test_isolation.sql` (som uendret består 12/12): kryss-org-lesing
av `order_of_merit`/`order_of_merit_link` gir 0 rader under feil
`app.current_org`, kryss-org-innsetting blokkeres av `WITH CHECK`.
- Ekte nettleser: hele flyten klikket gjennom som innlogget
organisasjonseier -- opprett OOM (alle fem resultattyper/kind-valg
i skjemaet), lenk en turnering med poeng-tabell-radeditoren (inkl.
å faktisk lenke en tredje turnering og se den dukke opp i listen
med riktig "Poeng: 10-6-3"-oppsummering), utvid/kollaps
innstillinger-seksjonen og bekreft alle feltverdier viste riktig
(inkl. aldersgrense-feltene 1985/2000), og bekreftet resultatlisten
viste EKSAKT de håndregnede tallene fra API-testen (gull-fremhevet
leder, riktig ekskluderte spillere med "Utenfor aldersgrensen."-
forklaring).
- Ekte typesjekket produksjonsbuild (`docker build`, samme steg som
`Dockerfile` faktisk bruker) -- måtte kjøres på nytt med lengre
timeout enn standard 2 minutter, selve bygget var uendret vellykket.
Scratch-miljøet (database, rolle, MinIO, containere, images,
midlertidige hemmeligheter) fullstendig ryddet opp etter
verifisering, TO GANGER (én gang etter backend-verifisering, én gang
etter frontend-verifisering).
**Kjent selvrettet feil under bygging:** funksjonen ble først
kodekommentert som "ADR-042", som viste seg allerede å være tatt
(bane-mal-biblioteket fra samme dag, punkt 19 over) -- oppdaget og
rettet til ADR-043 gjennomgående (migrasjon, motor, router,
frontend) FØR dokumentasjon ble skrevet, ingen funksjonell påvirkning
(kun kommentarer/docstrings, ingen kode leser ADR-nummeret).
**Rullet ut live 2026-08-04**, bruker bekreftet eksplisitt ("Ja
takk"): migrasjon 055 kjørt mot ekte `teecup_db` (fire nye tabeller,
bekreftet tomme/riktig strukturert, ingen databrudd), `docker
compose up -d --build teecup_api teecup_frontend`, begge containere
boot-et rent, `/health` og `/dashboard` → 200 over https. Gjenstår
fra opprinnelig plan (se plan-loggen 2026-08-04): Eclectic-
aggregering, lag-OOM sin faktiske leaderboard-beregning.
21. **Bugfiks: "Fullfør runde" i header-modalen ("Administrer runde")
svelget feil stille og lukket dialogen uansett utfall — 2026-08-04.**
Brukeren rapporterte (skjermbilder fra en ekte runde på
teecup.teeoff.no) at en Københavner-runde med kun 1 av påkrevde 3
spillere så ut til å la seg "fullføre" via "Administrer"-knappens
modal (bekreftelsesdialogen dukket opp, ble klikket OK på), men
runden forble ufullført -- ingen feilmelding vist noe sted.
**Rotårsak, `round-header.tsx`/`round-page-shell.tsx`:** rundens
sticky header (ADR-040 Beslutning C/D, brukt på alle tre
Score/Scorekort/Leaderboard-fanene) har sin EGEN, uavhengige
"Fullfør runde"-vei via "Administrer"-modalen -- parallell til, men
ikke delt med, `round-detail.tsx` sin allerede korrekte inline
"Spillere og runde"-fane (som riktig deaktiverer knappen og viser
`setup_message` når `setup_complete` er usann). Modal-veien hadde
ALDRI blitt koblet til `setup_complete`-gaten, OG knappens
`onClick` kalte `onClose()` rett etter å ha startet
`onFinishRound()` UTEN å vente på den asynkrone
bekreftelse+`fetch`-kjeden -- dialogen lukket seg altså alltid
umiddelbart, uavhengig av om `POST /rounds/{id}/complete` faktisk
lyktes (backend-en avviste korrekt med 409 `SETUP_INCOMPLETE` +
forklarende melding -- det var ren frontend som aldri fulgte opp
svaret).
**Fiks:** `onFinishRound`-kontrakten endret fra `() => void` til
`() => Promise<{ok, message} | null>` (`null` = brukeren avbrøt
bekreftelsen, dialogen blir stående). Modalen venter nå på svaret
før den lukker seg -- ved `ok: false` vises `message` inline i
dialogen i stedet. Ny `finishDisabledReason`-prop deaktiverer
knappen og viser samme `setup_message` som den inline fanen,
fjerner selve muligheten til å trykke seg forbi en ufullstendig
formatoppsett via denne veien. Ingen backend-endring -- valideringen
fantes allerede og var korrekt, kun frontend fulgte den aldri opp.
**Scratch-verifisert** (isolert `teecup_scratch`-DB, alle 55
migrasjoner kjørt friskt, isolert `teecup_app_scratch`-rolle,
isolert scratch-MinIO, engangs API-/frontend-container bygget fra
de faktiske endrede filene, ekte nettleser via magic-link-innlogging):
reproduserte brukerens NØYAKTIGE scenario (Københavner-runde, 1 av 3
spillere) -- bekreftet modalens "Fullfør runde" nå viser seg
deaktivert med "Københavner krever nøyaktig 3 spillere -- runden har
1." synlig i dialogen (samme tekst som den inline fanen). La til to
gjestespillere (nå 3/3), bekreftet knappen ble aktiv, klikket
gjennom bekreftelsesdialogen, og bekreftet både at modalen lukket
seg OG at `completed_at` faktisk ble satt server-side. Full
typesjekket produksjonsbuild av frontend kjørt som del av samme
Docker-bygg. Scratch-miljøet ryddet opp fullstendig etterpå.
**Rullet ut live 2026-08-04**, bruker bekreftet eksplisitt: `docker
compose up -d --build teecup_frontend` (ingen migrasjon involvert),
containeren boot-et rent, `/health` og `/dashboard` → 200 over
https.
22. **Nytt format: "Scramble mot enkeltspiller" (`scramble_solo`),
slagspill-varianten — 2026-08-05 (migrasjon 056).** Første
ASYMMETRISKE to-siders-format i prosjektet: et scramble-lag
(variabel størrelse, N>=2, delt ball) mot ÉN individuell spiller
(egen ball), avgjort ved netto SLAGSPILL-totalsum (ikke matchspill)
-- laveste nettosum vinner. Bevisst utenfor omfang denne runden:
match-play-varianten av formatet (egen runde senere), org-
lagturneringer (kun frittstående runder nå).
**Kilder undersøkt FØR design** (CLAUDE.md sin regel for nye
formater): de tre HCP-PDF-ene dekker IKKE lag-vs-individuell eller
vilkårlig lagstørrelse -- bekreftet ved grundig lesing. Lagets
Playing Handicap er derfor en bevisst NY, enkel TeeCup-regel
(`TeamAverage` -- rent snitt av medlemmenes course handicap, for
N>=2), bekreftet eksplisitt med bruker, IKKE en kildebelagt formel
som `scramble_2`/`scramble_4` sine faste `RankedSplit`-tabeller
(som forutsetter fast lagstørrelse og derfor bevisst IKKE gjenbrukt
her). Individualspillerens allowance er en konfigurerbar prosent
(`scramble_solo_individual_pct`, std 100 %, arrangør-satt) --
egen bespoke kolonne, IKKE den generiske `allowance_override`-JSON-
en (som forutsetter ÉN delt strategi for hele runden, mens dette
formatet trenger to uavhengige strategier).
**Skjema** (migrasjon 056): `round.scramble_solo_individual_pct`
(smallint, std 100) og `round_side.side_role`
(`'team'`/`'individual'`, unik per rolle per runde). `round_hole`
sin eksisterende XOR-struktur (migrasjon 031) trengte INGEN endring
-- lagsiden bruker `round_side_id`-veien (samme som foursome/
scramble), individualsiden bruker `round_participant_id`-veien
(samme som singles), begge coexisterer allerede fritt i samme
tabell.
**Motor** (`handicap_engine.py`): ny `TeamAverage`-strategi og ny,
domeneagnostisk `net_stroke_play_margin(net_total_a, net_total_b)`
-- eksplisitt SØSKEN til, ikke del av, `compute_match_state`
(ingen hull-for-hull-tilstand for dette formatet). Selve per-side-
nettosummen gjenbruker eksisterende `allocate_strokes_by_index`/
`stroke_play_net_total`, ingen ny funksjon trengtes der. 8 nye
enhetstester (115/115 totalt, opp fra 107).
**Backend** (`app/routers/rounds.py`): ny `_ASYMMETRIC_SIDE_FORMATS`-
konstant (bevisst adskilt fra `_TWO_SIDED_FORMATS`, siden formatet
verken er symmetrisk eller matchspill). Asymmetrisk sidekapasitet i
`_check_side_capacity` (laget: ingen øvre grense, individualsiden:
maks 1) -- kan IKKE uttrykkes av det eksisterende
`_SIDE_PLAYER_COUNT`-oppslaget, som forutsetter ett fast tall for
HELE formatet. Ny `_individual_round_hole_needed`-hjelpefunksjon
(avgjør delt-ball vs. individuell-ball-lagring per DELTAKER, ikke
per format, siden de to sidene har ulik struktur). Ny
`_recompute_scramble_solo_handicaps` (søsterfunksjon til
`_recompute_side_handicaps`, som ikke kunne gjenbrukes -- to
uavhengige strategier, ikke én). Ny `_build_scramble_solo_result`
(samme "regn ut ved lesing, ingen cache"-filosofi som Nassau/
High-low-high), dispatchet FØR den generiske to-sidede grenen i
`_build_format_result`.
**Frontend -- ALT promptet til V0** (brukerens eksplisitte instruks
denne runden, gjentatt: "Alt av frontend skal promptes mot V0" --
ingen håndkoding/design-beslutning av meg). To V0-prompter skrevet
(forankret i `DESIGN_SYSTEM.md` + tilgjengelighetskravet), levert
som zip 30 (oppsett-steget) og zip 31 (resultatvisning, bygget på
30). Integrert -- IKKE blindt kopiert (V0 sin egen full-app-eksport
bruker fortsatt mock-data for alt utenom det som faktisk ble
promptet) -- kun de faktiske diff-relevante stykkene ble portet inn
i de allerede API-koblede filene:
- `new-round.tsx`: ny `ScrambleVsSoloSides`-komponent (Steg 4,
asymmetrisk sideoppsett -- "Laget" åpen liste/"minst 2"-hint,
"Individuell motstander" låst til 1), ny `SoloAllowanceControl`
(Steg 2, 0-100 %-stepper). `submitWizard()` fikk en egen
side-opprettelses-gren for formatet (sender `side_role`, som
TWO_SIDED-formatenes gren ikke gjør). "Avanserte handicap-
innstillinger"-panelet skjules helt for formatet (ingen av
feltene der har noen effekt siden formatet aldri bruker
`allowance_override`) -- funnet og rettet UNDER integrasjonen,
ikke en del av V0-eksporten.
- `scramble-solo-result.tsx`: NY fil, ren V0-eksport uendret
(selvstendig presentasjonskomponent, `ScrambleSoloResultView`).
Adapter fra `ApiFormatResult` til komponentens props-form skrevet
lokalt i `round-leaderboard.tsx` (`buildScrambleSoloResult`) og
`round-detail.tsx` (lokal kopi, prosjektets vanlige mønster) --
foretrekker hull-entryens `label`-felt (deltakerens faktiske
navn) over sidens egen (faste) label, med fallback.
- `round-scorecard.tsx`: eneste sted som trengte GENUINT ny logikk
(ikke bare registrering) -- formatet har BLANDEDE enhetstyper i
samme fane-liste (laget = side, individualspilleren =
participant). Løst ved å slå opp faktisk enhetstype fra selve
`unitId` (`round.sides.some(s => s.id === unitId)`) i stedet for
et format-bredt `isSharedBall`-flagg, som ikke kan uttrykke en
blanding -- denne oppslags-tilnærmingen forble samtidig korrekt
uendret for alle eksisterende rene formater.
- `round-detail.tsx`: INGEN endring i selve scoreføringen
(`SideScoreWizard` for laget, vanlig per-deltaker-veiviser for
individualspilleren fungerer begge uendret) -- kun
`FormatResultPanel` fikk en ny gren (MÅ avskjæres FØR den
generiske to-sidede fallback-koden, ellers ville den krasjet/vist
feil for dette asymmetriske formatet).
- `FORMAT_LABELS` lagt til i 8 filer (`round-card.tsx`,
`round-leaderboard.tsx`, `round-detail.tsx`, `round-scorecard.tsx`,
`round-stats.tsx`, `friend-profile.tsx`, `watch-round.tsx`) --
`public-live.tsx` bevisst IKKE (den nøkler på org-turneringers
`session.format`, utenfor omfang).
**Scratch-verifisert grundig, TO RUNDER** (samme "backend alene,
så full stack"-disiplin som Order of Merit-runden):
1. Backend alene: isolert `teecup_scratch`-DB (alle 56 migrasjoner
friskt), isolert `teecup_app_scratch`-rolle, isolert scratch-
MinIO, engangs API-container. Håndregnet Python-testskript, 45/45
sjekker bestått -- 3-manns lag (HCP 8/14/20, snitt 14) mot
individualspiller (HCP 10), begge 100 % og 50 % allowance,
inkl. eksplisitt bevis på at en hull-SI mellom de to
allowance-tersklene (SI 7) korrekt skifter fra 1 til 0 mottatte
slag. Kapasitetsgrenser bekreftet begge veier (laget avviser
ALDRI en 4. spiller, individualsiden avviser alltid en 2.).
"Fullfør runde"-gating eksplisitt testet med et ufullstendig lag
(1 spiller) -- avvist med riktig forklarende melding, samme
type sjekk som avdekket punkt 21 sin bug.
2. Full stack: ny isolert scratch-runde (DB/rolle/MinIO/API-
container pluss et ekte `docker build` av frontend-imaget --
den faktiske typesjekkede produksjonsbuilden, ikke bare lokal
`tsc`), ekte nettleser via magic-link-innlogging
(`TEECUP_DEV_LOG_MAGIC_LINKS`). Opprettet en ekte runde gjennom
hele veiviseren (inkl. V0 sin `ScrambleVsSoloSides`-UI), satte
score på tre hull via ekte PATCH-kall, og bekreftet at
`ScrambleSoloResultView` viste EKSAKT de håndregnede tallene
("Laget leder med 1 slag", 12 mot 13 netto) med riktig
grønn/oransje leder-fremheving per hull. `test_isolation.sql`
uendret 12/12 (migrasjonen er rent additiv). Scratch-miljøet
ryddet opp fullstendig etter BEGGE runder.
**Rullet ut live 2026-08-05**, bruker bekreftet eksplisitt: migrasjon
056 kjørt mot ekte `teecup_db` (`round.scramble_solo_individual_pct`,
`round_side.side_role` + `round_side_role_unique`-indeksen, bekreftet
tilstede etterpå), `docker compose up -d --build teecup_api
teecup_frontend`, begge containere boot-et rent, `/health` og
`/my-rounds/new` → 200 over https, `round_play_format_check` bekreftet
å inkludere `'scramble_solo'`.
23. **Matchspill-varianten av samme format (`scramble_solo_match`) —
2026-08-05 (migrasjon 057), PLUSS en kritisk regresjonsbug funnet og
rettet i samme runde som rammet BEGGE variantene (se eget punkt
under).** Samme asymmetriske lag-mot-individualspiller-struktur som
punkt 22, men avgjort hull-for-hull (matchspill) i stedet for netto
slagspill-totalsum. Vesentlig mindre bygg enn punkt 22 -- nesten alt
gjenbrukes uendret.
**Motor -- ingen endring:** `match_play_strokes`/`compute_match_state`
(`handicap_engine.py`) var allerede domeneagnostiske og brukes uendret
-- konverterer lagets `TeamAverage`/individualspillerens
`PerPlayerPercentage(pct/100)` (samme `_recompute_scramble_solo_
handicaps` som punkt 22, uendret) til RELATIVE slag (laveste side
spiller "av scratch"), i motsetning til slagspill-variantens
ABSOLUTTE, uavhengige allowance per side -- bevisst forskjell, riktig
fordi matchspill per definisjon betyr relativ slagfordeling.
**Skjema** (migrasjon 057): ren CHECK-utvidelse, INGEN nye kolonner
-- `round.scramble_solo_individual_pct`/`round_side.side_role`
(migrasjon 056) er generiske nok til å gjenbrukes uendret.
**Backend:** ny delt konstant `_SCRAMBLE_SOLO_FORMATS = {"scramble_
solo", "scramble_solo_match"}`, widened seks eksakte
`== "scramble_solo"`-strengsjekker i `app/routers/rounds.py` til
settmedlemskap. Ny `_build_scramble_solo_match_result` gjenbruker det
eksisterende blandede hull-oppslaget fra `_build_scramble_solo_result`
(samme to queries: `round_hole` via `round_side_id` for laget, via
`round_participant_id` for individualspilleren), bygger en
`list[HoleResult]` fra netto-sammenligning per hull, mater
`compute_match_state` -- gir lead/holes_remaining/is_dormie/
is_closed/`describe()` gratis.
**Frontend -- maksimalt gjenbruk, bekreftet eksplisitt med bruker
(ingen ny V0-prompt for denne varianten):** `new-round.tsx` sin
oppsett-UI (`ScrambleVsSoloSides`/`SoloAllowanceControl`) gjenbrukt
HELT uendret. Resultatvisning gjenbruker `TwoSidedBoard`
(round-leaderboard.tsx) og den generiske to-sidede
`sideIdentity`/`LeadZone`-grenen (round-detail.tsx) -- begge allerede
korrekte for en N-mot-1-side siden de grener på FAKTISK ANTALL
SPILLERE per side, ikke på rolle. Én kritisk, selv-oppdaget
korrekthetsbug FØR noen verifisering i det hele tatt: første utkast
av `_build_scramble_solo_match_result` brukte en FAST konvensjon
(laget alltid "a", individualspilleren alltid "b") -- resten av appen
(inkl. `TwoSidedBoard`) forutsetter derimot at "a" = `round.sides[0]`
SORTERT ETTER UUID, ikke rolle. Rettet til `team_is_a = team_side["id"]
< individual_side["id"]`, tredd gjennom hull-sammenligningen og
`FormatHoleEntry`-side-tagger. `round-scorecard.tsx` sin
`MatchScorecardGrid` fikk en liten, dedikert (ikke-V0) gren for å vise
ÉN rad per SIDE for dette formatet (samme mønster som `isSharedBall`-
grenen der) -- uten den ville laget vist N dupliserte rader, siden
alle lagmedlemmer deler samme `TeamAverage`-tall.
**KRITISK REGRESJONSBUG funnet under full nettleser-verifisering,
rammet OGSÅ den allerede live slagspill-varianten (punkt 22) --
ikke bare den nye matchspill-varianten:** `round-detail.tsx` sin
"Score"-fane (`SHARED_BALL_FORMATS`) inneholdt aldri `scramble_solo`/
`scramble_solo_match` (kun ekte delt-ball-formater som foursome/
scramble_2/scramble_4/chapman) -- rimelig nok isolert sett, siden
scramble mot enkeltspiller er HALVPARTEN delt-ball (laget) og
HALVPARTEN individuell (motstanderen), ikke rent det ene eller det
andre. Konsekvens: et klikk på en LAGSPILLERS scorekort rutet
(feilaktig) til den vanlige per-deltaker `ScoringWizard`/
`wizardPlayerId`-stien i stedet for den delte `SideScoreWizard`/
`wizardSideId`-stien. Siden lagspilleres hull ALDRI lagres per
`round_participant_id` (kun per `round_side_id`), returnerte
`GET .../participants/{id}/holes` alltid `[]` for dem -- og siden
SPINNER-porten i samme fil (`holes.length === 0`) også var avledet
fra akkurat DENNE deltakerens egen (evig tomme) liste, satt den seg
fast i en evig spinner, permanent, uten noen feilmelding. Siden
`SHARED_BALL_FORMATS` aldri var endret for `scramble_solo` i punkt
22 sin utrulling heller, var dette allerede live og reelt ødelagt
for ekte lagspillere på scramble_solo-runder FØR denne rettelsen.
Rettet: `hole`/spinner-porten hentes nå fra HVILKEN SOM HELST kilde
som allerede har lastet inn hull (deltaker- ELLER side-basert,
uavhengig av hvem som er "aktiv spiller") i stedet for kun aktiv
spillers egen liste; ny `openPlayerOrTeamSideEntry`-klikkhåndterer
ruter lagets spillere til `wizardSideId`, individualspilleren
fortsatt til `wizardPlayerId`; `advanceWizardPlayer`/`advanceWizardSide`
justert til å ikke lenger kunne "vandre inn i" den andre kjeden.
Funnet OG rettet FØR migrasjon 057/redeploy -- ikke en kjent,
ureparert feil et øyeblikk i produksjon.
**Verifisering:**
1. Håndregnet API-testskript (`test_scramble_solo_match.py`, 31/31
bestått): 3-manns lag (HCP 8/14/20 → TeamAverage 14) mot
individualspiller (HCP 10, 100 %), `match_play_strokes([14,10])`,
5 spilte hull → "2 UP", `hole_results`/`strokes_received` stemte
hånd-for-hånd mot forventet forløp. Dynamisk `team_is_a`-utledning
i selve testen (ikke antatt fast) for å faktisk teste sorterings-
konvensjonen, ikke bare den ene retningen.
2. Full stack, ekte nettleser (fjerde scratch-miljø, ekte
`docker build` av frontend-imaget): opprettet en runde gjennom
hele veiviseren, satte team/individualspiller opp, entret score
via BEGGE veiviserne (fant og rettet bug-en HER, se over), bekreftet
`LeadZone` viste korrekt "X UP" og skiftet leder korrekt, bekreftet
`MatchScorecardGrid` viste eksakt ÉN rad for laget (ikke tre) og
riktig "N UP"-progresjon per hull, fullførte runden via ekte
"Fullfør runde"-knapp.
3. Regresjonsbekreftet SEPARAT at samme fiks løser samme bug for den
allerede-live slagspill-varianten (`scramble_solo`): opprettet en
ny runde med det formatet i samme scratch-miljø, bekreftet at
`SideScoreWizard` nå åpner korrekt for lagsiden (før fiksen: samme
evige spinner).
4. `test_isolation.sql` uendret 12/12. Scratch-miljøet (DB/rolle/
MinIO/API-/frontend-containere/images) ryddet opp fullstendig
etter bruk.
**Rullet ut live 2026-08-05** (samme økt som punkt 22): migrasjon 057
kjørt mot ekte `teecup_db` (`round_play_format_check` bekreftet å nå
inkludere `'scramble_solo_match'`), `docker compose up -d --build
teecup_api teecup_frontend` (inkluderer BÅDE matchspill-formatet OG
bug-fiksen over -- fiksen var derfor allerede live for ekte
`scramble_solo`-lagspillere idet denne utrullingen fullførte), begge
containere boot-et rent, `/my-rounds/new` → 200 over https.
24. **"Bruk som mal for egen bane" viste seg for bredt — 2026-08-05,
rent frontend, ingen skjema-/backend-endring.** Brukeren rapporterte
at knappen (fork en eksisterende personlig bane til utgangspunkt for
en NY) dukket opp i det vanlige "Egen bane"-søket i opprett-runde-
veiviseren, uansett om målet var å VELGE en eksisterende bane å
spille på eller å opprette en helt ny -- feil sted for en
mal-handling som kun gir mening når brukeren faktisk har sagt at
målet er å opprette.
**Avklart med bruker (tre alternativer presentert)**: knappen skal
KUN vises når brukeren eksplisitt har trykket "Opprett ny bane" og
derfra velger å basere den nye banen på en eksisterende, ikke i det
vanlige søket.
**Fiks (`frontend/components/new-round.tsx`):** ny `S1Sub`-verdi
`"own-search-template"`, distinkt fra det vanlige `"own-search"`.
`OwnSearch`s `onUseAsTemplate`-prop gjort valgfri (var påkrevd) --
"Bruk som mal for egen bane" rendres nå kun `{onUseAsTemplate &&
(...)}`, akkurat som den allerede korrekt gaterte offisiell-bane-
varianten i `CourseList`. Det vanlige `"own-search"`-kallet (nådd fra
Step1 sitt "Egen bane"-kort) slutter å sende med `onUseAsTemplate` i
det hele tatt. Ny lenke "Basér på en eksisterende bane i stedet" lagt
til øverst i `OwnCreateStep`s blanke skjema (kun synlig når
`!initial`, altså før noen mal er valgt) -- trykk navigerer til
`"own-search-template"`, som gjenbruker `OwnSearch` UENDRET, denne
gangen MED `onUseAsTemplate` satt (samme forhåndsutfyllings-bro som
allerede fantes for offisielle baner). Ny `ownCreateOrigin`-state
(løftet til toppnivå-hook-en, ved siden av `s1Sub`) sporer hvor
"own-create" skal gå TILBAKE til (vanlig søk, mal-søk, eller broen
fra offisielle baner) -- uten denne ville tilbake-knappen fra
mal-søket hoppet feil sted.
**Scratch-verifisert i ekte nettleser** (femte scratch-miljø, ekte
`docker build` av frontend-imaget): bekreftet at "Bruk som mal for
egen bane" IKKE lenger vises i det vanlige "Egen bane"-søket;
bekreftet at "Basér på en eksisterende bane i stedet"-lenken vises i
det blanke opprett-skjemaet; trykket den, bekreftet at knappen NÅ
vises i det samme søket; trykket "Bruk som mal", bekreftet at
opprett-skjemaet forhåndsfylte navn/18 hull/utslag korrekt fra den
valgte banen; bekreftet at tilbake-knappen fra opprett-skjemaet gikk
til mal-søket (ikke det vanlige søket eller helt til `source`).
Ingen konsoll-feil. Scratch-miljøet (DB/rolle/MinIO/API-/frontend-
containere/images) ryddet opp fullstendig etter bruk.
**Rullet ut live 2026-08-05** (kun `teecup_frontend` bygget/
restartet -- ren frontend-endring, ingen skjema-/API-endring).
`/my-rounds/new` → 200 over https etterpå.
**Oppfølging samme kveld, 2026-08-06:** brukeren sendte skjermdump og
rapporterte at knappen FORTSATT dukket opp -- men i en ANNEN gren enn
den akkurat rettet: `CourseList` (offisielle baner, "Tjøme Golfklubb"
→ "Hovedbanen") viste den for enhver offisiell bane, uansett om målet
var å spille eller å opprette. Punktet over rettet kun `OwnSearch`
(egne baner) -- samme feilmønster fantes uavhengig i den offisielle
grenen, siden begge alltid hadde vist knappen unconditionalt bortsett
fra en `templateHoles.length > 0`-sjekk.
**Utvidet fiks:** erstattet den forrige, EGEN-bane-spesifikke
`"own-search-template"`-S1Sub-verdien med en generell boolsk
`templateMode`-state (løftet til toppnivå, ved siden av `s1Sub`).
`CourseList`s `onUseAsTemplate` gjort valgfri på samme måte som
`OwnSearch`s allerede var -- begge rendrer nå kun
`{onUseAsTemplate && ...}`, styrt av `templateMode`. Ny mellomskjerm
`"template-source"` (samme to `SourceCard`-valg som det aller første
Bane&tid-steget, men med egen tittel "Basér på en eksisterende bane")
lar brukeren velge OFFISIELL eller EGEN bane som malkilde -- nådd fra
samme "Basér på en eksisterende bane i stedet"-lenke i det blanke
opprett-skjemaet. `templateMode` settes eksplisitt `false` i det
aller første Bane&tid-steget (fresh start) og ved "Avbryt" fra
opprett-skjemaet, `true` kun fra `"template-source"`.
**Scratch-verifisert på nytt** (sjette scratch-miljø): gjenskapte
brukerens eksakte scenario (offisiell-bane-søk → "Tjøme Golfklubb" →
"Hovedbanen") mot ekte teeoff-data, bekreftet at "Bruk som mal for
egen bane" IKKE lenger vises der; bekreftet at "template-source"-
mellomskjermen vises korrekt fra opprett-skjemaets lenke; valgte
"Offisiell bane" derfra, søkte samme klubb, bekreftet at knappen NÅ
vises (med justert beskrivelsestekst: "Velg hvilken bane du vil
bruke som mal..."); trykket den, bekreftet at opprett-skjemaet
forhåndsfylte navn/18 ekte hull/4 ekte utslag korrekt fra Hovedbanen.
Ingen konsoll-feil. Scratch-miljøet ryddet opp fullstendig.
**Rullet ut live 2026-08-06** (kun `teecup_frontend`).
`/my-rounds/new` → 200 over https etterpå.
25. **Kommentarer/bilder på frittstående runder + samlet `/my-feed`-side
(ADR-044, migrasjon 058) — BYGGET OG SCRATCH-VERIFISERT 2026-08-06,
venter på utrulling mot ekte `teecup_db`.** Brukeren ba om samme
"Banter Board"-mulighet (bilde+tekst) som org-turneringer allerede har
(ADR-025), 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 gammel "Neste steg"-seksjon i denne loggen sa fortsatt "ikke
bygget", aldri rettet opp. Denne runden gjenbrukte den infrastrukturen
uendret; omfanget ble dermed smalere enn først antatt.
**To beslutninger avklart eksplisitt med bruker (AskUserQuestion) FØR
planlegging:** (1) skriverett = alle som kan SE runden kan også POSTE
(ikke kun eier/medspillere -- overstyrer et snevrere utkast notert
tidligere samme dag i FEATURE_BACKLOG.md), (2) feed på en ny, dedikert
side (`/my-feed`), ikke en dashbord-seksjon. Fullstendig plan skrevet
og godkjent i Plan Mode før noe ble bygget (to Explore-runder + én
Plan-agent-syntese, grundig kodegrunnet mot eksisterende `_can_view_
round`/`list_friends_on_course`/`messaging.py`-mønstre).
**Datamodell:** ny, EGEN `round_message`-tabell (migrasjon 058), IKKE
en utvidelse av org sin `message`-tabell -- `round` har verken
`organization_id` eller RLS (ADR-033 Beslutning A). Samme feltform
(forfatter-snapshot, valgfri tekst, valgfri `image_key`) og samme
MinIO+AVIF-pipeline (`app/storage.py`) gjenbrukt uendret, egen
`prefix="round_messages"`. Scratch-verifisert alene først: skjema,
indekser, grants bekreftet, `test_isolation.sql` 12/12.
**Backend:** ny fil `app/routers/round_messages.py` (`rounds.py` er
allerede 5500+ linjer) -- `GET`/`POST /rounds/{id}/messages`, `DELETE
/rounds/{id}/messages/{message_id}` (forfatter ELLER rundeeier kan
slette, samme "forfatter-eller-admin"-modell som org-feedens
`delete_feed_message`), og en ny `GET /feed`-aggregering modellert på
`list_friends_on_course` sin reverserte retning (viewer → synlige
runder), justert til å inkludere egne runder uansett `visibility_mode`
og uten "spiller nå"-filteret (all-time, keyset-paginert). Gjenbruker
`_can_view_round`/`_get_viewable_round_or_404` fra `rounds.py` uendret
som BÅDE lese- og post-sjekk. Ingen ny WebSocket-kanal -- gjenbruker
eksisterende `broadcast_round_update`, `RoundMessages`-komponenten
henter på nytt via et nytt `messagesRefreshTick`-signal i
`round-detail.tsx` sin allerede eksisterende WS-lytter. Liten
refaktor i samme runde: `messaging.py` sin lokale `_read_optional_
image` flyttet til `app/storage.py` som delt `read_optional_image`,
brukt av begge routerne nå.
**En reell bug funnet OG rettet FØR noen verifisering:** `GET /feed`
sin `before`-parameter krasjet med 500 (`asyncpg.exceptions.DataError`)
-- asyncpg godtar ikke en ren tekststreng mot en `timestamptz`-cast i
SQL-en, krever et faktisk `datetime`-objekt. Rettet ved å la Pydantic
parse query-parameteren som `datetime` direkte i stedet for `str`.
**Håndregnet auth-testskript, 30/30 sjekker bestått:** eier poster/
sletter egen (private) runde; lenket medspiller (ikke eier) poster,
en ANNEN ikke-eier-medspiller kan IKKE slette den (403); eieren KAN
slette andres innlegg på egen runde (moderasjon); en fremmed uten
innsyn avvist på både GET og POST (403/404) for en privat runde; en
`friends`-synlig runde med RIKTIG venne-kategori gir GET+POST; med
FEIL/manglende kategori avvises selv GET; `GET /feed` bekreftet riktig
sett per bruker (eier ser alle egne uansett visibility, en venn ser
kun offentlige + riktig-kategoriserte `friends`-runder, en fremmed ser
kun offentlige), paginering (`before`-cursor) uten hopp/duplikat.
Ekte bildeopplasting testet direkte mot scratch-MinIO (utenfor HTTP,
`storage._client.stat_object`) -- bekreftet 488 byte AVIF,
`content_type=image/avif`.
**Frontend bygget via V0** (brukerens eksplisitte, stående
arbeidsfordeling -- se CLAUDE.md-notat): to prompter skrevet og
levert i chat (kommentarseksjon, feed-side), begge forankret i
`DESIGN_SYSTEM.md` sine fargetokens/tilgjengelighetsregler og eksakte
data-kontrakter fra det allerede bygde API-et. Zip 32 (`round-
messages.tsx`) og zip 33 (samme + ny `feed.tsx`) diffet ordrett mot
hverandre FØR integrering -- ingen utilsiktet drift utover den ene
nye filen. To små, bevisste integrasjonsjusteringer: 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 -- konsekvent med at alle andre sider
i appen er tynne wrappere som selv henter egen brukeridentitet.
`RoundMessages` montert som en ny seksjon NEDERST på `round-detail.
tsx` sin `pageTab === "score"` (ikke en ny rundefane, ikke inni
admin-"manage"-fanen -- se ADR-044 for begrunnelsen). Ny `/my-feed`-
side + inngangspunkter: `SeeAllLink` lagt til dashbordets "Venner på
banen"-seksjon, ny lenke i `/more`-menyen.
**KRITISK rute-/rewrite-kollisjon funnet under full-stack-
verifisering, samme klasse som `/rounds`→`/my-rounds` og
`/friends`→`/my-friends`:** frontend-siden ble først lagt på `/feed`
-- nøyaktig samme streng som det flate API-endepunktet. Uten en
eksplisitt rewrite-regel vant Next.js sin egen side-rute, så
`fetch("/feed")` fra klienten fikk appens egen HTML tilbake (stille
feilet JSON-parsing, "Kunne ikke laste feeden"-feilmelding i UI-et).
Rettet ved å (1) flytte siden til `/my-feed` OG (2) legge til en
manglende, eksplisitt `/feed`-rewrite-regel i `next.config.mjs` (som
rett og slett ikke fantes fra før -- retting nummer to var nødvendig
uavhengig av sideflyttingen). Ekte typesjekket produksjonsbuild fanget
IKKE denne feilen (kun kjøretid avslørte den) -- funnet ved faktisk
nettleser-testing, ikke ved bygging.
**En andre reell bug funnet under samme verifiseringsrunde:**
`RoundMessages` hentet meldinger KUN ved mount -- `round-detail.tsx`
sin eksisterende `/ws/rounds/{id}/live`-lytter (allerede der fra
tidligere, brukt av scorekort/format-resultat) var aldri koblet til
den nye kommentarseksjonen. Rettet med et nytt `messagesRefreshTick`-
signal, samme mønster som det eksisterende `formatResultRefreshTick`.
Verifisert med en EGEN, minimal scratch-Caddy (`handle /ws/* {
reverse_proxy ... }`, samme regel som den ekte `/opt/teeoff/deploy/
Caddyfile`) satt opp spesifikt for denne testen -- uten den ville
WebSocket-oppgraderingen aldri nådd API-et i det hele tatt (Next.js
sitt eget rewrite-lag proxyer ikke WS pålitelig i standalone-modus,
dokumentert allerede i den ekte Caddyfilen). Bekreftet: en kommentar
postet via et separat API-kall dukket opp automatisk i en åpen
nettleserfane, ingen interaksjon eller reload nødvendig.
**Scratch-miljøet ryddet opp fullstendig** etter bruk (DB/rolle/MinIO/
API-/frontend-/Caddy-containere og -images, V0-eksport-zip-ene fjernet
fra prosjektroten uten å bli spurt, samme rutine som alltid for merget
V0-eksport).
**Rullet ut live 2026-08-06**, bruker bekreftet eksplisitt: migrasjon
058 kjørt mot ekte `teecup_db` (`round_message`-tabellen bekreftet
tilstede etterpå), `docker compose up -d --build teecup_api
teecup_frontend`, begge containere boot-et rent. `/health`/`/my-rounds/
new`/`/my-feed` → 200 over https, `GET https://teecup.golf/feed`
bekreftet å proxye korrekt til backend (401 JSON `NOT_AUTHENTICATED`,
IKKE frontend-sidens HTML -- bekrefter rewrite-fiksen virker i
produksjon, ikke bare i scratch), `teeoff.no` upåvirket.
26. **To bugfikser, 2026-08-06, rullet ut samme dag:**
- **"Venner på banen"-kortet lenket til feil rute.** Brukeren
rapporterte (video vedlagt) at et klikk på en venns pågående runde
på dashbordet ga "Denne runden finnes ikke, eller du har ikke
tilgang til den" -- selv om runden faktisk var synlig for
vedkommende (`GET /friends/on-course` viste den korrekt). Rotårsak:
`dashboard.tsx` sin `LiveFriends`-komponent lenket til
`/my-rounds/{id}` (eier/medspiller-only-visningen,
`_get_accessible_round_or_404`) i stedet for `/watch/{id}` (den
allerede eksisterende tredjeparts-visningen bygget for nettopp
dette, ADR-036 fase 2). Samme feilmønster funnet OG rettet i den
nybygde `/my-feed`-siden (punkt 25) -- `FeedCard` lenket likeens
alltid til `/my-rounds/{id}` uansett eierskap. Begge rettet til å
route riktig basert på om runden faktisk er brukerens egen.
- **Kamera lot seg ikke aktivere ved bildeopplasting på mobil**, seks
steder i appen (rundekommentarer, org-feed, avatar, lag-chat,
turnering-hero/sponsor-bilder). Rotårsak: `
` -- flere
eksplisitte MIME-typer i `accept` gjør at mange mobilnettlesere kun
tilbyr galleri, ikke kamera, i filvelgeren. Rettet til
`accept="image/*"` alle seks steder (server-siden validerer uansett
strengt mot `storage.ALLOWED_INPUT_CONTENT_TYPES`, så dette svekker
ingen kontroll).
Begge ren frontend, ingen migrasjon. Ekte typesjekket produksjonsbuild
kjørt før utrulling. **Rullet ut live 2026-08-06** (kun
`teecup_frontend` bygget/restartet), `/dashboard` → 200, `teeoff.no`
upåvirket.
27. **Ekte TeeCup-logo tatt i bruk som app-ikon, erstatter den midlertidige
grønne golfflagg-placeholderen — 2026-08-06.** Brukeren lastet opp
`TeeCup logo.svg` (ikon+ordmerke i én fil, Inkscape-eksport med
raster-baserte masker/filtre, ikke rene vektor-tekstobjekter) og
spurte om splitting av ikon fra tekst var mulig uten flere opplastede
varianter.
**Splitting løst programmatisk, ikke manuelt:** brukte nettleserens
`getBBox()` på hver topp-nivå-`
` i den innlastede SVG-en for å
måle faktiske posisjoner -- avslørte at ikonet (ball/pokal/tee) består
av tre atskilte grupper (x: 0–169 av 656 totalt) mens "TeeCup"-
ordmerket er seks separate bokstav-grupper (x: 179–407) -- ingen
gjetning nødvendig. Beskåret til et eget ikon-SVG via et rent
`viewBox`-endring på en kopi av originalfilen (ingen gruppe fjernet
eller XML-struktur røsket i -- alle filter-/maske-/clipPath-
definisjoner ligger nestet INNI selve gruppene, bekreftet empirisk:
et forsøk på å trimme bort de ubrukte tekst-gruppene for å spare
filstørrelse brøt renderingen umiddelbart, samme "hele filen eller
ingenting"-begrensning som Inkscape sin raster-trace-eksport gir --
reverserte til den fullstendige, verifisert fungerende versjonen i
stedet for å risikere en ødelagt fil for en fil-størrelse-optimering).
`icon.svg` er dermed 416 KB (arves fra kildefilens embedded
raster-masker) -- fungerer korrekt, men en ekte vektor-reeksport fra
designer ville gitt en vesentlig lettere fil om ønskelig senere.
**Ekte nettleser brukt til rasterisering** (ingen lokal SVG->PNG-
rasterizer tilgjengelig i miljøet): Chrome DevTools sitt
skjermbilde-verktøy, med små HTML-wrapper-sider satt til eksakt
mål-pikselstørrelse per format, generert alle seks filene appen
faktisk bruker: `icon.svg` (transparent, favicon), `icon-light-
32x32.png`/`icon-dark-32x32.png` (transparent, samme innhold --
fungerer i begge fargetema), `apple-icon.png` (180×180, opak hvit
bakgrunn -- iOS støtter ikke transparens), `icons/icon-192.png` og
`icons/icon-512.png` (PWA-manifest, opak hvit bakgrunn), `icons/
icon-maskable-512.png` (ikonet holdt innenfor sentrale ~63 % --
trygt innenfor W3C sin ~80 %-sikre-sone for maskable-ikoner som
OS-en klipper med egen maske). Hvert format visuelt bekreftet
(`Read` på hver genererte PNG) før plassering.
Hele ordmerket (ikon+"TeeCup"-tekst) lagret uendret som `public/
teecup-wordmark.svg` for senere bruk (f.eks. innloggingsside),
ikke koblet inn noe sted ennå -- kun selve app-ikon-oppgaven løst nå.
`app/manifest.ts` sin kommentar oppdatert til å reflektere at
placeholder-fasen er over.
Ren frontend, ingen migrasjon. Ekte typesjekket produksjonsbuild
kjørt. **Rullet ut live 2026-08-06** (kun `teecup_frontend`),
`/icon.svg`/`/apple-icon.png`/`/icons/icon-512.png` → 200 over https,
`teeoff.no` upåvirket.
28. **Tilskuer-visning (`/watch/[id]`): tre faner + "Spillere og runde",
full paritet med eiersiden (ADR-045) — BYGGET OG SCRATCH-VERIFISERT
2026-08-06, venter på utrulling.** Direkte oppfølging av punkt 26 sin
bug (feil lenke fra "Venner på banen") -- brukeren ba samtidig om at
selve tilskuer-siden (fram til da ÉN enkelt side, kun kompakt status)
skulle få samme tre faner som eiersiden (Score/Scorekort/Leaderboard)
og "Spillere og runde" (deltakerliste, HCP/utslag/tildelte slag,
flight-gruppering), uten eier-kun-handlingene. Bekreftet med bruker
(AskUserQuestion, to spørsmål): full formatspesifikk detalj på
Scorekort-fanen (ikke en forenklet fellesvisning), og
flight-gruppering bygget nå (ikke utsatt).
**Nøkkelbeslutning, funnet under research (Explore-agent + egen
lesning av begge filene):** `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. Det eneste som hindret gjenbruk for
tilskuere var at alle interne data-hentinger (fire hooks/effekter
per fil, pluss WS-tilkoblingen) hardkodet `/rounds/*`-prefikset. I
stedet for å bygge nye, forenklede tilskuer-komponenter (ville gitt
UFULLSTENDIG formatparitet eller duplisert ~1000+ linjer formatlogikk),
la til en valgfri `publicMode`-prop på begge de EKSISTERENDE
komponentene -- default `false` (INGEN endring i eiersidens
oppførsel), `true` bytter alle interne URL-er til `/public/rounds/*`
og WS-kanalen til den offentlige. Samme kode, to datakilder, ekte
full formatparitet (alle 17 formater) uten duplisert vedlikehold.
**Backend:** ett nytt endepunkt, `GET /public/rounds/{id}/
flight-group` (`app/routers/rounds.py`) -- speiler den eksisterende
autentiserte varianten, men med en ny `_viewable_flight_rows`
(bruker `_can_view_round` PER søsken-flight uavhengig, siden en
flight-gruppe kan ha søsken med ulik `visibility_mode`). Ingen
migrasjon.
**Frontend, ny fil-for-fil:** `watch-tabs.tsx` (ny, liten fane-stripe
-- BEVISST ikke en gjenbruk av `RoundHeader`, som alltid render et
"Administrer"-tannhjul/admin-dialog uansett hvilke handlere som gis
inn -- den nye komponenten inneholder rett og slett ingen slik kode).
`watch-players.tsx` (ny "Spillere og runde", IKKE et forsøk på å
gjøre `round-detail.tsx` sin lokale, ikke-eksporterte `PlayerList`
gjenbrukbar via `canManage=false` -- egen, read-only-only komponent,
ingen +Medspiller-/Rediger-/Fullfør-/Slett-kode i det hele tatt).
`watch-round.tsx` omskrevet: Score-fanen viser nå fane-stripen +
kompakt status + "Spillere og runde" -- selve matchstatus-/skins-/
slagspill-visningen FLYTTET til den nye Leaderboard-fanen (erstattet
av `RoundLeaderboard publicMode`, som dekker alle formater -- lukker
et eksisterende gap der den gamle `watch-round.tsx` sin lokale
`TWO_SIDED_FORMATS`-liste kun dekket 8 av 17 formater). To nye tynne
ruter, `/watch/[id]/scorecard` og `/watch/[id]/leaderboard`
(`watch-scorecard.tsx`/`watch-leaderboard.tsx`, hver bare fane-stripe
+ `RoundScorecard`/`RoundLeaderboard` med `embedded publicMode`).
**Verifisering, samme scratch-Caddy-disiplin som ADR-044:** 6/6
håndregnede sjekker for det nye flight-group-endepunktet (anonym ser
kun offentlige søsken-flighter, venn med riktig kategori ser i
tillegg `friends`-synlige, eier ser alle, fremmed uten vennskap ser
kun offentlige, privat anker-runde avvist helt). `tsc --noEmit` rent
på hele frontend-prosjektet. Ekte nettleser: **regresjon FØRST** --
eiersidens `/my-rounds/{id}/scorecard`/`/leaderboard` bekreftet
PIKSEL-IDENTISK (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`) begge bekreftet rendret korrekt gjennom
`/watch/{id}/leaderboard` -- beviser formatparitet-lukkingen direkte.
En privat rundes tilskuer-sider (alle tre) bekreftet korrekt avvist
for en ANONYM leser i en egen, isolert nettleser-kontekst (ikke bare
et API-kall) med riktig feilmelding og "Tilbake til dashbordet"-
lenke. `test_isolation.sql` uendret 12/12. Scratch-miljøet (DB/rolle/
MinIO/API-/frontend-/Caddy-containere og -images) ryddet opp
fullstendig etter bruk.
**Rullet ut live 2026-08-06**, bruker bekreftet eksplisitt: `docker
compose up -d --build teecup_api teecup_frontend` (ingen migrasjon),
begge containere boot-et rent. `/health` → 200, `/dashboard` → 200,
nytt endepunkt bekreftet (`/public/rounds/{tilfeldig-id}/flight-group`
→ 404 for en ikke-eksisterende runde), alle tre nye/endrede
`/watch/[id]`-ruter → 200, `teeoff.no` upåvirket.
29. **Bugfiks: kommentarer/bilder (ADR-044) fantes ikke i tilskuer-
visningen — 2026-08-06.** Brukeren rapporterte at en person en runde
er delt med ikke kunne se selve samtalen. Rotårsak: `RoundMessages`
ble aldri montert på `/watch/[id]` -- kun på `round-detail.tsx` sin
egen Score-fane (eier/deltaker). Backend var allerede riktig
(`GET`/`POST /rounds/{id}/messages` bruker `_get_viewable_round_or_
404`, synlighets-gatet, ikke eier/deltaker-only, siden ADR-044 ble
bygget) -- ren frontend-mangel.
**Én reell backend-mangel funnet underveis:** `PublicRoundOut`
eksponerte `owner_display_name` men ALDRI eierens faktiske
`user_id` -- `RoundMessages` sin "forfatter ELLER rundeeier kan
slette"-sjekk (`roundOwnerUserId === currentUserId`) hadde dermed
ingen korrekt verdi å sammenligne mot fra tilskuer-siden. Lagt til
`owner_user_id: str` på `PublicRoundOut` (samme personvernsnivå som
det allerede eksponerte visningsnavnet -- en ugjennomsiktig UUID
avslører ingenting nytt).
**Rettet:** `watch-round.tsx` monterer nå `` på
Score-fanen, med `roundOwnerUserId={round.owner_user_id}` og
`currentUserId` fra en ny `/auth/me`-henting (tolerant for anonym,
samme mønster som resten av appen).
**Verifisert, isolert scratch-miljø:** 6/6 håndregnede sjekker
(`/public/rounds/{id}` eksponerer korrekt `owner_user_id`, en venn
kan poste via synlighet -- ikke eier/deltaker, begge kan lese, eier
kan moderere/slette vennens innlegg). `tsc --noEmit` rent. Ekte
nettleser: kommentarseksjonen bekreftet synlig på `/watch/{id}`,
ingen konsollfeil utover forventet 401 på `/auth/me` for en anonym
leser (allerede tolerert i koden). Scratch-miljøet ryddet opp.
30. **Bugfiks: kamera manglet helt ved bildeopplasting, ekte "prøv igjen"-
feil skjult bak en generisk melding — 2026-08-06.** Brukeren
rapporterte (skjermdump fra ekte enhet, Pixel 8 Pro, TeeCup installert
som PWA i Chrome) at "Legg til bilde" ALDRI viste noe kamera-
alternativ, kun galleri -- og at selve posten deretter feilet med
"Kunne ikke poste kommentaren. Prøv igjen." uten noen forklaring.
**To atskilte, reelle rotårsaker, begge bekreftet:**
1. Siden Android 13 bruker Chrome (og andre nettlesere) systemets
eget "Photo Picker" for ` ` UTEN
`capture`-attributt -- denne velgeren viser KUN galleri, ALDRI et
kamera-alternativ, uansett hvilken `accept`-verdi som er satt
(den tidligere fiksen, `accept="image/*"` alene, var altså
nødvendig, men ikke tilstrekkelig for Android 13+). Rettet ved å
splitte den ene filvelgeren i to atskilte inputs/knapper -- én
med `capture="environment"` (rett til kamera), én uten (rett til
galleri) -- i `round-messages.tsx`, `team-chat.tsx`, og
`public-tournament.tsx` sin `TournamentFeed` (de tre stedene som
faktisk brukes aktivt for bildeopplasting fra mobil; `account-
settings.tsx`/`tournament-presentation.tsx` sine engangs-avatar-/
hero-bilde-opplastinger har fortsatt kun galleri -- lavere
prioritet, tas senere om ønskelig).
2. Selve feilmeldingen ved en mislykket post var en blindt generisk
`catch { setPostError("Kunne ikke poste... Prøv igjen.") }` --
den faktiske, spesifikke backend-feilen (f.eks. "Bildet er for
stort (maks 8 MB)") ble aldri lest fra svaret og dermed aldri
vist. Rettet til å faktisk lese `{"detail":{"code","message"}}`-
kontrakten (samme form som resten av API-et, `app/errors.py`) og
vise den ekte meldingen, med en generisk norsk fallback kun hvis
svaret mot formodning ikke er JSON. Lagt til et klient-side
størrelsessjekk (samme 8 MB-grense som `storage.MAX_UPLOAD_BYTES`)
for umiddelbar, presis feilmelding uten en unødvendig tur-retur
til serveren for noe som uansett ville blitt avvist -- sannsynlig
den FAKTISKE årsaken til brukerens opprinnelige feil, siden
moderne telefonkamera-bilder lett kan overstige 8 MB.
**Oppfølging samme dag:** brukeren spurte -- helt riktig -- om selve
8 MB-grensen egentlig var et praktisk problem, siden alle bilder
uansett konverteres til AVIF (mye mindre) før lagring. Bekreftet i
`app/storage.py`: `MAX_UPLOAD_BYTES`-sjekken skjer på RÅ input, FØR
konverteringen -- den beskytter altså ikke lagringsstørrelsen (det
gjør AVIF-steget, uavhengig av inputstørrelse), den var bare en
vilkårlig øvre grense som kunne avvise et helt normalt telefonbilde
før det fikk sjansen til å konverteres ned. Hevet til 20 MB (server-
siden `app/storage.py` OG de tre nye klient-side sjekkene over,
samt feilteksten) -- rikelig for moderne telefonkamera-JPEG-er,
fortsatt en reell grense mot noe genuint urimelig stort.
Ren frontend + denne ene backend-konstanten, ingen migrasjon.
`tsc --noEmit` rent, `py_compile` rent. **Rullet ut live 2026-08-06**,
bruker bekreftet eksplisitt: `docker compose up -d --build teecup_api
teecup_frontend`, begge containere boot-et rent. `/health` → 200,
`/dashboard` → 200, `teeoff.no` upåvirket.
31. **Kommentarstrømmen på en runde sortert om: nyeste øverst — 2026-08-06.**
Brukeren ba om dette rett etter forrige punkt -- `round_message`-
listen (`round-messages.tsx`) var kronologisk eldst-først (samme
mønster som en vanlig chat), brukeren ville ha nyeste øverst i
stedet (en oppdateringsstrøm, ikke en samtale man leser fra start).
Endret `ORDER BY created_at` → `created_at DESC` i `GET /rounds/{id}/
messages` (`app/routers/round_messages.py`), og en ny post legges nå
øverst i listen lokalt (`[created, ...prev]` i stedet for `[...prev,
created]`). Lag-chatten (`messaging.py`) og org-oppslagstavlen
(`public-tournament.tsx`) er BEVISST uendret -- en ekte samtale
leses fortsatt kronologisk.
Verifisert i isolert scratch-miljø (postet tre meldinger, bekreftet
`GET`-rekkefølgen er nyeste-først). `tsc`/`py_compile` rent.
**Rullet ut live 2026-08-06**, bruker bekreftet eksplisitt (samme
utrulling som punkt 30). `teeoff.no` upåvirket.
32. **"Nyeste først" utvidet til ALLE meldingsstrømmer — 2026-08-06,
overstyrer punkt 31 sin "bevisst uendret"-begrunnelse.** Brukeren:
"Nyeste først. Over alt." -- en eksplisitt, direkte instruks om at
lag-chatten og org-oppslagstavlen (som punkt 31 bevisst lot forbli
kronologiske, med en UX-begrunnelse om at "en ekte samtale leses
fortsatt kronologisk") OGSÅ skulle bli nyeste-først, på tvers av min
egen tidligere vurdering.
Endret `ORDER BY created_at` → `created_at DESC` for BÅDE
`list_team_messages` og `get_feed` (`app/routers/messaging.py`).
`team-chat.tsx`: WS-mottak og lokal post-oppdatering endret fra
append (`[...prev, msg]`) til prepend (`[msg, ...prev]`); fjernet
`bottomRef`-baserte auto-scroll-til-bunn-`useEffect`en (unødvendig
når nyeste alltid er øverst, synlig uten scrolling).
`public-tournament.tsx` sin `TournamentFeed`: samme prepend-endring i
BÅDE WS-mottak og `send()`.
Verifisert i isolert scratch-DB (fra `teecup_db`-mal): satte inn tre
meldinger med forskjøvne tidsstempler direkte i `message`-tabellen
for både `scope='team'` og `scope='tournament_feed'`, kjørte de
EKSAKTE SELECT-spørringene fra koden, bekreftet nyeste-først-
rekkefølge for begge. `tsc --noEmit`/`py_compile` rent.
`test_isolation.sql` uendret 12/12. Scratch-DB/rolle ryddet opp.
**Rullet ut live 2026-08-06**, bruker bekreftet eksplisitt ("Ja
takk."): `docker compose up -d --build teecup_api teecup_frontend`,
begge containere boot-et rent (ingen migrasjon). `teeoff.no`
upåvirket.
33. **Emoji-reaksjoner + trådede kommentarer på innlegg — 2026-08-06, se
ADR-046 for full begrunnelse/beslutningsdetalj.** Samme melding som
punkt 32 ("... Husk også at man skal kunne like (eller bruke andre
emojier) og kommentere på innlegg."). Migrasjon 059
(`round_message_reaction`/`_comment`, `message_reaction`/`_comment`
-- to tabellpar, ett uten RLS speiler `round_message`, ett RLS'et
speiler `message`). Nye endepunkter i `round_messages.py` og
`messaging.py`: `PUT`/`DELETE .../reaction` (upsert, ett fast
kuratert emoji-sett, én reaksjon per bruker per innlegg), `GET`/
`POST .../comments` + `DELETE .../comments/{id}` (flat lagring,
frontend bygger tre-strukturen, kaskade-sletting av svar).
Autorisasjon speiler hver innleggstypes EKSISTERENDE post-/
slette-regler uendret (ingen ny modell).
**Én reell regresjon funnet og rettet FØR utrulling, under ekte
nettleserverifisering (ikke fanget av API-testskriptet alene):** de
nye kommentar-endepunktene i `round_messages.py` kalte først
`broadcast_round_update()` ved en inkonsekvens mot egen plan (planen
sa eksplisitt ingen live-push for kommentarer/reaksjoner i v1) --
dette trigget rundens eksisterende WS-tick, som fikk `RoundMessages`
til å refetche HELE meldingslisten og med det kollapse enhver
allerede-utvidet kommentartråd andre steder på siden ved hver eneste
kommentarhandling. Fjernet fra de nye endepunktene, bekreftet rettet
ved re-test (tråd forble utvidet gjennom en påfølgende reaksjon/
kommentar).
**Frontend:** ny delt `post-engagement.tsx` (via ÉN Claude-skrevet
V0-prompt), integrert i `round-messages.tsx`, `feed.tsx`
(`FeedCard`), `team-chat.tsx`, `public-tournament.tsx`
(`TournamentFeed`). To integrasjonsjusteringer utover ren copy-inn:
byttet V0-eksportens egendefinerte SVG-ikoner til `lucide-react`
(DESIGN_SYSTEM.md sin "ingen annen ikonpakke"-regel), og flyttet
`PostEngagement` UT av `feed.tsx` sin `FeedCard`-` ` (var
opprinnelig nøstet inni hele kort-lenken -- ville trigget navigering
ved reaksjonsklikk og vært ugyldig nøstet interaktivt innhold).
**Kjent, bevisst forenkling (ikke en bug):** `public-tournament.tsx`
sin Oppslagstavle setter `canModerate={false}` alltid i UI-et --
backend håndhever fortsatt korrekt "forfatter ELLER org-admin"
uansett, men en org-admin ser ikke en slett-knapp for ANDRES
kommentarer der ennå (ren UI-fullstendighets-luke, ikke et
sikkerhetshull).
**Scratch-verifisert grundig, to runder:** (1) API-nivå -- full
autorisasjonsmatrise for alle tre innleggstyper (upsert-reaksjon
bekreftet ikke-stablende, tråding, kaskade-sletting, riktig
avvisning per type sin faktiske regel, RLS-isolasjon for
`message_reaction` eksplisitt bekreftet), `test_isolation.sql`
12/12, ekte produksjonsbuild av frontend kjørt og bekreftet ren.
(2) Ekte nettleser (egen scratch-Caddy for sesjon/WS) -- reagert,
byttet reaksjon, postet trådet samtale, slettet med kaskade-
bekreftelse, bekreftet `feed.tsx` sin Link-nøsting-fiks IKKE
trigger navigering, bekreftet komponenten rendrer korrekt i alle
fire flater uten konsollfeil. Begge scratch-miljøer (DB/rolle/API-/
frontend-/Caddy-containere/images) ryddet opp fullstendig.
**Rullet ut live 2026-08-06**, bruker bekreftet eksplisitt ("Ja
takk"): migrasjon 059 kjørt mot ekte `teecup_db` (fire nye tabeller,
additiv, ingen endring av eksisterende data), deretter `docker
compose up -d --build teecup_api teecup_frontend`. Begge containere
boot-et rent. Bekreftet via live `openapi.json`: alle ni nye ruter
(`.../reaction` × 3, `.../comments` × 3, `.../comments/{id}` × 3)
riktig registrert og nåbare. `teeoff.no` upåvirket.
34. **Leaderboard lukket gapet mot en konkurrentapp-referanse — 2026-08-06,
se ADR-047 for full begrunnelse/beslutningsdetalj.** Brukeren viste en
video av GolfGameBook sitt leaderboard (trykk-for-å-utvide rad → fullt
hull-for-hull-scorekort + sosiale handlinger per spiller), ba om noe
minst like bra i TeeCup -- eksplisitt IKKE en kopi av utseendet.
Research avdekket at kjernemekanikken (utvidbare rader med
hull-for-hull-rutenett, egen form+farge-språk) allerede var bygget og
live for `IndividualBoard` siden 2026-07-26 -- denne runden lukket de
reelle gapene i stedet for å bygge om fra bunnen: ingen sosial-tilgang
noe sted i leaderboardet, ingen kompakt oppsummeringslinje,
`TwoSidedBoard`/`HighLowBoard` (to-sidede formater) hadde INGEN
utvidelse i det hele tatt, `TeamFlightBoard` hadde en flat,
ikke-utvidbar råscore-liste.
Avklart eksplisitt med bruker (AskUserQuestion) før bygging: (1)
`post-engagement.tsx` (ADR-046) reagerer på ETT spesifikt innlegg, ikke
runden -- alle utvidede rader lenker derfor til den SAMME, allerede
eksisterende kommentarseksjonen på rundesiden i stedet for en
(ikke-eksisterende) tråd per spiller. (2) To-sidede formater fikk ÉN
utvidbar detaljseksjon for hele kampen, ikke et per-rad-mønster (kun 2
sider, ikke en spillerliste).
Alt i `frontend/components/round-leaderboard.tsx` + anker-id på
kommentarseksjonen i `round-detail.tsx`/`watch-round.tsx`: `HoleByHole`
fikk en oppsummeringslinje + "Kommentarer"-lenke; `TeamFlightBoard`
sin råscore-liste konvertert til utvidbare rader (ny `TeamMemberRow`,
gjenbruker `HoleByHole` uendret); `TwoSidedBoard`/`HighLowBoard` fikk
en ny, delt `MatchDetailSection` som lazy-monterer den nå eksporterte
`MatchScorecardGrid` (`round-scorecard.tsx`, samme ADR-045-presedens).
Ingen ny V0-runde, INGEN backend-endring -- ren gjenbruk/eksport av
allerede godkjente komponenter.
**Funnet og rettet under nettleserverifisering:** nettleserens egen
anker-scroll (`#kommentarer`) rakk ikke frem siden elementet ikke
fantes i DOM-en før async rundedata var lastet -- løst med en liten
`useEffect` som trigger scrollet på nytt når dataen ankommer.
**Verifisert:** `tsc --noEmit` rent, ekte produksjonsbuild rent.
Isolert scratch-miljø, ekte nettleser: regresjon FØRST (slagspill-
formatets eksisterende utvidelse identisk før noe nytt ble testet),
deretter ny oppsummeringslinje+lenke (inkl. faktisk fungerende
anker-scroll) og ny `MatchDetailSection`/`MatchScorecardGrid`-utvidelse
bekreftet på et fourball-format med ekte 4-spiller-data, samme format
bekreftet identisk i `publicMode` (anonym tilskuer, egen isolert
nettleser-kontekst, kommentar-lenken riktig pekende til
`/watch/{id}#kommentarer`). `TeamFlightBoard`/`HighLowBoard` ikke
direkte browser-testet (ingen testdata for disse formatene i
scratch-DB-malen) -- vurdert lav risiko siden de gjenbruker nøyaktig
samme, allerede-testede kodeveier (`HoleByHole` uendret;
`MatchDetailSection`/`toGridRound`/`MatchScorecardGrid` uendret), se
ADR-047 for full begrunnelse.
**Egen feil funnet og rettet underveis, verdt å notere:** det første
forsøket på scratch-frontend-bygg pekte ved en kopier-lim-feil
`TEECUP_API_ORIGIN` mot den EKTE produksjons-API-en (`teecup_api`) i
stedet for scratch-containeren -- oppdaget da innlogging som en
scratch-testbruker konsekvent feilet. All interaksjon i det vinduet
var lesing (sidevisning/rad-utvidelse, ingen skriving/mutasjon), men
bildet ble likevel bygget på nytt med riktig scratch-API-peker og HELE
nettleser-verifiseringen kjørt om igjen fra bunnen mot den korrekt
isolerte stacken før noe ble konkludert.
Ren frontend, ingen migrasjon. **Rullet ut live 2026-08-06**, bruker
bekreftet eksplisitt ("Ja takk"): `docker compose up -d --build
teecup_frontend` (Compose gjenskapte også `teecup_api`-containeren i
samme kommando -- ingen kodeendring der denne runden, kun en ren
restart). Begge containere boot-et rent, `/health` → 200. `teeoff.no`
upåvirket.
35. **Reaksjonsraden viste kun et antall, ikke HVEM som reagerte —
2026-08-07.** Brukeren, rett etter å ha godkjent V0-eksperimentet på
`/logg-inn`: "Vi kan ikke se HVEM som liker noe i feeden. Det må på
plass." En reell, riktig funnet mangel i ADR-046-bygget fra dagen
før.
Løst uten noen ny tur-retur til serveren: `ReactionSummary` fikk et
nytt felt `reactors: list[str]` (fulle navn, roster-kontekst --
CLAUDE.md sin navneformat-regel), fylt i SAMME grupperte batch-
spørring som allerede beregnet antallet (`array_agg(...) ORDER BY
created_at`), i både `round_messages.py` (JOIN `app_user`, samme
"fornavn+etternavn eller display_name"-fallback som
`_resolve_round_message_author_name`) og `messaging.py` (LEFT JOIN
`player` for org-spesifikt visningsnavn, samme mønster som
`_resolve_author_display_name` -- fallback til e-postens lokaldel).
`post-engagement.tsx`: et alltid-synlig sammendrag under
reaksjonsraden ("Kari Nordmann og 3 andre reagerte" --
`formatReactorSummary()`, ALDRI kun synlig ved hover/tap alene,
tilgjengelighetsregelen), utvidbart (44px trykkflate) til en full
liste gruppert per emoji. Ingen ny henting -- dataen var allerede i
`reactionRow`.
**Scratch-verifisert:** isolert DB fra `teecup_db`-mal, egen API-
container. Bekreftet for begge tabellpar (`round_message_reaction`:
én reaksjon → riktig navn; `message_reaction`, org-scopet: to
brukere reagerer med samme emoji → begge navn i riktig rekkefølge,
org-spesifikt spillernavn brukt, ikke kontoens globale navn).
`tsc --noEmit`/`py_compile` rent. Ekte nettleser: sammendragslinjen
og den utvidbare per-emoji-listen begge bekreftet fungerende, ingen
konsollfeil. Scratch ryddet opp fullstendig.
Ren tillegg til eksisterende `ReactionSummary`-kontrakt (ingen
migrasjon, ingen brytende endring) + `post-engagement.tsx`.
**Rullet ut live 2026-08-07**, bruker bekreftet eksplisitt ("Ja
takk"): `docker compose up -d --build teecup_api teecup_frontend`.
Begge containere boot-et rent, `/health` → 200. `teeoff.no`
upåvirket.
36. **"Ingen"/"Alle"-hurtigknapp for venne-kategori-velgeren i
rundedeling — 2026-08-07.** Brukeren: "Der man, i et rundeoppsett,
velger hvem man skal dele runden med, så bør det være en 'bryter'
med 'Ingen (helt privat)' eller 'Alle'. Deretter velger eller
fravelger man etter det som passer en best." Avklart eksplisitt med
bruker (AskUserQuestion) hvilket av to funn passet: Privat/Venner/
Offentlig-hovedvalget forblir uendret (3-veis, uendret semantikk);
hurtigknappen legges TIL kategori-pillisten som vises når "Venner"
er valgt.
Fant samtidig et reelt, eksisterende avvik mellom de to stedene
denne velgeren finnes: opprettelses-veiviseren (`new-round.tsx`)
forhåndsvelger alle 10 kategoriene med opt-out-ordlyd, mens
rediger-dialogen (`round-detail.tsx`) verken har noen hurtigknapp
eller noe "ingen valgt"-varsel i det hele tatt (kun veiviseren hadde
det varselet fra før). Begge fikk nå identisk `Pill`-/knapp-basert
"Ingen"/"Alle" (setter kategorisettet til tomt/alle ti, finjusteres
deretter med enkeltpiller som før) OG samme "ingen kategori valgt"-
varsel -- lukker det avviket som en naturlig del av samme endring,
ikke en egen runde.
Ren frontend, ingen migrasjon, ingen ny backend-logikk (API-et tok
allerede imot tomme/fulle `visible_categories`-lister uendret).
`tsc --noEmit` rent. Scratch-verifisert i ekte nettleser for
rediger-dialogen (isolert DB-mal, egen API-/frontend-/Caddy-
container): "Ingen" tømmer alle ti pillene og viser varselet,
"Alle" fyller alle ti, begge fremhever seg selv korrekt når
tilstanden allerede matcher. Veiviserens versjon (identisk kode-
mønster, samme delte `Pill`-komponent) bekreftet via kodegjennomgang
i stedet for full klikk-gjennom (banesøk mot ekte teeoff-oppslag
gjorde en full E2E-kjøring uforholdsmessig tidkrevende for en
kodemessig identisk endring). Scratch ryddet opp fullstendig.
**Rullet ut live 2026-08-07**, bruker bekreftet ("ja"): `docker
compose up -d --build teecup_frontend`. Begge containere boot-et
rent, `/health` → 200. `teeoff.no` upåvirket.
37. **Slag-for-slag GPS-avstandsmåling — backend + skjema, 2026-08-07, se
ADR-048 for full begrunnelse/beslutningsdetalj.** Brukeren: "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."
Formell planleggingsrunde kjørt (Explore-agenter for
`round_hole`-skjema/scoreførings-UI/`BAG_CLUBS`/geolocation-bruk,
Plan-agent for konkret design), etterfulgt av AskUserQuestion for tre
gjenstående valg: v1 dekker BÅDE individuelle runder OG lagformater
fra start, delingstekst er auto-generert-men-redigerbar PLUSS et
satellitt-utsnitt av slaget, og måling er tilgjengelig både i
scoreførings-veiviseren og som en retroaktiv hull-kort-handling.
Migrasjon `060_round_shot.sql`: ny tabell, kjenner kun
`round_hole_id` (arver eierskap individuell/lagformat transitivt via
`round_hole` sin eksisterende XOR, migrasjon 031 -- ingen egen
XOR-logikk trengs). Eksplisitt, hull-tolerant `shot_number` (ikke
`ORDER BY captured_at`). Kun `UPDATE (shared_round_message_id)`
grantet -- ellers samme "slett og opprett på nytt"-prinsipp som
`round_message`.
Backend (`app/routers/rounds.py`): `ShotIn`/`ShotOut`/`ShotShareIn` +
seks endepunkter -- deltaker-GET/POST og side-GET/POST (speiler
`RoundHoleOut`/`RoundSideHoleOut` sitt eksisterende doble mønster for
å dekke lagformater), eierskap-agnostisk DELETE (join via
`round_hole` → `round_participant`/`round_side` for å bekrefte
tilhørighet til riktig runde), og `POST .../shots/{id}/share` som
genererer et satellitt-utsnitt SERVER-SIDE (Mapbox Static Images
API, ny `TEECUP_MAPBOX_SECRET_TOKEN`-setting i `config.py`, egen
polyline-encoder skrevet for topunkts-overlayet, URL-format
verifisert mot Mapbox sin offisielle dokumentasjon via WebFetch
fremfor antatt fra hukommelse) og laster opp via eksisterende
`storage.upload_image()` inn i en vanlig `round_message`-rad.
Rekkefølgen (slag opprettes ALLTID først, uten deling) unngår en
foreldreløs feed-melding ved en feilet slag-opprettelse. Mapbox-
kallet degraderer grasiøst til tekst-only ved manglende token/feil
(samme mønster som SMTP/push). `_resolve_round_message_author_name`
flyttet fra `round_messages.py` til `rounds.py` (unngår sirkulær
import, `rounds.py` var allerede den importerte parten) slik at
`share_shot` kunne gjenbruke navnefeltet uendret i stedet for en
duplisert kopi.
Frontend (kun det som IKKE avhenger av V0-eksporten ennå):
`frontend/lib/geo.ts` (ren Haversine, ingen avhengigheter). Det
eksisterende kølle-plukker-pill-mønsteret i `ScoringWizard`
(`round-detail.tsx`) trukket ut til en delt `ClubPicker`-komponent
(ren refaktor, `tsc --noEmit` rent før/etter). `mapbox-gl` lagt til
`package.json` (versjon slått opp direkte mot npm-registeret, ikke
gjettet) -- `@types/mapbox-gl` bevisst IKKE lagt til, `pnpm install`
varslet at den er en utdatert stub siden `mapbox-gl` nå leverer egne
typedefinisjoner. `pnpm-lock.yaml` regenerert (`pnpm install
--lockfile-only`) -- oppdaget under scratch-frontend-bygg at den
ELLERS ville vært ute av synk med `package.json`, noe som ville feilet
`pnpm install --frozen-lockfile` i BÅDE scratch- og ekte Docker-bygg
(fanget her, ikke først ved ekte utrulling). `NEXT_PUBLIC_MAPBOX_TOKEN`
tredd gjennom `frontend/Dockerfile` (KUN builder-steget, siden bruken
er ren klient-side -- speiler `TEECUP_API_ORIGIN`s build-tid-fallgruve
som allerede var dokumentert der) og `docker-compose.yml`. To tomme
plassholder-linjer lagt til i `.env` (`NEXT_PUBLIC_MAPBOX_TOKEN`,
`TEECUP_MAPBOX_SECRET_TOKEN`) -- MÅ fylles inn med reelle Mapbox-
kontoverdier før kart-/delings-bilde-flyten kan testes fullt ut eller
rulles ut live.
V0-prompt for selve `ShotMeasurementSheet` (alle steg + et bevisst
MOCK satellitt-kart-plassholder, ikke et ekte kartbibliotek -- ekte
Mapbox GL JS kobles på hånd-kodet etter eksport, for å unngå at V0
genererer en kart-komponent som remonterer per interaksjon og bryter
ADR-048 Beslutning B) skrevet og sendt til bruker. **Venter på
V0-eksport** før inngangspunktene (veiviser-knapp + hull-kort-merkelapp)
kan kobles til i `round-detail.tsx`.
**Scratch-verifisert grundig, kun backend** (isolert `teecup_scratch7`-
DB, alle 60 migrasjoner kjørt friskt, isolert `teecup_app_scratch7`-
rolle, isolert scratch-MinIO, engangs API-container). **Ett reelt
funn og umiddelbar retting UNDER selve scratch-oppsettet:**
migrasjon 002 hardkoder BÅDE rollenavn OG databasenavn i sin
`GRANT CONNECT ON DATABASE teecup_db`-linje -- rutinemessig
sed-omdøping av kun rollenavnet (`teecup_app` → `teecup_app_scratch7`)
før migrasjonene kjøres mot scratch-DB-en resulterte i at den nye
scratch-rollen fikk CONNECT-rettighet på den EKTE `teecup_db`
(kun en tilkoblings-rettighet, ingen tabell-/dataadgang fulgte med,
men uansett et utilsiktet avtrykk på ekte database uten bekreftelse).
Oppdaget og revokert umiddelbart (`REVOKE CONNECT ON DATABASE
teecup_db FROM teecup_app_scratch7`), verifisert at `teecup_db` sin
`datacl` var tilbake til nøyaktig samme tilstand som før. Ingen data i
`teecup_db` ble noensinne lest/skrevet. Notert her i tråd med
CLAUDE.md sin åpenhetsplikt -- vil unngå denne fallgruven ved å
ekskludere/håndtere migrasjon 002 sin databasenavn-linje separat neste
gang en fersk scratch-DB settes opp fra migrasjoner 001+.
Testmatrise kjørt mot ekte HTTP (magic-link-innlogging, ekte
sesjonscookie, `TEECUP_DEV_LOG_MAGIC_LINKS=true`): opprett slag
gps/gps og map_tap/gps (deltaker-eid hull), opprett slag på side-eid
hull (fourball-format), avstand >500m avvist (422), ugyldig lat
avvist (422), slett midt i sekvensen + bekreftet gap (neste slag fikk
nummer 3, ikke gjenbruk av 1), id-gjetting på tvers av runder avvist
(404), ikke-medlem avvist (403), linket medspiller kan måle for hele
flighten (bekreftet med en andre, faktisk innlogget testbruker),
slett/liste av ikke-eksisterende ressurser gir 404, delings-endepunkt
kjørt uten Mapbox-token satt (bekreftet grasiøs tekst-only-degradering,
`shared_image_url: null`, meldingen dukket opp korrekt i
`/rounds/{id}/messages` med riktig `author_display_name`-fallback).
`test_isolation.sql` 12/12 uendret. `py_compile` rent. Scratch-miljøet
(DB/rolle/API-/MinIO-container/image) ryddet opp fullstendig
etterpå, bekreftet tomt.
**Ikke testet i denne runden** (avhenger av ting bruker/V0 ennå ikke
har levert): selve satellitt-bilde-genereringen (krever en ekte
`TEECUP_MAPBOX_SECRET_TOKEN`), hele frontend-flyten (avhenger av
V0-eksporten), ekte nettleserverifisering av "null Mapbox-kall på
ren-GPS-veien"-kostnadskontroll-invarianten (ADR-048 Beslutning B) --
gjøres når V0-eksporten er integrert.
38. **Fast bunn-navigasjon manglet på fem av hovedsidene — 2026-08-07.**
Brukeren spurte, i forbindelse med dashbord-V0-forhåndsvisningen,
"hva skjedde med den sticky menyen i bunnen?" på andre sider enn
dashbordet. Undersøkt: `BottomTabBar` (eksportert fra
`dashboard.tsx`, migrasjon 2026-08-01) var kun faktisk importert i
`dashboard.tsx` selv og `more-menu.tsx` -- til tross for at BÅDE
`dashboard.tsx` sin egen kodekommentar ("vist på ALLE skjermer som
importerer denne") og CHANGELOG-oppføringen fra samme dag ("brukt av
flere sider") beskrev en bredere utrulling enn det som faktisk ble
koblet inn. Ingen ADR/CHANGELOG-oppføring dokumenterte en bevisst
innsnevring -- vurdert som en ufullstendig utrulling, ikke en
beslutning, og rettet direkte uten en egen avklaringsrunde (bekreftet
med bruker: fiks nå).
Lagt til i `own-rounds.tsx` (`active="rounds"` -- direkte fanemål),
`account-settings.tsx` (`active="profile"` -- direkte fanemål, Profil-
fanen peker allerede til `/account`), og `friends.tsx`/`feed.tsx`/
`notifications.tsx` (`active="more"` -- ingen av de tre er et direkte
fanemål, men alle tre er allerede listet som "Mer"-menyens egne
undersider i `more-menu.tsx`, så dette matcher appens eksisterende
IA i stedet for å oppfinne en ny). ``-bunnpolstring justert til
`pb-24` (samme verdi som dashbordet selv bruker for å ikke overlappe
den faste raden) i de fire filene som ikke allerede hadde nok
(`notifications.tsx` hadde fra før `pb-28`, urørt). Ren tillegging av
en allerede ferdig, gjenbrukt komponent -- ingen ny logikk.
**Reelt funn og rettet UNDER selve arbeidet, ikke i selve
bunn-nav-fiksen:** `frontend/pnpm-lock.yaml` hadde vært ute av synk
med `package.json` siden `mapbox-gl`/`@types/mapbox-gl` ble lagt til
(ADR-048, punkt 37 over) -- oppdaget først her fordi dette var første
gang siden den endringen at en full `pnpm install --frozen-lockfile`-
Docker-bygg faktisk ble kjørt. Ville ha feilet ETHVERT fremtidig
scratch- ELLER ekte frontend-bygg. Rettet: `@types/mapbox-gl` fjernet
helt (pnpm varslet at den er en utdatert stub -- `mapbox-gl` leverer
egne typer nå), `mapbox-gl` endret fra `^3.28.1` til eksakt `3.27.0`
(3.28.x var under ett døgn gammel og feilet pnpms egen
minimumReleaseAge-leverandørkjede-policy inni Docker-bygget -- en
fornuftig sikkerhetssperre mot ferskpubliserte pakker, ikke en feil
i verktøyet), lockfile
regenerert (`pnpm clean --lockfile && pnpm install`).
**Scratch-verifisert i ekte nettleser** (åttende scratch-miljø denne
økten: isolert DB fra migrasjoner 001-060 kjørt friskt, isolert
`teecup_app_scratch8`-rolle, isolert scratch-MinIO, scratch-API- og
-frontend-container -- frontend bygget med riktig scratch-
`TEECUP_API_ORIGIN`). Logget inn med ekte magic-link-flyt, fullførte
profil, besøkte alle fem rettede sider (`/my-rounds`, `/my-friends`,
`/account`, `/my-feed`, `/my-notifications`) pluss dashbordet --
bunn-raden vises korrekt nederst uten å overlappe innhold på noen av
dem, riktig fane fremhevet grønt på hver (Runder/Profil/Mer×3), ingen
konsollfeil på noen av sidene. Scratch-miljøet ryddet opp fullstendig
etterpå (denne runden traff samme fallgruve som punkt 37 med
migrasjon 002s hardkodede `teecup_db`-referanse -- rettet i den
kopierte migrasjonsfilen FØR kjøring denne gangen, ikke etterpå, og
eksplisitt bekreftet at ekte `teecup_db` sin `datacl` var uendret
etterpå).
**Rullet ut 2026-08-08** sammen med punkt 40 og 41 (`docker compose
build teecup_frontend` + `up -d` -- ingen migrasjon, kun ny
frontend-image). Bekreftet ingen konsollfeil på
`https://teecup.golf/` etter omstart.
39. **Dashbordet reskinnet til "clubhouse"-paletten — 2026-08-07, bruker
bekreftet eksplisitt "full overtagelse, også bakgrunnen".** Egen,
parallell V0-utforskning (ikke samme retning som "Forest Green" fra
2026-08-01/02) — sendt som en eksplisitt EKSPLORERENDE prompt ("samme
ånd som login-siden-utforskningen... ikke bedt om å matche eksisterende
stil"), forhåndsvist for bruker (screenshots av en `npm install`+
`next dev`-kjøring av selve V0-eksporten, mock-data) FØR noe ble
integrert i den ekte, datakoblede `dashboard.tsx`.
**Viktig funn før integrering:** paletten (`--tee`/`--tee-strong`/
`--cup`/`--cup-strong`/`--clubhouse-*`) og `TeeCupWordmark`-komponenten
fantes ALLEREDE i prosjektet — fra en tidligere, ennå ikke besluttet
`/logg-inn`-utforskning (egen isolert sammenligningsside, additive
CSS-tokens i `globals.css`, rørte ikke skya-tokens). V0 gjenbrukte dem
konsekvent for dashbord-eksporten fordi de allerede var etablert i
prosjektkonteksten, ikke fordi det gjenbrukte "gamle" farger — bekreftet
ved faktisk pikselsampling av skjermdumpen (`#2f6b1e`/`#ff5427`, begge
forskjellige fra "Forest Green" sin `#1f6b08`/ingen oransje i det hele
tatt). `/logg-inn`s egen etablerte presedens ble fulgt: Bricolage-
visningsfonten IKKE lagt til (samme utelatelse som der), kun paletten
og wordmarken.
**Omfangsgrenser, avklart eksplisitt med bruker underveis (ikke
antatt):** `RoundCard`/`TournamentCard` (delt med `/my-rounds`) IKKE
re-stylet — beholder sin eksisterende shadcn-token-baserte styling
(viste seg uansett visuelt kompatibelt, siden begge alt bruker et
grønt-for-primær/oransje-for-sekundær-mønster). `BottomTabBar` (delt
med de fem sidene fra punkt 38) IKKE re-stylet direkte av meg — bruker
ba eksplisitt om en egen V0-prompt for den ("lag et prompt til V0, og
la den bestemme utseendet") fremfor at jeg skulle avgjøre det selv,
siden komponenten må fungere rimelig godt mot BÅDE den nye og den gamle
paletten avhengig av hvilken side den vises på. Egen prompt skrevet og
sendt, venter på eksport.
**Faktisk re-stylet:** header (erstattet flagg-i-sirkel med ekte
`TeeCupWordmark`), hilsen, `InstallPrompt` (KUN dens egen selvstendige
JSX-retur, mørkegrønn gradient endret til en tee-strong-forankret
gradient — all ekte iOS/Android-deteksjons-/14-dagers-utsettelses-logikk
uendret, samme "logikk uendret, kun styling"-disiplin som Forest Green-
runden fulgte), `QuickAction` (fikk `primary`/`expanded`-varianter som
matcher V0-eksportens mønster — "Ny runde" solid, de to andre kort-
stil), delt seksjon-"chrome" (`SectionHeader`/`SeeAllLink`/`EmptyState`/
`ShortcutButton`), "Venner på banen", `Sparkline`/`StatTile`/
`StatsSection`, "Spilte baner", "Venner"-oppsummeringen, og
organisasjon-foten. `AVATAR_HUES` (venne-avatar-fargene) beholdt
uendret — allerede nøytrale nok til å fungere i begge paletter.
`tsc --noEmit` rent. **Scratch-verifisert i ekte nettleser** (niende
scratch-miljø denne økten, samme mønster som punkt 37/38 — inkludert
samme forsiktighet med migrasjon 002s databasenavn-linje, bekreftet
`teecup_db` uendret både før og etter): logget inn, fullført profil,
besøkt dashbordet i både tom tilstand og med ekte seed-data (pågående
runde, aktiv turnering som arrangør, statistikk, spilt bane) —
korrekt rendret i begge tilstander, `RoundCard`/`TournamentCard` sitter
visuelt fint på den nye bakgrunnen, "Ny turnering"-hurtighandlingen
utvidet korrekt og det eksisterende skjemaet (uendret) fungerte som før.
Ingen konsollfeil. Scratch-miljøet ryddet opp fullstendig.
**Rullet ut 2026-08-08** sammen med BottomNav (punkt 40) og
ny-runde-veiviseren (punkt 41), som planlagt -- se punkt 41 for
utrullingsdetaljer.
40. **BottomTabBar erstattet med `BottomNav` fra egen V0-prompt —
2026-08-08.** Bruker ba eksplisitt om at bunn-navigasjonens utseende
skulle avgjøres av V0 selv, ikke av meg (se punkt 39) -- prompt skrevet
med eksplisitt kontekst om at komponenten deles mellom clubhouse- og
Forest Green-sider og må fungere rimelig godt mot begge, uten å få
oppgitt en fasit-retning. V0 valgte en bevisst palett-nøytral løsning:
en "flytende hvit flate" (halvtransparent hvit, backdrop-blur, egen
skygge) som ikke arver noen sideb palett -- kun den grønne
`tee`/`tee-strong`-aksenten (felles for begge paletter) brukes for
aktiv-tilstand.
Ny fil `components/teecup/bottom-nav.tsx`, eksporterer `BottomNav`
(ikke lenger `BottomTabBar`). Strukturell endring fra forgjengeren:
aktiv fane leses nå fra `usePathname()` internt (ikke en `active`-prop
utenfra) -- hrefs rettet fra V0s plassholdere (`/hjem`, `/runder` osv.)
til de faktiske rutene under integrering. Fjernet den gamle
`BottomTabBar`-definisjonen (og dens nå-ubrukte `C`-fargekonstant og
fire lucide-ikon-importer) fra `dashboard.tsx`; alle seks sider
(`dashboard.tsx`, `own-rounds.tsx`, `friends.tsx`, `account-
settings.tsx`, `feed.tsx`, `notifications.tsx`, `more-menu.tsx`)
importerer nå `BottomNav` fra den nye filen i stedet, uten `active`-
prop.
**To reelle bugs funnet og rettet UNDER scratch-verifisering (ikke i
selve V0-eksporten -- begge i MIN EGEN integreringskode):**
1. Forsøkte først å "forbedre" aktiv-fane-sammenligningen ved å
strippe bort et evt. `#hash` før sammenligning (siden "Turneringer"
sin href er `/dashboard#kommende-turneringer`). Dette var FEIL --
browser-verifisert til å få BÅDE "Hjem" og "Turneringer" til å vise
som aktive samtidig på `/dashboard`, siden begge da strippet til
samme sti. Reversert til V0s opprinnelige, rå strengsammenligning
(som aldri hadde dette problemet -- "Turneringer" matcher rett og
slett aldri via ren pathname, akkurat som forgjengeren uansett aldri
fremhevet den).
2. `/my-friends`, `/my-feed` og `/my-notifications` viste INGEN fane
som aktiv i det hele tatt (ren pathname-matching kjenner dem ikke
igjen som noen fanes mål) -- forgjengeren løste dette implisitt ved
at hver side selv sendte inn riktig `active`-prop. Lagt til en ny,
liten `matchPaths`-mekanisme på `Tab`-typen; "Mer" sin oppføring
fikk `matchPaths: ["/my-friends", "/my-feed", "/my-notifications"]`
-- speiler nøyaktig hvilke tre sider `more-menu.tsx` selv allerede
lister som sine undersider, ingen ny gruppering oppfunnet.
`tsc --noEmit` rent. **Scratch-verifisert i ekte nettleser** (tiende
scratch-miljø denne økten, samme migrasjon-002-forsiktighet som
punkt 37-39, `teecup_db` bekreftet uendret): logget inn, besøkt alle
syv sider som bruker `BottomNav` (dashbord, my-rounds, my-friends,
account, my-feed, my-notifications, more) -- riktig (og ETT AV GANGEN,
etter fiks 1) fane fremhevet på hver, ingen konsollfeil. Scratch-miljøet
ryddet opp fullstendig.
**Rullet ut 2026-08-08** sammen med punkt 39 og 41 -- se punkt 41 for
utrullingsdetaljer.
41. **`/my-rounds/new` erstattet med ny-runde-veiviser fra egen V0-prompt
— 2026-08-08.** Samme mønster som punkt 40: prompt skrevet med full
teknisk spesifikasjon (alle 18 spilleformer, alle steg/felt/
valideringsregler, alle 12 backend-endepunkt, `submitWizard()`-
kontrakten) og eksplisitt UTEN visuell føring -- V0 fikk velge
utseendet fritt. Resultat: en ny, selvstendig palett (`--nr-*` i
`globals.css`, kald blå/grå, `--nr-accent: #1f5fd6`), additiv og
scoped til veiviseren alene, samme "co-eksisterende paletter via
Tailwind arbitrary-value-syntaks"-mønster som clubhouse-paletten
(punkt 39) -- ingen `@theme inline`-kobling, ingen påvirkning på
resten av appen.
Gammel `components/new-round.tsx` (3364 linjer) SLETTET -- bekreftet
via grep at kun `app/my-rounds/new/page.tsx` importerte den (de tre
andre grep-treffene, i `course-template-editor.tsx`/
`round-leaderboard.tsx`/`round-detail.tsx`, var bare beskrivende
norske kommentarer, urørt). Ny modul under
`components/ny-runde/` (`wizard-shell.tsx`, `wizard-context.tsx`,
fem steg-filer, `primitives.tsx`, `progress.tsx`, `footer-bar.tsx`,
`create-course-form.tsx`) + `lib/ny-runde/` (`types.ts`,
`formats.ts`, `api.ts`).
V0s eksport brukte mock-data gjennomgående -- hele portingsjobben
var å koble hvert steg til de faktiske backend-kontraktene fra
`app/routers/rounds.py` og den gamle `new-round.tsx`, IKKE bare et
overflatisk bytte av komponentnavn: `Gender`-oversettelse
(`"m"|"f"` API ↔ `"mann"|"kvinne"|"annet"` UI), `Tee`-formen
forenklet til kun `{id, name, genders}` (aldri CR/Slope til klienten),
`CourseMeta`-discriminated-union for teeoff- vs. egen-bane, ekte
`POST /rounds` → `POST .../sides` → `POST .../participants` →
`PATCH .../participants/{id}`-sekvens i `submit()`, ekte
kontosøk/gjeste-oppslag i steg 3, ekte banesøk (teeoff-fasilitet +
egne baner, inkl. geolokasjon for "i nærheten") i steg 1.
**Fire reelle bugs funnet og rettet under scratch-verifisering:**
1. `TextInput` i `primitives.tsx` manglet `forwardRef` (feil i selve
V0-eksporten, ikke i portingen) -- ga 3 TS-feil der nedstrøms kode
sendte `ref` til komponenten. Rettet ved å pakke inn med
`forwardRef`, `tsc --noEmit` gikk fra 3 feil til rent.
2. Steg 3 viste "Utslag: Ikke valgt" for eieren selv etter at tee var
valgt i steg 1 -- min egen portingsfeil, jeg hadde ikke tatt med
den ekte `chooseCourse()`s side-effekt som synker valgt tee inn i
spillerlisten. Rettet ved å legge `players:
state.players.map(...)` til i `pickCourse()`
(`step1-course-time.tsx`). Verifisert rettet ved reload.
3. Manglende "Ingen"/"Alle"-hurtigknapp og "ingen kategori
valgt"-advarsel i steg 5s kategorivelger (samme mangel som ble
oppdaget og rettet i selve appen 2026-08-06, se punkt 36 -- V0s
eksport hadde ikke fått denne konteksten). Lagt til i
`step5-sharing.tsx`, samme mønster som punkt 36 (`role="group"`,
`aria-pressed`, `role="alert"` ved null valgt). Browser-verifisert
i scratch: "Ingen" tømmer alle 10 avkrysninger og viser advarselen,
"Alle" gjenoppretter alle.
4. Fulgte først en blindvei: trodde "Legg til gjest"-knappen i steg 3
var usynlig/klikk-slukende bak den klissede (`sticky bottom-0`)
`FooterBar`-en (samme feilklasse som `pb-32`-saken 2026-08-02,
nevnt i den gamle `new-round.tsx`s egne kommentarer) -- la til
`pb-28` på ``. Padding-fiksen var faktisk RIKTIG og
nødvendig (uten den er det for lite scroll-klaring til å noensinne
få knappen helt fri av footeren), men den første reproduksjonen av
"feilen" var selv et testverktøy-artefakt: et koordinat-basert
klikk uten forutgående scroll traff footeren fordi den, ved
`scrollY: 0`, visuelt ligger over knappen (klissete element som
ikke har "festet seg" ennå fordi normal dokumentflyt allerede
plasserer det nederst i viewport). Bekreftet ved å måle
`getBoundingClientRect()` for begge før/etter scroll: uten scroll
overlapper de nesten fullstendig (y:776 vs. top:775); etter
`scrollIntoView` (174px, alt tilgjengelig scroll) står knappen på
y:602, godt klar. Et ekte, koordinat-basert klikk etter scroll
fungerte perfekt. Konklusjon: `pb-28`-fiksen var korrekt og er
beholdt (den gir nødvendig klaring når brukeren scroller helt ned),
ingen ytterligere kodefeil forelå.
`tsc --noEmit` rent. **Scratch-verifisert i ekte nettleser** (ellevte
scratch-miljø denne økten, samme migrasjon-002-forsiktighet som
punkt 37-40, `teecup_db`s ACL bekreftet uendret før og etter): full
veiviser-gjennomkjøring for Fourball -- egen bane opprettet og valgt,
tee/HCP riktig synket til eier, gjest uten konto lagt til, begge
spillere tildelt hver sin side i steg 4, "Ingen"/"Alle"-toggle testet
i steg 5, fullført innsending. **DB-verifisert direkte**: `round`-rad
med riktig `play_format`/`visibility_mode`/bane-/tee-snapshot,
to `round_side`-rader, begge `round_participant`-rader korrekt
knyttet til hver sin `round_side_id` med riktig
HCP-/tee-/stat_level-snapshot. Ingen konsollfeil. Scratch-miljøet
(containere, images, DB, rolle) ryddet opp fullstendig etterpå.
**Rullet ut 2026-08-08** sammen med punkt 39 og 40, etter eksplisitt
brukerbekreftelse ("Rull ut alle tre samlet"): `docker compose build
teecup_frontend && docker compose up -d teecup_frontend` (kun
frontend-image, ingen migrasjon). Bekreftet `teecup_api`s image
UENDRET (bygget 2026-08-07, ikke i dag) -- containeren ble kun
re-opprettet fra samme eksisterende image pga. avhengighetskjeden i
`docker-compose.yml`, ikke bygget på nytt, så de uncommittede
backend-endringene som lå i arbeidstreet (`rounds.py`,
`handicap_engine.py` m.fl., urelatert til denne økten) ble IKKE
utilsiktet rullet ut. `https://teecup.golf/` bekreftet oppe, ingen
konsollfeil på innloggingssiden. Autentisert gjennomgang av
dashbord/BottomNav/ny-runde-veiviseren i produksjon overlatt til
brukeren selv (krever ekte pålogging).
42. **`/logg-inn` gjort til den EKTE innloggingssiden (erstatter `/` sin
gamle `LoginForm`) — 2026-08-08.** Brukeren la merke til at
innloggingssiden fortsatt viste "det gamle grensesnittet" etter
punkt 39-41s utrulling -- undersøkelse avdekket at `/logg-inn`
(clubhouse-palett, `TeeCupAuth`) var en fullstendig FORELDRELØS V0-
utforskning: INGENTING i appen lenket eller redirectet dit (bekreftet
ved grep), alle ~11 uautentisert-steder pekte fortsatt til `/` (gammel
`LoginForm`, Forest Green). Se dashboard.tsx sin egen kommentar
(linje 26-34) som allerede erkjente dette -- "fra en tidligere, ennå
ikke integrert /logg-inn-utforskning".
**Kritisk oppdagelse FØR arbeidet startet:** `TeeCupAuth` var 100 %
mock -- `apiSendMagicLink`/`apiPasswordLogin`/`apiJoinByCode` var alle
`sleep()`-baserte stubber (hardkodet demo-passord `"teecup123"`,
hardkodede demo-koder `TEECUP`/`RYDER-25`/`HOST2026`, en falsk
"2FA-kode sendes"-tekst som ALDRI faktisk sendte noe). Dette ble
eksplisitt flagget til brukeren (AskUserQuestion) FØR noe ble bygget,
siden dette er sikkerhetskritisk kode og omfanget var langt større enn
en ren redirect-ombytting -- brukeren bekreftet "gjør full port nå".
**Full port utført:**
- `components/teecup/teecup-auth.tsx` skrevet om fra bunnen: ekte
`POST /auth/request-link` (magic link), ekte
`POST /auth/login-password` (med `LoginResult`-status-håndtering
identisk med den gamle `LoginForm`), ekte
`GET /public/tournaments/by-code/{code}`. Alle demo-hint/mock-tekster
fjernet. `TwoFactorVerifyForm`/`TwoFactorSetupForm`
(`components/two-factor-flow.tsx`) gjenbrukt UENDRET -- samme
"delt komponent ikke re-stylet"-mønster som `RoundCard`/
`TournamentCard` i dashbord-reskinnet (punkt 39) -- lavest mulig
risiko for sikkerhetskritisk 2FA-kode.
- `app/logg-inn/page.tsx`: fikk samme server-side allerede-innlogget-
sjekk (`cookies()` + `/auth/me`) som `/`-siden hadde.
- `app/page.tsx` (root): redusert til en tynn videresending
(autentisert → `/dashboard`/`/account`, uautentisert → `/logg-inn`)
-- beholdt for gamle bokmerker/lenker til `teecup.golf/`. Gamle
`LoginForm`/`components/login-form.tsx` SLETTET (bekreftet ingen
andre importer via grep).
- Alle 11 `router.replace("/")`-steder (401-håndtering + logout) i
`dashboard.tsx`, `friends.tsx`, `more-menu.tsx`,
`account-settings.tsx`, `notifications.tsx`, `course-rounds.tsx`,
`own-rounds.tsx`, `rounds-stats-summary.tsx` byttet til
`router.replace("/logg-inn")`. `verify-form.tsx` sin
"Be om en ny lenke"-fallback-lenke (`href="/"`) byttet til
`/logg-inn`.
**Én reell bug funnet og rettet under scratch-verifisering:**
`app/logg-inn/page.tsx` kalte `redirect()` INNI `try`-blokken som
henter `/auth/me` -- Next.js sin `redirect()` fungerer ved å kaste en
egen `NEXT_REDIRECT`-kontrollflyt-exception som MÅ boble videre
urørt til Next.js sin render-maskineri. Den tomme `catch {}`-en rundt
slukte denne stille, så en allerede innlogget bruker som besøkte
`/logg-inn` fikk se innloggingsskjemaet på nytt i stedet for å bli
sendt videre -- oppdaget fordi en ekte innlogget test-bruker IKKE ble
omdirigert i scratch, mens en direkte nettleser-`fetch("/auth/me")`
(som går via `next.config.mjs` sin rewrite, ikke gjennom denne
server-komponentens egen kode) bekreftet sesjonen var helt gyldig --
avslørte at feilen satt i AKKURAT denne serverkomponentens egen
try/catch-struktur. Den opprinnelige `/`-sidens ekvivalente kode
unngikk dette ved å KUN sette `authenticated`/`profileComplete`-
variabler inni try/catch og kalle `redirect()` etterpå, UTENFOR
blokken -- samme mønster gjeninnført her. `tsc --noEmit` rent etter
fiks.
**Scratch-verifisert i ekte nettleser** (tolvte scratch-miljø denne
økten, samme migrasjon-002-forsiktighet som punkt 37-41 --
`teecup_db`s ACL bekreftet uendret før og etter; `TEECUP_DEV_LOG_MAGIC_LINKS=true`
brukt for å hente ekte magic-link-tokens fra API-loggen i stedet for å
sende ekte e-post til testadresser): full runde -- magic-link-
forespørsel + `/verify?token=`-innlogging (ny bruker auto-opprettet,
korrekt sendt til `/account` pga. ufullstendig profil), passord-
innlogging med feil passord (ekte feilmelding "E-post eller passord
er feil." fra `/auth/login-password`), invitasjonskode med ugyldig
kode (ekte feilmelding fra `/public/tournaments/by-code/`), logout
(→ `/logg-inn`), uautentisert besøk til `/dashboard` (→ `/logg-inn`,
ikke lenger `/`), `/` og `/logg-inn` besøkt allerede innlogget med
ufullstendig profil (→ `/account`) og med fullført profil
(→ `/dashboard`, satt via direkte `PATCH /auth/profile`-kall for å
unngå å klikke gjennom fødselsdato-datepickeren manuelt). Ingen
konsollfeil. **Ikke click-through-testet:** `2fa_required`-grenen
(krever en konto med 2FA allerede aktivert) -- vurdert lav risiko
siden `TwoFactorVerifyForm`/`TwoFactorSetupForm` er gjenbrukt helt
uendret fra den allerede beviste `LoginForm`-implementasjonen, kun
kablingen frem til dem (`handleLoginResult`) er ny kode, og den er
identisk portert fra `LoginForm`. Scratch-miljøet (containere, images,
DB, rolle) ryddet opp fullstendig etterpå, inkl. en disk-full-hendelse
underveis (`docker builder prune` frigjorde 11,75 GB build-cache fra
denne øktens mange scratch-bygg -- ingen kjørende containere eller
volumer berørt).
**Rullet ut 2026-08-08**, etter egen, eksplisitt brukerbekreftelse
separat fra punkt 39-41s batch-bekreftelse (sikkerhetskritisk --
autentisering). `docker compose build teecup_frontend && up -d` --
denne gangen ble kun `teecup_frontend` gjenskapt (`teecup_api` forble
"Running" med bekreftet uendret image-tidsstempel før/etter, ulikt
forrige utrulling i punkt 39-41 der en avhengighets-kjede-bivirkning
gjenskapte -- men ikke bygde om -- `teecup_api`). `https://teecup.golf/`
bekreftet å redirecte til `/logg-inn` med clubhouse-designet, ingen
konsollfeil.
43. **PRODUKSJONSBUG: "Laster baner…" hang for alltid ved offisiell
banevalg i ny-runde-veiviseren — funnet og fikset 2026-08-08.**
Brukeren rapporterte (skjermbilde) at steget etter å ha valgt et
teeoff-anlegg ("Baner") aldri kom videre fra "Laster baner…". Dette
hadde vært live siden punkt 41s utrulling -- treffer ALLE nye runder
som starter fra en offisiell (teeoff-) bane, ikke egne baner.
**Rotårsak:** `fetchFacilityCourses()` i `lib/ny-runde/api.ts` antok
at `GET /rounds/official-search/{slug}` returnerer banene direkte som
en array (`data.map(...)`), men det ekte endepunktet returnerer et
OBJEKT -- `OfficialFacilityDetail = {slug, name, courses: [...]}`
(`app/routers/rounds.py` linje 530). `data.map` er ikke en funksjon
på et objekt, så kallet kastet en `TypeError` -- og siden
`OfficialCourses` sin `useEffect` i `step1-course-time.tsx` kun hadde
`.then(...)` uten `.catch(...)`, ble denne exceptionen en stille,
ufanget promise-rejection: `setCourses` ble aldri kalt, og
`courses === null`-grenen ("Laster baner…") viste seg for alltid.
Bug i min egen porting (punkt 41) -- **denne konkrete stien
(offisiell teeoff-bane) ble aldri scratch-testet den runden**, kun
"egen bane"-veien ble klikket gjennom (se punkt 41s test-notat).
**To rettelser:**
1. `fetchFacilityCourses()`: leser nå `data.courses.map(...)` i
stedet for `data.map(...)`.
2. `OfficialCourses` (`step1-course-time.tsx`): la til en reell
`.catch()` + en `loadError`-tilstand (`role="alert"`, samme
`--nr-danger`-stil som resten av veiviseren) -- uten denne ville
ENHVER fremtidig feil her (f.eks. teeoff nede, 502) gitt nøyaktig
samme uendelige "Laster baner…"-hang på nytt, uansett om
datakontrakt-bugen over er rettet. Dette er defensivt, ikke bare
en fiks for det spesifikke tilfellet.
`tsc --noEmit` rent. **Scratch-verifisert i ekte nettleser** (trettende
scratch-miljø denne økten, samme migrasjon-002-forsiktighet, `teecup_db`
bekreftet uendret) MOT EKTE teeoff-data (samme `teeoff_db`/`teeoff_api`
som produksjon deler, nådd direkte på `teeoff_default`-nettverket):
søkte opp "Hvaler Golfklubb" (reell fasilitet), banen ("Hovedbanen, 2
utslag") lastet korrekt i stedet for å henge, gikk videre til
felt-steget og bekreftet ekte utslag (Gul/Rød) hentet riktig fra
teeoff. Ingen konsollfeil (kun én forhåndseksisterende, ikke-relatert
"Deprecated feature"-info-melding fra nettleseren selv). Scratch-miljøet
ryddet opp fullstendig.
**Rullet ut umiddelbart** (produksjonsbug som traff alle live
brukere som prøvde å starte en runde på en offisiell bane) --
`docker compose build teecup_frontend && up -d`, kun frontend-image,
`teecup_api`s image-tidsstempel bekreftet uendret før/etter. Kunne
ikke click-through-verifisere selve produksjonssiden med ekte
brukerkonto (krever brukerens egen pålogging) -- basert på grundig
scratch-verifisering mot ekte teeoff-data rett før utrulling.
44. **PRODUKSJONSBUG: rundedeling med venner i flere kategorier virket
ikke som forventet — funnet og fikset 2026-08-08, se ADR-036
Beslutning E.** Brukeren rapporterte at en runde delt med kategorien
"Make" ikke ble synlig for vedkommendes ektefelle, til tross for at
"Make" var huket av. Diagnostisert direkte mot ekte `teecup_db`
(skrivebeskyttede spørringer): eieren (Erol) hadde kategorisert
ektefellen (Gina) under TRE kategorier (`close_family`,
`extended_family`, `spouse`), mens runden kun delte
(`close_family`, `golf_friends`, `spouse`) -- `extended_family`
manglet. `_can_view_round` krevde den gang at ALLE en venns
kategorier måtte være i rundens synlige sett (presisert av bruker
2026-07-29, kun dokumentert i kode-kommentarer, ALDRI i
ARCHITECTURE_DECISIONS.md -- selve dokumentasjonshullet som gjorde
dette vanskelig å spore tilbake), ikke bare én relevant kategori.
Brukeren fikk vist mekanismen konkret (hvilke kategorier Gina var
tagget i, hvilke runden delte, den strenge AND-regelen) og valgte via
et eksplisitt spørsmål å reversere til "minst én kategori er nok"
(som var den OPPRINNELIGE ADR-036 Beslutning B-regelen fra
2026-07-25 -- 2026-07-29-presiseringen hadde altså strammet inn en
regel utover det som noensinne ble ordentlig dokumentert som en
bevisst arkitekturbeslutning).
**Rettet i alle fire duplikate SQL-steder** (samme
kopier-inn-i-SQL-mønster som allerede omtalt i ADR-036 Beslutning B):
`app/routers/rounds.py` sin `_can_view_round` (selve
tilgangssjekken), `_friends_who_can_see_round` (varsel-fan-out ved
rundeopprettelse/fullføring), `list_friends_on_course`
(dashbordets "Venner på banen"); `app/routers/round_messages.py` sin
`/feed`-listing. Alle gikk fra "`NOT EXISTS` en kategori UTENFOR
synlig sett" til et enklere "`EXISTS` en kategori INNENFOR synlig
sett" -- enklere spørringer, ikke bare annen semantikk.
**Scratch-verifisert i ekte API-kall** (fjortende scratch-miljø denne
økten, samme migrasjon-002-forsiktighet, `teecup_db` bekreftet
uendret): gjenskapte den ekte Erol/Gina-situasjonen (venn kategorisert
i to kategorier, runde deler kun én av dem) -- vennen fikk nå 200 på
`GET /public/rounds/{id}` (tidligere 403). Bekreftet at motsatt
tilfelle (venn med INGEN overlappende kategori) fortsatt korrekt gir
403 -- ingen utilsiktet åpning av tilgangskontrollen. `/friends/
on-course` og `/feed` (de to andre endrede spørringene) begge 200
uten SQL-feil. Scratch-miljøet ryddet opp fullstendig.
**Rullet ut umiddelbart** (produksjonsbug, brukerens egen delte runde
var konkret berørt) -- `docker compose build teecup_api && up -d`,
KUN backend-image (ingen migrasjon, `visibility_mode`/
`round_visible_category` er uendret skjema), `teecup_frontend`s
image-tidsstempel bekreftet uendret før/etter. **Bekreftet direkte
mot ekte data etter utrulling**: samme spørring som
`_can_view_round` nå bruker, kjørt skrivebeskyttet mot den ekte
Erol/Gina/runde-situasjonen, returnerer nå `true`.
45. **Slag-for-slag GPS-avstandsmåling — inngangspunkt 1 av 2, i
scoring-veiviseren — 2026-08-08 (ADR-048).** Backend (migrasjon
`060_round_shot.sql`, `ShotIn`/`ShotOut`/`ShotShareIn`-endepunktene i
`rounds.py`, `lib/geo.ts` Haversine, Mapbox-token-plumbing) var
allerede bygget og scratch-verifisert fra tidligere i økten (se
ADR-048 i ARCHITECTURE_DECISIONS.md). Denne runden: selve
frontend-flaten.
V0-prompt skrevet med `DESIGN_SYSTEM.md`s tokens som en HARD
begrensning (i motsetning til dashbord-/veiviser-/login-promptene
tidligere i økten, som bevisst fikk null designføring) -- dette
arket lever INNI den eksisterende Forest Green-skjermen
(`round-detail.tsx`), ikke som en ny frittstående side. Eksporten
(zip 4) var meget tro mot spesifikasjonen: `next/dynamic({ssr:false})`
for kartsteget, ingen `mapbox-gl`-import i det hele tatt på
GPS-only-stien, kartet mountes kun én gang per arkåpning
(tom-deps `useEffect`, klikk/drag re-initialiserer aldri).
`components/shot/shot-measurement-sheet.tsx` +
`components/shot/map-point-picker.tsx` kopiert inn uendret (kun
én import-sti rettet: V0s egen plassholder-`ClubPicker` byttet til
den nå faktisk utrukne, delte `components/teecup/club-picker.tsx`
-- ren utrekking fra `round-detail.tsx`s tidligere lokale kopi,
ingen atferdsendring for veiviserens eksisterende bruk). Ny
`components/ui/textarea.tsx`-shadcn-primitiv lagt til (fantes ikke
fra før).
Koblet inn som `ScoringWizard`s nye `roundId`-prop + lokal
`shotSheetOpen`/`shotCount`-state: "Mål et slag"-knapp rett etter
kølle-plukkeren i detalj-steget, henter eksisterende slag-antall on
mount (`GET .../holes/{n}/shots`), sender til ekte
`POST .../holes/{n}/shots` ved innsending og (hvis deling valgt)
en påfølgende `POST /rounds/{id}/shots/{shot_id}/share`.
**Scratch-verifisert** (femtende scratch-miljø denne økten, samme
migrasjon-002-forsiktighet, `teecup_db` bekreftet uendret): full
klikk-gjennomgang fra "Mål et slag" til arket åpner riktig, korrekt
feilmelding+"Prøv igjen" når GPS-tillatelse mangler (bekreftet ekte
-- CDP-automatiserte nettlesersesjoner har `geolocation`-tillatelse
permanent `denied` uten noen dialog å akseptere, en verktøy-
begrensning i selve test-miljøet, ikke i appen). Siden selve
GPS-suksess-stien derfor ikke lot seg klikke gjennom i denne
økten, ble de eksakte kallene `submitShot()` sender (opprett slag,
list slag, del) i stedet verifisert direkte mot API-et med samme
data-kontrakt -- alle tre 200/201, ingen feil i API-loggen. Bekreftet
i ekte nettleser at slag-tellingen faktisk oppdateres og vises i
knappeteksten ("Mål et slag (1 målt)") etter et slag er opprettet.
Scratch-miljøet ryddet opp fullstendig.
**Rullet ut 2026-08-08** -- se punkt 48 for migrasjons-/
utrullingsdetalj (samlet med inngangspunkt 2 og gjest-tee-fiksen).
46. **Slag-for-slag GPS-avstandsmåling — inngangspunkt 2 av 2, alltid-
synlig merkelapp på hull-kortet — 2026-08-08 (ADR-048).** Fullfører
v1-kravet om å dekke BEGGE hull-eiertyper fra start (punkt 45 dekket
kun deltaker-eide hull via `ScoringWizard`).
Refaktorerte punkt 45s inline logikk (state + fetch + submit, som satt
direkte i `ScoringWizard`) ut til en ny, delt lokal funksjon
`ShotMeasurementEntry` i `round-detail.tsx` -- samme
"lokal gjenbruk innad i filen, ikke en egen delt-fil"-mønster som
resten av filens komponenter (`NumberPicker`/`Stepper`/`ChoiceRow`
m.fl., se `DESIGN_SYSTEM.md`s "Komponentmønstre"-seksjon), siden alle
tre bruksstedene nå ligger i samme fil. Komponenten bygger riktig
URL-base (`.../participants/{id}/holes/{n}/shots` vs.
`.../sides/{id}/holes/{n}/shots`) fra en enkel `owner: {kind, id}`-
prop -- ingen egen gren utover selve URL-en trengs, ADR-048s speilede
endepunkt-par dekker begge hull-eiertyper identisk.
Koblet inn tre steder:
- `ScoringWizard`s detalj-steg (uendret fra punkt 45, nå bare kalt
via den delte komponenten i stedet for egen kopi).
- `PlayerHoleCards` (deltaker-eide hull, hoved-"Score"-fanens
spillerkort): en liten merkelapp-rad lagt til som SØSKEN av kortets
store klikkbare knapp (ikke nøstet inni -- ugyldig HTML/ARIA å neste
`` i ``), synlig uavhengig av om veiviseren er åpen,
for retroaktiv måling.
- `SideScorecardGrid` (side-eide hull, delt-ball-formater): siden selve
scorekort-gridet er en tett 52px-per-hull-tabell uten plass til en
egen knapp per rute, ble to merkelapper (én per side, med lag-
etikett, f.eks. "Rødt lag: 2 slag målt") lagt til som en egen rad
rett under gridet, ved siden av "Forrige/Neste hull"-knappene.
**Scratch-verifisert i ekte nettleser** (sekstende scratch-miljø
denne økten, samme migrasjon-002-forsiktighet, `teecup_db` bekreftet
uendret) -- **med et ekte Mapbox-token satt inn i `.env` av brukeren
midt i denne runden** (`NEXT_PUBLIC_MAPBOX_TOKEN`, offentlig/URL-
restriktert; `TEECUP_MAPBOX_SECRET_TOKEN` for delings-satellittbilder
fortsatt ikke mottatt): opprettet én vanlig slagspill-runde (deltaker-
eid) og én foursome-runde med to sider (side-eid, via egen-opprettede
sider + en gjest lagt til side B) direkte mot API-et. Bekreftet
merkelappen vises korrekt og uavhengig av kortknappen i
`PlayerHoleCards`, åpner arket direkte (ikke via veiviseren). Testet
"Velg punkt på kart"-veien med det ekte tokenet -- Mapbox avviste med
403 (URL-restriksjonen tillater kun det ekte domenet, ikke dette
scratch-miljøets `localhost`-opprinnelse), som bekreftet at
feilhåndteringen i `MapPointPicker` fungerer rent (ren "Kunne ikke
laste kartet"-melding, ingen krasj) -- selve kart-SUKSESS-stien kunne
ikke click-through-testes i dette miljøet av samme grunn. For
side-eide hull: bekreftet begge merkelapper ("Blått lag"/"Rødt lag")
henter riktig antall fra sine respektive `.../sides/{id}/holes/{n}/
shots`-endepunkt (bekreftet i API-loggen), og at et slag opprettet på
én side kun oppdaterer DEN sidens merkelapp, ikke den andres. Samme
GPS-tillatelse-miljøbegrensning som punkt 45 gjaldt fortsatt for
GPS-suksess-stien; de eksakte `submitShot`-kallene ble derfor igjen
verifisert direkte mot API-et for begge eiertyper. Scratch-miljøet
ryddet opp fullstendig.
**Rullet ut 2026-08-08** -- se punkt 48.
47. **Ny-runde-veiviser: utslag lagt til direkte i gjesteskjemaet —
2026-08-08, brukertilbakemelding.** Brukeren rapporterte at
utslag-valget for en nyopprettet gjest ("midlertidig spiller") ikke
var intuitivt -- måtte inn i det ferdigopprettede gjestekortet
("Rediger") for i det hele tatt å SE hvilket utslag som var valgt.
Viste seg at `addGuest()` allerede valgte utslag automatisk
(`compatibleTees(course, gender)[0]?.id`), men helt stille -- selve
gjesteskjemaet (`GuestForm` i `step3-players.tsx`) hadde aldri et
synlig Utslag-felt, kun Fornavn/Etternavn/E-post/Kjønn/HCP/
Statistikk. Lagt til et kontrollert `teeId`-felt direkte i skjemaet,
samme `NativeSelect`-mønster som `PlayerCard`s tilsvarende felt,
med samme kjønn→utslag-resynk-logikk (`stillOk`-sjekk) som
`PlayerCard` allerede hadde -- inkludert i "Fyll inn automatisk"-
knappen for kjente gjester (samme feilklasse, ikke tidligere
rettet der).
`tsc --noEmit` rent. **Scratch-verifisert i ekte nettleser**
(syttende scratch-miljø denne økten, samme migrasjon-002-
forsiktighet, `teecup_db` bekreftet uendret): opprettet en egen bane
med kjønns-eksklusive utslag (Gul kun menn, Rød kun kvinner) for å
bevise resynk-logikken fungerer -- byttet kjønn til Kvinne i
gjesteskjemaet, utslag-feltet oppdaterte seg umiddelbart til "Rød",
la til gjesten, og det ferdige spillerkortet viste "Utslag: Rød"
med det samme, uten å måtte åpne "Rediger". Ingen konsollfeil.
Scratch-miljøet ryddet opp fullstendig.
**Rullet ut 2026-08-08** -- se punkt 48.
48. **Slag-for-slag GPS-avstandsmåling rullet ut mot ekte `teecup_db`/
`teecup_api`/`teecup_frontend` — 2026-08-08. Samler utrullingen for
punkt 45-47.** Bruker satte inn begge Mapbox-tokens midt i økten:
`NEXT_PUBLIC_MAPBOX_TOKEN` (offentlig, URL-restriktert til
`teecup.golf` -- bekreftet BEVISST, ikke en feil: avviste med 403 fra
et scratch-miljøs `localhost`-opprinnelse under punkt 46s
verifisering, nøyaktig som en URL-restriksjon skal virke) og
`TEECUP_MAPBOX_SECRET_TOKEN` (server-side, ingen URL-restriksjon
siden Static Images-kallet er server-til-server og aldri sender en
nettleser-`Referer`; kun offentlige `styles:tiles`/`styles:read`-
scopes, ingen skrive-/bruker-/tokens-tilganger -- backend-koden
trenger ikke mer).
**Attende scratch-miljø denne økten** (kun API + MinIO, ingen
frontend nødvendig -- rent backend-kall): bekreftet
`TEECUP_MAPBOX_SECRET_TOKEN` faktisk fungerer ende-til-ende --
opprettet en runde+slag, kalte `share_shot`, fikk et REELT,
ikke-null `shared_image_url` tilbake (176 KB AVIF-fil i MinIO, ikke
en tom/feilet fil). Scratch-miljøet ryddet opp fullstendig.
**Migrasjon + utrulling mot ekte systemer, etter eksplisitt
brukerbekreftelse ("ja"):**
1. `060_round_shot.sql` kjørt mot ekte `teecup_db` -- kun ny tabell +
to `GRANT`-er (`SELECT/INSERT/DELETE` full, `UPDATE` begrenset til
`shared_round_message_id`), ingen endring i eksisterende skjema.
`\d round_shot` bekreftet alle CHECK-constraints/FK-er riktige.
`test_isolation.sql` kjørt på nytt mot ekte `teecup_db` rett
etter -- fortsatt 12/12 grønt.
2. `docker compose build teecup_api teecup_frontend && up -d
teecup_api teecup_frontend` -- begge bygget rent, ren oppstart i
loggene, ingen feil.
3. `https://teecup.golf/` bekreftet oppe, ingen konsollfeil.
Autentisert gjennomgang ("Mål et slag" i ekte nettleser) overlatt
til brukeren selv (krever brukerens egen pålogging, samme
begrensning som tidligere utrullinger denne økten).
Med dette er ADR-048 (slag-for-slag GPS-avstandsmåling) fullt bygget,
verifisert og live -- begge inngangspunkt, begge hull-eiertyper,
begge Mapbox-token-veier (kart-valg + delings-satellittbilde).
49. **PRODUKSJONSBUG: slagmåling ga alltid 0 meter, delinger forsvant
stille — 2026-08-08, brukerrapport rett etter punkt 48s utrulling.**
Brukeren: "den målte aldri mer enn 0 meter. Jeg så ingen bilder."
**Rotårsak 1 (selve 0-meter-buggen):** `shot-measurement-sheet.tsx`
sitt "end"-steg (ballens posisjon) avfyrte GPS-målingen AUTOMATISK i
et `useEffect` idet steget ble aktivt -- rett etter at startpunktet
var målt, med null tid for brukeren til faktisk å gå fra utslagsstedet
til ballen. Start- og sluttpunkt endte dermed på praktisk talt samme
sted, samme øyeblikk -- avstanden ble alltid ~0m, uavhengig av hvor
langt slaget faktisk var. Rettet ved å fjerne auto-avfyringen og
kreve et eksplisitt "Jeg er ved ballen nå"-trykk (samme mønster som
"Prøv igjen" ved feil, nå gjenbrukt for begge) -- brukeren går fysisk
til ballen FØR målingen skjer, i stedet for at appen antar de allerede
er der.
**Rotårsak 2 (stille tap, "ingen bilder"):** en konsekvens av
rotårsak 1 -- en 0m-avstand ble avvist av backendens
`distance_meters > 0`-validering (422), men `submitShot()` i
`round-detail.tsx` sin `ShotMeasurementEntry` gjorde da bare
`setOpen(false)` og returnerte -- INGEN feilmelding, arket lukket seg
stille som om alt var i orden. Brukeren fikk aldri vite at slaget
(og dermed en eventuell deling) aldri ble lagret. Rettet: `submitShot`
setter nå en `submitError`-state (parser backendens feilrespons,
som kan være enten appens vanlige `{detail:{message}}`-form ELLER
FastAPI/Pydantic sin rå valideringsform `{detail:[{msg,...}]}` --
bekreftet eksakt hvilken av de to ved å faktisk trigge en 422 mot
scratch-API-et: det er listeformen), viser feilen i arket
(`role="alert"`, ny `submitError`/`submitting`-prop på
`ShotMeasurementSheetProps`), og holder arket ÅPENT ved feil i stedet
for å anta suksess og lukke.
`tsc --noEmit` rent. **Scratch-verifisert i ekte nettleser** (nittende
scratch-miljø denne økten, samme migrasjon-002-forsiktighet,
`teecup_db` bekreftet uendret): CDP-automatiserte nettlesersesjoner
har `geolocation`-tillatelse permanent `denied` i dette miljøet (samme
begrensning som punkt 45-46), så `navigator.geolocation.
getCurrentPosition` ble midlertidig overstyrt via `evaluate_script`
for å drive den EKTE React-komponentens fulle flyt (ikke bare et
API-nivå-kall) -- bekreftet "Jeg er ved ballen nå"-knappen faktisk
vises i stedet for å auto-måle, at et ekte gap mellom start-/
sluttkoordinat ga "MÅLT LENGDE: 130 m" (ikke 0), at delingen faktisk
postet riktig tekst til `/rounds/{id}/messages`, OG (motsatt test,
identisk start-/sluttkoordinat) at en avvist 0m-innsending nå viser
feilmeldingen i arket i stedet for å lukke seg stille. Scratch-miljøet
ryddet opp fullstendig.
50. **Slag-for-slag GPS-avstandsmåling: liste over egne slag + fant en
TREDJE stille-feil-bug — 2026-08-08, brukeroppfølging.** Brukeren
prøvde på nytt etter punkt 49s fiks (bekreftet: målte nå 13 meter,
ikke 0), men spurte "jeg kan ikke se det delte slaget i ettertid?".
**Diagnose (skrivebeskyttede spørringer mot ekte `teecup_db` + ekte
API-logger):** slaget lå riktig i `round_shot` (13m, ikke 0 -- punkt
49s fiks virket), men `shared_round_message_id` var tom, og
`round_message`-tabellen hadde null rader for runden. API-loggene
viste at `POST .../shots` ga 201 Created, men det fantes INGEN
etterfølgende kall til del-endepunktet i det hele tatt. Konklusjon:
"Del i feeden"-valget var ikke aktivt idet brukeren trykket "Lagre
slag" (arket starter forfra, inkl. tilbake til "Behold privat"
-standardvalget, hver gang det åpnes på nytt -- forklart til
brukeren).
Brukerens motspørsmål var det egentlig viktige: **et privat, ikke-delt
slag burde uansett kunne SES i ettertid** -- merkelappen viste
tidligere kun et tall ("N slag målt"), aldri kølle/avstand for de
faktiske slagene. Dette var en reell mangel i inngangspunkt 2 (punkt
46), ikke bare en brukerforvirring.
**Bygget:** `ShotMeasurementEntry` lagrer nå hele slag-listen (ikke
bare `.length`), med en ny utvidbar `ShotList` (kølle + avstand per
slag, "Delt"-merke eller en "Del"-lenke for et ikke-delt slag, en
slette-knapp per slag via det allerede eksisterende
`DELETE /rounds/{id}/shots/{shot_id}`-endepunktet). Splittet den
tidligere ene knappen i to: en utvidbar "N slag målt"-disclosure
(kun synlig når `count > 0`) og en separat, alltid synlig "+"-knapp
for å måle et NYTT slag -- unngår at å åpne listen og å starte en ny
måling er samme handling.
**Tredje stille-feil-bug funnet og rettet i samme runde:** akkurat
som punkt 49s rotårsak 2, sjekket heller ikke re-del-fra-liste-kallet
(`shareExisting`) svaret sitt. Rettet parallelt med bygningen av
listen, IKKE etter en ny brukerrapport denne gangen. En mislykket
deling (enten ved førstegangs innsending eller re-del fra listen)
vises nå som en egen, kortvarig feiltekst ved siden av merkelappen
(`shareError`-state) -- BEVISST atskilt fra `submitError` (som
fortsatt holder selve MÅLE-arket åpent ved en avvist innsending):
siden selve slaget alerede er lagret på tidspunktet en delingsfeil
kan oppstå, ville gjenbruk av `submitError` (som holder arket åpent)
risikert at brukeren trykker "Lagre slag" på nytt og oppretter et
duplikat-slag.
`tsc --noEmit` rent. **Scratch-verifisert i ekte nettleser** (tjuende
scratch-miljø denne økten, samme migrasjon-002-forsiktighet,
`teecup_db` bekreftet uendret): opprettet to slag direkte mot API-et
(Driver 215,5m, 7-jern 145m), bekreftet merkelappen viser "2 slag
målt" som en egen utvidbar knapp ved siden av en separat "+"-knapp,
utvidet listen og så begge slag med riktig kølle/avstand, klikket
"Del" på Driver-slaget og bekreftet det ble til "Delt" OG at meldingen
faktisk postet riktig tekst til `/rounds/{id}/messages`, slettet
7-jern-slaget og bekreftet det forsvant fra listen ("1 slag målt"
etterpå). Scratch-miljøet ryddet opp fullstendig.
51. **PRODUKSJONSBUG: kart-steget viste "Kunne ikke laste kartet" for
alle -- CSS-arv-krasj med mapbox-gl.css — 2026-08-08, brukeroppfølging
("jeg ser ikke noe kart eller satellittfoto").** Bruker bekreftet
"alle tre" da spurt hvor de forventet kart/foto: (1) kart-valg under
måling, (2) forhåndsvisning i resultatsteget, (3) bilde i den delte
meldingen.
**(1) Rotårsak:** `mapbox-gl.css` (selve biblioteket sin stilark,
importert i `map-point-picker.tsx`) definerer `.mapboxgl-map {
position: relative }`. Mapbox GL JS legger `mapboxgl-map`-klassen til
PÅ kart-beholderen sin egen `` ved initialisering -- denne
klassen kom SENERE i CSS-cascaden enn Tailwind sin `absolute`-klasse
(samme element), og vant dermed kappløpet, og overstyrte `position`
fra `absolute` til `relative`. Så snart `position` ikke lenger var
`absolute`, mistet Tailwind sin `inset-0`-klasse all effekt på
STØRRELSEN (den styrer kun posisjon for absolutt/fixed-plasserte
elementer) -- beholderen kollapset til `height: 0`, og selve
Mapbox-kartet (som TEKNISK sett lastet helt fint -- style/tiles/
events ga alle 200/204) ble usynlig, klippet vekk av en 0px-høy
forelder. Bekreftet direkte via `getBoundingClientRect()`-kjeden opp
DOM-treet: `containerRect.height: 0` til tross for at Mapbox sitt
eget `
`-element hadde en normal størrelse. Rettet ved å bytte
`absolute inset-0` til `h-full w-full` på beholder-diven -- løser
størrelsen via prosent-arv, upåvirket av hvilken `position`-verdi som
vinner cascade-kappløpet.
Denne konkrete feilen kunne IKKE vært fanget opp i noen av de
tidligere scratch-testene denne økten (femtende, sekstende), siden
Mapbox sitt eget offentlige token alltid ble avvist av URL-
restriksjonen mot `localhost`-scratch-opprinnelser der -- kartet
kom aldri langt nok til å faktisk RENDRE for at CSS-krasjen skulle
bli synlig. Verifisert denne runden ved å midlertidig overstyre
nettleserens `Referer`-header til `https://teecup.golf/` (CDP-nivå,
forbi selve nettleserens fetch()-header-restriksjon) for å simulere
det ekte, godkjente domenet i scratch -- avdekket samtidig at
scratch-testen selv hadde en snubletråd: en `grep`-basert token-
utpakking (`grep NEXT_PUBLIC_MAPBOX_TOKEN .env`, uten `^`-anker)
matchet FEILAKTIG også `.env` sin forklarende kommentarlinje over
selve variabelen (som også inneholder teksten "NEXT_PUBLIC_MAPBOX_
TOKEN"), og satte sammen kommentarteksten med selve tokenet til en
ugyldig verdi -- bekreftet at DENNE spesifikke feilen kun rammet
scratch-testverktøyet mitt, ikke selve produksjonsutrullingen
(`docker-compose.yml` sin `${NEXT_PUBLIC_MAPBOX_TOKEN}`-variabel-
substitusjon er upåvirket, bekreftet ved å grepe direkte i den ekte,
kjørende frontend-containerens bygde JS-bunt).
Underveis avdekket også en ekte SERVICE WORKER-cache-fallgruve verdt
å ha i bakhoden for senere feilsøking: en omstart av scratch-
frontend-containeren (SAMME port) beholdt en GAMMEL, cachet JS-bunt
i nettleseren til service workeren ble eksplisitt avregistrert og
cachen tømt -- PWA-installasjonen cacher altså aggressivt nok til å
overleve en full container-utrulling, noe som er relevant å huske
ved fremtidig feilsøking av "jeg ser fortsatt det gamle" -rapporter.
**(2) Bygget (ikke opprinnelig planlagt, brukerønske):** en client-
side forhåndsvisning i resultatsteget, FØR "Lagre slag" trykkes.
Bruker Mapbox Static Images API direkte som en ` `, med det
OFFENTLIGE tokenet (trygt i nettleseren) -- ingen server-tur-retur
nødvendig kun for en forhåndsvisning, adskilt fra selve delingens
server-side-genererte bilde (som fortsatt inkluderer en linje mellom
punktene, ikke bare to nåler).
**(3) Allerede fungerende:** bekreftet tidligere denne økten
(scratch18, punkt 48) at selve delings-bildegenereringen fungerer
server-side med `TEECUP_MAPBOX_SECRET_TOKEN` -- brukerens opplevelse
av "ingen bilde" der skyldtes at selve DELINGEN aldri fullførte
(punkt 50s diagnose: "Del i feeden"-valget nullstilles hver gang
arket åpnes på nytt), ikke en feil i bildegenereringen selv.
`tsc --noEmit` rent. **Scratch-verifisert i ekte nettleser** (tjueførste
scratch-miljø denne økten, samme migrasjon-002-forsiktighet,
`teecup_db` bekreftet uendret) MED `Referer`-overstyring for å komme
forbi URL-restriksjonen: kartet viste nå ekte satellittbilder (Oslo
sentrum, default-koordinat), et simulert klikk på kartet plasserte en
markør korrekt og aktiverte "Bekreft punkt", og resultatsteget viste
en korrekt forhåndsvisning med A/B-markører og riktig avstand (130m).
Scratch-miljøet ryddet opp fullstendig.
**Rullet ut umiddelbart** (produksjonsbug som traff ALLE brukere som
prøvde kart-basert måling) -- kun frontend-image, `teecup_api`
bekreftet uendret.
52. **PRODUKSJONSBUG: kartet sentrerte alltid på en hardkodet Oslo-
koordinat, aldri brukerens faktiske posisjon — 2026-08-08, umiddelbar
brukeroppfølging etter punkt 51.** Bruker: "Kartet viser ikke hvor
jeg faktisk er, men en eller annen vilkårlig by."
**Rotårsak:** `MapPointPicker` sin `center: [10.7522, 59.9139]` var
en HARDKODET Oslo-koordinat, ledsaget av en kommentar som hevdet
"real app centers on last-known position" -- den logikken fantes
ALDRI, kun påstanden i kommentaren. Kartet åpnet dermed alltid på
samme faste punkt i Oslo, uansett hvor i verden (eller landet)
brukeren faktisk befant seg.
**Rettet:** kartet henter nå brukerens ekte GPS-posisjon (ett
`getCurrentPosition`-kall, samme mønster som "Min posisjon nå"-veien
-- ingen løpende `watchPosition`) FØR selve Mapbox-kartet
initialiseres, og sentrerer der. Oslo-koordinaten beholdt KUN som
fallback for det tilfellet posisjon ikke kan hentes (avslått
tillatelse, tidsavbrudd, ingen støtte i nettleseren).
`tsc --noEmit` rent. **Scratch-verifisert i ekte nettleser** (tjueandre
scratch-miljø denne økten, samme migrasjon-002-forsiktighet,
`teecup_db` bekreftet uendret): overstyrte `navigator.geolocation` til
Bergen sentrum (60,39°N 5,32°Ø) og bekreftet kartet faktisk åpnet der
(synlig annen bystruktur enn Oslo-testen fra punkt 51, pluss Mapbox
sin egen posisjons-markør synlig i visningen) i stedet for på den
tidligere faste Oslo-koordinaten. Scratch-miljøet ryddet opp
fullstendig.
**Rullet ut umiddelbart** (samme produksjonsbug-alvorlighet som
punkt 51 -- traff alle som brukte kart-valget utenfor Oslo sentrum)
-- kun frontend-image, `teecup_api` uendret.
53. **Ballens posisjon (sluttpunktet) kan nå OGSÅ velges på kart, ikke
bare GPS — pluss et vist referansepunkt for utslaget — 2026-08-08,
brukerønske umiddelbart etter punkt 52.** Bruker: "Jeg må også kunne
velge på kartet hvor ballen ligger. Dessuten: Jeg vil gjerne se
kartet mens jeg går frem til ballen. Slagpunktet må være en del av
det jeg ser." Dette opphever ADR-048s opprinnelige "sluttpunkt:
alltid GPS, aldri kart"-del av flyten — se "Tillegg 2026-08-08" i
ARCHITECTURE_DECISIONS.md for full begrunnelse, historikken der er
beholdt, ikke overskrevet.
**Endringer:**
- Migrasjon `061_round_shot_end_method.sql`: ny `round_shot.end_method
text NOT NULL DEFAULT 'gps' CHECK (IN ('gps','map_tap'))` — speiler
`start_method` nøyaktig. `DEFAULT 'gps'` er historisk korrekt: ALLE
slag før dette tillegget ble faktisk målt med GPS for sluttpunktet.
- Backend (`app/routers/rounds.py`): `ShotIn.end_method` (default
`"gps"` for bakoverkompatibilitet), `ShotOut.end_method`,
`_SHOT_SELECT`/`_shot_out`/`_insert_shot` oppdatert til å lese/skrive
kolonnen.
- `frontend/components/shot/map-point-picker.tsx`: ny valgfri
`referencePoint`/`referenceLabel`-prop. Når satt (kun på
ball-steget): en FAST, ikke-flyttbar oransje markør ("Utslag") vises
på kartet, og punktet som faktisk plasseres ved tap er grønt i
stedet for oransje (samme `ff5a1f`/`2f7a3f`-fargekonvensjon som
delings-bildet allerede brukte, nå ført konsekvent i selve
UI-et også). Kartet henter brukerens live GPS-posisjon OG kjenner
referansepunktet, og bruker `map.fitBounds([nåværende posisjon,
referansepunkt])` slik at BEGGE garantert er synlige med det samme
(ikke bare sentrert på ett av dem) — faller tilbake til å sentrere
på referansepunktet alene (zoom 17) hvis GPS feiler.
- `frontend/components/shot/shot-measurement-sheet.tsx`: nytt
`end_map`-steg. "end"-steget tilbyr nå samme valg som "start":
"Jeg er ved ballen nå" (GPS, uendret) vs. "Vis kart mens jeg går"
(nytt — åpner `MapPointPicker` med `referencePoint={startPoint}`).
`goBack()` og `onMapStep`-sjekken oppdatert for det nye steget;
`endMethod`-state lagt til og sendt med i `onSubmit`-payloaden.
- `frontend/components/round-detail.tsx`: `submitShot()` sender nå
`end_method` i POST-kroppen.
`tsc --noEmit` og `python3 -c "import ast; ast.parse(...)"` begge
rene.
**Scratch-verifisert** (tjuetredje scratch-miljø denne økten, samme
migrasjon-002-forsiktighet, `teecup_db`s ACL bekreftet uendret
før/etter): (1) alle 61 migrasjoner (inkl. ny 061) kjørte rent mot
scratch-DB, `test_isolation.sql` fortsatt 12/12 grønt; (2)
`\d round_shot` bekreftet ny kolonne + CHECK-constraint, et direkte
forsøk på å sette `end_method='not_valid'` ble korrekt avvist; (3)
API-nivå via ekte innlogging (magic-link, `TEECUP_DEV_LOG_MAGIC_
LINKS=true`) og direkte HTTP-kall: `POST .../shots` med
`end_method: "map_tap"` lagret og returnerte korrekt, samme endepunkt
UTEN `end_method` i kroppen falt korrekt tilbake til `"gps"`
(bakoverkompatibilitet bekreftet); (4) full nettleser-gjennomgang via
Chrome DevTools MCP (samme Referer-header-omgåelse for det
URL-restrikterte Mapbox-tokenet, og samme
`navigator.geolocation.getCurrentPosition`-monkey-patch for CDP sin
faste geolocation-`denied`-begrensning, begge kjente fra punkt 51/52):
valgte "Min posisjon nå" for startpunktet, deretter "Vis kart mens jeg
går" for ballen — bekreftet visuelt at kartet åpnet med BÅDE
"Utslag"-referansemarkøren (oransje) og var zoomet/panorert slik at
den var synlig med det samme (fitBounds), trykket et punkt på kartet
og fikk en distinkt GRØNN markør for ballen, fullførte flyten til
lagring, og verifiserte til slutt med direkte SQL mot scratch-DB-en at
det lagrede slaget faktisk hadde `start_method=gps, end_method=
map_tap` — den nøyaktige blandede kombinasjonen brukerens ønske
beskriver. Scratch-miljøet (DB, rolle, MinIO, API- og
frontend-container/-images) ryddet opp fullstendig etterpå.
**Rullet ut** samme økt, bruker bekreftet eksplisitt ("Ja") — migrasjon
061 kjørt mot ekte `teecup_db`, `teecup_api` og `teecup_frontend`
bygget/restartet rent, `teecup_db`s ACL bekreftet uendret før/etter,
verifisert live på `teecup.golf` uten konsollfeil.
54. **Ball-steget viser nå alltid kartet med løpende (sanntids) posisjon
og avstand, og allerede målte slag viser satellittfoto i listen —
2026-08-08, umiddelbar brukeroppfølging etter punkt 53.** Bruker,
etter å ha sett skjermbilder av den nye funksjonen: "Jeg får ikke sett
slagene jeg allerede har målt... jeg ønsker å kunne se på et
satelittfoto hvor jeg har slått hvert slag. Det andre er at selv om
jeg velger 'min posisjon nå', så skal jeg se start og slutt på et
satelittfoto... I alle sammenhenger... ønsker jeg at jeg skal se
lengden så langt i sanntid mens jeg nærmer meg ballen." Se
"Tillegg 2026-08-08 (del 2)" i ARCHITECTURE_DECISIONS.md (ADR-048) for
full arkitektur-begrunnelse, inkl. det bevisste, avgrensede unntaket
fra "ingen løpende watchPosition"-prinsippet.
**Endringer, kun frontend (ingen migrasjon, `ShotOut` hadde allerede
alle koordinatene):**
- `map-point-picker.tsx`: `onConfirm` tar nå `(lngLat, method)` i
stedet for bare `(lngLat)`. Når `referencePoint` er satt (ball-
steget) startes `navigator.geolocation.watchPosition()` ved mount,
med en egen blå "du er her"-markør som oppdateres løpende og et
stort sanntids-avstand-tall ("AVSTAND SÅ LANGT") nederst på kartet,
beregnet med `haversineMeters(referencePoint, livePosition)`.
Footeren viser nå TO knapper når `referencePoint` er satt: "Bekreft
ballens posisjon" (trykket punkt, kun aktiv etter tap) og "Jeg er
ved ballen nå" (siste sporede posisjon, aktiv så snart første
GPS-fix er mottatt). `watchPosition` ryddes opp (`clearWatch`) ved
avmontering.
- `shot-measurement-sheet.tsx`: `end_map`-steget fjernet igjen (varte
kun én runde) — "end"-steget rendrer nå ALLTID `MapPointPicker`
direkte med `referencePoint={startPoint}`, ingen egen GPS/kart-valg-
skjerm lenger (den valget ligger nå inne i selve kartkomponentens
footer, se over). `measureEndPoint()`/`endStatus` fjernet (ikke
lenger i bruk). Egen duplikat Haversine-implementasjon
(`previewDistance`) erstattet med `haversineMeters` fra
`frontend/lib/geo.ts` (som alt fantes, men var ubrukt inntil nå) —
ren opprydding, ingen atferdsendring.
- `round-detail.tsx`: `ShotRecord`-typen utvidet med
`start_lat`/`start_lng`/`end_lat`/`end_lng` (allerede returnert av
API-et, kun frontend-typen manglet dem). `ShotList` viser nå et
160×160 satellitt-thumbnail per slag (samme offentlige token og
`pin-s-a`/`pin-s-b`-fargekonvensjon som forhåndsvisningen), bygget
client-side direkte fra de lagrede koordinatene — løser "jeg burde
jo se slaget selv, selv om det ikke er delt" sitt neste lag: ikke
bare klubbe/avstand som tekst, men HVOR slaget faktisk ble slått.
`tsc --noEmit` rent (ingen backend-endring denne runden).
**Scratch-verifisert** (tjuefjerde scratch-miljø denne økten, ingen
ny migrasjon å teste denne runden — samme 61 migrasjoner + `teecup_db`
ACL-forsiktighet som før): full nettleser-gjennomgang via Chrome
DevTools MCP med BÅDE `getCurrentPosition`- og `watchPosition`
monkey-patchet (sistnevnte simulerer en spiller som beveger seg fra
start mot ballen over ~5 sekunder via et `setInterval`). Bekreftet:
(1) sanntids-avstanden i kartet regner nøyaktig samme tall som en
uavhengig Python Haversine-kontroll (315,04 m); (2) "Jeg er ved ballen
nå" bruker siste sporede posisjon og lagret korrekt med
`end_method=gps` i databasen; (3) tap på kartet plasserer en grønn
markør og aktiverer "Bekreft ballens posisjon"; (4) en reell
grensesnitt-verifisering av EKSISTERENDE feilhåndtering (fra punkt 50)
skjedde underveis helt av seg selv: et tap som (ved et scratch-
test-uhell) landet ~0 m fra referansepunktet ble korrekt avvist av
backendens `distance_meters > 0`-sjekk (422), og arket viste riktig
feilmelding i stedet for å late som suksess — bekrefter at den
beskyttelsen fortsatt virker uendret gjennom hele denne
ombyggingen; (5) slag-listens nye satellitt-thumbnails lastet korrekt
for begge tidligere lagrede slag. Ingen uventede konsollfeil (kun
kjente scratch-miljø-artefakter: WebGL-fallback-advarsel, en
irrelevant WebSocket-tidsavbrudd, og den FORVENTEDE 422-en fra punkt
4). Scratch-miljøet (DB, rolle, MinIO, API- og frontend-container/
-images) ryddet opp fullstendig etterpå, `teecup_db`s ACL bekreftet
uendret.
**Rullet ut** samme økt, bruker bekreftet eksplisitt ("Ja") —
`teecup_frontend` bygget/restartet rent (ingen migrasjon eller
backend-endring denne runden, `teecup_api` uendret), verifisert live
på `teecup.golf` uten konsollfeil.
55. **Ny landingsside `/velkommen` for innlogget-men-ikke-fullført-profil
— 2026-08-09, eksplisitt brukerønske (ADR-049, se
ARCHITECTURE_DECISIONS.md for full begrunnelse og drøfting).** Bruker:
"Skjemaet med personlig informasjon vises for tidlig for ikke
registrerte spillere etter at de logger seg inn... Jeg tror det beste
er at man kommer til en side med oppfordring til å installere som app
(som vanlig) og med deaktiverte knapper for runde, turneringen og bli
med med kode. Trykker man på noen av disse skal man få beskjed om at
personlig informasjon må fylles ut først, med lenke til skjemaet."
Midtveis presisert: "Alle knappene bør egentlig være der, deaktivert.
bortsett fra til profilen" — dvs. HELE den vanlige bunn-navigasjonen
skal vises, ikke bare de tre hurtighandlingene.
**Rotårsak til det opprinnelige problemet:** tre steder rutet en
innlogget-men-ikke-fullført-profil-bruker RETT til `/account` sitt
påtvungne skjema uten noen kontekst: `app/page.tsx` (root-redirect),
`app/logg-inn/page.tsx` (post-innlogging-redirect), og
`dashboard.tsx` sin egen klient-side `profile_complete`-vakt
(`loadMe()`, fyres hvis en bruker skulle lande direkte på
`/dashboard`, f.eks. via `verify-form.tsx` som ALLTID sender dit
etter innlogging uansett profil-status).
**Bygget:**
- Ny `frontend/app/velkommen/page.tsx` (server-komponent, samme
autentiserings-/redirect-mønster som `/logg-inn/page.tsx` --
`redirect()`-kall bevisst holdt UTENFOR try/catch, samme fallgruve
som ble funnet og fikset i `/logg-inn/page.tsx` 2026-08-08 unngås
her fra start) + `frontend/components/teecup/velkommen.tsx`
(klient-komponent). Alle tre stedene over pekt om til `/velkommen`
i stedet for `/account`. `/velkommen` selv redirecter videre til
`/dashboard` (komplett profil) eller `/logg-inn` (ikke innlogget)
-- kan ikke nås "feil" via direkte URL.
- Siden gjenbruker eksisterende, allerede etablerte komponenter
uendret: `InstallPrompt` (samme PWA-oppfordring som dashbordet),
`TeeCupWordmark`, og samme "clubhouse"-palett-tokens som
`/logg-inn`/dashbordet (reskinnet 2026-08-07/08). Personlig
hilsen bruker `first_name ?? display_name`, samme navneformat-regel
som dashbordets hilsen (CLAUDE.md).
- Tre synlige, LÅSTE hurtighandlinger ("Ny runde"/"Ny turnering"/"Bli
med med kode" -- hengelås-ikon, `aria-disabled`, IKKE HTML
`disabled` siden de fortsatt skal være klikkbare for å forklare
hvorfor de er låst).
- `frontend/components/teecup/bottom-nav.tsx` utvidet med valgfrie
`disabledHrefs`/`onDisabledClick`-props (bakoverkompatibelt --
andre 6 sider som allerede bruker `BottomNav` uendret, ingen prop
sendt). Når en fane er i `disabledHrefs`, rendres den som en
`` i stedet for ` ` -- `/velkommen`
viser dermed HELE den vanlige bunn-navigasjonen (Hjem/Runder/
Turneringer/Profil/Mer), med alle unntatt "Profil" låst, per
brukerens presisering midtveis.
- Trykk på EN HVILKEN SOM HELST låst kontroll (hurtighandling ELLER
bunnfane) viser samme delte påminnelse (`role="alert"`, fast
posisjonert rett over bunn-navigasjonen slik at den er synlig
uansett hvilken av de to gruppene som trigget den) med lenke videre
til `/account`.
- Presentasjons-seksjonen nederst er hentet fra `teecup-beskrivelse.md`,
skrevet om til kort UI-tekst (fire punkter: turneringsformater, live
scoreføring, WHS-handicap, sosialt) -- ikke limt inn rått.
`tsc --noEmit` rent. Ingen migrasjon eller backend-endring.
**Scratch-verifisert** (tjuefemte scratch-miljø denne økten): full
nettleser-gjennomgang av HELE kjeden med en HELT NY bruker (aldri sett
før, ikke forhåndsopprettet i databasen som tidligere scratch-økter)
-- registrerte e-post, verifiserte magic-link, landet automatisk på
`/velkommen` (bekrefter at BÅDE `verify-form.tsx` sin `/dashboard`-
redirect OG dashbordets egen vakt samvirker riktig -- vakten er den
som faktisk avgjør endestasjonen). Bekreftet: hilsen viser riktig navn,
`InstallPrompt` vises, hurtighandlinger viser hengelås og er
ikke-navigerende, bunn-navigasjonen viser alle fem faner med fire
låst og "Profil" aktiv, trykk på en låst hurtighandling viser
påminnelsen med korrekt lenke. Fulgte "Fyll ut nå" til `/account`,
fylte ut skjemaet ende-til-ende, endte korrekt på `/dashboard` med
riktig personlig hilsen ("God dag, Ny"). Ingen konsollfeil gjennom
hele kjeden. Scratch-miljøet (DB, rolle, MinIO, API- og
frontend-container/-images) ryddet opp fullstendig etterpå,
`teecup_db`s ACL bekreftet uendret.
**Rullet ut** samme økt, bruker bekreftet eksplisitt ("ja") —
`teecup_frontend` bygget/restartet rent (`teecup_api` uendret, ingen
migrasjon), verifisert live på `teecup.golf` uten konsollfeil.
56. **Fjernet "Bekreft ballens posisjon" (trykk-på-kartet for ballens
posisjon) igjen — 2026-08-09, samme dag som punkt 54/55, etter faktisk
bruk på ekte bane.** Bruker sendte et ekte skjermbilde fra telefonen
(utslags- og live-markør nesten overlappende, "AVSTAND SÅ LANGT 1 m")
og spurte om forskjellen mellom de to bekreftelsesknappene. Etter
forklaring: "bekreft ballens posisjon er unødvendig." Avklart via
spørsmål (siden dette reverserer noe brukeren selv ba om bare timer
tidligere, jf. CLAUDE.md "spør heller enn å gjette" ved usikkerhet om
omfang): fjern trykk-alternativet HELT, behold kartet (referansepunkt
+ sanntidsposisjon/-avstand), ballens posisjon bekreftes nå
UTELUKKENDE med "Jeg er ved ballen nå" (sporet GPS).
**Endringer, kun `map-point-picker.tsx`:**
- `map.on("click", ...)`-registreringen (trykk-for-å-plassere-markør)
er nå betinget på `!referencePoint` -- kjører fortsatt uendret på
start-steget, aldri lenger på ball-steget.
- "Trykk der ballen ligger"-banneret fjernet for ball-steget (ingen
trykk-handling å instruere om lenger); start-stegets "Trykk på
kartet for å plassere punktet" uendret.
- Footeren på ball-steget viser nå kun ÉN knapp ("Jeg er ved ballen
nå", primærstil) i stedet for to stablede knapper.
- Ingen backend-/migrasjonsendring: `end_method`-kolonnen og
`Literal["gps", "map_tap"]`-typen beholdes uendret (historiske
`map_tap`-rader fra den korte perioden funksjonen var live skal
fortsatt leses/vises korrekt -- kun hvordan NYE slag kan opprettes
er endret).
`tsc --noEmit` rent.
**Scratch-verifisert** (tjuesjette scratch-miljø denne økten, samme
forsiktighetsrutine, `teecup_db`s ACL bekreftet uendret): full
nettleser-gjennomgang med `getCurrentPosition`/`watchPosition`
monkey-patchet. Bekreftet at ball-steget nå viser kartet direkte med
KUN "Jeg er ved ballen nå" i footeren (ingen "Bekreft ballens
posisjon"), at et trykk midt på kartet ikke lenger gjør noe (ingen ny
markør, ingen banner om å trykke), og at "Jeg er ved ballen nå"
fortsatt lagrer korrekt -- `start_method=gps, end_method=gps`
bekreftet direkte i databasen etter en full måle-runde (start via GPS,
kølle, lagring). Ingen konsollfeil. Scratch-miljøet ryddet opp
fullstendig.
**Rullet ut** samme økt, bruker bekreftet eksplisitt ("ja") —
`teecup_frontend` bygget/restartet rent (`teecup_api` uendret),
verifisert live på `teecup.golf` uten konsollfeil.
57. **Sikkerhetsgjennomgang + fiks av alle funn — 2026-08-09/10, bruker:
"sjekk alle felter... sjekk alle URL-er. Er alt sikret godt nok? Lar
siden/appen seg hacke?", deretter "tett sikkerhetshullene først."**
Full gjennomgang (dedikert agent) av autentisering, autorisasjon på
tvers av alle 16 routere, input-validering, filopplasting, CORS/
nettverk, hemmeligheter i git. Se ADR-050 i ARCHITECTURE_DECISIONS.md
for full begrunnelse. Konklusjon: autorisasjonslaget er uvanlig
solid (ingen bekreftet IDOR), men fire reelle hull ble funnet og
fikset:
1. **Rate limiting** (HØY) lagt til på fem auth-endepunkter (ny
`app/rate_limit.py`) — ingen fantes fra før, passord/2FA-koder
kunne i praksis brute-forces.
2. **Sikkerhetshoder** (LAV-MIDDELS) lagt til i Caddy for
teecup.golf: CSP, X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, HSTS.
3. **Input-validering** (LAV) — manglende `max_length`/`ge`/`le` lagt
til konsekvent i `rounds.py`/`players.py`/`round_messages.py`/
`messaging.py`, etter mønster som allerede fantes andre steder i
samme filer.
4. **BBB-endepunkt informasjonslekkasje** (informativt) — autorisasjon
flyttet til FØR formatsjekk i `update_bbb_hole`.
**Reell driftsfallgruve funnet og løst underveis:** `caddy reload`
(og en direkte admin-API-PUT) rapporterte suksess, men Caddyfile-
endringen slo aldri igjennom — `teeoff_caddy`s bind-mount av
Caddyfile-EN ENKELT FIL var bundet til en nå frikoblet inode etter en
atomisk skriving (skriv+omdøp) på verten. Løst med `docker restart
teeoff_caddy` (ikke bare reload) — se ADR-050 for full forklaring,
dette er en STÅENDE fallgruve for enhver fremtidig Caddyfile-endring.
`python3 -m py_compile` rent på alle endrede filer.
**Scratch-verifisert** (tjuesjuende scratch-miljø denne økten, samme
`teecup_db`-ACL-forsiktighet): rate limiting bekreftet å faktisk
utløse 429 ved riktig terskel på BÅDE `login-password` (10 tillatt,
11. avvist) og `request-link` (5 tillatt, 6. avvist); input-
validering bekreftet begge veier (HCP=99 avvist, HCP=18.4 akseptert,
50-tegns mobilnummer avvist); BBB-endepunktet bekreftet å returnere
403 NOT_AUTHORIZED (ikke lenger en 400 format-lekkasje) for en bruker
uten tilgang til en fremmed runde, OG fortsatt 200 OK for den
faktiske eieren på en ekte BBB-runde (regresjonssjekk). CSP-en
verifisert direkte mot PRODUKSJON i ekte nettleser (ikke scratch,
siden Caddy-konfigen er delt infrastruktur som ikke er en del av
scratch-oppsettet): ingen CSP-brudd i konsollen, et ekte Mapbox-kall
med gyldig token ga 200 OK, login-siden rendret visuelt korrekt (inline
styles uendret). `teecup_db`s ACL bekreftet uendret før/etter,
scratch-ressurser ryddet opp fullstendig.
**Rullet ut** — Caddy-delen ble rullet ut/verifisert direkte mot
produksjon underveis (se over). `teecup_api`-delen (rate limiting,
validering, BBB-fiks) bygget/restartet etter eksplisitt
brukerbekreftelse ("ja takk"), verifisert LIVE: `POST /auth/
request-link` mot ekte `teecup.golf` ga 200 på de fem første
forsøkene og 429 på det sjette (nøyaktig terskelen satt i koden),
ingen konsollfeil i ekte nettleser etterpå.
58. **13-årsgrense for kontoregistrering + to presentasjonstekst-
korrigeringer — 2026-08-10, bruker etter å ha lest to PDF-vedlegg.**
Se ADR-051 i ARCHITECTURE_DECISIONS.md for full begrunnelse og
drøfting av anbefalingene i "Barns personvern i golfapp"-PDF-en.
**Endringer:**
- `app/routers/auth.py`: `update_profile` avviser nå `PATCH /auth/
profile` med `400 UNDER_MINIMUM_AGE` hvis `birth_date` innebærer
under 13 år (eksakt dagsberegning). Dette er den ENESTE veien inn i
appen (profil må være komplett for å komme forbi `/velkommen`,
ADR-049), så dette ene stedet håndhever grensen for hele appen.
Bevisst IKKE utvidet til `player`-tabellen (organisasjonens
roster/CRM, `players.py`/`registration.py`) -- en `player`-rad er
en arrangørs registrering AV en person, ikke personen som
registrerer SEG SELV, og er selve PDF-ens egen anbefalte, tryggere
løsning for yngre spillere.
- `account-settings.tsx`: ny delt `computeAge()`-hjelpefunksjon,
brukt i BÅDE `ProfileOnboarding` (førstegangs-utfylling) og
`ProfileSection` (senere redigering under Konto) for umiddelbar
rød feilmelding + deaktivert lagre-knapp -- før serveren i det
hele tatt kontaktes.
- `install-prompt.tsx`: ny `iosBrowser()` skiller Safari fra Chrome
på iOS (`CriOS` i user agent) -- installasjonsteksten hevdet
tidligere feilaktig at man MÅTTE til Safari, og pekte til "nederst
i Safari" uansett nettleser. Siden iOS 16.4 kan Chrome installere
PWA-er direkte, med Del-ikonet et ANNET sted (oppe til høyre i
adressefeltet, ikke nederst). Ukjente iOS-nettlesere får nå en
nøytral "i nettleseren din"-tekst i stedet for en gjetning.
- `teecup-beskrivelse.md` + `velkommen.tsx`: "golfklubber" fjernet
fra målgruppe-beskrivelsen (to steder i beskrivelses-dokumentet,
ett i `/velkommen`s "Hva er TeeCup?"), nytt punkt lagt til om at
turneringsadministrasjon/-presentasjon fungerer minst like godt på
PC som på telefon. "Klubbhus-stemning" (design-retningens navn,
ren tone-beskrivelse) er bevisst uendret.
`tsc --noEmit` og `python3 -m py_compile` begge rene.
**Scratch-verifisert** (tjueåttende scratch-miljø denne økten, samme
`teecup_db`-ACL-forsiktighet): eksakt dagsgrense bekreftet med tre
API-kall (10-åring avvist, nøyaktig 13 år i dag akseptert med
`profile_complete: true`, én dag under 13 avvist). Ekte nettleser:
klientside rød feilmelding + deaktivert knapp bekreftet ved
2018-fødselsdato, `/velkommen`s presentasjonstekst bekreftet uten
"golfklubber" og med det nye PC-punktet. Ingen konsollfeil. Scratch-
miljøet (DB, rolle, MinIO, API- og frontend-container/-images) ryddet
opp fullstendig, `teecup_db`s ACL bekreftet uendret.
**Rullet ut** samme økt, bruker bekreftet eksplisitt ("Ja") — begge
containere bygget/restartet rent, verifisert live på `teecup.golf`
uten konsollfeil.
59. **Designrunden: full clubhouse-overtagelse (lys + mørk) — 2026-08-10,
se ADR-052.** Bruker ba om "designrunden" og valgte, via
`AskUserQuestion`, det bredeste av tre foreslåtte omfang: "Full
overtagelse, alt på én gang" — resten av appen (unntatt
ny-runde-veiviserens `nr-*`-palett) skulle over på "clubhouse"-
paletten i én samlet runde, ikke gradvis skjerm for skjerm som
tidligere planlagt.
**Root cause / tilnærming:** tre paletter levde side om side —
"Forest Green" (shadcn-tokens, ~90 filer), "clubhouse" (7 filer:
innlogging/dashbord/bunnnav/velkommen), "nr-*" (ny-runde-veiviseren,
utenfor omfang). En Explore-agent bekreftet at `components/ui/*` OG
de ~90 gjenværende filene er RENE token-konsumenter (null hardkodet
hex funnet noe sted utenfor de 7 clubhouse-filene) — konsekvensen:
hele overtagelsen kunne gjøres ved å redefinere shadcn-tokenene i
`frontend/app/globals.css` (`:root`/`.dark`/media-blokk) til
clubhouse sine faktiske verdier, i stedet for å endre className i 90
filer. `--destructive`/`--brand-orange`/`--info`/`--gold`/
`--chart-1..6` bevisst UENDRET (egne semantiske signalfarger, ikke
del av identiteten paletten styrer).
**Kontrast-korreksjon funnet FØR utrulling:** `--primary-foreground`
måtte endres fra nesten-svart til hvit — clubhouse sin
`--tee-strong` (`#2f6b1e`) er en MØRK grønn (brukes allerede med
`text-white` i `velkommen.tsx`), ulikt den gamle LYSE primærgrønnen.
**Mørk variant designet av V0 på bestilling** (ingen fantes fra før —
clubhouse var "fast lys-modus"). Claude skrev en ren fargeoppgave-
prompt (ti låste lyse verdier som fasit, eksplisitt WCAG AA-krav),
bruker limte den inn i V0 og limte svaret tilbake som en prosjekt-zip
(`globals.css`-diffen inneholdt kun ny `.dark`-blokk). Claude
verifiserte V0s egne kontrasttall uavhengig (egen WCAG-beregning) —
alle stemte eksakt: ink/bg 15.7:1, muted/bg 8.2:1, muted/card 7.2:1,
hvit/tee-strong 4.76:1.
**Reelt funn under planlegging, sparte en unødvendig kodeendring:**
opprinnelig plan antok de 7 clubhouse-filenes hardkodede
`bg-clubhouse-*`/`bg-tee-strong`-klassenavn måtte skrives om til
generiske tokens for å følge mørk modus. Viste seg unødvendig —
siden en `.dark`/media-blokk for de RÅ `--tee`/`--cup`/
`--clubhouse-*`-variablene ble lagt til (symmetri med
hovedremappen), arver disse 7 filene mørk modus automatisk via
CSS-variabel-cascade, uansett hvilket klassenavn de bruker. Ingen av
de 7 filene endret.
Liten tilleggsrettelse: `app/layout.tsx`s `themeColor`-metadata
(`#8BC24A`/`#1c261d`) pekte fortsatt på gamle farger — oppdatert til
de nye bakgrunnsfargene (`#f3f6ec`/`#131a0f`).
`DESIGN_SYSTEM.md` oppdatert til å beskrive clubhouse som selve
hovedpaletten (var tidligere "Forest Green").
**Scratch-verifisert** (full stack: ny scratch-DB med alle 61
migrasjoner, `teecup_app_dr1`-rolle, hyphenert scratch-MinIO, scratch
API- og frontend-container — ekte produksjonsbuild av frontend, ikke
bare `tsc`). Data seedet via ekte API-kall (personlig bane, runde,
hullscore), ikke rå SQL. `tsc --noEmit` og produksjonsbuild begge
rene. Browserverifisert i BÅDE lys og mørk modus (Chrome DevTools
MCP, `emulate colorScheme`) på `/logg-inn`, `/velkommen`,
`/dashboard` (tom + med data), `/my-rounds/[id]` (appens største
fil, 6726 linjer, tidligere Forest Green), scorekort-tabellen (tett
datagrid), `/account` (skjematung side) — alle konsistente, god
kontrast, ingen konsollfeil utover en harmløs PWA-infomelding.
`/my-rounds/new` bekreftet visuelt uendret (ingen smitte inn i
`nr-*`). `BottomNav`s bevisst palett-nøytrale hvite flate forblir
hvit i mørk modus også — eksisterende, bevisst V0-designvalg, ikke
en regresjon. `teecup_db`s ACL bekreftet uendret før/etter, alle
scratch-ressurser ryddet opp fullstendig.
**Rullet ut 2026-08-10**, bruker bekreftet eksplisitt ("Ja takk") —
`docker compose build teecup_frontend && docker compose up -d
teecup_frontend`, ren omstart. Bekreftet live på `teecup.golf` i ekte
nettleser i BÅDE lys og mørk modus, ingen konsollfeil utover den
harmløse PWA-infomeldingen.
60. **Ny-runde-veiviseren retemaet til clubhouse — 2026-08-10, se
ADR-053.** Rett etter forrige punkt ba bruker om at veiviseren
(`nr-*`-paletten, bevisst holdt utenfor punkt 59 som en tredje,
uavhengig V0-palett) også skulle få det nye designet.
Samme remap-prinsipp som punkt 59, samme fil (`globals.css`): de 16
`--nr-*`-variablene komponentene i `components/ny-runde/*` leser
(rå `var(--nr-x)`, bekreftet null hardkodet hex noe sted) fikk nye
verdier — `--nr-bg/-surface/-surface-2/-ink/-muted/-border/-accent/
-accent-ink` byttet til clubhouse (lys+mørk), `--nr-danger(-soft)`/
`--nr-ok(-soft)` beholdt sin egen røde/grønne hue i lys modus
(samme prinsipp som uendret `--destructive`), men fikk NYE mørke
varianter siden `.nr` aldri hadde mørk-modus-støtte før. Fire
variabler uten direkte clubhouse-kilde (`--nr-faint`, `--nr-border-
strong`, `--nr-accent-soft`, `--nr-accent-ring`) ble avledet
(alfa-blandet) fra clubhouse sine godkjente farger, kontrastsjekket
til aldri å havne under originalens egne verdier. Ingen av de 11
filene i `components/ny-runde/*` endret.
**Scratch-verifisert** (eget scratch-miljø, samme fulle DB/rolle/
MinIO/API/frontend-oppsett som punkt 59). `tsc --noEmit` rent.
Browserverifisert i BÅDE lys og mørk modus: bane-valg, "Egen
bane"-tom-tilstand (øver `--nr-faint`), og det 18-raders tette
hull-for-hull-skjemaet (par/stroke-indeks-dropdowns). Alle
konsistente, god kontrast. Tre pre-eksisterende DOM-lint-varsler
(manglende `autocomplete`/`id`-attributter i selve V0-markupen) —
uendret fra før denne CSS-only-runden, ikke forårsaket av den.
Scratch-ressurser ryddet opp fullstendig, `teecup_db`s ACL bekreftet
uendret.
**Rullet ut 2026-08-10**, bruker bekreftet eksplisitt ("Deretter;
slett kontoen, og gjør det resterende live") — ren omstart av
`teecup_frontend`, bekreftet live på `teecup.golf` uten konsollfeil
utover den harmløse PWA-infomeldingen.
61. **PRODUKSJONSHENDELSE: konto under 13-årsgrensen slettet — 2026-08-10.**
Bruker ba om en engangssjekk av ekte `teecup_db` (read-only SELECT,
eksplisitt bedt om av bruker) etter at 13-årsgrensen (punkt 58) gikk
live: fantes det allerede kontoer registrert med fødselsdato under
13 år? Ett treff — "Fredrik Mathisen" (`fredrik.f.mathisen@gmail.com`),
registrert 2026-08-09 med fødselsdato SATT TIL SAMME DAG (2026-08-09,
altså 0 år) — mest sannsynlig en dato-velger som ble stående på
dagens dato i stedet for en reell fødselsdato, ikke nødvendigvis en
bekreftet mindreårig, men nøyaktig signalet regelen er bygget for å
reagere på uansett årsak. Sjekket tilknyttet data først (runder,
medlemskap, meldinger, venner) — null treff overalt, en helt tom,
ubrukt konto.
Bruker godkjente eksplisitt både e-postteksten (norsk, forklarer
sletting og gir mulighet til å opprette ny konto med riktig
fødselsdato ved feilregistrering) og selve slettingen FØR noe ble
utført. Rekkefølge: (1) `DELETE FROM app_user WHERE id = ...` mot
ekte `teecup_db` (teeoff_admin) — bekreftet FK-kaskade ren siden
kontoen ikke hadde noen tilknyttet data i noen av de NO ACTION-
begrensede tabellene (message/round_message/round_participant/
round_shot m.fl.), (2) e-post sendt til den registrerte adressen via
appens eksisterende `_send_sync`-mekanisme i `app/email.py` (samme
SMTP-oppsett som magic-link-utsending), kjørt som et engangsskript
inni den ekte `teecup_api`-containeren, ikke en ny permanent
funksjon (ett engangstilfelle, ingen gjenbruk planlagt ennå).
Bekreftet i etterkant: kontoen finnes ikke lenger i `app_user`.
**Bevisst utenfor omfang:** ingen automatisert, gjentakende sjekk
for fremtidige under-13-registreringer er bygget — aldersgrensen i
`PATCH /auth/profile` (punkt 58) hindrer allerede NYE registreringer,
dette var en engangsopprydding av kontoer fra FØR den regelen gikk
live. Et automatisert varsel/rutine for dette kan vurderes som egen,
fremtidig sak om det blir aktuelt.
62. **Fjernet e-postvarsel for "venn startet en runde du kan følge" —
2026-08-10.** Bruker ba eksplisitt om å fjerne kun e-posten for denne
hendelsen (`type="round"`-notifikasjonen sendt fra `create_round` i
`rounds.py` til venner som kan følge runden) — in-app-varselet
(klokke-ikonet) og push-varselet skal fortsatt fungere som før.
**Kompleksitet:** samme `type="round"` deles av EN ANNEN, fortsatt
ønsket e-post ("du ble lagt til som medspiller", sendt fra
`add_participant`) — å slå av e-post for hele `"round"`-typen ville
fjernet begge. Løst med et nytt `allow_email: bool = True`-parameter
på `create_notification()` (`app/routers/notifications.py`), satt
til `False` KUN på kallstedet i `create_round`. In-app-innsettingen
og `_push_for_notification` kjører uendret uansett — kun selve
e-post-blokken hopper over. Ingen migrasjon, ingen endring i
`user_notification_email_pref`-skjemaet.
`frontend/components/account-settings.tsx`: "Runder"-varselvalgets
beskrivelsestekst rettet ("Du blir lagt til som medspiller, eller en
venn starter en runde du kan følge." → "Du blir lagt til som
medspiller på en runde.") — teksten lovet tidligere en e-post som nå
aldri sendes.
**Scratch-verifisert** (egen scratch-DB/rolle/MinIO/API-container,
`TEECUP_DEV_LOG_MAGIC_LINKS=true` + tomme SMTP-variabler for å
observere e-post-forsøk som en tydelig loggmelding i stedet for en
ekte utsending): to testbrukere, akseptert vennskap, `watcher` opt-et
inn på `round`-e-post. (1) `owner` opprettet en offentlig synlig
runde — in-app-varsel opprettet for `watcher`, INGEN "[DEV]
Varsel-e-post"-linje i loggen. (2) `owner` la `watcher` til som
medspiller på samme runde — in-app-varsel opprettet OG nøyaktig én
"[DEV] Varsel-e-post"-linje, bekreftet riktig melding. Begge
in-app-radene bekreftet i `notification`-tabellen etterpå. `tsc
--noEmit` og `python3 -m py_compile` begge rene. Scratch-ressurser
ryddet opp fullstendig, `teecup_db`s ACL bekreftet uendret.
**Rullet ut 2026-08-10**, bruker bekreftet eksplisitt ("Ja, kjør") —
begge containere bygget/restartet rent, bekreftet live på
`teecup.golf` uten konsollfeil utover den harmløse PWA-infomeldingen.
63. **Kartretning ved slagmåling: "opp = fremover på hullet", uten
green-koordinater — 2026-08-10, se ADR-054.** Bruker spurte om
kartet kunne roteres slik at man "går oppover" under slagmåling,
uten å registrere green-koordinater (bekreftet tidligere at ingen
slike finnes noe sted) — og ba deretter eksplisitt om at det bygges.
Løsning: bearingen (kompassretningen) kartet roteres til regnes ut
fra spillerens EGET forrige slag på samme hull (`round_shot` sine
allerede lagrede start-/sluttkoordinater), ikke fra en lagret
hull-linje. Ny `bearingDegrees()` i `frontend/lib/geo.ts` (ren
funksjon, standard forward-azimuth). Satt ÉN gang ved
kart-initialisering (`bearing` i Mapbox-konstruktøren), aldri
løpende oppdatert under gange — vurdert og bevisst avvist, se
ADR-054 Beslutning B (GPS-heading er for støyete ved lav fart til
kontinuerlig rotasjon). Første slag på et hull (ingen forrige å
regne fra) faller tilbake til ett engangs-forsøk på enhetens
`coords.heading`, deretter nord.
Rent klientside: `lib/geo.ts`, `components/shot/map-point-picker.tsx`,
`components/shot/shot-measurement-sheet.tsx`,
`components/round-detail.tsx` (`ShotMeasurementEntry`, som allerede
har slag-listen for hullet fra sin eksisterende henting). Ingen
migrasjon, ingen backend-endring.
**Verifisert i to lag:** `bearingDegrees()` unit-testet frittstående
(fire kjente himmelretninger — nord/øst/sør/vest ga 0/90/180/270
eksakt). Ende-til-ende i ekte nettleser (eget scratch-miljø): seedet
ett slag rett øst på hull 1 via ekte API-kall, midlertidig
konsollogg (fjernet igjen etter verifisering) bekreftet kartet fikk
`bearing≈90` på hull 1 og `bearing=0` (nord-fallback) på hull 2 (uten
tidligere slag). `tsc --noEmit` rent. Scratch-ressurser ryddet opp
fullstendig, `teecup_db`s ACL bekreftet uendret.
**Rullet ut 2026-08-10**, bruker bekreftet eksplisitt ("Kjør den.") —
ren omstart av `teecup_frontend`, bekreftet live på `teecup.golf`
uten konsollfeil utover den harmløse PWA-infomeldingen.
64. **Nytt app-ikon (ball/pokal/tee), full-bleed, hakk-feil rettet —
2026-08-10, se ADR-055.** Bruker: dagens ikon var "fremdeles et
gammelt utkast" — bekreftet ved å åpne `icon-512.png`/
`icon-maskable-512.png`/`apple-icon.png` direkte: riktig motiv, men
grov beskjæring med enorm hvit padding rundt en liten grafikk,
nesten ulesbart ved 32×32.
V0-prompt (samme motiv, ny komposisjon: full-bleed bakgrunn,
sentrert i safe-zone, ingen egen avrunding) ga et første svar med to
synlige hakk i pokal-formen. Claude sammenlignet FØRST mot feil
kildefil og konkluderte feilaktig at V0 hadde "tegnet på nytt etter
øyemål" — bruker rettet dette ved å dele den faktiske kilde-SVG-en
(`TeeCup-logo-kun.svg`) direkte. Rendret stort viste at hakkene var
en ekte, pre-eksisterende unøyaktighet i selve kildekunsten (to
overlappende oransje former som ikke dekker hverandre helt i to
punkter) — usynlig på hvit bakgrunn, synlig mot mørk. V0s "tatt
verbatim"-påstand var korrekt; Claudes første mistanke var feil.
Presis rettemelding til V0 (identifiserte de to path-fyllfargene)
ga en fiks (tettet gapet med en `stroke` i samme farge som fyllet,
ikke en omtegning) — verifisert visuelt før integrering, hakkene
bekreftet borte, ingen nye artefakter.
Integrert: ett 1024×1024 master-SVG er eneste kilde. Alle
pikselstørrelser (`icons/icon-192.png`, `icons/icon-512.png`,
`icons/icon-maskable-512.png`, `apple-icon.png` 180×180,
`icon-light/dark-32x32.png`) rastret av Claude selv via ekte
nettleser-rendring (`devicePixelRatio=1`, bekreftet eksakte
pikseldimensjoner etterpå) — ikke V0-genererte PNG-er, for å unngå
eksport-unøyaktighet. `public/icon.svg` (SVG-favikon) erstattet med
master-SVG-et. `app/manifest.ts` sin `theme_color`/`background_color`
(fortsatt gamle Forest Green-farger, `#8BC24A`/`#ffffff`) oppdatert
til clubhouse (`#2f6b1e`/`#f3f6ec`).
**Scratch-verifisert**: `tsc --noEmit` rent, egen frontend-only
scratch-container (ekte produksjonsbuild), `GET
/manifest.webmanifest` bekreftet nye farger, ikonfilene bekreftet
200 og visuelt korrekte i ekte nettleser. Scratch-ressurser ryddet
opp fullstendig.
**Rullet ut 2026-08-10**, bruker bekreftet eksplisitt ("Kjør") — ren
omstart av `teecup_frontend`, bekreftet live på `teecup.golf`:
`icon.svg` og `manifest.webmanifest` (nye farger) begge korrekte,
ingen konsollfeil utover den harmløse PWA-infomeldingen.
65. **WHS Rule 3.1b-unntak (>54 banehandicap + 4 mottatte slag → par+5)
implementert og fasit-testet + `round.tee_name_snapshot` synk-bug
rettet — 2026-08-10, se ADR-056.** Bruker delte en ekstern vurdering
av appens to "harde kjerner" (WHS-beregning, live-scoring/
samtidighet) og ba om en ærlig sjekk. To Explore-agenter gransket
hver sin kjerne parallelt.
**WHS-funn:** `handicap_engine.py` manglet et reelt, publisert
unntak (verifisert direkte mot kilde-PDF-en, side 37, ikke bare
agentens gjenfortelling): banehandicap over 54 OG 4+ mottatte slag
på ett hull → maks hullscore par+5, ikke den vanlige
par+2+mottatte-slag. Lagt til som et nytt, bakoverkompatibelt
`course_handicap`-parameter på `max_hole_score_for_handicap`/
`adjusted_gross_score`, wired inn i de to produksjonskallstedene i
`rounds.py`. Fire nye tester i `test_handicap_engine.py` (eksplisitte
grensetilfeller: eksakt 54, eksakt 3 slag, samt end-til-ende) — 117/117
grønne. Scratch-verifisert også via ekte API-kall (deltaker med
banehandicap 63, "plukket opp" på et hull med 4 slag ga korrekt 9,
ikke det gamle 10).
**Egen, urelatert bug funnet samtidig** (bruker viste skjermdump av
en live runde der toppheaderens utslag ikke fulgte et utslagsbytte):
`PATCH .../participants/{id}` skrev kun til `round_participant`,
aldri til `round.tee_name_snapshot` (som selve header-visningen
leser). Fikset: skriver nå begge når det er EIERENS EGEN
deltaker-rad som endres (ikke en gjests — ulike deltakere kan
bevisst ha ulikt utslag). Scratch-verifisert: eierens bytte
oppdaterer runden, en gjests bytte gjør det ikke.
`tsc`/`py_compile` rene der aktuelt. Scratch-ressurser ryddet opp
fullstendig, `teecup_db`s ACL bekreftet uendret gjennom hele
verifiseringen.
**Ikke rettet ennå:** den konkrete live runden brukeren viste (feil
historisk data i ekte `teecup_db`) — venter på egen bekreftelse for
en engangs datakorreksjon. Samtidighets-/konflikt-halvparten av
vurderingen er bevisst IKKE besluttet i denne runden — krever en
designbeslutning fra bruker først, se ADR-056.
**Rullet ut 2026-08-10**, bruker bekreftet eksplisitt ("Kjør alt") —
`teecup_api` bygget/restartet rent. Den konkrete runden brukeren
rapporterte (`round.tee_name_snapshot` feilaktig "50") korrigert til
"55" med en engangs `UPDATE` mot ekte `teecup_db`, bekreftet i
etterkant.
66. **Optimistisk versjonssjekk for samtidig hull-redigering — 2026-08-10,
se ADR-057.** Direkte oppfølging av punkt 65: bruker fikk valget
mellom tre tilnærminger til samtidighets-halvparten av den eksterne
vurderingen (versjonssjekk / feltvis skrivning / la det ligge), valgte
"enkel versjonssjekk + tydelig feilmelding".
Migrasjon 062: ny `version`-kolonne på `round_hole` (dekker begge
eierskapstyper, deltaker og side, én tabell). `update_hole`/
`update_side_hole` sjekker nå `expected_version` ATOMISK i selve
UPDATE-en (`WHERE ... AND (expected_version IS NULL OR version =
expected_version)`, ingen separat les-så-skriv-race), 409 med tydelig
norsk feiltekst ved konflikt, skiller korrekt fra 404 (hull finnes
ikke). `expected_version` valgfri — bakoverkompatibelt.
Offline-køen (`offline-queue.ts`) kjeder nå versjonen fremover
innad i én flush-runde — uten dette ville en spillers EGNE
påfølgende offline-redigeringer av samme hull avvist hverandre som
falske konflikter (funnet under design, før noe ble bygget).
**Reelt UX-hull funnet under scratch-verifisering, rettet før
utrulling:** første versjon gjenbrukte komponentens eksisterende
`error`-tilstand for konfliktmeldingen — viste seg (kun synlig ved
ekte to-enhets-test i nettleser) å være en FATAL, hele-siden-
erstattende tilstand. En forbigående hull-konflikt tok dermed over
hele skjermen. Rettet med en egen, dismissbar `conflictNotice`-
banner (gjenbrukt også for den eksisterende kø-synk-feilmeldingen,
som hadde samme problem fra før).
**Scratch-verifisert i tre lag, alt i ekte nettleser/API:** (1)
begge backend-endepunktene hver for seg — riktig 409/404-skille,
vinnerens data bekreftet bevart, bakoverkompatibilitet uten
`expected_version` bekreftet. (2) To ISOLERTE nettleser-kontekster
(ekte to-enheter-simulering) — konflikt ga 409, veiviseren viste
automatisk riktig (den andres) verdi, banner dismissbart, RESTEN AV
SIDEN forble fullt brukbar (dette avdekket UX-hullet over). (3)
Ekte offline-emulering — to redigeringer av samme hull frakoblet,
begge synkroniserte korrekt ved reconnect, ingen falsk konflikt.
`tsc`/`py_compile` rene. Scratch-ressurser ryddet opp fullstendig,
`teecup_db`s ACL bekreftet uendret.
**Bevisst utenfor omfang:** org-turneringenes match-scoring (helt
separat system/tabell) ikke undersøkt eller endret denne runden.
**Rullet ut 2026-08-10**, bruker bekreftet eksplisitt ("Kjør alt") —
migrasjon 062 kjørt mot ekte `teecup_db` FØR `teecup_api`/
`teecup_frontend` ble bygget/restartet (riktig rekkefølge, ny kode
leser `version`-kolonnen). Bekreftet live: begge containere startet
rent, ingen konsollfeil på `teecup.golf`.
67. **Automatisert backend-testinfrastruktur — RLS, versjonssjekk,
aldersgrense — 2026-08-10, se ADR-058.** Direkte oppfølging av
investor-statusrapporten samme dag: "nesten ingen automatisert
testdekning utenfor HCP-motoren" var det klart største enkeltfunnet.
Bruker ba om å ta tak i akkurat dette.
`scripts/run_backend_tests.sh` automatiserer det som til nå har
vært en manuell prosedyre hver runde: oppretter en scratch-database
+ scratch-rolle i den samme `teeoff_db`-containeren, kjører alle 62
migrasjonene i rekkefølge (med `sed`-patch av migrasjon 002 sitt
hardkodede rollenavn OG databasenavn til scratch-spesifikke verdier
— sistnevnte et nytt funn: uten patch ville testrollen fått en
reell, om enn ufarlig, `GRANT CONNECT`-rettighet på den EKTE
`teecup_db`), bygger en dedikert `Dockerfile.test`-container
(pytest, IKKE i prod-imaget) tilkoblet samme `teeoff_default`-
nettverk som `teecup_api` selv bruker (nødvendig — Postgres-
containeren har ingen host-publisert port i dette miljøet), kjører
testene, dropper scratch-database + -rolle igjen. Rører aldri ekte
`teecup_db` — bekreftet uendret ACL og radantall etter hver kjøring.
17 nye tester, tre områder valgt etter risiko: RLS/tenant-isolasjon
(4 — inkl. regresjonsvern for migrasjon 005s NULL-guard), den
optimistiske versjonssjekken fra punkt 66 (6 — begge
`update_hole`/`update_side_hole`), 13-årsgrensen (5 — inkl. eksakt
dagsgrense begge veier). Testene kaller de FAKTISKE router-/auth-
funksjonene direkte (ikke en SQL-gjenimplementering) — samme
kodesti som produksjon.
**Verifisert:** alle 17 nye + de eksisterende 117 HCP-testene
grønne. Ingen produksjonskode endret denne runden — ren
testinfrastruktur, ingen deploy.
**Bevisst utenfor omfang:** frontend-tester, og dekning av
org-turneringenes match-scoring (`scoring.py`) — som ADR-057
allerede har dokumentert mangler samtidighetsvern, men som
fortsatt ikke har en test som beviser det empirisk.
CI-workflow-filen ble løst SAMME dag, se punkt 68.
68. **Selvhostet Forgejo Actions-runner — 2026-08-10, se ADR-059.**
Direkte oppfølging av punkt 67: en `.forgejo/workflows/
backend-tests.yml` ble lagt til og pushet som en empirisk test —
Forgejo-instansen svarte `has_actions: true`, men det beviste ikke
at noe faktisk plukket opp jobber. Jobben la seg som "Waiting" og
sto uendret i over et minutt. Ingen runner fantes. Bruker ba om
at det rettes ("start med ci").
Satt opp en selvhostet `act_runner` PÅ DENNE serveren (må være her
— `run_backend_tests.sh` er avhengig av `docker exec teeoff_db` og
`teeoff_default`-nettverket, som kun finnes lokalt). Docker-
utenfor-Docker (`docker_host: automount`): jobb-containeren får
host-ens `docker.sock` bind-montert inn, slik at testskriptets
egne `docker build`/`run`/`exec`-kall lager ekte søsken-containere
på host-nivå. Jobb-image `catthehacker/ubuntu:act-latest` (docker-
CLI/git/bash forhåndsinstallert).
Registrerings-tokenet ble ALDRI limt inn i chatten — bruker hentet
det selv fra Forgejo sitt UI, la det i en `chmod 600`-fil på
serveren, Claude leste filen direkte og makulerte den (`shred -u`)
rett etter registrering. Den ekte, langlevde runner-legitimasjonen
som oppsto (`.forgejo-runner/.runner`) er gitignored og `chmod 600`
— samme disiplin som `.env`.
Startet først manuelt for å bevise at det virket, deretter flyttet
inn som en egen `teecup-forgejo-runner`-tjeneste i
`docker-compose.yml` (`restart: unless-stopped`) for samme
driftsdisiplin som resten av stacken.
**Sikkerhetsnotat, bevisst akseptert:** `docker.sock`-tilgang er
root-ekvivalent kontroll over verten — en reell heving av
angrepsflaten. Akseptert fordi det er samme prinsipp
`run_backend_tests.sh` allerede krevde (nå automatisert fremfor
kjørt fra en allerede-priviligert shell), og runneren betjener kun
denne ene repoens CI på en enkeltpersons egen server.
**Verifisert:** ekte push → workflowen kjørte → "Success" i
Forgejo sitt UI (grønn hake, samme kø-oppføring som nettopp sto
"Waiting"). `teecup_db`s rolleoppsett og fravær av gjenglemte
scratch-databaser bekreftet uendret etter kjøring.
69. **Optimistisk versjonssjekk for org-turneringers match-scoring —
2026-08-10, se ADR-060.** Fortsettelse av robusthetslinjen: den
siste dokumenterte, kjente svakheten (ADR-057/058: org-
turneringenes match-scoring hadde ingen samtidighetsvern, ren
siste-skriving-vinner) tettet på brukerens eksplisitte forespørsel.
Migrasjon 063: `version`-kolonne på BÅDE `hole_score` og
`match_hole_result` (to separate tabeller/scoring-modi). Ulikt
round_hole er disse UPSERT (`INSERT ... ON CONFLICT DO UPDATE`),
ikke ren UPDATE — løst med Postgres sin `DO UPDATE ... WHERE`, som
kun evalueres på selve konflikt-grenen. Konsekvens: enklere enn
round_hole — en avvist skrivning er ALLTID en versjonskonflikt,
aldri en "finnes ikke"-tvetydighet.
Reelt funn under design: offline-kø-kjedingen fra punkt 66 nøklet
på URL alene, som IKKE er entydig for `/hole-scores`/`/hole-
results` (samme URL for alle hull i en match). Generalisert med et
nytt, valgfritt `resourceKey`-felt på kø-oppføringen (default url,
100 % bakoverkompatibelt med round_hole sin bruk).
`submit_hole_score`/`submit_hole_result` i `app/routers/scoring.py`,
`session-scorecard.tsx`, `offline-queue.ts`. 6 nye pytest-tester
(`tests/test_scoring_concurrency.py`) — delt-ball og individuell-
ball-grenen hver for seg, begge endepunktene, vellykket skrivning +
409-konflikt + bekreftet at avvist skrivning ikke lagres. Alle 23
backend-tester grønne (kjørt via CI-runneren fra punkt 68). `tsc
--noEmit` rent.
**Rullet ut 2026-08-10**, bruker bekreftet eksplisitt — migrasjon
063 kjørt mot ekte `teecup_db`, begge containere bygget/restartet
rent, ingen konsollfeil. CI plukket opp pushen og kjørte grønt.
70. **Frontend-testinfrastruktur (Vitest) — 2026-08-10, se ADR-061.**
Fortsettelse av robusthetslinjen: frontend-tester var eksplisitt
utenfor omfang i både punkt 67 og 69. Bruker ba om å ta tak i det.
Vitest satt opp (node-miljø, ingen komponenttester ennå). 30 tester
i tre filer: `lib/geo.test.ts` (GPS-avstand/retning, fasit-
forankret mot kjente geodetiske verdier), `lib/offline-queue.
test.ts` (regresjonsvern for `resourceKey`-fiksen fra punkt 69 --
beviser eksplisitt at to ulike hull som deler samme URL ikke lenger
blander sammen versjonsnummer, med `fake-indexeddb` som ekte
IndexedDB-implementasjon), `lib/ny-runde/formats.test.ts`
(fullstendighetssjekk på tvers av format-listene -- fanger opp det
TypeScript sin `as`-cast i `FORMAT_MAP` IKKE garanterer).
To reelle funn underveis: (1) prosjektet bruker pnpm, ikke npm --
et første forsøk med `npm install` feilet (intern npm/arborist-
krasj mot en pnpm-strukturert `node_modules`), ingen skade, løst
ved å bruke riktig verktøy. (2) `frontend/pnpm-workspace.yaml` var
ignorert i `.gitignore` (arv fra V0-sandbox-malen) -- fila fikk nå
reelt CI-relevant innhold (`allowBuilds: sharp: true`), og ville
latt CI feile stille på `[ERR_PNPM_IGNORED_BUILDS]` hver gang uten
å bli sporet i git. Rettet.
Egen, lettvekts CI-workflow (`.forgejo/workflows/frontend-tests.
yml`) -- INGEN Docker-utenfor-Docker, siden denne suiten ikke
rører database/containere. `actions/setup-node@v4` (Node 22) +
`corepack enable` + `pnpm install --frozen-lockfile` + `pnpm test`.
**Verifisert:** 30/30 grønt lokalt. Ingen produksjonskode endret.
71. **Åtte brukerrapporterte UX-funn — "Ny runde" og score-registrering
— 2026-08-11, se ADR-062.** Bruker sendte åtte konkrete, skjermbilde-
dokumenterte problemer. Delt i to spor: fire rettet direkte (reelle
logikkfeil/tekstvalg), fire sendt via V0 (reelt interaksjonsdesign).
**Direkte:** `round-card.tsx` sin "Hull"-celle viste alltid planlagt
antall hull, aldri faktisk spilt, selv for tidlig avsluttede
"Fullført"-runder — rettet (`played < round.holes` avgjør nå om
avviket vises, uansett status). Putt-avstand-knappenes etiketter
endret fra et blandet "<Xm ... 8m+"-mønster til konsekvent
"X-Ym" (kun visningstekst, ikke backend-kontrakten) — bekreftet via
git-historikk at dette var Claude-forfattet, ikke V0. "Se feed" på
dashbordet endret til "Se venneaktivitet". @-tagging av medspillere
i feeden vurdert og UTSATT (egen fremtidig funksjon) — anbefalt
personvern-begrensning (kun faktiske relasjoner) og ekte søkbar
nedtrekksliste (ikke fritekst-tolkning) notert til den runden.
**Via V0** (`tee-cup (9).zip`, kun `delivery/`-mappen tatt inn, diffet
mot live-treet først): ny-runde steg 2 sin "Antall hull"/"Avanserte
handicap-innstillinger" flyttet FØR formatrutenettet (var sist, lett
oversett). Score-registreringens "detaljer"-steg delt i "Retning"/
"Detaljer per hull"-seksjoner + en ny `ScrollFade`-hjelpekomponent
(bunn-fade + nedoverpil, ResizeObserver, auto-skjuler ved bunn) --
løser at brukere ikke visste de måtte skrolle. Anywayslag endret fra
tall-rutenett til +/−-stepper (samme mønster som Chip/Bunker/
Straffeslag nå). Netto par-markør (oransje prikk + kontrastring +
tekstlig forklaring, tilgjengelig uten fargesyn) på Slag-knappene,
kan opptre samtidig med "valgt"-tilstand. Putter-velgeren fikk egen
oransje aksent + flagg-ikon for å skille den fra Slag-velgeren.
**Verifisert:** `tsc --noEmit` rent. Full scratch-stack (DB, MinIO,
API, frontend — ekte produksjonsbuild). Ekte testbruker, egen bane
opprettet via API med HCP 24 for å garantere synlig netto-par-
markør. Browserverifisert (mobil viewport, lys+mørk modus): ny
rekkefølge, seksjonsdeling, scroll-hint (bekreftet både synlig og
auto-skjult), netto-par-prikk alene og kombinert med valgt-tilstand,
Putter-aksent, nye putt-avstand-etiketter. Ingen konsollfeil.
Scratch-miljøet ryddet opp fullstendig, `teecup_db` bekreftet uendret.
**Rullet ut 2026-08-11**, bruker bekreftet eksplisitt ("Kjør på") —
`docker compose build teecup_frontend && up -d`, ren omstart,
bekreftet live på `teecup.golf`, ingen konsollfeil utover den
harmløse PWA-infomeldingen.
**Oppfølging samme dag — punkt 5 (ChoiceRow) ikke løst.** Bruker
rapporterte at "Avstand første putt" fortsatt viste 5+1 i stedet for
3+3. Root cause: `ChoiceRow` (delt komponent) brukte `flex flex-wrap`
fremfor et fast rutenett, pakket knapper etter tekstbredde i stedet
for et forutsigbart antall. Rettet til `grid grid-cols-3` (samme
mønster `NumberPicker` allerede brukte). Visuelt uendret for de fire
andre `ChoiceRow`-bruken (alle har 3 valg fra før). Browserverifisert
lys+mørk. Committet (`17e4793`), ikke deployet ennå.
72. **@-tagging av medspillere/venner i runde-feeden — 2026-08-11, se
ADR-063.** Bakt inn i FEATURE_BACKLOG.md som egen sak under
ADR-062, bruker ba om å sette den i gang.
Migrasjon 064: `round_message_tag`/`round_message_comment_tag` (to
parallelle tabeller, samme mønster som `round_message_reaction` vs.
`message_reaction`). Kun FAKTISKE relasjoner er taggbare (rundens
lenkede deltakere UNION avsenderens venner) — aldri fritekstsøk i
hele brukerbasen, håndhevet både i nytt søkeendepunkt
(`GET /rounds/{id}/taggable-people`) og server-side ved innsending.
Tag-posisjon lagres som tegn-offset inn i den (aldri redigerbare)
body-teksten — trygt, teksten kan aldri gå ut av synk. Ugyldige tags
forkastes stille, hele innlegget avvises aldri av den grunn. Varsel
(`type="round"`, gjenbruker eksisterende kategori) til hver gyldig
tagget person.
**Verifisert:** 8 nye pytest-tester
(`tests/test_message_tags.py`) — kandidatliste, søkefilter, gyldig
tag + varsel bekreftet, tre typer ugyldig-tag-forkastelse. Alle 31
backend-tester grønne. `teecup_db` uendret.
**Frontend — samme dag, egen-implementert.** Frontend var
opprinnelig sendt som eget V0-prompt, men bruker gikk tom for
V0-credits ("Kan du gi promptet til deg selv, og gjøre det beste
ut av det?") — eksplisitt, avgrenset unntak fra prosjektets
stående "frontend via V0"-konvensjon for akkurat denne funksjonen.
`frontend/lib/mentions.ts` (ren offset-/diff-logikk: @-deteksjon,
innsetting, diff-justering av eksisterende tags ved redigering,
tekst-segmentering — 15 enhetstester) +
`frontend/components/mention-input.tsx` (`MentionTextarea` med
debouncet autocomplete-dropdown + tastaturnavigasjon, `TaggedText`
som rendrer tagger som lenker til `/my-friends/{id}`). Koblet inn i
`round-messages.tsx` (innlegg), `post-engagement.tsx` (kommentarer
— delt komponent mellom FIRE meldingssystemer, `roundId`/`tags`
gjort valgfrie for å ikke bryte `feed.tsx`/`public-tournament.tsx`/
`team-chat.tsx`, hvorav de to siste bevisst IKKE får tagging) og
`feed.tsx` (aggregert `/my-feed`-visning). Fant og rettet et hull
underveis: backendens `GET /feed` manglet `tags` i `FeedEntryOut` —
kun `list_round_messages`/kommentar-endepunktene hadde det fra
før, selv om `/my-feed` var nettopp det opprinnelige eksempelet
("Fredrik i farta") som motiverte hele funksjonen.
**Verifisert (frontend):** `tsc --noEmit` rent, 45/45 vitest
grønt. Full scratch-stack (DB, MinIO, API, frontend — ekte
produksjonsbuild). To ekte testbrukere (venn-relasjon + felles
runde) browserverifisert: autocomplete-dropdown i både innleggs-
og kommentar-komposereren, tag rendret som lenke i selve
rundevisningen OG i aggregert feed, varsel bekreftet opprettet
(`GET /notifications`), personvern-scoping bekreftet i selve UI-et
(skriver "@" alene → kun faktisk relasjon tilbys, aldri seg selv
eller en fremmed), lys+mørk modus. Ingen konsollfeil. Scratch-
miljøet ryddet fullstendig opp, `teecup_db` bekreftet uendret før
og etter.
Org-turneringenes "Banter Board" (`message`/`message_comment`)
fikk IKKE samme funksjon — separat system, fortsatt bevisst
utenfor omfang.
**Rullet ut 2026-08-11**, bruker bekreftet eksplisitt ("ja").
Migrasjon 064 kjørt mot ekte `teecup_db` (kun nye tabeller, ingen
endring av eksisterende), deretter `docker compose build teecup_api
teecup_frontend && up -d` — samme kommando ryddet også inn den
tidligere upubliserte ChoiceRow-fiksen (`17e4793`). Ren omstart,
ingen feil i containerloggene, ingen konsollfeil på
`https://teecup.golf/logg-inn` etter omstart.
73. **Fiks: ScrollFade-scrollhintet (fra ADR-062 punkt 6) hadde to
reelle feil — 2026-08-11, se oppfølgingsnotatet i ADR-062.**
Bruker sendte skjermbilde: "Den sprettende ned-pilen fungerer
ikke. Den ligger OPPÅ en annen nedpil."
To separate rotårsaker i samme `ScrollFade`-komponent
(`round-detail.tsx`): (1) `ResizeObserver` observerte scroll-
beholderen i stedet for innholdet — beholderens boksstørrelse er
fast, så observeren fyrte aldri ved stegbytte i veiviseren, og
hintet virket dermed rett og slett ikke på steg med reelt
overflow-innhold (som Retning-steget). Rettet ved å observere en
egen `contentRef`-div rundt innholdet i stedet for beholderen selv.
(2) Fade-/piloverlayet (absolutt posisjonert, siste 64px av
scroll-området) hadde ingen garanti mot å dekke ekte innhold —
på Retning-steget landet "Kort"-knappens eget `ArrowDown`-ikon i
akkurat den sonen, to piler oppå hverandre. Rettet med en usynlig
64px-buffer etter innholdet (`hasMore`-utregningen trekker fra
samme høyde for å unngå falske positiver på kort innhold).
**Verifisert:** Full scratch-stack, testbruker med fullt
kølle-utvalg for å reprodusere nøyaktig samme rutenett som i
brukerens skjermbilde. Bekreftet: hintet vises (`opacity: 1`) når
steget faktisk overflower, forsvinner (`opacity: 0`) ved reell
bunn, "Kort" fullt synlig og klar av overlay-sonen ved skrolling,
lys+mørk, ingen konsollfeil. `tsc --noEmit` rent, 45/45 vitest
grønt. `teecup_db` urørt (ren frontend-endring). Committet
(`ee91d03`).
**Rullet ut 2026-08-11**, bruker bekreftet ("ja"). Kun
`teecup_frontend` bygget og restartet (ren frontend-endring, ingen
migrasjon). Ingen feil i containerloggene, ingen konsollfeil på
`https://teecup.golf/logg-inn` etter omstart.
74. **Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert
/my-rounds/stats — 2026-08-12.** Tre separate tilbakemeldinger fra
bruker etter å ha sett gjennom statistikksidene med skjermbilder.
**round-stats.tsx:** trekkspillseksjonene starter nå ekspandert.
Reverserer en eksplisitt tidligere beslutning (2026-07-25, "trekk-
spillet skal vises sammenslått") — kommentaren i koden oppdatert til
å forklare hvorfor, ikke bare strøket.
**Dashbordets "HCP nå"** viste tidligere det manuelt satte tallet i
profilen (`handicap_index`) — brukeren ville ha "spilt til"-HCP, dvs.
`computed_handicap_index` (allerede beregnet og eksponert på
`/auth/me`, brukt av Konto-sidens "Faktisk HCP (beregnet)"-kort, bare
ikke koblet inn på dashbordet). Historikk-sparklinen filtreres nå
også til kun `source="computed"` — den blandet tidligere manuelle og
beregnede punkter i samme linje.
**`/my-rounds/stats` fikk tre endringer:**
- Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig
selvforklarende. Teksten fantes tidligere kun som skjermleser-tekst
— seende brukere så bare et ikon og et tall, ingen kontekst for hva
det ble sammenlignet mot. Bruker: "Selv ikke jeg... forstår helt
hva som menes."
- Greentreff (GIR) og Innspill splittet i to separate kort (GIR er en
beregnet andel, Innspill er en retningsbeskrivelse — samme prinsipp
som round-stats.tsx sin eksisterende splitt mellom "Greentreff
(GIR)" og "Bom-retning på green"). Utslag flyttet til å vises
først. Redning og "Til par: med vs. uten" flyttet ned under Annet.
- Ny stolpegraf med glidende snitt-trendlinje (snitt til par per
runde, eldste runde til venstre) — tidligere bevisst utsatt ("for
lite rundehistorikk til å vise noe meningsfullt", se punkt 2026-
07-28 tidligere i denne loggen), bygget nå som brukeren har nok
fullførte runder. Backend: `RoundStatsSummary` fikk et nytt
`round_series`-felt (kronologisk snitt-til-par per fullført runde
i valgt tidsvindu, sortert eldste først).
**Verifisert:** `tsc --noEmit` rent, 45/45 vitest, 31/31 backend-
tester (inkl. `round_series`-feltet, kjørt mot en scratch-database).
Full scratch-stack med seks syntetiske runder (varierende dato og
resultat, for å faktisk se en trend) browserverifisert lys+mørk:
HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt
15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-
bildetekster på et vindu med reell forrige-periode-sammenligning,
graf med trendlinje, trekkspill ekspandert på rundesiden. Ingen
konsollfeil. `teecup_db` urørt gjennom hele verifiseringen.
**Rullet ut 2026-08-12**, bruker bekreftet ("Jeg bekrefter"). Ingen
migrasjon (kun et nytt beregnet felt i et eksisterende API-svar) --
`docker compose build teecup_api teecup_frontend && up -d`. Ren
omstart, ingen feil i containerloggene, ingen konsollfeil på
`https://teecup.golf/logg-inn` etter omstart.
75. **Statistikk: lineær regresjon i stedet for glidende snitt, trend på
alle variabler — 2026-08-12.** Oppfølging til punkt 74, samme dag.
Bruker: "Trendlinjen din viser jo egentlig bare det samme som
stolpediagrammet" — korrekt, et 3-runders glidende snitt over så få
punkter følger bare stolpene med én runde forsinkelse, smoother
ingenting reelt.
Presenterte tre alternativer via AskUserQuestion (lineær regresjon /
eksponentielt glidende snitt / kumulativt løpende snitt) FØR noe ble
bygget, jf. brukerens eksplisitte "Gi meg forslag før vi
implementerer". Bruker valgte lineær regresjon (minste kvadraters
metode) — en rett linje, stigningstall = "endring per runde", vist i
bildeteksten (f.eks. "+7 pp per runde"). Visuelt en helt annen form
enn stolpene, løser selve klagen.
Bruker ba samtidig om trend på ALLE variabler. Lagt til stolpe+trend
for Fairwaytreff, Greentreff (GIR), Putt/18, Én-putt, Chip/runde,
Bunkerslag/runde, Straffeslag/runde og Anywayslag/runde, hver i sitt
eksisterende kort. Scrambling/sand save unntatt (brukervalg via
samme AskUserQuestion): for få kvalifiserte hull per runde til et
per-runde-tall, vises i stedet som én kumulativ linje.
Backend: `round_series` flyttet fra `RoundStatsSummary` til
`RoundStatsWindowSummary`-nivå, utvidet med per-runde-tall for alle
variabler pluss kumulativ scrambling/sand save. Fanget og rettet en
enhets-bug under egen scratch-verifisering (før commit): scrambling/
sand save ble først bygget på 0–1-skala i stedet for samme 0–100-
skala som resten av API-et.
**Verifisert:** `tsc --noEmit` rent, 45/45 vitest, 31/31 backend-
tester. Full scratch-stack med åtte syntetiske runder (bevisst
støyende fremgang) browserverifisert lys+mørk: regresjonslinjen
bekreftet rett og visuelt distinkt fra stolpene på alle variabler,
riktig fortegn/farge på stigningstall, ingen dobbel enhet i
bildetekstene, kumulative linjer konvergerer som forventet. Ingen
konsollfeil. `teecup_db` urørt.
**Rullet ut 2026-08-12**, bruker bekreftet ("Jeg bekrefter"). Ingen
migrasjon -- `docker compose build teecup_api teecup_frontend &&
up -d`. Ren omstart, ingen feil i containerloggene, ingen konsollfeil
på `https://teecup.golf/logg-inn` etter omstart.
76. **Statistikk: y-akse-benevnelser + verdi i begge ender på
trendgrafene — 2026-08-12.** Oppfølging til punkt 75, samme dag.
Bruker: "Y-axen må ha verdi i begge ender" + ønske om benevnelse på
y-aksen.
`BarTrendChart`/`CumulativeTrendLine` (`rounds-stats-summary.tsx`)
fikk en ny `yAxisLabel`-prop -- rendres som EKTE HTML-tekst over hver
graf (ikke roterte SVG-glyffer), slik at den skalerer med brukerens
skriftstørrelse i stedet for å bli uleselig ved zoom
(tilgjengelighetsregelen i CLAUDE.md). Benevnelser: "Slag til par"
(Utvikling), "%" (alle prosentgrafer: Fairwaytreff, Greentreff,
Én-putt, Scrambling/Sand save), "Putt" (Putt/18), "Antall" (Chip/
Bunkerslag/Straffeslag/Anywayslag).
`BarTrendChart` viste tidligere kun verdi-etikett på SISTE (nyeste)
stolpe. Lagt til samme etikett på FØRSTE stolpe også (dempet stil,
samme mønster som `CumulativeTrendLine` allerede brukte for sine
start-/sluttpunkt-etiketter) -- grafen kan nå leses uten en egen
tallskala i begge ender, ikke bare ved siste punkt.
**Verifisert:** `tsc --noEmit` rent, 45/45 vitest.
77. **Avstand til mål (rangefinder) via GolfAPI.io — Tjøme Golfklubb,
2026-08-12.** Ny, tredje banekilde (`app/golfapi_client.py`,
`app/golfapi_cache.py`) for baner utenfor TeeOffs dekning. Full
detalj og alle beslutninger i **ADR-064**, kort oppsummert her:
Brukeren fikk et GolfAPI.io-token (20 kall) og ba om avstands-
visning til grønn på baner TeeOff ikke dekker -- konkret testet mot
Tjøme Golfklubb, som brukeren bekreftet IKKE finnes i TeeOff. Live
validert mot ekte GolfAPI FØR noe ble bygget (søk, fullt scorekort,
167 koordinatpunkter for Tjøme -- grønn front/midt/bak, hindringer,
tee-punkter), 2,3 av 20 kall brukt på research.
Migrasjon 065: delt, globalt cache-lag (`golfapi_course`/`_hole`/
`_tee`/`_coordinate`) -- hentes KUN én gang noensinne per fysisk
bane (GolfAPIs avtale tillater eksplisitt permanent caching), via
én sentral chokepoint-funksjon som all import-kode må gå via.
`course_source` utvidet med `'international'` (org-turneringer);
`personal_course` fikk en ny `external_golfapi_course_id`-kolonne
(frittstående runder) -- bekreftet med bruker at BEGGE skal
støttes. Nye endepunkter: `international-search`/`-import` i både
`courses.py` (org) og `rounds.py` (personal), samt
`GET /rounds/{id}/holes/{n}/target-points` (returnerer RÅ
koordinater, aldri en ferdigregnet avstand -- klienten Haversine-
regner selv mot spillerens ferske GPS-posisjon, samme
`frontend/lib/geo.ts`-funksjon som ADR-048).
v1-visning: ren tall-/tekstvisning ("142 m front/151 m midt/163 m
bak"), intet kart -- bekreftet med bruker (AskUserQuestion), null
ekstra Mapbox-kostnad. En fremtidig freemium-idé (gratis=tall,
betalt=interaktivt kart) ble foreslått av bruker og notert i
ADR-064, ikke bygget (ingen betalingsinfrastruktur finnes).
Frontend: søk-og-koble-UI lagt til i `tournament-program.tsx`
(org-siden, to kallsteder) og `round-detail.tsx` (frittstående
runder, "Fant ikke banen? Søk internasjonalt"-fallback under egen
bane-søk).
**Rangefinder-visningen, samme dag:** V0-prompten
(`v0-prompt-rangefinder.md`) ble kjørt av bruker (zip 10,
`Temp-uploads/tee-cup (10).zip`) -- `components/target-distance.tsx`
kom tilbake nøyaktig som spesifisert (props-drevet, ingen egen GPS-/
fetch-logikk, clubhouse-paletten korrekt brukt). Koblet inn av
Claude via en ny wrapper, `components/hole-target-distance.tsx`
(henter `target-points`, kjører `watchPosition` -- samme
`enableHighAccuracy`/`maximumAge: 1000`/grasiøs-degraderings-mønster
som `map-point-picker.tsx` allerede bruker for slagmåling, ADR-048
tillegg 2026-08-08 del 2 -- og Haversine-regner selv), montert i
`ScoringWizard` sin hull-header i `round-detail.tsx`. Viser INGEN
rad i det hele tatt for baner uten GolfAPI-koordinatdata (det store
flertallet av runder) -- ikke en "ingen data"-lapp på hver eneste
scorekort-åpning.
**Reell driftsfeil fanget under denne utrullingen:**
`TEECUP_GOLFAPI_TOKEN` var satt i `.env`, men `docker-compose.yml`
sin `teecup_api`-tjeneste videreførte den aldri til containeren --
endepunktene ville svart "ikke konfigurert" i produksjon uansett
hva `.env` sa. Lagt til i miljø-blokken, samme mønster som de andre
`TEECUP_*`-hemmelighetene.
**Verifisert:** migrasjon 065 kjørt mot automatisert scratch-
database (`scripts/run_backend_tests.sh`), 31/31 eksisterende
backend-tester uendret grønne. Egen scratch-runde med de FAKTISKE
Tjøme-svarene som fixtures (monkeypatchet `golfapi_client`, for å
ikke bruke flere av de knappe API-kallene under iterasjon):
cache-chokepunktet bekreftet idempotent (henter GolfAPI nøyaktig én
gang, aldri på nytt ved gjentatte kall), 18 hull + 4 tees + 167
koordinater cachet korrekt, org-import ga riktig antall
hull/tee_rating-rader, duplikat-import avvist i BEGGE modeller,
target-points-oppslaget ga korrekte front/midt/bak-punkter. **Ett
bevisst, siste ekte kall** mot GolfAPI (2 kall) bekreftet at hele
kjeden -- inkl. `golfapi_client.py` sitt faktiske HTTP-kall, ikke
bare cache-logikken -- fungerer mot den virkelige tjenesten.
`teecup_db` bekreftet uendret gjennom hele verifiseringen
(scratch-database+rolle droppet etterpå). Frontend: `tsc --noEmit`
rent, 45/45 vitest.
**Full-stack scratch-verifisering av rangefinder-koblingen**
(separat scratch-database/API/frontend/MinIO-sett, GolfAPI-cachen
forhåndsseedet fra de samme lagrede Tjøme-fixturene -- null nye
API-kall): ekte innlogging, import av Tjøme via det virkelige
`POST /personal-courses/international-import`-endepunktet (cache-
treff, 0 kall), opprettet en runde, åpnet scoreførings-veiviseren
for hull 1 i nettleseren (Chrome DevTools MCP, geolocation-
emulering). Bekreftet visuelt i BÅDE lys og mørk modus: "Front
118 m / Midt 132 m / Bak 144 m" (Midt uthevet), "● Live"-indikator,
nærmeste hindring korrekt identifisert og vist ("Bunker (grønn)
(front): 104 m"), ingen konsollfeil. `tsc --noEmit` rent, 45/45
vitest. Scratch-database/rolle/containere ryddet opp etterpå,
`teecup_db` bekreftet uendret (`\l`-sjekk).
**Rullet ut 2026-08-12**, bruker bekreftet ("kjør" / "NÅ kan du
kjøre på" etter å ha lastet opp V0-eksporten). Migrasjon 065 kjørt
mot ekte `teecup_db` (additiv, ingen eksisterende data rørt) --
`docker exec -i teeoff_db psql ... -f 065_golfapi_courses.sql`,
deretter `docker compose build teecup_api teecup_frontend && up -d`
(kjørt TO ganger denne runden -- første gang før rangefinder-
komponenten/docker-compose-fiksen var klare, brukeren stanset
bevisst midtveis for å laste opp V0-eksporten først, andre gang
etter). Rene containerlogger begge ganger, ingen konsollfeil på
`https://teecup.golf/logg-inn` etter siste omstart. ~15,5 av 20
GolfAPI-kall gjenstår.
**Tillegg 2026-08-13 -- den PRIMÆRE "Ny runde"-veiviseren manglet
GolfAPI-søket, og en reell ruting-bug ble funnet og fikset.**
Brukeren spurte "hvor, nøyaktig, kan jeg se dette implementert?" --
undersøkelsen avdekket at søk-og-koble-UI-en fra 2026-08-12 KUN var
koblet inn i "endre bane på en allerede opprettet runde"-skjemaet
(`ChangeCourseForm`, round-detail.tsx), IKKE i den faktiske
`/my-rounds/new`-veiviseren (`components/ny-runde/step1-course-
time.tsx` + `lib/ny-runde/api.ts`) -- en helt separat komponenttre
brukeren faktisk møter FØRST. Lagt til der også: ny
`"international-search"`-delstilstand (`Sub1`-typen),
`InternationalSearch`-komponent (samme klubb->baner-todelte søk som
de to andre stedene, i `--nr-*`-designtokens), `searchInternationalClubs`/
`importInternationalCourse` i `lib/ny-runde/api.ts`.
**Ekte bug fanget under nettleserverifisering av DENNE
integrasjonen** (ikke funnet i forrige runde fordi kun POST
`/personal-courses/international-import` ble reelt HTTP-testet der,
aldri GET `/personal-courses/international-search`): 500-feil,
`asyncpg.exceptions.DataError: invalid input for query argument $1:
'international-search' (invalid UUID...)`. Årsak: FastAPI matcher
ruter i REGISTRERINGSREKKEFØLGE -- `GET /personal-courses/
{personal_course_id}` (allerede eksisterende, generisk) var
registrert FØR den nye, mer spesifikke `GET /personal-courses/
international-search`, så `{personal_course_id}` fanget grådig opp
"international-search" som en (ugyldig) UUID. Samme fellgruve
fantes IKKE på org-siden (courses.py) fordi det der ikke finnes noen
bar `/courses/{course_id}`-rute å kollidere med (kun `/{course_id}/
tees` og `/{course_id}/holes`, annen path-form). Fikset ved å flytte
de to nye endepunktene til FØR `get_personal_course` i
`rounds.py` -- lærdom kommentert direkte i koden for å unngå samme
feil neste gang en literal-path-rute legges til ved siden av en
eksisterende parameterisert rute.
**Verifisert på nytt, fullt ut, etter fiksen:** frisk scratch-stack,
ekte innlogging, full klikk-gjennom-flyt i den FAKTISKE "Ny
runde"-veiviseren (Egen bane -> "Fant ikke banen? Søk
internasjonalt" -> søk «Tjøme» -> Tjøme Golfklubb -> importer ->
landet korrekt på "Bane & tid"-steget med "Tjøme Golfklubb – Bane",
4 utslag), ingen konsollfeil, bekreftet via nettverksfane at søket
nå returnerer 200 med reelt klubbtreff (ikke lenger 401/500).
`tsc --noEmit` rent, 45/45 vitest, 31/31 backend-tester. Scratch-
ressurser ryddet opp, `teecup_db` bekreftet uendret.
**Rullet ut 2026-08-13**, samme økt. `docker compose build
teecup_api teecup_frontend && up -d`. Rene containerlogger, ingen
konsollfeil på `https://teecup.golf/logg-inn` etter omstart.
**Tillegg samme dag (del 3) -- feil skjerm.** Bruker sendte et
skjermbilde fra ekte produksjon (hadde selv importert Tjøme og
opprettet en runde) og pekte ut at avstandsindikatoren skulle vært
synlig i HOVEDVISNINGEN (`PlayerHoleCards`, hull-navigator-kortet
over spillerlisten -- det brukeren faktisk ser når de går ut på
hullet), ikke inne i scoreførings-veiviseren (dit den ble montert
2026-08-12 -- feil skjermøyeblikk, en spiller åpner den veiviseren
ETTER hullet er spilt, ikke før tilnærmingsslaget). Ba også om mer
bredde enn kompaktvarianten som satt der.
`HoleTargetDistance` sin egen rad-wrapper (border/padding, tilpasset
veiviser-headeren) fjernet -- kalleren styrer nå plassering/bredde
selv. Flyttet fra `ScoringWizard`-headeren til `PlayerHoleCards`,
rett under hull-navigatoren (Forrige/Hull N/Neste + hurtig-hopp-
stripen) og over spillerkortene -- nøyaktig der brukeren pekte,
`size="full"` i stedet for `"compact"`.
**Verifisert:** ny scratch-runde (samme fixture-cachede Tjøme-data,
0 nye API-kall), ekte innlogging, ekte runde, geolocation-emulering
+ `initScript`-injisert `watchPosition`-shim (persisterer over en
reload, ulikt en engangs `evaluate_script`-injeksjon). Bekreftet
visuelt lys+mørk: full-bredde "Til grønn"-kort rett under hull-
navigatoren, front/midt/bak + hindringslinje + live-indikator,
ingen konsollfeil. `tsc --noEmit` rent, 45/45 vitest. Scratch-
ressurser ryddet opp, `teecup_db` uendret.
**Rullet ut 2026-08-13**, samme økt. Ren frontend-endring (ingen
backend/migrasjon rørt) -- `docker compose build teecup_frontend &&
up -d teecup_frontend`. Ingen konsollfeil på
`https://teecup.golf/logg-inn` etter omstart.
**Tillegg samme dag (del 4) -- strømsparing.** Bruker rapporterte
høyt batteriforbruk nå som GPS-en er aktivert. To justeringer i
`hole-target-distance.tsx`, foreslått med tradeoffs FØR bygging (jf.
"Gi meg forslag før..."-arbeidsmåten): (1) `maximumAge` økt fra
1000ms til 4000ms -- en golfer beveger seg ~1,5 m/s, oppdatering
hvert 4. sekund er umerkelig men ber GPS-brikken om et ferskt fix
langt sjeldnere. (2) `watchPosition` pauses nå helt når fanen/appen
ikke er synlig (`document.visibilitychange`) -- ingen vits å holde
GPS aktiv for en skjerm ingen ser på (skjerm låst, annen app i
forgrunnen) -- og gjenopptas automatisk når brukeren kommer tilbake.
`enableHighAccuracy` bevisst UENDRET -- lav nøyaktighet ville gitt
titalls meters feilmargin, for upresist til at front/midt/bak-
tallene gir mening.
**Verifisert:** ny scratch-runde (samme fixture-cachede Tjøme-data),
ekte innlogging/import/runde, geolocation-emulering +
`initScript`-injisert `watchPosition`/`clearWatch`-spion (telte
faktiske kall). Bekreftet i nettleseren: `document.hidden = true` +
`visibilitychange` → `clearWatch` kalt umiddelbart, visningen gikk
til "Ikke live — sist målte posisjon" (siste kjente tall beholdt,
ikke nullstilt); `document.hidden = false` + `visibilitychange` →
`watchPosition` kalt på nytt (kall-telleren gikk fra 1 til 2), "●
Live" gjenopprettet. `tsc --noEmit` rent, 45/45 vitest, ingen
konsollfeil. Scratch-ressurser ryddet opp, `teecup_db` uendret.
**Rullet ut 2026-08-13**, samme økt. Ren frontend-endring --
`docker compose build teecup_frontend && up -d teecup_frontend`.
Ingen konsollfeil på `https://teecup.golf/logg-inn` etter omstart.
78. **Per-deltaker-fullføring av frittstående runder + scorekort på
e-post — 2026-08-13, se ADR-065.** Bruker: fire spillere sammen,
to går 18 hull, en 9, en må avslutte etter 14 -- disse rundene
skulle kunne markeres fullført per spiller, uten å måtte fullføre
hele runden, og uten å låse scoreføring for de andre.
Migrasjon 066: `round_participant.completed_at timestamptz`.
Backend (`app/routers/rounds.py`): `ParticipantUpdate.completed`,
ny `_FLIGHT_MANAGED_PARTICIPANT_FIELDS = {"completed"}` -- speiler
`update_hole`s "lenket medspiller kan registrere for hele
flighten"-filosofi, IKKE eier-/selv-only. `ALREADY_COMPLETED` (409)
hvis `round.completed_at` allerede er satt. `_send_guest_round_
summaries` erstattet av `_send_participant_round_summary(conn,
round_id, participant_id, round_label) -> bool`, som nå dekker
BÅDE gjester og lenkede kontoer (LEFT JOIN `app_user`) -- kun
første `null -> true`-transisjon trigger e-post (idempotent).
`complete_round` har en fallback-løkke som fullfører+e-poster
enhver deltaker som IKKE allerede ble individuelt avsluttet, ingen
dobbel-sending for de som var det. `app/email.py`s `send_round_
summary_email` fikk `is_linked_account`/`round_id` -- lenkede
mottakere får direktelenke til `/my-rounds/{id}` og hilsen med
`first_name` (navneformat-regelen), gjester beholder magic-link.
**Tillegg samme dag under samtalen:** må også kunne sende
scorekort til en enkelt spiller i ETTERTID, lenge etter at runden
er fullført. Nytt endepunkt `POST /rounds/{id}/participants/{id}/
send-scorecard` -- samme flight-styrte tilgang, men bevisst INGEN
`ALREADY_COMPLETED`-sperre (leser/sender, muterer aldri
databasetilstand). 204 ved suksess, `NO_EMAIL_ADDRESS` (400) uten
registrert adresse.
**Verifisert:** `python3 -m py_compile` + full
`./scripts/run_backend_tests.sh`. Egen scratch-database: 4-spiller
runde (lenket bruker + gjest med e-post), ulikt hull-antall,
avsluttet to deltakere via PATCH -- bekreftet e-post umiddelbart
med riktig mottaker-variant, `ALREADY_COMPLETED` etter
rundefullføring, fallback-løkken sender kun til ikke-avsluttede,
lenket medspiller (ikke eier) kan avslutte ANNEN deltaker
(flight-tilgang bekreftet), angre (`completed: false`) fungerer
før rundefullføring, on-demand-endepunktet fungerer også lenge
etter fullføring.
**Rullet ut 2026-08-13**, samme økt, sammen med migrasjon 067
(punkt 79 under). `docker compose build teecup_api && up -d`.
**Tillegg samme dag -- frontend-UI bygget og rullet ut.**
`Player.completed` (fra `ApiParticipant.completed`), delt
`ParticipantCompletionActions`-komponent i BÅDE `PlayerHoleCards`
(Score-fanen) og `PlayerList` ("Spillere og runde"-fanen):
"Avslutt for [navn]" (skjult når runden er fullført),
"Ferdig ✓ (N hull)" + "Angre" (kun før rundefullføring),
"Send scorekort" (alltid synlig, uansett fullføringsstatus).
`allHolesEnteredForEveryone` + hurtig-hopp-stripen teller en
`completed`-spiller som ferdig uansett faktisk hull-dekning.
`advanceWizardPlayer` hopper over fullførte spillere. Et ikke-spilt
hull for en fullført spiller viser "Ferdig" i stedet for
"Registrer".
**Verifisert:** `tsc --noEmit` rent, 45/45 vitest. Ekte
nettleserverifisering (ekte innlogging inkl. reell TOTP-2FA-kode
generert fra kontoens lagrede secret, ekte runde på Tjøme -- cachet
bane, 0 nye GolfAPI-kall -- to deltakere): avslutning av gjesten
trigget e-post umiddelbart (ingen feil i loggen), UI oppdaterte til
"Ferdig ✓"/"Angre"/"Ferdig"-tekst korrekt, "Send scorekort" for
eieren (lenket konto, annen kodesti enn gjesten) ga "Sendt ✓",
"Angre" tilbakestilte og var persistent etter fane-bytte. Lys+mørk
bekreftet visuelt, ingen konsollfeil. Testrunden slettet etter
verifisering, bekreftet fjernet fra `teecup_db`.
**Rullet ut 2026-08-13**, samme økt. Ren frontend-endring (ingen ny
migrasjon) -- `docker compose build teecup_frontend && up -d
teecup_frontend`.
79. **To nye baner (Larvik/Seasidebanen, Nesbyen/Nesfjellet) via
GolfAPI + en reell datakvalitetsbug funnet under importen —
2026-08-13, se ADR-064-tillegg.** Bruker ba om å mappe Larvik
(Seasidebanen) og Nesbyen, sistnevnte manuelt GPS-registrert (124
feltbefarte punkter, CSV) siden GolfAPI selv mangler koordinater
for banen (`hasGPS=0`).
To nye `poi_type`-verdier (migrasjon 067): `rock` (fjellknaus --
hinder) og `layup` (til fairway -- kun informativt). Bruker
korrigerte terminologi underveis: "Bunker (green)" ikke "Bunker
(grønn)" (green er et låneord, ikke oversatt), og "Over vann"
skal telle som "Bakkant vann" (samme `poi_type`/`location`).
**Reell bug funnet under import:** GolfAPI returnerer noen ganger
tom STRENG (`""`) i stedet for `null`/fraværende for course
rating/slope-felt -- ikke bare for kvinner (ADR-019s tilsvarende
antakelse for teeoff), Nesbyen manglet `slopeMen` også. Fikset på
tre nivåer: `golfapi_cache.py` normaliserer `""` -> `None` for
alle fire ratingfelt, migrasjon 068 gjør `golfapi_course_tee.
course_rating_men`/`slope_men` nullable (var feilaktig NOT NULL),
og begge import-endepunktene (`courses.py`, `rounds.py`) bygger nå
rating per utslag defensivt -- hopper over et utslag uten brukbar
rating, avviser hele importen med `EXTERNAL_DATA_INCOMPLETE` FØR
transaksjonen hvis INGEN utslag har noen brukbar rating. Nesbyens
6 utslag hadde faktisk ingen rating i det hele tatt hos GolfAPI --
bruker oppga alle 6 (herre+dame, ett uten damerating) manuelt fra
klubbens scorekort, satt inn i `golfapi_course_tee`-cachen før
import fullførte.
**Verifisert:** `python3 -m py_compile` rent, full
`./scripts/run_backend_tests.sh` (31/31, migrasjon 068 bekreftet
anvendbar). Selve importen kjørt mot ekte `teecup_db` via et
engangsskript i `teecup_api`-containeren (speiler
`import_international_personal_course`s kopier-inn-logikk).
Etterpå bekreftet med read-only spørring: Larvik (18 hull, 6
utslag, 11 tee-ratinger, 95 koordinater) og Nesbyen (18 hull, 6
utslag, 11 tee-ratinger, 124 koordinater), begge eid av Erol
Haagenrud, `golfapi_course.has_gps`/`num_coordinates` korrigert
for Nesbyen.
**Rullet ut 2026-08-13**, samme økt. Migrasjon 067+068 kjørt mot
ekte `teecup_db`, `docker compose build teecup_api && up -d`, ren
oppstartslogg.
80. **Scorekort-e-posten (punkt 78/ADR-065) erstattet med DET FULLE
scorekortet, matcher selve Scorekort-siden visuelt — 2026-08-13/14,
se ADR-065-tillegg.** Bruker, rett etter at punkt 78 var rullet ut:
ville ha "det fulle scorekortet, det som inneholder all statistikk
for runden" i e-posten (den sendte da kun slag/netto), pluss dato/
klokkeslett/spilletid i hilsenen. Et første forsøk (enkel vertikal
hull-per-rad-tabell) ble erstattet samme kveld etter at bruker sendte
et skjermbilde av selve appens Scorekort-side og ba om at e-posten
skal SE UT SOM den, ikke bare inneholde de samme tallene.
`send_round_summary_email` (email.py) bygget helt om: hull som
KOLONNER i to 9-hulls Ut/Inn-blokker (Tot for 9-hullsrunder), porterer
`ScoreBlock`/`TotalTile` fra frontend/components/round-scorecard.tsx
til inline-styled HTML -- fargede score-merker (eagle/birdie/par/
bogey/double), Fw/Innspill-glyffer (◎/↖/↗/↑/↓/←/→), GIR-hake, fire
total-tiles (Par/Score/Til par/Poeng) nederst. Rader graderer seg
(kun tatt med når minst ett hull har data). `RoundSummaryHole`
utvidet med `stroke_index`/`picked_up`/`points`(Stableford)/`putts`/
`tee_shot_result`/`approach_result`/`chip_count`/`bunker_shot_count`/
`penalty_strokes`/`anyway_strokes`. Rad-etikettene `Score`/`Netto`/
`Poeng` kortet til `Scr`/`Net`/`Pnt` etter enda et bruker-tilbakemeldt
skjermbilde av selve e-posten (for trangt i en 9-kolonners tabell).
**To reelle bugs funnet under ekte ende-til-ende-testing (ikke bare
isolert forhåndsvisning):** (1) `round.played_at` er KUN en
`date`-kolonne, ikke timestamptz -- "Utslagstid" i ny-runde-veiviseren
fanges opp av wizard-state men sendes faktisk ALDRI til backend
(`wizard-context.tsx` sender kun `played_at: s.date`), egen
allerede-eksisterende feil, ikke rettet her. Løsning: brukte
`round.started_at` for klokkeslettet i stedet, med graceful fallback.
(2) Samme rot-årsak forplantet seg til spilletid-utregningen
(`duration_start` falt tilbake til `played_at`, en `date`, ville
krasjet mot en ekte `datetime` ved subtraksjon) -- fjernet fallbacken,
ingen varighet vises i stedet for å blande typer.
**Verifisert:** `python3 -m py_compile` + 31/31 backend-tester ved
hver iterasjon. Isolert forhåndsvisning (monkeypatchet `_send_sync`,
18 hull med data for ALLE graderte felt inkl. eagle/dobbel bogey/
plukket opp) nettleser-rendret og skjermbilde-sammenlignet direkte
mot brukerens eget skjermbilde av Scorekort-siden -- bekreftet
visuelt samsvar. Ekte ende-til-ende `POST .../send-scorecard` mot en
reell, tidligere fullført runde i produksjon, gjentatt etter hver
fiks -- endte med 204 og ren `teecup_api`-logg.
**Rullet ut 2026-08-14** (flere delrunder samme kveld/natt, spenner
over datogrensen) -- ren backend-endring, ingen ny migrasjon,
`docker compose build teecup_api && up -d teecup_api` hver gang.
81. **FEATURE_BACKLOG.md-gjennomgang: hele filen (4457 linjer) lest linje
for linje, seks stale "ikke bygget"-seksjoner rettet — 2026-08-14.**
Bruker spurte om alt arbeid siste dager faktisk var registrert i
.md-filene (foranlediget av at punkt 80 over nesten ble glemt
committet, se dét punktets historie) — svaret utvidet seg til et
ønske om å gå gjennom hele `FEATURE_BACKLOG.md` for glemte/utdaterte
punkter. Full, systematisk gjennomgang (ikke stikkprøver) skilte
reelt åpne oppgaver (15 punkter, bl.a. Bøtekasse, per-hull-historikk,
OOM sin eclectic-/lag-/offentlig-visning, scramble-mot-enkeltspiller
for org-lagturneringer, det nyoppdagede `played_at`-klokkeslett-hullet)
fra seks steder der filen selv var stale (markert "ikke bygget" på
noe som faktisk var levert, bare uten oppdaterings-notat):
- **Internasjonale baner** — foreslo GolfAPI.io presist, som senere
faktisk ble valgt og bygget som ADR-064/065 (Tjøme/Larvik/Nesbyen,
live). Den klart viktigste av de seks.
- **Individuelle turneringer** — siste notat sa "ikke rullet ut mot
ekte teecup_db", men migrasjon 040 er bekreftet live (verifisert
direkte mot skjemaet) -- må ha blitt rullet ut et sted mellom
2026-07-30 og 2026-08-04 (Order of Merit, migrasjon 055, forutsetter
det) uten at et eget "rullet ut"-notat noensinne ble skrevet.
- **PWA-app-ikon** — merket "MIDLERTIDIG, skal erstattes senere";
et ekte ikon ble designet og rullet ut som ADR-055 2026-08-10.
- **Push-varsler** — en tidlig, isolert "📋 planlagt"-linje som aldri
ble fjernet etter at en egen, senere seksjon i samme fil bygde og
rullet ut hele varsel-systemet (in-app + push) 2026-07-28.
- **Dashboard-blokkforslaget** — merket "IKKE BYGGET", men bekreftet
bygget nesten ordrett i `dashboard.tsx` (samme dag, som del av
ADR-035) -- rekkefølgen i filen ga bare et misvisende inntrykk.
- **Flaggturnering sin GPS-kobling** — noterte en avhengighet til GPS
som senere ble bygget (ADR-048/064); selve formatet var allerede
live, kun GPS-posisjon-ved-siste-slag-ideen er fortsatt åpen.
Hver rettelse skrevet som et eksplisitt **RETTELSE**-avsnitt (samme
disiplin filen selv allerede etablerte andre steder, f.eks.
"RETTELSE 2026-08-03" i turneringsformat-seksjonen) -- historikken
under hver rettelse er beholdt uendret, ikke slettet. `Sist
oppdatert`-datoen øverst i filen (stod på 2026-07-17) rettet
tilsvarende. Ingen kodeendring, ren dokumentasjonsopprydding.
82. **Flaggturnering "Del A": GPS-flaggplanting + runde 2+-scoreføring
(ADR-066) — 2026-08-14.** Første av tre bedt-om utvidelser
(Del A GPS-planting/runde 2, Del B kartoversikt+synlighetsbryter,
Del C Eclectic-format) -- kun Del A bygget denne runden, se ADR-066
for full begrunnelse/avklaringer og plan-filens spesifikasjon av
Del B/C.
Motor: ny `flag_lap_and_hole()`-hjelpefunksjon i
`handicap_engine.py` (`flag_result()` selv trengte ingen endring --
kalleren konkatenerer lap 1+lap 2s gross_strokes til én flat liste
før kallet). 5 nye tester, full suite 122/122.
Migrasjon 069 (`round_participant_flag_plant` +
`round_hole_flag_overflow`, frittstående) og 070 (samme par,
org-individuelle turneringer, RLS-beskyttet) -- to HELT ISOLERTE
tabellpar, bevisst IKKE en `lap`-kolonne på delte
`round_hole`/`tournament_round_hole` (se ADR-066 for
blindsone-begrunnelsen).
API: speilende `flag-plant`/`flag-overflow`-endepunkter i
`rounds.py` og `individual_tournaments.py`. Server beregner faktisk
"hull i gang" fra registrert scoredata og avviser
(`VALIDATION_FAILED`) ethvert forsøk som ikke stemmer -- klientens
"Hullet du ut?"-flyt er kun en UX-guide. To ulike, bevisst beholdte
autorisasjonsmodeller: flight-styrt (frittstående) vs. selv-only
(org), speiler hver fils eksisterende `update_hole`-mønster.
Frontend: `FlagPlantSheet`/`FlagPlantSheetOrg` -- ny UI-overflate,
derfor bygget via en Claude-skrevet V0-prompt (sendt til bruker),
med en tydelig MERKET midlertidig håndkodet versjon (samme
props-kontrakt) i mellomtiden for å kunne verifisere hele
backend-flyten uten å vente på eksporten. **V0-eksporten (zip 11)
kom tilbake samme dag** og erstattet den midlertidige versjonen i
begge filer (org-varianten fikk i tillegg `expectedLap`-prop lagt
til kallstedet, som den manglet før).
**Verifisert:** `python3 -m py_compile` + full
`./scripts/run_backend_tests.sh` (46/46, 15 nye tester i to nye
testfiler). Egen scratch-database + scratch `teecup_api`-container
(port 18000) + lokal `next dev` (port 13000): ende-til-ende for
BEGGE hjem inkl. server-side avvisning av for-tidlig planting og
full runde 2-scoreføring, geolocation emulert via Chrome DevTools
MCP (måtte omgå at `emulate()` kun setter koordinater, ikke
Permissions API-tilstanden -- løst med `navigate_page({type:
"reload", initScript: ...})`), lys+mørk bekreftet, lap 2 sin
`(runde {lap})`-markering bekreftet live. Etter V0-swap: `tsc
--noEmit` rent, 45/45 vitest, begge filer. Scratch-stacken revet
ned igjen etterpå.
**Rullet ut 2026-08-14** -- migrasjon 069/070 kjørt mot ekte
`teecup_db` som `teeoff_admin` (`teecup_app` har bevisst ingen
`CREATE`-rett på schema public, jf. ADR-002 -- migrasjoner kjøres av
admin-rollen, aldri runtime-rollen), etter eksplisitt bekreftelse fra
bruker. Alle fire nye tabeller verifisert direkte i skjemaet
etterpå. `docker compose build teecup_api teecup_frontend && up -d`
-- begge containere startet rent (`Application startup complete.`
henholdsvis `✓ Ready`), ingen feil i logg.
83. **Flaggturnering "Del B": kartoversikt over plantede flagg +
synlighetsbryter (ADR-067) — 2026-08-14.** Andre av tre bedt-om
utvidelser (se punkt 82/ADR-066 for Del A). Underveis avdekket:
org-individuelle turneringer har INGEN offentlig tilskuer-side i det
hele tatt (`/t/[id]/live` er rendyrket for lagturneringer) --
avklart med bruker via AskUserQuestion, som valgte å avgrense Del B
sin org-side til org-medlemmer/deltakere fremfor å bygge en helt ny
spectator-infrastruktur bare for kartet. Frittstående runder fikk
full tilskuerstøtte (gjenbruker eksisterende `/watch/[id]`/
`/public/rounds/*`-mønster).
Migrasjon 071: to boolean-kolonner (`round.flag_map_visible`,
`tournament.flag_map_visible`, begge default false) -- ingen nye
tabeller, gjenbruker flagg-plant-tabellene fra migrasjon 069/070
direkte. Bryteren endres via de allerede eksisterende PATCH-
endepunktene (kun nye felt på whitelist-drevne modeller, ingen ny
handler-kode). Nytt leseendepunkt per hjem (`GET .../flag-map`),
delt `_build_flag_map()`-hjelper mellom autentisert og offentlig
variant -- alle flagg når bryteren er PÅ, ellers kun requesterens
eget.
Frontend: enda en ny UI-overflate via V0-prompt (`flag-map-
overview.tsx`, FØRSTE flerpunkts-Mapbox-kart i appen), midlertidig
håndkodet versjon bygget parallelt for å unngå å vente. Tre tynne
"seksjon"-wrappere (`FlagMapSection`/`FlagMapSectionOrg`/
`WatchFlagMap`) i de tre relevante skjermene, samme
"egen fetch + next/dynamic(ssr:false)"-mønster som `NassauPanel`.
Lap 1/2+ skilles med BÅDE farge OG en "R{lap}"-tekstbadge på selve
markøren (aldri farge alene), pluss alltid synlig tekst-legende.
**Verifisert:** `python3 -m py_compile` + full
`./scripts/run_backend_tests.sh` (50/50, 4 nye tester). `tsc
--noEmit` rent + 45/45 vitest. Egen scratch-database + scratch
`teecup_api`-container (port 18000, live-mountet kode for rask
iterasjon) + lokal `next dev` (port 13000): to fulle scenarioer
(frittstående + org, hver med to spillere, ett lap 1- og ett lap
2-flagg med on-green-avstand) sådd via `tests/conftest.py` sine
hjelpefunksjoner. Bekreftet i ekte nettleser: bryter av/på for begge
hjem, korrekt "kun eget flagg"-banner, lap 2-merking, popup-innhold
(navn/hull/runde/avstand), tilskuer-siden viser begge flagg uten
innlogging når bryteren er på. Lys+mørk bekreftet. Samme `Referer`-
header-omgåelse for Mapbox sin URL-restrikterte token som tidligere
(se punkt 51). Scratch-stacken (db/rolle/container/MinIO-bucket)
fullstendig revet ned -- ekte `teecup_db`/`teecup_api`/
`teecup_frontend` urørt.
**Rullet ut 2026-08-14** -- migrasjon 071 kjørt mot ekte `teecup_db`
som `teeoff_admin`, etter eksplisitt bekreftelse fra bruker. Begge
nye kolonner verifisert direkte i skjemaet. `docker compose build
teecup_api teecup_frontend && up -d` -- begge containere startet
rent. Del C (Eclectic) gjenstår.
84. **Eclectic-format for org-individuelle turneringer "Del C" (ADR-068)
+ tre ikke-relaterte UI-rettelser — 2026-08-14.** Siste del av den
tredelte utvidelsen (se punkt 82/83, ADR-066/067 for Del A/B).
Eclectic: spillerens beste resultat PER HULL på tvers av ALLE
turneringens runder, summert -- brutto/netto/Stableford-varianter.
Krever samme bane for alle runder (avvises tydelig ved
rundeopprettelse ellers, ADR-019-filosofi).
Migrasjon 072: kun tre nye verdier i
`tournament_scoring_method_check` -- ingen nye tabeller/kolonner,
Eclectic regnes ut ved lesing fra eksisterende `tournament_round_
hole`/`course_handicap`. Motor: ny `eclectic_best_per_hole()` i
handicap_engine.py, 5 nye tester (inkl. ett scenario som beviser
at beste-per-hull faktisk plukkes hullvis, ikke "beste hele
runde") -- full motor-suite 127/127.
**Bevisst avvik fra opprinnelig plan:** planla først et helt nytt
leseendepunkt, men fant at Eclectic passer renere inn som en ny
gren i den ALLEREDE eksisterende `_compute_individual_standings`
(samme funksjon bak `individual_leaderboard`, brukt av alle andre
scoring_method-verdier) -- INGEN nytt endepunkt bygget, frontend
gjenbruker det samme kallet. 5 nye pytest-tester -- full
backend-suite 55/55.
Frontend (individual-tournament-detail.tsx, håndkodet -- filens
egen konvensjon): ny utvidbar "vis hull-for-hull"-rad per deltaker
i leaderboardet (`EclecticHoleTable`), viser hull/par/verdi/
kilde-runde -- beviser visuelt at totalen er satt sammen på tvers
av runder.
**Verifisert:** `python3 -m py_compile` + full
`./scripts/run_backend_tests.sh` (55/55) + standalone
`test_handicap_engine.py` (127/127). `tsc --noEmit` rent + 45/45
vitest. Egen scratch-database + scratch `teecup_api`-container
(port 18001, live-mountet kode) + lokal `next dev` (port 13001):
to runder samme bane, hull-for-hull-forhåndsberegnet total (7)
stemte nøyaktig med visningen, samme-bane-avvisningen ga tydelig
feilmelding ved forsøk på annen bane. Lys+mørk bekreftet.
Scratch-stacken revet ned -- ekte `teecup_db`/`teecup_api`/
`teecup_frontend` urørt.
**Samme runde, tre små UI-rettelser** (brukerrapportert via
skjermbilder, ikke Eclectic-relatert): avstandsindikatoren
(`target-distance.tsx`) rettet fra "grønn"/"Midt" til de riktige
golf-uttrykkene "green"/"senter", "Oppdateres live"-badgen (og det
nå ubrukte `isLive`-sporet) fjernet helt. "Antall hull"-bryteren i
Ny runde-veiviseren (`Segmented`-primitiven) hadde en avvikende
hvit valgt-stil sammenlignet med resten av samme skjerm -- rettet
til samme grønne aksent-stil som `ChoiceCard`/`ToggleButton`,
gjelder alle `Segmented`-instanser appen-vidt.
**Rullet ut 2026-08-14** -- migrasjon 072 kjørt mot ekte `teecup_db`
som `teeoff_admin`, etter eksplisitt bekreftelse fra bruker.
`docker compose build teecup_api teecup_frontend && up -d` -- begge
containere startet rent (ruller også ut de tre UI-rettelsene).
Dette var siste del av den tredelte Flaggturnering-utvidelsen --
Del A/B/C alle bygget OG rullet ut.
85. **Offline kommentar-/bildeposting i rundefeeden (ADR-069) —
2026-08-14.** Bruker spurte om installasjonsbannerets "virker
delvis uten nett"-påstand faktisk stemte -- undersøkelse bekreftet
at den gjorde det, men bruker ønsket å tette det største gjenværende
gapet: kun hull-scoreføring hadde offline-støtte, kommentarer/bilder
krevde fortsatt nett. Valgte dette fremfor to andre foreslåtte
retninger (proaktiv full-runde-precaching, generelt app-skall for
aldri-besøkt kaldstart).
Migrasjon 073: `round_message.client_message_id` (nullbar uuid) +
delvis unik indeks -- IKKE en ny funksjon i seg selv, men
idempotens. Ulikt hull-score-køens PATCH+expected_version (naturlig
trygg å gjenta), er `POST /messages` IKKE idempotent -- en avbrutt
synk kunne skrevet samme kommentar to ganger uten dette. Klienten
sender en `crypto.randomUUID()`-generert id ved både første forsøk
OG et evt. køet gjenforsøk; et gjentatt kall med samme id returnerer
den allerede opprettede meldingen fremfor å duplisere. 3 nye
backend-tester -- full suite 58/58.
`offline-queue.ts` utvidet med et `isMultipart`-flagg -- `body` blir
da et flatt string/Blob-objekt (IndexedDB lagrer Blob/File nativt)
i stedet for JSON, og `flushQueue` bygger `FormData` ved synk.
`round-messages.tsx` fikk sin EGEN, isolerte kø (`matchId:
\`${roundId}:messages\``, ikke bare `roundId`) -- blandes aldri med
round-detail.tsx sin hull-score-kø selv om begge er montert
samtidig. Optimistisk "Venter på synk"-kort med egen bilde-object-
URL (ikke composerens, som revokes med det samme).
**Verifisert:** full `./scripts/run_backend_tests.sh` (58/58,
migrasjon 073 inkludert). `tsc --noEmit` rent + 45/45 vitest. Egen
scratch-database + scratch `teecup_api`-container (port 18002,
live-mountet kode) + lokal `next dev` (port 13002), ekte
nettverk-frakoblet-emulering: tekst-only kommentar offline → synk →
nøyaktig 1 rad i databasen (ingen duplikat); samme med ekte
opplastet bilde (AVIF-konvertert av MinIO etter synk); reload-mens-
offline-scenario (postet offline, lastet siden på nytt MENS
fortsatt offline, satt online) -- den køede skrivingen ble funnet
og synket automatisk ved mount, nøyaktig 3 rader totalt (ingen
duplikat på tvers av en reload). Lys+mørk bekreftet. Scratch-stacken
fullstendig revet ned -- ekte `teecup_db`/`teecup_api`/
`teecup_frontend` urørt.
**Rullet ut 2026-08-14** -- migrasjon 073 kjørt mot ekte `teecup_db`
som `teeoff_admin`, etter eksplisitt bekreftelse fra bruker.
`docker compose build teecup_api teecup_frontend && up -d` -- begge
containere startet rent.
86. **Utvidet spillerskjema i "Deltakere" (ADR-070) — 2026-08-14.**
Bruker viste skjermbilde av "Deltakere"-flyten i en org-individuell
turnering: søkefeltet tilbød KUN "Opprett ny spiller: «X»", ingen
måte å velge en eksisterende spiller i org-poolen. Ba samtidig om at
nye spillere skal fange Fornavn, Etternavn, Kjønn, Fødselsdato,
E-post, Medlemsnummer i hjemmeklubb, Hjemmeklubb, Land, Hcp, Betalt,
Kommentar.
Rotårsak for "kan ikke velge eksisterende": `AddParticipantControl`
sin `matches`-liste ble kun utledet ved ikke-tomt søk -- ingen måte å
bla i poolen uten å kjenne et treffende navn fra før. Fikset med et
`available`-memo (pool minus allerede lagt til) som `matches` faller
tilbake til ved tomt søk.
Migrasjon 074: 7 av 11 ønskede felt fantes allerede på `player`
(migrasjon 007), kun `first_name`/`last_name`/`paid`/`comment` var
reelt nye. Samme for-/etternavn-splitt-mønster som migrasjon 052
(`display_name` uendret som det faktisk viste navnet, `first_name`/
`last_name` en ny valgfri kilde ved siden av) -- men her beregner
FRONTEND `display_name` før sending i stedet for at backend synker
det, for å unngå å røre `create_player`/`update_player` sin
eksisterende generiske PATCH-kontrakt. Backfill av eksisterende
spillere med samme heuristikk som migrasjon 052.
Frontend: progressivt avslørt skjema i `AddParticipantControl`
("+ Flere detaljer"), `splitName()`-hjelper forhåndsutfyller Fornavn/
Etternavn fra søkefeltets frittekst ved "Opprett ny spiller".
**Fant og fikset underveis (selvfunnet, ikke brukerrapportert):**
misvisende tom-tilstand-tekst når alle pool-spillere allerede var
lagt til turneringen ("ingen spillere i org ennå" -- faktisk feil,
de var bare allerede lagt til). Splittet i riktige meldinger for de
tre reelle tilstandene.
**Verifisert:** `python3 -m py_compile` + full
`./scripts/run_backend_tests.sh` (58/58, migrasjon 074 inkludert).
`tsc --noEmit` rent + 45/45 vitest. Egen scratch-database + scratch
`teecup_api`-container (live-mountet kode) + lokal `next dev`: bla-i-
eksisterende-fikset bekreftet med en forhåndsopprettet spiller
synlig direkte ved tomt søk, alle 11 felt bekreftet lagret korrekt
for en ny spiller opprettet via det utvidede skjemaet. Lys+mørk
bekreftet inkl. avkrysningsboks-styling. Scratch-stacken fullstendig
revet ned -- ekte `teecup_db`/`teecup_api`/`teecup_frontend` urørt.
**Verifiseringsfallgruve, ikke app-bug:** Chrome DevTools MCP sin
`fill`-verktøy satte visuelt riktig verdi på et natively multi-
segment ` `, men trigget ikke Reacts `onChange`
pålitelig -- `birth_date` lagret som `null` til tross for korrekt
visning rett før innsending. Bekreftet ved tastatur-drevet
`press_key` inn i dato-feltets spinbuttons i stedet -- lagret
korrekt. Kun et automatiseringsverktøy-kvirk med native dato-inputs,
ingen kodeendring nødvendig.
**Rullet ut 2026-08-14** -- migrasjon 074 kjørt mot ekte `teecup_db`
som `teeoff_admin` (4 eksisterende spillere backfillet), etter
eksplisitt bekreftelse fra bruker. `docker compose build teecup_api
teecup_frontend && up -d` -- begge containere startet rent.
**Tillegg samme dag:** flagget den strukturelt identiske "bla i
eksisterende"-begrensningen i lag-turneringers roster-tillegg
(`tournament-detail.tsx`) som bevisst utenfor omfang -- bruker ba om
samme fiks der. Samme "available"-fallback-mønster lagt til
`AddPlayerControl` (spillere på DET ANDRE laget forblir synlige,
nedgradert/deaktivert -- kun de på DETTE laget ekskluderes). Ren
frontend-endring, ingen migrasjon. `tsc --noEmit` rent + 45/45
vitest. Scratch-database + scratch `teecup_api` (port 18004) +
lokal `next dev` (port 13004): to lag, tre poolspillere (én allerede
rostret på det andre laget) -- bekreftet browsing, søk-innsnevring
og eksisterende-spiller-tillegg alle fungerer, lys+mørk. Ingen
migrasjon å rulle ut. **Rullet ut 2026-08-14** -- `docker compose
build teecup_frontend && up -d`, etter eksplisitt bekreftelse fra
bruker. Containeren startet rent.
87. **Full statistikkdybde i org-turneringers hull-scoring, "Steg 1" av
spillerens per-hull-historikk (ADR-071) — 2026-08-15.** Bruker ba om
å se full statistikk basert på ALLE tidligere ganger en spiller har
spilt et gitt hull. `tournament_round_hole` har aldri hatt noe utover
`gross_strokes` (uendret siden migrasjon 040) -- historikk-funksjonen
kan derfor kun bli "full" for turneringssiden hvis denne datadybden
bygges først. Bekreftet med bruker: begge datakilder (frittstående +
turnering) skal telle med, med full dybde i begge.
Migrasjon 075: `tournament_round_hole` fikk samme nye felt som
`round_hole` (putts, klubb, utslag-/innspillretning, chip/bunker/
straffe-/anywayslag, puttavstand-bøtte) pluss en NY `version`-kolonne
(optimistisk lås -- endepunktet var til nå rent siste-skriver-vinner).
`tournament_participant` fikk `stat_level` (samme tre nivåer som
`round_participant`), lagt PÅ TURNERING-NIVÅ (ikke per runde) siden en
deltaker normalt spiller flere runder i samme turnering. Trygg
default (strokes_only) -- ingen eksisterende turnering endrer
oppførsel.
Backend (`individual_tournaments.py`): `HoleUpdate`/`RoundHoleOut`
utvidet feltnavn-for-feltnavn etter `rounds.py`. `update_hole` sin
upsert fikk en versjonssjekk på DO UPDATE-grenen -- MERK reell
forskjell fra `round_hole`: raden opprettes først ved FØRSTE score
(ikke ved rundestart som `round_hole`), så INSERT-grenen er
ubetinget -- en fersk innsending 409-blokkeres aldri av en
`expected_version` som ennå ikke gir mening. `TournamentParticipant*`
+ `RoundParticipantOut` fikk `stat_level` som ren whitelist-
tilføyelse, samme mønster som migrasjon 074.
Frontend: `NumberPicker`/`ChoiceRow`/`DirectionCross`/`Stepper`/
`WizardSection` trukket ut av `round-detail.tsx` til ny delt fil
`hole-stat-inputs.tsx` (ren mekanisk utrekking, ingen atferdsendring
for `ScoringWizard`) -- begge scoringsflytene trenger nå identiske
trykkbaserte inputs. Nytt `HoleStatsSheet` i
`individual-tournament-detail.tsx`: ETT skjermbilde (ikke en
flerstegs-veiviser -- HoleGrid har allerede valgt ett hull for én
deltaker). `HoleGrid` sin opprinnelige inline-tallfelt-redigering er
BEVISST UENDRET for `strokes_only`-deltakere. `stat_level` redigeres
via ny `` i deltakerlisten under Oppsett.
**Bevisst avgrenset:** `ownBagClubs` sendes tom -- "din egen
kølle-bag" krever å vite om scoreren ER spilleren selv, som denne
filen ikke allerede sporer. `ClubPicker` faller tilbake til fritekst
uten den, en utelatt nicety, ikke en mangel.
**Verifisert:** `python3 -m py_compile` + full
`./scripts/run_backend_tests.sh` (64/64, 6 nye tester i
`test_tournament_hole_stats.py`). `tsc --noEmit` rent + 45/45 vitest.
Scratch-database + scratch `teecup_api` (port 18005, live-montert
kode) + lokal `next dev` (port 13005): to deltakere (strokes_only og
full) i samme runde -- HoleStatsSheet åpner/lagrer/gjenåpner med
forhåndsutfylte verdier korrekt for full-deltakeren, strokes_only-
deltakerens opprinnelige inline-felt uendret (ingen regresjon).
Lys+mørk bekreftet. Scratch-stacken fullstendig revet ned -- ekte
`teecup_db`/`teecup_api`/`teecup_frontend` urørt.
**Neste steg (Steg 2, egen ADR):** selve historikk-aggregeringen på
tvers av alle spilte runder/turneringer.
**Rullet ut 2026-08-15** -- migrasjon 075 kjørt mot ekte `teecup_db`
som `teeoff_admin`, etter eksplisitt bekreftelse fra bruker.
`docker compose build teecup_api teecup_frontend && up -d` -- begge
containere startet rent.
88. **Spillerens per-hull-historikk på tvers av alle runder/turneringer,
"Steg 2" (ADR-072) — 2026-08-15.** Selve historikk-funksjonen bruker
opprinnelig ba om (se #87/ADR-071 for statistikkdybde-forutsetningen).
Bekreftet med bruker: custom baner ekskluderes helt, ordinære
(teeoff-/GolfAPI-importerte) baner telles med fra BÅDE frittstående
runder og org-turneringer, slått sammen.
Ny delt modul `app/hole_history.py` -- kjernen er bane-broen mellom
to identitetssystemer: teeoff (`round.teeoff_facility_slug`+
`teeoff_course_id` mot `course.external_course_ref = "facility:
course_id"` når `source='official'`, eksakt samme format som
`import_official_course()` bygger) og GolfAPI (`personal_course.
external_golfapi_course_id` mot samme rå ID i `course.external_
course_ref` når `source='international'`). Custom gir bevisst `None`
fra begge resolve-funksjonene -- historikk-panelet skjules stille.
Turneringssiden må håndtere at spilleren kan ha spilt i FLERE
organisasjoner -- `tournament_round_hole` er org-scopet/RLS-
beskyttet. Løst med samme N+1-per-org-mønster som `/auth/me`
allerede bruker: `player_organizations_for_user()` (migrasjon 015,
smal SECURITY DEFINER-bro) gir org-listen trygt, ett `org_
connection()`-kall per org deretter. Ingen bypass-RLS-snarvei.
GIR/fairway-formlene speiler den etablerte `score - putts <= par -
2` (samme presisering som ADR-071, IKKE skjema-kommentarens avvikende
formel). `summarize_hole_history()` er en ren funksjon -- regner
GIR%/fairway%/snitt-putter kun over instanser MED faktisk registrert
data, sorterer mest-nylig-først.
To tynne endepunkt (ett i `rounds.py`, ett i `individual_
tournaments.py`) kaller begge inn i samme `hole_history_for_user()`
og returnerer SAMME kombinerte historikk uansett hvilken side
spørringen kom fra. Historikken er for DELTAKEREN, ikke nødvendigvis
innlogget bruker (samme tilgang som selve scoringen) -- gjester
(`user_id IS NULL`) gir `None`, samme kontrakt som en custom bane.
Frontend: nytt `HoleHistoryPanel` i `hole-stat-inputs.tsx` (henter
selv, viser ingenting ved `null`-respons). Ekspanderbar: lukket
viser ett sammendrag, åpen viser hver enkeltinstans. Lagt til i
`ScoringWizard` (frittstående) og det nye `HoleStatsSheet`
(turnering, ADR-071) -- samme komponent, ulik URL.
**Verifisert:** `python3 -m py_compile` + full
`./scripts/run_backend_tests.sh` (73/73 -- 12 nye tester i
`test_hole_history.py`: 6 rene enhetstester av aggregeringsformlene,
pluss integrasjonstester som BEVISER selve bane-broen -- samme
spiller/samme teeoff-bane spilt frittstående OG i en turnering i en
ANNEN organisasjon, kombinert historikk viser begge; custom bane og
gjeste-deltaker gir korrekt `None`). `tsc --noEmit` rent + 45/45
vitest. Scratch-database + scratch `teecup_api` (port 18006, live-
montert kode) + lokal `next dev` (port 13006): samme spiller/samme
teeoff-bane, ett frittstående hull + ett turneringshull i en ANNEN
org -- bekreftet BEGGE kontekster viser identisk, håndregnet-korrekt
kombinert historikk ("2 ganger, 1 frittstående/1 turnering, snitt 4.5
slag, GIR 50%, 1.5 putter"), ekspandert instansliste riktig merket,
et uspilt hull viste ingen panel. Lys+mørk bekreftet. Scratch-stacken
fullstendig revet ned -- ekte `teecup_db`/`teecup_api`/
`teecup_frontend` urørt.
**Ingen migrasjon** -- rent lesefunksjon oppå migrasjon 075 sitt
skjema.
**Rullet ut 2026-08-15** -- ingen migrasjon, `docker compose build
teecup_api teecup_frontend && up -d` etter eksplisitt bekreftelse fra
bruker. Begge containere startet rent.
89. **Fiks: eierens statistikknivå (og utslag ved endring) ble ikke
lagret i "Ny runde"-veiviseren (ADR-073) — 2026-08-15.** Bruker
rapporterte at de "til stadighet må" sette utslag/statistikktype på
nytt, og at "det må ha blitt borte i prosessen, for det fungerte
tidligere" -- vedla en skjermopptak-video som viste to symptomer.
Rotårsak: eierens spillerobjekt i veiviserens `state.players[]` har
EGNE `teeId`/`statLevel`-felt, atskilt fra `state.teeId`/`state.
statLevel` (de faktiske feltene steg 1/steg 2 redigerer) -- kun
synkronisert ÉN gang, da banen først velges. For utslag var
konsekvensen rent kosmetisk (steg 3 viste et gammelt utslag, men
selve innsendingen leste `state.teeId` direkte for eieren) -- for
statistikknivå var det en REELL datafeil: `submit()` leser eierens
`stat_level` fra spillerobjektet (hardkodet `"score"` fra `/auth/me`-
lastingen), ALDRI fra `state.statLevel` -- "Statistikk for deg
selv"-valget i steg 2 var dermed virkningsløst for eieren, kun
default for NYE medspillere lagt til senere.
Fiks: én samlet `useEffect` i `wizard-context.tsx` som holder
eierens spillerobjekt kontinuerlig i synk med `state.teeId`/`state.
statLevel`, uansett hvor mange ganger de endres eller fra hvilket
steg -- erstatter den tidligere punktvise engangs-synken i
`pickCourse()` (fjernet, nå overflødig).
**Verifisert:** `tsc --noEmit` rent + 45/45 vitest (ingen eksisterende
testinfrastruktur for `ny-runde/`-veiviseren i dette repoet -- ingen
ny automatisert test lagt til, browserverifisering ansett
tilstrekkelig for en ren tilstandssynk-fiks). Scratch-database +
scratch `teecup_api` (port 18007, live-montert kode) + lokal `next
dev` (port 13007) med en egen bane med to utslag: gjenskapte
videoens eksakte scenario (bytt utslag etter førstevalg, velg "All
statistikk") -- steg 3 viste nå korrekt utslag+nivå, OG bekreftet
direkte i databasen at både `round.tee_name_snapshot` og
`round_participant.stat_level='full'` faktisk ble lagret (ikke bare
riktig i visningen) -- scoreregistreringen auto-hoppet deretter
videre til Putter-steget, kun mulig for `full`. Lys+mørk bekreftet.
Scratch-stacken fullstendig revet ned -- ekte `teecup_db`/
`teecup_api`/`teecup_frontend` urørt.
**Ingen migrasjon** -- ren frontend-tilstandssynk-fiks.
**Rullet ut 2026-08-15** -- ingen migrasjon, `docker compose build
teecup_frontend && up -d` etter eksplisitt bekreftelse fra bruker.
Containeren startet rent.
90. **Order of Merit: lag-leaderboard + eclectic-total-bug fikset
(ADR-074) — 2026-08-15.** Går videre med de gjenstående OOM-punktene
fra ADR-043 (2026-08-04): lag-OOM sin leaderboard, eclectic-
aggregering på tvers av turneringer, og (nytt oppdaget) offentlig
visning. Denne runden: bug-fiks + lag-OOM. Se ADR-075/076 for de to
andre.
Bifunn fikset FØRST: `_contribution_for_entry` leste alltid rå
gross/net/stableford_total for en lenket turnering, ALDRI
`eclectic_total` -- en klubb som lenket en eclectic-scoret turnering
(ADR-068, bygget etter OOM selv) inn i en stableford/brutto/netto-OOM
fikk stille feil tall (rå slagsum i stedet for turneringens faktiske
"drømmerunde"-resultat). Fikset ved å gi funksjonen tilgang til
scoring_method.
Lag-OOM: ny CRUD for `order_of_merit_team`/`_team_member` (tabellene
fantes fra migrasjon 055, ingen ny migrasjon). Leaderboard-grenen for
`kind='team'` regner ut hvert medlems individuelle OOM-sluttresultat,
slår dem sammen med SAMME `order_of_merit_aggregate`-kall gjenbrukt
på lagnivå -- bevisst: ett lagret aggregeringsvalg styrer begge
nivåer (count_best_n=1 betyr "beste turnering per spiller" OG "beste
medlem per lag" samtidig, bekreftet eksplisitt i test). Frontend:
fjernet "ikke bygget ennå"-plassholderen -- den eksisterende
leaderboard-tabellen var allerede generisk nok til å vise lag
uendret. Ny "Lag"-administrasjonsseksjon lagt til.
**Verifisert:** full `./scripts/run_backend_tests.sh` (77/77 -- 5 nye
tester, inkl. håndregnet lag-sum 162+198=360 og en eksplisitt test av
to-nivås count_best_n-anvendelsen). `tsc --noEmit` rent + 45/45
vitest. Scratch-database + scratch `teecup_api` (port 18008, live-
montert kode) + lokal `next dev` (port 13008): opprettet ekte lag-OOM
i nettleseren, lenket to turneringer, la til to medlemmer ett om
gangen -- leaderboardet oppdaterte seg live til riktig verdi ved hver
endring. Lys+mørk bekreftet. Scratch-stacken fullstendig revet ned --
ekte `teecup_db`/`teecup_api`/`teecup_frontend` urørt.
**Ingen migrasjon.**
**Rullet ut 2026-08-15** -- ingen migrasjon, `docker compose build
teecup_api teecup_frontend && up -d` etter eksplisitt bekreftelse fra
bruker. Begge containere startet rent.
91. **Order of Merit: eclectic-aggregering på tvers av lenkede
turneringer (ADR-075) — 2026-08-15.** Tredje av de gjenstående OOM-
punktene fra ADR-043/074 -- sesong-"drømmerunde": beste resultat PER
HULL på tvers av ALLE lenkede turneringer (kun `result_type IN
('stableford','gross','net')`), tidligere eksplisitt avvist i
leaderboard-endepunktet.
Samme-bane-håndheving lagt til (ny, ingen migrasjon): en eclectic-
OOM kan ikke lenke en turnering på en annen bane enn de allerede
lenkede, verken ved lenking eller ved å bytte `aggregation_mode` til
eclectic med ulike baner allerede lenket. Datainnsamlingen (ny
`_compute_eclectic_oom_values`) gjenbruker `eclectic_best_per_hole()`
fra `handicap_engine.py` HELT UENDRET -- samme motor som ADR-068
(flere runder i én turnering), kun datainnsamlingen foran er
tilpasset til å samle på tvers av turneringer i stedet, keyed på
`player_id` med en sammensatt (turnering-id, runde-sekvens)-kilde-
indeks. Frontend: "Eclectic" lagt til som tredje aggregeringsvalg
både i opprettelsesskjemaet (`order-of-merit-list.tsx`, med auto-
reset til Sum hvis kind/resultattype endres til noe eclectic ikke
støtter) og innstillinger for eksisterende OOM-er (`order-of-merit-
detail.tsx`, ingen reset nødvendig der siden kind/resultattype er
låst etter opprettelse) -- begge gated identisk til (og speilende)
serverens regel.
**Verifisert:** full `./scripts/run_backend_tests.sh` (80/80 -- 3 nye
tester: cross-tournament eclectic velger faktisk beste-per-hull på
TVERS av turneringer med hånd-utregnet forventet verdi, avvisning
ved lenking på annen bane, avvisning ved modus-bytte med ulike baner
allerede lenket). `tsc --noEmit` rent + 45/45 vitest. Scratch-
database + scratch `teecup_api` (port 18102, live-montert kode,
`TEECUP_DEV_LOG_MAGIC_LINKS=true` for innlogging uten SMTP) + lokal
`next dev` (port 13102): opprettet ekte eclectic-OOM i nettleseren,
lenket to turneringer på samme bane -- leaderboard viste korrekt
kryss-turnering-tall (72). Forsøk på å lenke en turnering på en annen
bane ga en tydelig feilmelding i UI-et. Opprettelsesskjemaets gating
verifisert live (Eclectic dukker opp/forsvinner + nullstiller
korrekt ved endring av type/resultattype). Lys+mørk bekreftet.
Scratch-stacken fullstendig revet ned (database, rolle, container,
MinIO-bøtte, `next dev`) -- ekte `teecup_db`/`teecup_api`/
`teecup_frontend` urørt.
**Ingen migrasjon.**
**Rullet ut 2026-08-15** -- ingen migrasjon, `docker compose build
teecup_api teecup_frontend && up -d` etter eksplisitt bekreftelse fra
bruker. Begge containere startet rent.
92. **Order of Merit: offentlig/delt visning (ADR-076) — 2026-08-15.**
Siste av de tre gjenstående OOM-punktene fra ADR-043/074/075.
`order_of_merit.public_visible` fantes fra migrasjon 055, aldri brukt
til noe før nå.
Migrasjon 076: ny SECURITY DEFINER-bro `public_order_of_merit_by_id`
(fjerde i rekken etter public_org_by_slug/public_tournament_by_code/
link_player_by_email), samme NULL-for-begge-tilfeller-anti-
enumerering. Backend: ny `/public/order-of-merits/{id}`-router i
`order_of_merit.py` selv, gjenbruker `order_of_merit_leaderboard()`
direkte i stedet for å duplisere beregningen. Frontend: ny
`app/order-of-merit/[id]/page.tsx` + `components/public-order-of-
merit.tsx`, modellert på `public-club.tsx`. "Del offentlig lenke"-
knapp lagt til i innstillinger (samme kopier-lenke-mønster som
`JoinCodeChip`), gated på den faktisk lagrede `public_visible`-
verdien.
**Verifisert:** full `./scripts/run_backend_tests.sh` (83/83 -- 3 nye
tester: 404 for ikke-eksisterende id, 404 for privat OOM -- bevist
IDENTISK statuskode, reell anti-enumerering -- og 200 med korrekt
resultatliste for offentlig OOM). `tsc --noEmit` rent + 45/45 vitest.
Scratch-database (migrert fra bunnen, inkl. 076) + scratch
`teecup_api` (port 18103) + lokal `next dev` (port 13103): offentlig
side nådd i en EGEN isolert nettleser-kontekst UTEN session-cookie
(reelt inkognito-scenario) -- viste korrekt navn/resultatliste,
`generateMetadata` satte riktig fanetittel server-side. Privat OOM ga
identisk feilside som en oppdiktet id. Av-toggling av "Offentlig
synlig" fjernet lenke-knappen i UI-et OG ga umiddelbar 404 fra API-et
(bekreftet direkte). Lys+mørk bekreftet. Scratch-stacken fullstendig
revet ned -- ekte `teecup_db`/`teecup_api`/`teecup_frontend` urørt.
**Migrasjon 076 vist og bekreftet av bruker FØR kjøring mot ekte
teecup_db** (additiv -- én ny funksjon, ingen skjemaendring).
**Rullet ut 2026-08-15** -- migrasjon 076 kjørt mot ekte `teecup_db`
etter eksplisitt bekreftelse (verifisert med `\df` + et direkte NULL-
kall før kode-utrullingen), deretter `docker compose build teecup_api
teecup_frontend && up -d`. Begge containere startet rent. Bekreftet
direkte mot `teecup.golf` etterpå: `/public/order-of-merits/`
ga 404, `/order-of-merit/` ga 200 (feil-tilstanden i siden).
Med dette er alle tre punktene fra ADR-043 sin opprinnelige
"gjenstår"-liste fullført.
93. **Tilbake-navigering til en tidligere spiller i scoringsveiviseren
(ADR-077) — 2026-08-16.** Første av tre forbedringer i score-
registreringen brukeren ba om etter OOM. I samlebånd-flyten
(`ScoringWizard`, frittstående runder) var det umulig å rette opp en
feilregistrering hos en tidligere spiller uten å fullføre hele
veiviseren og lete opp riktig kort manuelt etterpå.
Kontekst-raden med alle spillerne (bevisst ikke-interaktiv ved bygging
i 2026-07-26-runden) er nå interaktiv -- trykk på en annen spiller
(inkl. "Ferdig"-markerte) bytter veiviseren dit umiddelbart. Trygt
fordi hvert felt allerede lagres for seg med en gang (PATCH per felt,
ikke batch) -- ingen datatap ved bytte. Round-only (org-turneringer
har ingen tilsvarende kjede-flyt).
**Verifisert:** `tsc --noEmit` rent + 45/45 vitest. Scratch-database +
scratch `teecup_api` (port 18104) + lokal `next dev` (port 13104): tre
spillere, byttet spiller midt i steg uten datatap, rettet en allerede
lagret score etter bytte, bekreftet "Ferdig"-markerte spillere fortsatt
er trykkbare og viser korrekt lagret data (ikke nullstilt). Lys+mørk
bekreftet. Scratch-stacken fullstendig revet ned.
**Ingen migrasjon.**
**Rullet ut 2026-08-16** -- ingen migrasjon, `docker compose build
teecup_frontend && up -d` etter eksplisitt bekreftelse fra bruker.
Containeren startet rent.
94. **Chip/Bunker/Straffeslag/Anywayslag som egen skjerm (ADR-078) —
2026-08-16.** Andre av tre forbedringer i score-registreringen. Disse
fire tellerne var klemt inn under retningsvalget for utslag/innspill i
begge scoringsflytene (frittstående runder OG org-turneringer, etter
eksplisitt "opplevelsen skal være lik uansett").
`ScoringWizard`s "details"-steg splittet i "direction"/"holeDetails".
`HoleStatsSheet` (org-turneringer) bygget om fra ÉN lang skjerm til en
tilsvarende liten intern steg-flyt -- lagringen selv er BEVISST
uendret (fortsatt én batched "Lagre" der, ikke konvertert til runde-
sidens PATCH-per-felt). Visuell polering via V0 (Claude-skrevet
prompt): de fire tellerne fikk et 2x2-rutenett av flis-kort i stedet
for en 1-kolonne-stabel med mye tomrom -- `Stepper`-komponenten (delt
av begge flyter) oppdatert ett sted, automatisk identisk begge steder.
**Verifisert:** `tsc --noEmit` rent + 45/45 vitest. To scratch-runder
(strukturell splitt, så V0-integrering): begge flyter testet
ende-til-ende gjennom alle steg, GIR-auto-inferens fortsatt virker,
lagring bekreftet i begge flyter, gjenåpning viste korrekt lagrede
verdier, 2x2-rutenettet identisk og fungerende i begge flyter.
Lys+mørk bekreftet. Scratch-stackene fullstendig revet ned.
**Ingen migrasjon.**
**Rullet ut 2026-08-16** -- ingen migrasjon, `docker compose build
teecup_frontend && up -d` etter eksplisitt bekreftelse fra bruker.
Containeren startet rent.
95. **Personlig hull-historikk ut av slagvinduet + full historikk med
grafer (ADR-079) — 2026-08-16.** Tredje og siste av tre forbedringer i
score-registreringen. Ingen backend-endring -- `app/hole_history.py`
(ADR-072) returnerte allerede alt som trengs.
Flyttet historikk-panelet ut av selve slagvinduet/-arket i begge
flyter: frittstående runder -- inn i `PlayerHoleCards`-kortet, rett
før "Avslutt for {navn}". Org-turneringer -- som et nytt FØRSTE steg
(`"overview"`) i ADR-078 sin steg-flyt, med automatisk hopp forbi når
det ikke er noe å vise (unngår en ekstra obligatorisk trykk-runde for
hver hull-registrering). Klikk åpner nå en ny delt fullskjerm-
komponent (`hole-history-detail.tsx`) i stedet for å ekspandere en
liste inline -- stat-fliser, en score-fordeling som fargede stolper
(samme `CategoryBar`-mønster/farger som `round-stats.tsx`, bekreftet
med bruker som ØNSKET graf-form), og full instansliste under.
Bevisst avvik fra planen: ingen V0-runde her (i motsetning til
ADR-078) -- brukeren hadde allerede navngitt og bekreftet det
eksakte mønsteret å gjenbruke, ingenting å utforske via V0.
**Verifisert:** `tsc --noEmit` rent + 45/45 vitest. Scratch-database +
scratch `teecup_api` (port 18107): to frittstående runder + en
org-turnering på matchende bane-nøkkel, kombinert historikk bekreftet
i BEGGE flyter, aggregerte tall og score-fordelingsgraf bekreftet
håndregnet-korrekt, hull uten historikk bekreftet hoppet automatisk
forbi "overview"-steget. Lys+mørk bekreftet. Scratch-stacken
fullstendig revet ned.
Med dette er alle tre forbedringene i score-registreringen fullført.
**Ingen migrasjon.**
**Rullet ut 2026-08-16** -- ingen migrasjon, `docker compose build
teecup_frontend && up -d` etter eksplisitt bekreftelse fra bruker.
Containeren startet rent.
96. **Kontosammenslåing, selvbetjent (ADR-080, "Del 2" av flere
e-postadresser ADR-032) — 2026-08-17.** `request_secondary_email`
avviste tidligere en adresse som allerede eide av en ANNEN konto med
en 409 og "Ekte konto-sammenslåing støttes ikke ennå." Bekreftet med
bruker (AskUserQuestion): selvbetjening (ikke superadmin-verktøy),
hele sammenslåingen i én runde (forhåndsvisning + utførelse).
Ny migrasjon 077: `account_merge_token` (samme token-mønster som
`email_change_token`/`secondary_email_token`). Ny delt hjelpefunksjon
`resolve_user_id_by_email()` i `app/auth.py`, faktorert ut av
duplisert primær/sekundær-oppslag i `verify_magic_link`/`login_with_
password`. Ny modul `app/account_merge.py`: `compute_merge_preview()`
(les-only) + `execute_merge()` (N+1-transaksjoner -- én global
`plain_connection()` + én `org_connection(org_id)` per berørt org,
påkrevd av RLS-grensen, se ADR-080 for fullt konfliktkart). Nye
endepunkter i egen `app/routers/account_merge.py` (`/preview`,
`/request`, `GET /token/{token}`, `/confirm`). Ny frontend-seksjon
"Slå sammen med en annen konto" i `account-settings.tsx` + ny side
`app/kontosammenslaing/[token]/page.tsx`.
**Fant og fikset underveis (før produksjon, ren testsuite-fangst):**
seks RLS-beskyttede tabeller (`tournament`, `lineup_lock`,
`organization_invitation`, `message`, `message_comment`,
`tournament_round_participant_flag_plant`) + `message_reaction` var
feilaktig lagt i den globale "trygt å re-peke uten RLS"-løkken.
Krasjet (ikke stille no-op) med `invalid input syntax for type uuid:
""` -- en Postgres GUC-kvirk der `current_setting('app.current_org',
true)` returnerer tom streng, ikke NULL, på en pooled forbindelse som
tidligere hadde en committet `SET LOCAL` fra en annen transaksjon.
Flyttet til egen `_repoint_rls_scoped_safe_tables()`, kjørt inni riktig
`org_connection(org_id)`. Testsuite gikk fra 5 feilet/90 bestått til
95/95 bestått.
**Verifisert:** `tests/test_account_merge.py`, 12 nye tester (hver
rad i konfliktkartet + full ende-til-ende), full backend-suite 95/95.
`tsc --noEmit` rent + 45/45 vitest. Scratch-database + scratch
`teecup_api` (port 18108) + lokal `next dev` (port 13108): to ekte
kontoer med org-rolle-kollisjon (medlem→eier), org-repeking uten
kollisjon (→administrator), player/team_roster-kollisjon, felles
venn. Forhåndsvisning i kontoinnstillinger og fersk forhåndsvisning
på bekreftelsessiden (åpnet i egen, helt uautentisert nettleser-
kontekst) viste identisk, korrekt konfliktkart. Etter bekreftelse:
taperens `app_user`-rad bekreftet slettet direkte mot databasen,
taperens e-post nå keeperens sekundæradresse, org-roller korrekte,
player/team_roster-dedup korrekt, venn-selvlenke slettet. Innlogging
med taperens gamle e-post routet korrekt til keeper-kontoen (utløste
umiddelbart 2FA-oppsett, siden keeper nå er org-eier -- god indirekte
bekreftelse på at oppslaget faktisk traff riktig konto). Lys+mørk
bekreftet på bekreftelsessiden. Scratch-stacken (Docker-container,
database+rolle, MinIO-bucket, `next dev`) fullstendig revet ned --
ekte `teecup_db` urørt gjennom hele verifiseringen.
**Rullet ut 2026-08-17** -- bruker bekreftet ("Git Commit og deretter
ja"). Migrasjon 077 kjørt mot ekte `teecup_db` (additiv -- kun
tabellen `account_merge_token`, ingen eksisterende data rørt),
deretter `docker compose build teecup_api teecup_frontend && up -d`.
Rene containerlogger, `https://teecup.golf/logg-inn` bekreftet 200 OK
etterpå.
97. **Tjømes GolfAPI-koordinater erstattet med feltbefarte data (migrasjon
078) — 2026-08-17.** Bruker lastet opp en CSV (`Temp-uploads/Regneark
uten navn - Ark 2.csv`, 180 punkter) med koordinater samlet inn på
selve banen -- mer presise og mer detaljerte enn GolfAPI.io sin
automatiske cache (167 punkter, hentet 2026-08-12, se punkt 79/
ADR-064). Berører KUN `teecup_db` sin egen tredjeparts-cache
(`golfapi_course_coordinate`, course_id `0121250146602173`) -- ingen
kode endret, ingen container-restart nødvendig.
Egen research (Explore-agent) bekreftet FØRST hvor "eksisterende"
koordinater faktisk bor (IKKE teeoff -- Tjøme finnes ikke der, jf.
ADR-064) og at skjemaet allerede støtter den detaljerte per-hazard-
strukturen CSV-en beskriver. Flere reelle tolkningsspørsmål avklart
med bruker (AskUserQuestion) FØR noe ble skrevet:
- Ett "Tee"-punkt per hull (ikke to) → lagret som `tee_front` alene.
- Hull 4/13 hadde tre kandidat-par for "Forkant/bakkant høyre
fairwaybunker" (to i lat/long, ett i UTM/EPSG:25833) -- egen manuell
UTM→WGS84-konvertering (ingen `pyproj` tilgjengelig, implementert
fra Snyder-formlene) viste ~35 m avstand til nærmeste av de to
andre, for langt til å være GPS-støy. Bruker bekreftet: tre reelle,
atskilte bunkere, alle beholdt.
- "Voll" (steingjerde med gress over, tverrs over fairwayen, hull
3+12) → `rock` (samme kategori som fjellknaus).
- "Over vei"/"Over tre" (hull 7/8/16/17/18 og 17/18) → bruker
bekreftet "carry road"/"carry tree" -- mappet til eksisterende
`road`/`trees`, samme `back`/`center`-konvensjon som GolfAPIs egne
eksisterende road-punkter på hull 7/8/16/17.
- "Bjella" (hull 5+14, en fysisk bjelle spillerne ringer i for å
signalisere til gruppen bak) passet ikke noen av de 14 eksisterende
`poi_type`-verdiene -- ny verdi `landmark` lagt til (migrasjon 078,
samme mønster som `rock`/`layup` i migrasjon 067), rent informativt,
ikke en hindring (se `_HAZARD_LABELS` i `hole-target-distance.tsx`
-- viser i dag uansett kun green_bunker/fairway_bunker/water/rock,
`landmark` er dermed ikke synlig i appen ennå, samme status som
trees/marker_*/dogleg/road/tee_front/tee_back/layup allerede har).
- "Fairway" (rene referansepunkter uten hindringsbetydning, 6 stk) →
`layup` (samme bruk som Nesbyen-importen i punkt 79).
**Verifisert:** transformasjons-mappingen (Python, ren funksjon --
ikke committet, engangsskript samme mønster som Nesbyen-importen i
punkt 79) ga 180/180 rader mappet (ingen uidentifiserte navn), talt
opp per `poi_type` og kryssjekket for hånd mot CSV-ens egne
navnetellinger. `./scripts/run_backend_tests.sh` (95/95, migrasjon
078 bekreftet anvendbar). Selve slett+sett-inn-operasjonen KJØRT FØRST
mot en egen scratch-database (egen `golfapi_course`-rad seedet,
samme engangsskript kjørt der via `teecup_api`-containerens asyncpg),
verifisert der (180 rader, riktig `landmark`/UTM-konverterte
koordinater, `num_coordinates` oppdatert) FØR noe rørte ekte
`teecup_db`. Scratch-databasen droppet etterpå.
**Rullet ut 2026-08-17.** Migrasjon 078 kjørt mot ekte `teecup_db`.
Deretter kjørt mot ekte data: 167 gamle rader slettet, 180 nye satt
inn, `golfapi_course.num_coordinates` oppdatert til 180. Bekreftet
direkte mot databasen etterpå: riktig `poi_type`-fordeling, `landmark`-
punktene og de tre hull-4-bunkerne til stede med korrekte
koordinater. Ingen kodeendring -- ingen container-restart nødvendig.
98. **Rangefinder-koordinater for offisielle (TeeOff-koblede) baner
(ADR-081, migrasjon 079) — 2026-08-17.** Bruker påpekte at forrige
rundes Tjøme-koordinater havnet på Erol sin PERSONLIGE GolfAPI-bane,
ikke den offisielle TeeOff-koblede banen "Tjøme Gents" faktisk
bruker til org-turneringer -- og ba om at koordinatene også legges
inn der, OG at dette blir en generell mulighet for alle offisielle
baner fremover.
Rangefinder fantes til nå kun for GolfAPI-importerte personlige
baner -- verken org-turneringer (individuell slagspill/lag-matchplay)
eller frittstående runder på en EKTE teeoff-bane hadde noen
koordinatkilde. Bekreftet med bruker (AskUserQuestion): begge
org-turnering-scoringsflytene wires inn i samme runde, ikke faset.
Migrasjon 079: delte domener (`course_poi_type`/`location`/`side`)
for å stoppe et allerede observert vedlikeholdsproblem (067 og 078
måtte begge utvide samme dupliserte CHECK-liste), ny global tabell
`teeoff_course_coordinate` (nøkkel `course.external_course_ref`, samme
"delt cache, ikke per-org"-begrunnelse som `golfapi_course_
coordinate`). Ny delt modul `app/target_points.py` +
`resolve_match_course_key()` (tredje søster til de to eksisterende
bane-bro-funksjonene i `hole_history.py`, ADR-072) -- alle tre
kallesteder (frittstående runder, individuell org-turnering,
lag-matchplay) bruker nå samme `CourseKey`-abstraksjon. `rounds.py`
sitt eksisterende endepunkt refaktorert til samme mønster (villet
sideeffekt: frittstående runder på en ekte teeoff-bane får nå også
rangefinder). Ny skrive-vei `PUT`/`GET .../courses/{id}/coordinates`
i `courses.py` -- generell, gjenbrukbar, ikke en engangsfiks.
`HoleTargetDistance` generalisert (`roundId` -> `baseUrl`-prop) og
wiret inn i `HoleStatsSheet` (individuell slagspill) og `session-
scorecard.tsx` (lag-matchplay, samme mønster som `NassauPanel`).
**Verifisert:** `tests/test_target_points.py`, 11 nye tester (106/106
backend totalt). `tsc --noEmit` rent + 45/45 vitest. Scratch-database
+ scratch `teecup_api` + lokal `next dev`: koordinater satt via det
nye PUT-endepunktet over ekte HTTP, rangefinder bekreftet korrekt i
BEGGE org-turnering-UI-ene (riktig data på hull med koordinater,
ingen synlig rad på hull uten -- nettverksspor bekreftet nøyaktig
hvilke punkter som ble hentet). Lys+mørk bekreftet begge steder.
Regresjonssjekk: eksisterende GolfAPI-personlig-bane-rangefinder
satt opp i samme scratch-miljø, bekreftet uendret oppførsel etter
refaktoreringen. Scratch-stacken fullstendig revet ned.
**Rullet ut 2026-08-17** -- bruker bekreftet ("Git commit og rull
ut"). Migrasjon 079 kjørt mot ekte `teecup_db` (eksisterende
`golfapi_course_coordinate`-data verifisert uendret, 399 rader),
deretter `docker compose build teecup_api teecup_frontend && up -d`.
Rene containerlogger, `https://teecup.golf/logg-inn` 200 OK. Tjømes
offisielle bane fylt med de samme 180 koordinatene fra forrige runde
(samme mapping gjenbrukt, ikke re-avklart) via et engangsskript i
`teecup_api`-containeren mot det nye endepunktets tabell -- bekreftet
180/180 rader med korrekt `poi_type`-fordeling.
99. **Massimport av spillere til turneringer (CSV) + redigerbar
spillertabell (ADR-082) — 2026-08-17.** Bruker ba om en CSV-basert
vei inn i stedet for én-og-én-skjema. Bekreftet med bruker: begge
steg samlet (org-pool + påmelding til turneringen du står i), og
både lagturneringer (lagtildeling via CSV-kolonne) og individuelle
turneringer (valgfri klasse-kolonne) dekket samtidig.
Nytt endepunkt `POST /orgs/{id}/players/bulk` -- samme "match på
e-post, fyll kun tomme felt"-mønster som selvregistreringens
ADR-017 Beslutning B, utvidet til alle felt. `display_name`/`paid`
røres aldri på en eksisterende match. Selve turnering-/lag-
påmeldingen gjenbruker de eksisterende participants-/roster-
endepunktene i en frontend-løkke -- ingen ny bulk-registrerings-vei,
ingen migrasjon. Ny `papaparse`-avhengighet, CSV parses klient-side,
enkel fuzzy kolonnegjetting (norsk/engelsk), overstyrbar per
kolonne. Nytt "Importer fra CSV"-inngangspunkt i BÅDE
`tournament-detail.tsx` og `individual-tournament-detail.tsx`.
**Midlertidig hånd-kodet UI (samme rekkefølge som FlagPlantSheet):**
`player-import-panel.tsx` bygget hånd-kodet FØRST for å bevise hele
kjeden ende-til-ende, venter nå på en V0-eksport for den polerte
visningen -- selve logikken (CSV-parsing/kolonne-gjetting/API-
orkestrering) forblir uendret ved bytte, komponenten er allerede en
kontrollert "dum" visning utad.
**Verifisert:** `tests/test_players_bulk.py`, 5 nye tester (111/111
backend totalt). `tsc --noEmit` rent + 45/45 vitest.
Scratch-database + scratch `teecup_api` + lokal `next dev`: én org
med lagturnering (to lag) + individuell turnering (én klasse), CSV
med blandede rader (ny/matchende-på-e-post/ukjent-lagnavn) importert
i lagturneringen -- bekreftet direkte mot databasen at match-på-
e-post kun fyller tomme felt, ukjent lagnavn flagget uten å
blokkere. Individuell-import med klasse-kolonne bekreftet riktig
tilknytning. Lys+mørk bekreftet på importpanelet. Scratch-stacken
revet ned.
**Kjent begrensning (ikke rettet, UI-et erstattes uansett):**
kolonnegjettingen normaliserer ikke æ/ø/å -- "Kjønn" traff ikke
automatisk, måtte rettes manuelt i kolonnevalget (fungerte som
forventet, ikke blokkerende).
**Ingen migrasjon.**
**V0-eksport mottatt og integrert samme dag.** Delt i `player-
import-view.tsx` (V0, ren visning) + `player-import-panel.tsx`
(Claude, orkestrering) -- FlagPlantSheet-mønsteret. Fant og fikset
én reell integreringsfeil i scratch: V0s kjønn-nedtrekk bruker
`female`/`male`/`other`, rå CSV-tekst ("m"/"f") ble kopiert inn
uendret og ville stille tapt data ved lagring -- fikset med egen
normalisering ved kolonnetilknytningen. æøå-diakritikk-begrensningen
fra tidligere fikset samtidig. Ny scratch-runde bekreftet begge
turneringsformater med den polerte visningen, korrekt lagret
`m`/`f` i databasen, lys+mørk. Se ADR-082 for full detalj.
Underveis: bruker ga en ny STÅENDE regel (turneringsoppsett skal
designes "stor skjerm først", ikke mobil-først, siden organisatoren
sitter foran en PC) -- lagt til i CLAUDE.md, V0-prompten for denne
komponenten oppdatert og sendt på nytt før kjøring i v0.app.
**Rullet ut 2026-08-17** -- bruker bekreftet ("Git Commit og rull
ut"). `docker compose build teecup_api teecup_frontend && up -d`,
ingen migrasjon. Rene containerlogger, `https://teecup.golf/logg-
inn` 200 OK.
100. **Visuelt hull-diagram for rangefinderen (ADR-083) — 2026-08-17.**
Bruker rapporterte fra et ekte skjermbilde (produksjon) at kun ÉN
hindring vises selv om hullet har flere, og ba om at tee vises
nederst/senter green øverst -- bekreftet som ønske om et ekte
visuelt diagram med full 2D-plassering (langs + venstre/høyre), ikke
bare listerekkefølge.
To atskilte fikser: (1) `hole-target-distance.tsx` sin "kun
nærmeste"-reduksjon erstattet med en full, sortert hindringsliste
(`target-distance.tsx` fikk en `HazardList`) -- gjelder ALLE baner,
uavhengig av diagram. (2) Ny geometri-modul
`frontend/lib/hole-geometry.ts` (`projectOntoAxis`, bygget på
eksisterende `haversineMeters`/`bearingDegrees`) projiserer
hindringer+spiller ned på en FAST tee->green-akse (tee_front/
tee_back-midtpunkt, IKKE spillerens bevegelige posisjon). 10 nye
enhetstester beviste geometrien matematisk riktig FØR noe visuelt
ble bygget. Valgt ren SVG/CSS-skjematisk fremstilling fremfor et nytt
Mapbox-kart (kostnadskontroll-prinsipp fra ADR-048, konsistent
orientering uansett gangretning).
Midlertidig hånd-kodet visning (`hole-diagram-view.tsx`) bygget
først for å bevise kjeden -- venter på V0-eksport for den polerte
versjonen, samme rekkefølge som FlagPlantSheet/PlayerImportPanel.
Rendres kun når tee-geometri finnes; `target-distance.tsx` sin flate
visning (nå med full hindringsliste) forblir uendret fallback.
Gjelder automatisk alle tre rangefinder-kallesteder (frittstående
runder, individuell org-turnering, lag-matchplay).
**Verifisert:** `hole-geometry.test.ts` 10/10, full `vitest run`
55/55, `tsc --noEmit` rent. Scratch-database med ETT hull med
presist kjente syntetiske koordinater -- GPS simulert via
`initScript` (headless Chrome nekter ekte geolokasjon). ALLE viste
tall stemte eksakt med håndregnede fasitverdier (front 140/senter
150/bak 160/hindringer 54 m og 131 m). Visuelt bekreftet riktig
venstre/høyre- og langs-plassering av begge hindringene og
spilleren. Fallback bekreftet på hull uten tee-koordinater. Lys+mørk
bekreftet. Scratch-stacken revet ned.
**Ingen migrasjon** -- ren frontend, backend uendret (target-points
returnerte allerede tee_front/tee_back, bare ikke brukt før nå).
**V0-eksport mottatt og integrert samme dag.** Traff props-
kontrakten eksakt -- ingen integreringsfeil å rette denne gangen
(til forskjell fra ADR-082/spillerimportens kjønn-mismatch), `tsc`
rent på første forsøk. To ekstra kvalitetstillegg fra V0 utover det
som ble bedt om: full `aria-label`-oppsummering av hele diagrammet
for skjermlesere, og en lett kollisjons-unngåelse for hindrings-
etiketter som havner nær hverandre. Re-verifisert i scratch mot
samme kjente fixture -- identiske tall/posisjoner som den
midlertidige versjonen. Lys+mørk bekreftet.
**Rullet ut 2026-08-17** -- bruker bekreftet. `docker compose build
teecup_frontend && up -d`, ingen migrasjon. Ren containerlogg,
`https://teecup.golf/logg-inn` 200 OK.
101. **Hull-diagram v2: kategoriske akser + hazard_group (ADR-084) —
2026-08-17.** Bruker viste et ekte skjermbilde fra Tjøme hull 18:
ADR-083 sin kontinuerlige `crossMeters`-forskyvning ga for lite
synlig venstre/høyre-forskjell på ekte koordinater -- bunker
(faktisk venstre) og vann (faktisk senter) havnet begge nesten på
senterlinjen. Løsning: tre FASTE kategoriske baner
(venstre/senter/høyre) drevet direkte av `side_fairway`, ikke av
utledet `crossMeters`. Green og tee alltid senterakse (uansett egen
`side_fairway`); spiller ("Deg") også alltid senterakse, kun
langs-hull-posisjon. Carry-hindringer (front+bak-par) -> ett ikon,
forkant under/bakkant over; enkeltpunkt -> én avstand under.
Første forsøk grupperte forkant/bakkant via `poi_type`+
`side_fairway`-heuristikk -- bruker avviste eksplisitt: to atskilte
hindringer av samme type på samme side (f.eks. to venstre-bunkere)
må ALDRI slås sammen. Undersøkt: ingen eksisterende gruppe-/
hindrings-id fantes i verken `golfapi_course_coordinate` eller
`teeoff_course_coordinate`. Løst med ny migrasjon 080:
`hazard_group text` (nullable) på begge tabeller -- `NULL` (alt
eksisterende data i dag) = alltid enkeltstående, aldri slått
sammen; ikke-null = eksplisitt menneskelig bekreftet pardata, satt
ved data-inntasting (samme tillitsnivå som `poi_type`/`location`,
ADR-081). Eksponert i `app/target_points.py` og
`app/routers/courses.py` (`CoursePointIn`/`Out`,
`_COORDINATE_COLUMNS`, INSERT i `replace_course_coordinates` --
eneste skrive-vei, kun offisielle TeeOff-baner; Tjømes personlige
`golfapi_course_coordinate`-data har ingen skrive-endepunkt, kun
engangsskript).
Nye ikoner fra bruker (PNG, 32×31 sand/vann, 40×40 green) i
`frontend/public/hole-diagram/` -- IKKE de tre opplastede
landskaps-SVG-ene (fulle illustrerte banekart, 237-495 path-
elementer hver), avvist som for detaljerte til å være lesbare
nedskalert til 32-40px (CLAUDE.md sin ståenede tilgjengelighets-
regel). Typer uten dedikert ikon (i dag kun `rock`) faller tilbake
til `TriangleAlert`.
`hole-target-distance.tsx` fikk `groupDiagramHazards()` (grupperer
UTELUKKENDE på delt, ikke-null `hazard_group`) som erstatter den
gamle per-punkt-mappingen til `HoleDiagram`; den flate `hazards`-
listen til `TargetDistance`-fallback (baner uten tee-koordinater)
er uendret. `hole-diagram-view.tsx` skrevet om fra kontinuerlig
`crossToLeftPct` til `grid-cols-3` med tre uavhengige baner, hver
med egen kollisjons-forskyvning.
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55, `pytest`
111/111 (migrasjon 080 kjørt i scratch-testsuiten). Egen scratch-
database+rebygd scratch `teecup_api`+lokal `next dev`, presist
kjent syntetisk datasett med to ATSKILTE venstre fairway-bunkere
(én paret via `hazard_group`, én enkeltstående) -- bekreftet TO
atskilte ikoner (ikke slått sammen), riktig forkant(54m)/
bakkant(63m) på den parede, riktig enkelttall(103m) på den
enkeltstående. Vannhinder (senter, 130m) og rock (høyre, generic-
ikon, 18m) riktig plassert. Green/tee bekreftet alltid midtbane.
Lys+mørk bekreftet. Scratch-stacken revet ned.
**Rullet ut 2026-08-18** -- bruker bekreftet migrasjon 080
eksplisitt ("Jeg bekrefter"). `ALTER TABLE ... ADD COLUMN
hazard_group text` kjørt mot ekte `teecup_db` (begge tabeller),
deretter `docker compose build teecup_api teecup_frontend && up
-d` (egen bekreftelse). Rene containerlogger, `https://teecup.golf/
logg-inn` 200 OK.
**Gjenstår:** liste over `_HAZARD_CONFIG`-typer uten eget ikon
(i dag kun `rock`/"Fjellknaus") leveres til bruker. V0-prompt for
polert visning skrives etter godkjenning av den hånd-kodede
versjonen.
102. **Hull-diagram v2, retting fra ekte produksjonsbruk (ADR-084 del
2) — 2026-08-18.** Bruker viste et ekte skjermbilde av v2-
diagrammet i produksjon og meldte fem avvik: (1) hindringer med
forkant+bakkant viste to ikoner i stedet for ett -- skyldtes at
ekte Tjøme-data ennå ikke hadde `hazard_group` satt, ikke en
kode-feil. (2) Et ikon overlappet green-ikonet i midtbanen -- ekte
kode-feil, `layoutLane()` tok ikke hensyn til green-ikonets egen
plass; fikset med en reservert sone (`CENTER_TRACK_TOP`). (3-4)
Listen under diagrammet manglet forkant/senter/bakkant- og
venstre/senter/høyre-info, og viste hvert par to ganger -- skrevet
om til én rad per hindring med begge avstander på samme rad, pluss
undertekst for bane/posisjon. (5) Ikonene i listen byttet fra
universelt 20px `TriangleAlert` til type-spesifikke ikoner ved
36px.
Underveis, da hull 4 sin "bakre greenbunker" skulle pares: bruker
påpekte at den fysisk ligger BAK selve greenen og derfor må vises
OVER green-ikonet, ikke i vanlig-sonen. `alongPercent` fra
`projectOntoAxis` klippes bevisst ikke i geometrimodulen (kan bli
>100), men visningen klippet den likevel -- fikset ved å flytte
green-ikonet ned og åpne en egen sone over det
(`GREEN_TOP_PCT_PUSHED`/`BEHIND_GREEN_ZONE_*`,
`layoutBehindGreenLane` kaskaderer oppover) når minst én
midtbane-hindring faktisk ligger bak green.
Fem nye ikoner (fjellknaus/trær/dogleg/layup/landemerke, alle
32px) integrert -- disse typene var i domenet fra før men manglet
egen visning i diagrammet (fjellknaus hadde generisk ikon, resten
var usynlige der).
**Ny ekte `hazard_group`-data for Tjøme, fra kildedata, ikke
heuristikk:** bruker lastet opp klubbens eget koordinat-regneark,
som viste seg å ha en EKSPLISITT gruppenummer-kolonne som allerede
parer forkant/bakkant -- akkurat identiteten `hazard_group` (del 1)
var designet for. Kryssjekket regnearkets 38 grupper mot databasens
180 punkter via lat/long (engangs Python-script) -- løste
DEFINITIVT de to sakene forrige runde bevisst lot stå
(hull 4: tre høyre fairway-bunkere, hull 13: to) ved eliminasjon,
til tross for at 2 rader i regnearket hadde feil koordinatformat
(UTM/EPSG:25833 -- trolig kopi-lim-feil, uskadelig siden kun
gruppenummeret var nødvendig). Fant i tillegg at `rock` også har
forkant/bakkant-par på fire hull (ikke fanget opp i del 1). Bruker
bekreftet manuelt én ekstra paring uten gruppenummer i regnearket
(hull 4 sin bakre greenbunker). Resultat: 78 rader satt til 39
distinkte `hazard_group`-verdier (`{hull}-{regneark-gruppe}`,
sporbart til kilden) via en frittstående `UPDATE ... FROM
(VALUES ...)` -- datainnhold, ikke skjema, ingen ny migrasjon.
**Bevisst utsatt:** bruker lastet opp et bekk (creek)-ikon, men
domenet `course_poi_type` skiller ikke bekk fra dam (begge
`water`) -- å vise dem ulikt krever en ny domeneverdi +
reklassifisering av spesifikke punkter. Bruker ba eksplisitt om at
dette blir en EGEN runde, ikke bakt inn her.
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55 (to ganger).
Ny scratch-runde med presist konstruert "bak green"-scenario --
bekreftet visuelt at bakre greenbunker rendres over green-ikonet,
normal-sonens hindringer uendret ved siden av. Lys+mørk bekreftet
begge runder. Begge scratch-stackene revet ned.
**Rullet ut 2026-08-18** -- bruker bekreftet kode og data hver for
seg. `docker compose build teecup_frontend && up -d` (kun
frontend-kode, ingen migrasjon), deretter UPDATE-en mot ekte
`teecup_db`. Rene containerlogger, `https://teecup.golf/logg-inn`
200 OK, 78/78 rader bekreftet satt (39 distinkte grupper).
**Gjenstår:** bekk som egen `poi_type` (egen runde, bedt om
eksplisitt av bruker). Ikon-mangel-lista er nå kun `road`. V0-
prompt for polert visning fortsatt ikke skrevet.
103. **Egen poi_type "creek" for bekk (ADR-084 del 3) — 2026-08-18.**
Del 2 samme dag lot bekk-ikonet ligge ukoblet -- domenet
`course_poi_type` skilte ikke bekk fra dam (begge `water`), og
bruker ba om egen runde for det. Migrasjon 081 la til `creek` i
det delte domenet (`ALTER DOMAIN ... DROP/ADD CONSTRAINT`, gjelder
automatisk begge koordinattabellene). Egen `UPDATE` (ikke
migrasjon) reklassifiserte de 8 kjente Tjøme-bekk-punktene (hull
2/3/11/12) fra `water` til `creek` -- identifisert presist via
samme koordinat-regneark+lat/long-matching som ga hazard_group-
dataen i del 2, `hazard_group` urørt. Nytt ikon
(`hazard-creek.png`) koblet inn i `_HAZARD_KIND`/`_ICON_SRC` under
egen `HazardKind`, label "Bekk". Ingen backend-endring nødvendig
(`poi_type` er `str`, ikke et strengt enum, i både
`target_points.py` og `courses.py`).
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55, full
`./scripts/run_backend_tests.sh` 111/111 (migrasjon 081 i scratch-
testsuitens egen database). Ny scratch-runde med konstruert bekk-
par -- bekreftet eget, distinkt ikon (ikke forvekslet med vann),
lys+mørk. Scratch-stacken revet ned.
Samtidig ryddet: flere gamle, TOMME MinIO scratch-bøtter fra
tidligere økter (`teecup-scratch-hh/hs/oom/players/roster/wz`) som
aldri ble fjernet ved forrige teardown -- bekreftet adskilt fra
ekte data (`teecup-media`) og tomme før sletting, ingen andre
glemte scratch-ressurser funnet.
**Rullet ut 2026-08-18** -- bruker bekreftet migrasjon,
reklassifisering og kode hver for seg. Kjørt mot ekte `teecup_db`,
deretter `docker compose build teecup_frontend && up -d`. Rene
containerlogger, `https://teecup.golf/logg-inn` 200 OK, 8/8 rader
bekreftet `poi_type = 'creek'`.
104. **Road-ikon, siste hull i ikon-mangel-lista (ADR-084 del 4) —
2026-08-18.** Bruker lastet opp `road.png` (32×32) -- eneste
gjenstående klassifiserte hindringstype uten eget ikon. Ren
frontend-endring: `road` fantes allerede som domeneverdi og som
ekte data (5 enkeltpunkter på Tjøme, hull 7/8/16/17/18), men var
usynlig i diagrammet siden `_HAZARD_LABELS`/`_HAZARD_KIND` ikke
inkluderte den. Lagt til med identisk mønster som de fem
foregående ikonene denne økten -- ingen migrasjon, ingen
datareklassifisering.
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Ingen ny
scratch-visningsrunde -- identisk, gjentatte ganger allerede
bekreftet kodevei.
**Rullet ut 2026-08-18** -- bruker bekreftet. `docker compose
build teecup_frontend && up -d`, ingen migrasjon. Rene
containerlogger, `https://teecup.golf/logg-inn` 200 OK.
Ikon-mangel-lista er nå tom.
105. **Sortering, side_fairway-diagnose, standardisert CSV-format
(ADR-084 del 5) — 2026-08-18.** Hindringslisten manglet en
eksplisitt "nærmest spilleren først"-sortering (falt tilbake til
rå API-rekkefølge) -- fikset med `sort((a,b) => a.below - b.below)`
i `groupDiagramHazards()`. Kryssjekket samtidig alle punkter i den
gamle CSV-en med eksplisitt venstre/høyre mot databasens
`side_fairway` -- fant 12 avvik (satt til `center` når kilden sa
noe annet), som bekreftet at enkeltpunkt-lapping ikke holdt.
Avtalte i stedet et standardisert kolonneformat
(`ID,HULL,TYPE,PLASS,PUNKT,LAT/LNG`) med bruker for fremtidig
reimport -- `ID` unik kun innenfor ett hull, samme prinsipp som
`hazard_group`.
106. **Full reimport av Tjøme, to nye poi_type (ADR-084 del 6) —
2026-08-18.** Bruker leverte hele 18-hulls-fila i det avtalte
formatet. Validering avdekket og avklarte seks ting: to nye
TYPE-verdier (`Fairway`/`Voll`, egne ikoner fra bruker + migrasjon
082 for `fairway`/`stone_fence`), ett par uten PUNKT løst via
brukerens regel (siste siffer i koordinaten avgjør forkant/
bakkant), én bekreftet kopier-lim-feil (hull 11), seks UTM/
EPSG:25833-korrupte rader løst med ordentlig `pyproj`-projeksjon
(konvergerte på under 1mm avvik fra forrige rundes elimineringsvis
gjettede verdier -- god kryssbekreftelse), og ett bevisst fjernet
veipunkt (hull 18).
Hull 4 sine green-/greenbunker-koordinater viste seg vesentlig
endret mellom database og ny fil -- inkludert det forrige runde
identifiserte som "bakre greenbunker" via eliminasjon, som var feil
koordinater. For risikabelt å patche inkrementelt -- kjørte i
stedet full `DELETE`+`INSERT` (180 gamle rader → 194 nye) i én
transaksjon, beregnet direkte fra kolonnene.
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55, full
`pytest` 111/111 (migrasjon 082). Reimport-SQL kjørt FØRST mot en
scratch-kopi av ekte Tjøme-data -- 194/39 riktig, fordeling
matchet plan eksakt. Visuell bekreftelse av `fairway`/
`stone_fence`-ikonene og sorteringsfiksen med ekte reimportert
data (hull 1/2/3), lys+mørk. Scratch-stacken revet ned.
**Rullet ut 2026-08-18** -- bruker bekreftet migrasjon, reimport
og kode hver for seg. Migrasjon 082 + reimport (DELETE 180/INSERT
194, én transaksjon) kjørt mot ekte `teecup_db`, deretter `docker
compose build teecup_frontend && up -d`. Hull 7 sitt vann
bekreftet `side_fairway='left'` etter reimport.
107. **Tjøme fantes i to atskilte tabeller -- kun én reimportert
(ADR-084 del 7) — 2026-08-18.** Bruker rapporterte at en gruppert
hindring fortsatt viste to ikoner -- viste seg at forrige rundes
reimport kun traff `golfapi_course_coordinate` (frittstående
runder). Fant én til: `teeoff_course_coordinate` for
`external_course_ref='tjome-golfklubb:140'` (den "official"
org-tilknyttede banen "Hovedbanen", brukt av turneringer/lag-
matchplay) -- fortsatt med de gamle 180 punktene, 0 hazard_group.
Ingen tredje representasjon funnet (bekreftet via `course`/
`golfapi_course`/`personal_course`).
Samme 194-rads datasett fra forrige runde satt inn i
`teeoff_course_coordinate` i stedet -- identiske kolonner/domener
siden migrasjon 079. Kjørt FØRST mot en scratch-kopi av de ekte
180 radene, 194/39 bekreftet identisk med forrige runde. Ingen
kode-endring nødvendig (samme deployerte kode leser nå riktig
data fra begge kilder).
**Rullet ut 2026-08-18** -- bruker bekreftet. Reimport-SQL (DELETE
180/INSERT 194, én transaksjon) kjørt mot ekte `teecup_db`.
Bekreftet 194/39/poi_type-fordeling identisk med forrige runde,
hull 7 sitt vann `side_fairway='left'`.
**Lærdom:** Tjøme har to helt separate koordinat-datasett (én for
frittstående runder, én for org-turneringer) -- verdt å huske ved
fremtidige datakorreksjoner, for denne eller andre baner med
begge tilkoblingstyper.
108. **Hull-valget overlevde ikke en refresh — 2026-08-18.** Bruker
rapporterte at en refresh mens man sto på f.eks. hull 7 alltid
hoppet tilbake til starthullet. Årsak: `currentHole` i
`round-detail.tsx` levde KUN i React-state (seedet fra
`round.start_hole` ved hver mount, se den eksisterende `null`- vs
`1`-forklaringen fra 2026-07-24) -- aldri i URL-en, så en full
remount (refresh) mistet valget fullstendig.
Fikset ved å gjenbruke det samme `?param`-i-URL-mønsteret siden
allerede har for fane-valget (`?tab=manage`): ny `?hole=N`-
parameter, lest ved mount (`initialHole`), holdt i sync ved
senere navigasjon via en ny felles `setHoleAndUrl()`-funksjon
(erstatter ALLE direkte `setCurrentHole`-kall utenom selve
URL-seedingen i `loadRound`) som oppdaterer BÅDE state og URL med
`router.replace` (ikke `push` -- hull-bytte skal ikke fylle
historikken med ett tilbake-steg per hull). En egen `useEffect`
speiler `?hole=`-parameteren tilbake inn i state ved eksterne
URL-endringer (nettleserens frem/tilbake).
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Scratch-
runde (database+API+next dev): naviger til hull 7 (URL bekreftet
`?hole=7`), hard refresh (`ignoreCache`) -- siden viste fortsatt
hull 7, ikke hull 1. "Neste hull" bekreftet også oppdaterer URL-en
(`?hole=8`). Scratch-stacken revet ned.
**Ingen migrasjon** -- ren frontend-endring
(`frontend/components/round-detail.tsx`).