2026-07-16 07:18:01 +02:00
|
|
|
|
# TeeCup — Arkitektur-beslutningslogg (ADR)
|
|
|
|
|
|
|
|
|
|
|
|
> Dette dokumentet er den autoritative kilden til *hva* som er bestemt og *hvorfor*.
|
|
|
|
|
|
> Det leses av mennesker og av AI-modeller (Claude, Gemini) i starten av hver økt.
|
|
|
|
|
|
> Endre aldri en beslutning uten å legge til en ny ADR som erstatter den — historikken skal bevares.
|
|
|
|
|
|
|
|
|
|
|
|
**Status:** Levende dokument
|
ADR-033 er nå fullt kildebelagt. Kort oppsummert hva som endret seg:
Banedata: bekreftet — live oppslag mot teeoff, ikke import.
HCP: leste hele den offisielle WHS Rules of Handicapping 2024 (Rule 2, 3, 5, 6). To viktige presiseringer av det du selv antok:
«Minst 9 hull» stemmer ikke helt — en 9-hulls-runde må ha alle 9 hull i et faktisk ratet sett (front eller back), mens en 18-hulls-runde bare trenger minst 10 av 18 (resten fylles med en «expected score», ny metode fra 2024 som erstattet den gamle «net par»-metoden). Dette betyr også at fritt starthull (Beslutning E) fungerer fint for vanlig logging, men en runde blir kun HCP-tellende hvis de spilte hullene faktisk samsvarer med banens ratede 18/front-9/back-9 — en vilkårlig 9-hulls-strekning har ingen egen rating.
Fant en presis, tidligere ukjent detalj: en 9-hulls Course Handicap halverer indeksen først (Index÷2 × Slope/113 + (Rating−Par)) — helt annen formel enn 18-hulls-varianten. Viktig: dette gjelder frittstående 9-hulls-runder, ikke det eksisterende front_9/back_9-øktoppsettet i turneringsflyten, som løser et annet problem (slagfordeling internt i en match) og bevisst skal la det være.
Full kjede (Score Differential, Net Double Bogey, beste-8-av-20, Low Handicap Index, soft/hard cap, Course/Playing Handicap) er nå skrevet inn i ADR-033 med eksakte tall fra kilden. PCC og Exceptional-Score-justering er bevisst avgrenset ut av v1 (forstått, men krever data på tvers av alle brukeres runder samme dag — egen, senere runde).
2026-07-22 09:26:08 +02:00
|
|
|
|
**Sist oppdatert:** 2026-07-22
|
2026-07-16 07:18:01 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Kontekst
|
|
|
|
|
|
|
|
|
|
|
|
TeeCup er en kommersiell (SaaS) webapplikasjon for å opprette, administrere og
|
|
|
|
|
|
gjennomføre golfturneringer i «Ryder Cup»-format. Den driftes på samme VPS som
|
|
|
|
|
|
`teeoff.no`, men skal fungere som et isolert økosystem med eget subdomene
|
|
|
|
|
|
(`teecup.teeoff.no`) og egen database (`teecup_db`).
|
|
|
|
|
|
|
|
|
|
|
|
Målgruppe: klubber, bedrifter og vennegjenger som arrangerer turneringer over tid.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-001 — Tenant = Organisasjon
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** Isolasjonsenheten («tenant») er en **organisasjon**, ikke en turnering.
|
|
|
|
|
|
|
|
|
|
|
|
En organisasjon er et bevisst nøytralt begrep som dekker klubb, bedrift *og*
|
|
|
|
|
|
vennegjeng. Ved å ikke kalle den «klubb» i datamodellen unngår vi refaktorering
|
|
|
|
|
|
den dagen første bedriftskunde kommer.
|
|
|
|
|
|
|
|
|
|
|
|
**Hierarki:**
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
Organisasjon (tenant — det som isoleres og faktureres)
|
|
|
|
|
|
├── Medlemmer/spillere (gjenbrukbare på tvers av turneringer)
|
|
|
|
|
|
├── Egendefinerte baner
|
|
|
|
|
|
└── Turneringer
|
|
|
|
|
|
├── Lag
|
|
|
|
|
|
└── Matcher
|
|
|
|
|
|
└── Scores
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** En turnering er en hendelse med start og slutt, ikke en kunde.
|
|
|
|
|
|
Gjenbrukbare ting (spillere, baner, historikk) må leve over turneringens levetid.
|
|
|
|
|
|
Turnering er derfor en entitet *inne i* en organisasjon.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** `tenant_id` i alle skjemaer heter `organization_id`.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-002 — Bruker og organisasjonsmedlemskap er adskilt
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** Identitet (innlogging) og medlemskap i en organisasjon er to
|
|
|
|
|
|
forskjellige ting. Én `user` kan ha flere `organization_memberships`.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Samme person spiller ofte både klubbturneringen og
|
|
|
|
|
|
jobbturneringen. En bruker må kunne krysse organisasjoner med samme innlogging.
|
|
|
|
|
|
Dette er lett å bygge inn fra start og smertefullt å legge til senere.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** Utelukker «database-per-tenant» (se ADR-003), fordi en bruker som
|
|
|
|
|
|
krysser organisasjoner da måtte eksistere i flere databaser samtidig.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-003 — Isolasjonsstrategi: Shared schema + Row-Level Security
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** Én database (`teecup_db`), delte tabeller, isolert på
|
|
|
|
|
|
`organization_id`-kolonne, håndhevet av PostgreSQL **Row-Level Security (RLS)**.
|
|
|
|
|
|
|
|
|
|
|
|
**Vurderte alternativer:**
|
|
|
|
|
|
|
|
|
|
|
|
| Strategi | For | Mot | Valgt |
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
| Shared schema + `organization_id` + RLS | Enkel drift, billig, skalerer til mange org. | Krever disiplin; RLS må settes riktig | **Ja** |
|
|
|
|
|
|
| Database/schema per tenant | Sterk isolasjon | Tung migrering; bryter med ADR-002 | Nei |
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** RLS flytter isolasjonen fra applikasjonskoden (der én glemt
|
|
|
|
|
|
`WHERE organization_id = ...` lekker data mellom kunder) ned til databasen, som
|
|
|
|
|
|
håndhever den uansett hva koden gjør. Kombinert med ADR-002 er dette det eneste
|
|
|
|
|
|
praktiske valget.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens / oppgave:** Hver økt/tilkobling må sette `SET app.current_org` (eller
|
|
|
|
|
|
tilsvarende) slik at RLS-policyen kan filtrere. Dette må inn i tilkoblingslaget
|
|
|
|
|
|
tidlig, ikke ettermonteres.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-004 — Banedata fra `teeoff_db` via enveis, lesende API-kontrakt
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** TeeCup henter offisielle banedata fra hovedplattformen gjennom et
|
|
|
|
|
|
veldefinert, lesende API — **ikke** via direkte databasekobling på tvers.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** En direkte kobling ville låst TeeCup til hovedplattformens
|
|
|
|
|
|
skjemaendringer, og et brudd ett sted ville tatt ned begge produktene.
|
|
|
|
|
|
`teeoff_db` er «master» for offisielle banedata; alt brukergenerert innhold
|
|
|
|
|
|
(egendefinerte baner, turneringer, brukere) forblir strengt i `teecup_db`.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** API-kontrakten mot TeeOff må versjoneres og behandles som en
|
|
|
|
|
|
ekstern avhengighet, selv om den kjører på samme server.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-005 — Handicap-motoren er et frittstående, testet bibliotek
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** All handicap- og score-logikk bygges som en ren Python-modul uten
|
|
|
|
|
|
avhengigheter til database, API eller web-rammeverk. Den testes grundig i
|
|
|
|
|
|
isolasjon.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Dette er produktets hjerte og den mest risikofylte biten. Feil
|
|
|
|
|
|
her koster mest troverdighet. Ved å isolere den kan reglene enhetstestes mot kjente
|
|
|
|
|
|
fasitverdier uavhengig av resten av systemet.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens / viktig presisering:** Prosentbaserte «allowances» (f.eks. 75 %,
|
|
|
|
|
|
90 %, 3/4) er **konfigurasjon, ikke hardkodet logikk**. Motoren vet ikke at
|
|
|
|
|
|
«fourball = 90 %»; den mottar tildelingen som parameter. De konkrete
|
|
|
|
|
|
standardprosentene per format må verifiseres mot gjeldende WHS/lokale regler før
|
|
|
|
|
|
produksjon — de skal ikke antas fra hukommelse.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-006 — Teknologistack
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:**
|
|
|
|
|
|
- **Backend:** Python + FastAPI (gjenbruker stacken fra TeeOff → enklere vedlikehold).
|
|
|
|
|
|
- **Database:** PostgreSQL (+ PostGIS der banegeometri trengs).
|
|
|
|
|
|
- **Frontend:** React (Vite) som PWA, offline-first (Service Workers + IndexedDB).
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Offline-first er *kjernefunksjonalitet*, ikke luksus — golfbaner har
|
|
|
|
|
|
ofte dårlig mobildekning, og score må kunne registreres uten nett og synkes senere.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-007 — Miksede formater via økter, og spiller-pool
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** En turnering er en **ordnet sekvens av økter** (sessions), ikke ett
|
|
|
|
|
|
enkelt format. Hver økt bærer format, hullomfang og allowance. Ryder Cup =
|
|
|
|
|
|
foursome/18 → fourball/18 → singles/18 er tre økter.
|
|
|
|
|
|
|
|
|
|
|
|
Hvert lag har en **pool** (`team_roster`) som kan være større enn antall
|
|
|
|
|
|
matchplasser. Deltakelse avgjøres per match via `match_participant`. En reserve
|
|
|
|
|
|
er en spiller i poolen uten deltaker-rad i en gitt økt; en spiller kan spille
|
|
|
|
|
|
kun enkelte økter (f.eks. bare singelen).
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Dette er selve Ryder Cup-strukturen. Å modellere format på
|
|
|
|
|
|
turneringsnivå ville gjort miksede formater umulig; å modellere deltakelse på
|
|
|
|
|
|
turneringsnivå ville gjort reserver og delvis deltakelse umulig.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** Handicap-snapshot fryses per turnering i `team_roster`
|
|
|
|
|
|
(`handicap_index_snapshot`) for reproduserbare resultater.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-008 — 9-hulls-slag: "slagene som faller på 18-hulls-kortet"
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** For 9-hulls-økter (front/back) brukes spillerens fulle
|
|
|
|
|
|
18-hulls-tildeling, og slagene fordeles over **hele** 18-hulls stroke index —
|
|
|
|
|
|
deretter tas kun de spilte hullene ut. Spilleren mottar slag på de spilte hullene
|
|
|
|
|
|
der SI ≤ mottatte slag.
|
|
|
|
|
|
|
|
|
|
|
|
**Vurdert alternativ:** WHS' formelle 9-hulls course handicap (eget 9-hulls
|
|
|
|
|
|
rating, grovt sagt halvparten). Kan gi et litt annet totaltall. Ikke valgt som
|
|
|
|
|
|
standard fordi ikke alle baner publiserer 9-hulls-ratinger, og den valgte
|
|
|
|
|
|
metoden er den vanlige i vennegjeng-/klubbmatch-spill.
|
|
|
|
|
|
|
|
|
|
|
|
**Kritisk implementasjonsdetalj:** Man må IKKE sende bare de ni spilte hullene inn
|
|
|
|
|
|
i slagfordelingen med et 18-hulls slagtall — det gir feil ved høye slagtall
|
|
|
|
|
|
(12 slag på back-9 blir da 10 i stedet for riktige 6). Motoren har derfor
|
|
|
|
|
|
`allocate_over_played_holes(...)` som fordeler over alle 18 og så tar ut de spilte.
|
|
|
|
|
|
Dekket av test `test_nine_hole_twelve_strokes_is_six_not_ten`.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** `tee_rating` beholder likevel front/back-omfang, slik at WHS'
|
|
|
|
|
|
alternativ kan tilbys senere som konfig per turnering uten skjemaendring.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-16 07:26:04 +02:00
|
|
|
|
## ADR-009 — Egen innlogging, uavhengig av teeoff
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** TeeCup har egne brukerkontoer (`app_user`), uavhengig av teeoffs
|
|
|
|
|
|
innlogging.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** TeeCup selges kommersielt til klubber/bedrifter som kanskje aldri
|
|
|
|
|
|
har hørt om teeoff.no. Delt innlogging ville vært forvirrende for dem og koblet de
|
|
|
|
|
|
to systemenes auth tettere sammen enn ellers ønskelig. Styrker isolasjonen fra
|
|
|
|
|
|
ADR-004.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** Egen auth (registrering, Google/magic-link e.l.), egne secrets
|
|
|
|
|
|
(jf. åpent spørsmål 1), egen sesjonshåndtering. Kaptein- og tilskuer-roller
|
|
|
|
|
|
defineres innenfor TeeCups eget auth-lag.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-010 — Feed krever innlogging (ingen anonym lesesti i v1)
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** Den «offentlige» runde-feeden er felles for begge lag i
|
|
|
|
|
|
turneringen, men bak innlogging. Ingen verdenssynlig lenke i v1.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** «Offentlig» betyr her «alle innloggede deltakere i turneringen»,
|
|
|
|
|
|
ikke «hvem som helst med en lenke». Dette fjerner den uautentiserte lesestien og
|
|
|
|
|
|
den tunge modereringen (som ellers kreves når innhold er synlig for verden) fra
|
|
|
|
|
|
v1.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** Synlighet modelleres som kanal-scope (lag / turnering). En ekte
|
|
|
|
|
|
verdenssynlig delingslenke kan legges til senere som egen bryter, med moderering,
|
|
|
|
|
|
uten å bygge om.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-011 — Turneringsstruktur: lagformat i v1, bracket som egen fremtidig type
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** v1 bygges for Ryder Cup-lagformatet (økter → uavhengige matcher →
|
|
|
|
|
|
summerte poeng), og låses til **nøyaktig to lag**.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** En match/flight er alltid to-sidig. Tre lag ville krevd
|
|
|
|
|
|
kryss-oppgjør (A–B, A–C, B–C), nok spillere til å møte to motstandere samtidig,
|
|
|
|
|
|
balansert oppsett, og tre-veis blind draw — mye kompleksitet for lite gevinst. To
|
|
|
|
|
|
lag er både enklere og riktigere for formatet. Et knockout-bracket (f.eks. 128
|
|
|
|
|
|
spillere, utslagsmatcher, finale + bronsefinale) er en helt egen turneringstype:
|
|
|
|
|
|
rundene er *avhengige* (vinner av match A møter vinner av match B), og krever
|
|
|
|
|
|
progresjon mellom matcher, seeding, fripass og bronsegren — noe dagens modell ikke
|
|
|
|
|
|
har.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** To-lags-grensen håndheves i app-laget (validering ved
|
|
|
|
|
|
turneringsoppsett), ikke med DB-trigger. Match-modellen holdes generell (to sider,
|
|
|
|
|
|
vilkårlige lag-referanser), så flere lag eller en knockout-type kan komme senere
|
|
|
|
|
|
uten dataomskriving — kjernen (spillere, motor, hull-score, RLS) er typeuavhengig.
|
|
|
|
|
|
Knockout er fanget som egen type i FEATURE_BACKLOG.md.
|
|
|
|
|
|
|
2026-07-19 22:26:45 +02:00
|
|
|
|
**Merk (2026-07-19):** brukeren har reist ønske om flere turneringsformater
|
|
|
|
|
|
UTOVER Ryder Cup-lagformatet (f.eks. «Københavner» — se
|
|
|
|
|
|
FEATURE_BACKLOG.md sitt eget punkt for alle fire eksemplene). Minst ett av
|
|
|
|
|
|
disse («Københavner»: alle-mot-alle poengfordeling i et felt av spillere,
|
|
|
|
|
|
ikke to lag i det hele tatt) passer IKKE inn i denne ADR-ens to-lags-modell —
|
|
|
|
|
|
en fremtidig, egen ADR trengs når/hvis dette tas fatt på, samme mønster som
|
|
|
|
|
|
knockout-punktet over. Kun notert her ennå, ikke designet eller bygget.
|
|
|
|
|
|
|
2026-07-26 07:54:00 +02:00
|
|
|
|
**Merk (2026-07-26), presisert og utvidet:** brukeren bekreftet at ønsket
|
|
|
|
|
|
er STØRRE enn Københavner som ett format blant flere — TeeCup skal etter
|
|
|
|
|
|
hvert støtte ekte INDIVIDUELLE turneringer (helt uten lag) som eget
|
|
|
|
|
|
generelt tilfelle, disse skal kunne gå over FLERE RUNDER, og det skal
|
|
|
|
|
|
være mulig å sette opp et Order of Merit (sesong-sammenlagt på tvers av
|
|
|
|
|
|
flere separate arrangementer). Full analyse, inkl. en reell strukturell
|
|
|
|
|
|
kollisjon med ADR-033 sitt bevisst org-uavhengige rundesystem, i
|
|
|
|
|
|
FEATURE_BACKLOG.md ("Utvidelse 2026-07-26: individuelle turneringer,
|
|
|
|
|
|
flerrunde-turneringer, og Order of Merit"). Ren notat-runde, ingen ADR
|
|
|
|
|
|
skrevet ennå.
|
|
|
|
|
|
|
2026-07-16 07:26:04 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-012 — To scoring-moduser per økt
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** Hver økt har en `scoring_mode`: `stroke` (spillere taster slag per
|
|
|
|
|
|
hull) eller `hole_result` (registrer bare hvem som vant hullet / delt).
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Ønsket fra start: kunne føre score enten detaljert (slag) eller
|
|
|
|
|
|
raskt (tapp «lag rød vant hullet»). De to modusene har ulik datainngang.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** `stroke` skriver til `hole_score` (brutto, motor utleder netto og
|
|
|
|
|
|
hull-resultat). `hole_result` skriver til ny tabell `match_hole_result`
|
|
|
|
|
|
(vinnende side per hull, NULL = delt). Begge mater samme
|
|
|
|
|
|
`compute_match_state` i motoren, så matchstatus beregnes likt uansett modus.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-013 — Blind draw via lås per lag per økt
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** Lagoppstilling settes skjult; matchene avsløres når BEGGE lag har
|
|
|
|
|
|
låst. Modellert med tabell `lineup_lock` (én rad per lag per økt).
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Kjerneønske i Ryder Cup-formatet — kapteinene låser i blinde, og
|
|
|
|
|
|
oppgjørene avsløres samtidig.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** `match_participant`-radene (oppstillingen) opprettes per lag, men
|
|
|
|
|
|
motstanderens side skjules i app-laget til det finnes en `lineup_lock` for begge
|
|
|
|
|
|
lag. Etter lås kan egen oppstilling ikke endres. Dette er finkornet synlighet
|
|
|
|
|
|
(lag-nivå) og håndheves i app-/spørrelaget, ikke RLS — begge lag ligger i samme
|
|
|
|
|
|
organisasjon (jf. ADR-010).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-16 14:03:50 +02:00
|
|
|
|
## ADR-014 — Konfigurerbar handicap-pipeline: fire uavhengige brytere
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** Handicap-anvendelsen i en økt er FIRE uavhengige, valgfrie steg —
|
|
|
|
|
|
ikke bare én allowance-prosent:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Bruk handicap** (av/på) — helt av gir scratch-spill.
|
|
|
|
|
|
2. **Bruk course handicap** (av/på) — om slope/rating-justeringen
|
|
|
|
|
|
(`course_handicap_raw`) påføres, eller om rå `handicap_index` brukes direkte.
|
|
|
|
|
|
3. **HCP-prosent** (allowance-strategien, allerede dekket av ADR-005 /
|
|
|
|
|
|
`session.allowance_override`).
|
|
|
|
|
|
4. **Bruk matchplay-handicap** (av/på) — om resultatet konverteres til relative
|
|
|
|
|
|
slag (`match_play_strokes`: beste enhet spiller «av 0», resten får
|
|
|
|
|
|
differansen), eller brukes som absolutt Playing Handicap.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Skjermbilder fra Golf GameBook (referanseprodukt, delt
|
|
|
|
|
|
2026-07-16) viser nøyaktig denne firedelte bryter-strukturen per runde/format i
|
|
|
|
|
|
deres oppsettsdialog. Deres standard-prosenter (foursome 50 %, better ball/
|
|
|
|
|
|
fourball 90 %, singles 100 %) stemmer eksakt med `DEFAULT_MATCHPLAY_ALLOWANCES`
|
|
|
|
|
|
i `handicap_engine.py` — uavhengig bekreftelse på at defaultene er riktige,
|
|
|
|
|
|
slik ADR-005 krever verifisert. Motoren støtter allerede alle fire som atskilte,
|
|
|
|
|
|
komponerbare funksjonskall (`course_handicap_raw`, en `AllowanceStrategy`,
|
|
|
|
|
|
`match_play_strokes`) — hver kan hoppes over uavhengig av de andre — men
|
|
|
|
|
|
API-et/skjemaet eksponerer dem ikke som brytere ennå.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** `session.allowance_override` (jsonb) skal utvides til å bære
|
|
|
|
|
|
alle fire bryterne, ikke bare prosent, når motor-/scoring-integrasjonen bygges
|
|
|
|
|
|
(se FEATURE_BACKLOG.md). Ingen skjemaendring nødvendig (feltet er allerede
|
|
|
|
|
|
jsonb), men API-lagets validering og motor-kall må håndtere: `bruk_handicap =
|
|
|
|
|
|
false` → hopp over hele kjeden (brutto = netto), `bruk_course_handicap = false`
|
|
|
|
|
|
→ bruk `handicap_index`/`handicap_index_snapshot` direkte uten
|
|
|
|
|
|
slope/rating-justering, `bruk_matchplay_handicap = false` → bruk absolutt
|
|
|
|
|
|
Playing Handicap i stedet for å kalle `match_play_strokes`.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-17 21:40:42 +02:00
|
|
|
|
## ADR-015 — Utledet tee-time og maskinlesbar feilkode-kontrakt
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — klokkeslett er utledet, ikke lagret per match:** `session`
|
|
|
|
|
|
får `scheduled_at` (starttid for FØRSTE match) + `tee_interval_minutes`
|
|
|
|
|
|
(minutter mellom hver "flight"). En matchs `tee_time` er
|
|
|
|
|
|
`scheduled_at + (sequence-1) * tee_interval_minutes`, beregnet i Python ved
|
|
|
|
|
|
lesing (`app/routers/matches.py`), ALDRI skrevet til `match`-raden. Unntak:
|
|
|
|
|
|
`match.tee_time_override` (nullable `timestamptz`) for enkeltmatcher som må
|
|
|
|
|
|
justeres uavhengig (forsinkelser) — vinner alltid over den utledede verdien
|
|
|
|
|
|
når satt. `session.start_hole` er eksplisitt lagret (ikke utledet av
|
|
|
|
|
|
`hole_config`).
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Brukeren beskrev selv mønsteret rett fra domenet: "man sier
|
|
|
|
|
|
at førstematchen starter på hull X klokka Y, og det er Z minutter mellom hver
|
|
|
|
|
|
flight" — det er slik tee-tider faktisk fungerer i golf, og å lagre ett
|
|
|
|
|
|
tidspunkt per match ville vært duplisert, avledet data som kunne komme ut av
|
|
|
|
|
|
synk med intervallet. Et lite unntak (override) dekker det ene tilfellet der
|
|
|
|
|
|
den avledede regelen ikke holder.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** Alle tre nye tidsfelter er nullable/additive (migrasjon
|
|
|
|
|
|
`006_scheduling_and_locale.sql`) — en økt uten planlagt klokkeslett gir
|
|
|
|
|
|
`tee_time: null` i API-et, ikke en feil. Det finnes ennå intet
|
|
|
|
|
|
PATCH-endepunkt for `tee_time_override` (satt direkte i databasen ved
|
|
|
|
|
|
verifisering); bygges når frontend faktisk trenger å justere enkeltmatcher.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — én uniform feilrespons på tvers av HELE API-et:**
|
|
|
|
|
|
`{"detail": {"code": "...", "message": "..."}}` på ALLE stedene som kaster
|
|
|
|
|
|
`HTTPException` (39 steder ved innføring), via en liten factory
|
|
|
|
|
|
`app_error(status_code, code, message)` i `app/errors.py` — ingen egen
|
|
|
|
|
|
exception-klasse, ingen global exception handler. Kodene er en liten,
|
|
|
|
|
|
gjenbrukt taksonomi (~15 koder, IKKE én unik kode per kastested) —
|
|
|
|
|
|
f.eks. `NOT_FOUND`, `DUPLICATE`, `LIMIT_REACHED`, `NOT_ROSTERED_ON_TEAM`,
|
|
|
|
|
|
`NOT_AUTHENTICATED`. Pydantic sine egne 422-valideringsfeil er bevisst
|
|
|
|
|
|
UTENFOR denne kontrakten og beholder FastAPI sin standard `{"detail": [...]}`.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Hardkodet norsk prosa i `detail` (slik det var før denne
|
|
|
|
|
|
runden) kan ikke brukes til noe annet enn "vis strengen" av en frontend —
|
|
|
|
|
|
den kan ikke skille en 409 pga. dupliserte rader fra en 409 pga. et
|
|
|
|
|
|
forretningstak, og kan ikke oversettes (se i18n under). En liten, gjenbrukt
|
|
|
|
|
|
kode-taksonomi lar frontend bygge stabil logikk (f.eks. "vis en spesifikk
|
|
|
|
|
|
inline-feil for `LIMIT_REACHED`, en generisk toast for alt annet") uten å
|
|
|
|
|
|
måtte parse norsk tekst. Skulle vært gjort FØR frontend startet — dyrt å
|
|
|
|
|
|
ettermontere når klientkode allerede har begynt å parse tekststrenger.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** Enhver ny `HTTPException` i fremtidig API-kode SKAL bruke
|
|
|
|
|
|
`app_error()`, aldri en rå `HTTPException(status_code, detail="...")` — se
|
|
|
|
|
|
`app/errors.py` sin docstring. Nye forretningsbetydninger som ikke passer
|
|
|
|
|
|
noen eksisterende kode får en ny kode i taksonomien, ikke gjenbruk av en
|
|
|
|
|
|
semantisk feil kode.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — locale er klient-oppgitt, ikke server-gjettet:**
|
|
|
|
|
|
`app_user.preferred_locale` og `magic_link_token.locale` (kun `nb`/`en`
|
|
|
|
|
|
foreløpig, håndhevet med `CHECK`) settes fra en `locale`-verdi klienten
|
|
|
|
|
|
sender eksplisitt ved `POST /auth/request-link` — ALDRI gjettet server-side
|
|
|
|
|
|
(f.eks. fra `Accept-Language`). En NY bruker får `preferred_locale` satt fra
|
|
|
|
|
|
forespørselens locale ved førstegangsopprettelse; en EKSISTERENDE bruker som
|
|
|
|
|
|
ber om en ny lenke på et annet språk får IKKE sin lagrede preferanse
|
|
|
|
|
|
overskrevet — kun selve e-posten sendes på forespørselens språk. Ingen
|
|
|
|
|
|
endepunkt for å ENDRE `preferred_locale` på en eksisterende bruker ennå
|
|
|
|
|
|
(egen, senere sak — profilinnstillinger).
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Frontend vet sitt eget gjeldende visningsspråk (brukerens
|
|
|
|
|
|
valg i UI-et) — det er en bedre kilde enn å gjette fra headere eller IP.
|
|
|
|
|
|
At en påfølgende innlogging på et annet språk (f.eks. en gjest som låner en
|
|
|
|
|
|
enhet) IKKE skal endre den lagrede preferansen er bevisst: en
|
|
|
|
|
|
innloggingshandling bør ikke ha den overraskende bivirkningen å endre
|
|
|
|
|
|
brukerens varige profilinnstilling.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
Alt rent, ingen byggeartefakter liggende igjen, dokumentasjonen oppdatert. Oppsummering:
Live og verifisert ende-til-ende:
/verify-siden bygget og live, e-postmalen sender nå en ekte klikkbar lenke (app/email.py/app/config.py)
Frontend containerisert (frontend/Dockerfile, standalone Next.js) og rullet ut som egen teecup_frontend-tjeneste
Caddy peker nå teecup.teeoff.no på frontend-en, som selv proxyer API-kall server-side — same-origin, ingen CORS, cookie uendret (dokumentert som ny ADR-016)
Fant og fikset en reell fallgruve: Next.js sin rewrites() bakes inn ved build-tid for standalone-output, ikke lest ved kjøretid — løst med en Docker build-time ARG
Ekte e-post sendt, ekte lenke klikket, sesjon opprettet — bekreftet av deg
Én ting du må huske: Caddy-endringen ligger i det separate /opt/teeoff-repoet (deploy/Caddyfile), ikke i teecup-repoet — den er ikke committet ennå. Lett å glemme siden resten av denne økten kun har jobbet i /opt/teecup. Vil du at jeg minner deg, eller committer du den nå selv i teeoff-repoet?
CLAUDE.md, FEATURE_BACKLOG.md og ARCHITECTURE_DECISIONS.md (ny ADR-016) er oppdatert. Klar for commit i teecup-repoet når du vil.
2026-07-17 22:28:43 +02:00
|
|
|
|
## ADR-016 — Frontend som eneste offentlige overflate, API-et server-side proxyet
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning:** `teecup.teeoff.no` peker (Caddy) på Next.js-frontenden
|
|
|
|
|
|
(`teecup_frontend`), IKKE direkte på FastAPI-et. Frontenden proxyer selv
|
|
|
|
|
|
kjente API-sti-prefikser (`/auth/*`, `/orgs/*`, `/health` — som til sammen
|
|
|
|
|
|
dekker HELE dagens API-overflate, verifisert med et grep av samtlige
|
|
|
|
|
|
rutedefinisjoner) videre til `teecup_api:8000` server-side, via Next.js sin
|
|
|
|
|
|
egen `rewrites()`-mekanisme (`frontend/next.config.mjs`). API-et er ikke
|
|
|
|
|
|
lenger separat Caddy-rutet eller offentlig eksponert under eget navn.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Alt kjører dermed under samme opprinnelse (origin) sett fra
|
|
|
|
|
|
nettleseren — ingen CORS-konfigurasjon trengs, og HttpOnly-sesjonscookien
|
|
|
|
|
|
(ADR-009) fungerer helt uendret enten kallet "egentlig" går til frontend
|
|
|
|
|
|
eller API. Alternativet (eget subdomene for API-et, `SameSite=None`-cookie
|
|
|
|
|
|
eller CORS-hull) ville svekket cookie-sikkerhetsmodellen som allerede var
|
|
|
|
|
|
bevisst bygget stram. Samme mønster generaliserer til alle fremtidige
|
|
|
|
|
|
skjermer uten videre Caddy-endringer — nye API-ruter under `/auth`, `/orgs`
|
|
|
|
|
|
trenger ingen ny proxy-regel, kun nye Next.js-sider som kaller dem med
|
|
|
|
|
|
relative URL-er.
|
|
|
|
|
|
|
|
|
|
|
|
**Reell fallgruve funnet og fikset ved bygging (2026-07-17):** Next.js sin
|
|
|
|
|
|
`rewrites()` løses ved BUILD-tid for `output: "standalone"` (bakes inn i
|
|
|
|
|
|
server-bunten), ikke ved container-oppstart. En `docker run -e
|
|
|
|
|
|
TEECUP_API_ORIGIN=...` ved kjøretid ble derfor stille ignorert (falt tilbake
|
|
|
|
|
|
til default `localhost:8000`, som ikke fantes i containeren — proxy-kall
|
|
|
|
|
|
feilet med `ECONNREFUSED`). Løst med en Docker build-time `ARG
|
|
|
|
|
|
TEECUP_API_ORIGIN` (default `http://teecup_api:8000`, matcher alltid det
|
|
|
|
|
|
delte nettverkets tjenestenavn) i `frontend/Dockerfile`, satt via
|
|
|
|
|
|
`docker-compose.yml` sin `build.args`. Generell lærdom for fremtidige
|
|
|
|
|
|
Next.js/Docker-oppsett i dette prosjektet: alt som brukes inne i
|
|
|
|
|
|
`next.config.mjs` er en BUILD-tids verdi, ikke en runtime-verdi, med mindre
|
|
|
|
|
|
det eksplisitt leses på nytt et sted som faktisk kjører per request (en
|
|
|
|
|
|
route handler, ikke selve config-filen).
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** Enhver fremtidig ny API-sti-prefiks (utenfor `/auth`,
|
|
|
|
|
|
`/orgs`, `/health`) MÅ legges til i `frontend/next.config.mjs` sin
|
|
|
|
|
|
`rewrites()`-liste, ellers blir den utilgjengelig fra nettleseren selv om
|
|
|
|
|
|
API-et selv fungerer (kun nåbar internt på Docker-nettverket). `teecup_api`
|
|
|
|
|
|
sin port er ikke lenger tenkt nåbar direkte utenfra i prod.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
ADR-017 er skrevet inn, og migrasjonen er ferdig og verifisert:
007_registration_and_player_fields.sql — kjørt rent gjennom hele kjeden 001→007 på en fersk scratch-database, test_isolation.sql fortsatt 12/12.
Ett reelt arkitekturproblem løst underveis, ikke bare skjema: et offentlig påmeldingskall kjenner en turnering-id, men ingen org-kontekst — og uten den slipper RLS ingen rader gjennom, heller ikke oppslaget for å finne riktig org. Løst med en snever SECURITY DEFINER-funksjon (public_tournament_org) som kun eksponerer koblingen turnering→org, ingenting annet. Testet presist: kalt som teecup_app-rollen med ingen org-kontekst satt — funksjonen fant riktig org, ga NULL (ikke feil) for en ukjent turnering, og et rått SELECT på samme tilkobling/rolle ga fortsatt 0 rader — beviser at RLS ikke er brutt generelt, bare dette ene smale unntaket finnes.
Ikke gjort ennå (bevisst, dette var kun migrasjonssteget):
Migrasjonen er ikke kjørt mot ekte teecup_db.
Ingen API-endepunkter (offentlig registrerings-router, utvidet players.py).
2026-07-18 08:24:55 +02:00
|
|
|
|
## ADR-017 — Selvregistrering og utvidet spillerprofil
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** Reist av brukeren rett etter at lag/roster-skjermen var live.
|
|
|
|
|
|
Dagens modell antar at organisator kjenner og legger inn alle spillere selv
|
|
|
|
|
|
— i praksis vet organisator ofte ikke hvem som faktisk blir med før de
|
|
|
|
|
|
melder seg på selv.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Påmelding er offentlig, krever IKKE innlogging.** En
|
|
|
|
|
|
delbar lenke (`/register/{tournament_id}` — turneringens UUID er allerede
|
|
|
|
|
|
uforutsigelig nok, ingen ny token-mekanisme) viser et minimalt skjema. Ingen
|
|
|
|
|
|
magic-link, ingen konto kreves for å melde seg på.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Å kreve innlogging FØR man kan melde seg på er unødvendig
|
|
|
|
|
|
friksjon for "jeg blir med lørdag"-bruksmønsteret. Kontosammenkobling skjer
|
|
|
|
|
|
gratis senere (Beslutning B), ikke som et eget steg i selve påmeldingen.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — E-post er sammenkoblingsnøkkelen** mellom en organisator-
|
|
|
|
|
|
forhåndsopprettet `player`-rad og en spiller som senere melder seg selv på
|
|
|
|
|
|
eller logger inn. Finnes det en `player`-rad i org-en med samme e-post ved
|
|
|
|
|
|
påmelding, fylles manglende felt inn på DEN raden i stedet for å opprette en
|
|
|
|
|
|
duplikat. `player.user_id` kobles først når noen med matchende e-post
|
|
|
|
|
|
faktisk logger inn via magic-link — `verify_magic_link` utvides til også å
|
|
|
|
|
|
slå opp org-scopede `player`-rader på e-post, ikke bare `app_user`.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** `player.email` er bevisst IKKE en `UNIQUE`-constraint —
|
|
|
|
|
|
familier deler av og til e-post (forelder melder på barn), en hard unik-
|
|
|
|
|
|
regel ville krasje akkurat den vanlige situasjonen. Matching er et mykt,
|
|
|
|
|
|
applikasjonslags-oppslag.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Påmelding er et eget, lettvekts steg, atskilt fra
|
|
|
|
|
|
`team_roster`**, med konfigurerbar godkjenning, kapasitet og samtykke. Ny
|
|
|
|
|
|
tabell `tournament_registration` (status: `pending`/`confirmed`/
|
|
|
|
|
|
`waitlisted`/`declined`/`withdrawn`). Tre nye felt på `tournament`:
|
|
|
|
|
|
`registration_capacity` (nullable — organisators valg om det i det hele
|
|
|
|
|
|
tatt skal være en grense), `registration_overflow_policy`
|
|
|
|
|
|
(`waitlist`/`closed`, kun relevant når kapasitet er satt),
|
|
|
|
|
|
`registration_requires_approval` (boolean). Rekkefølge ved en ny
|
|
|
|
|
|
påmelding: (1) er fristen passert → avvis; (2) er kapasitet nådd →
|
|
|
|
|
|
`waitlisted` eller avvis, avhengig av policy; (3) ellers `pending` eller
|
|
|
|
|
|
`confirmed`, avhengig av godkjenningsbryteren.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** `team_roster` betyr i dag "committed til et bestemt lag".
|
|
|
|
|
|
Å blande "vil kanskje spille" med "spiller garantert" i samme tabell ville
|
|
|
|
|
|
gjort det umulig å skille en påmeldt-men-ikke-plukket spiller fra en som
|
|
|
|
|
|
aldri var interessert. Kapasitet og godkjenning er to reelle, uavhengige
|
|
|
|
|
|
organisator-beslutninger — å låse ett svar for alle turneringer ville vært
|
|
|
|
|
|
feil for minst noen av dem (brukeren bekreftet eksplisitt: begge skal være
|
|
|
|
|
|
konfigurerbare valg, ikke faste regler).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Utvidet spillerprofil + samtykke.** Nye felt på `player`:
|
|
|
|
|
|
`mobile`, `email`, `birth_date` (IKKE alder — alder blir feil neste år,
|
|
|
|
|
|
fødselsdato er ikke det), `nickname`, `country`, `club`,
|
|
|
|
|
|
`club_member_number`. Samtykke er obligatorisk ved påmelding — API-et
|
|
|
|
|
|
avviser innsending (400) uten `consent: true`, og
|
|
|
|
|
|
`tournament_registration.consent_given_at` er beviset på at det faktisk ble
|
|
|
|
|
|
gitt, ikke bare antatt. Samtykket lever på REGISTRERINGEN, ikke på
|
|
|
|
|
|
`player`, fordi det er selve påmeldingshandlingen for DENNE turneringen
|
|
|
|
|
|
samtykket knytter seg til.
|
|
|
|
|
|
|
|
|
|
|
|
**Ikke et nytt felt:** "utslagssted for anledningen" er sannsynligvis
|
|
|
|
|
|
allerede dekket av `match_participant.tee_id` (per match, ikke per
|
|
|
|
|
|
spillerprofil, siden det kan variere fra runde til runde).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — `public_tournament_org()`: den eneste broen fra en
|
|
|
|
|
|
uautentisert forespørsel til riktig RLS-kontekst.** Et offentlig
|
|
|
|
|
|
påmeldingskall kjenner en turnering-id, men ikke organisasjonen den hører
|
|
|
|
|
|
til — og uten `app.current_org` satt slipper RLS ingen rader gjennom
|
|
|
|
|
|
(heller ikke selve oppslaget for å FINNE riktig org). Løst med en snever
|
|
|
|
|
|
`SECURITY DEFINER`-SQL-funksjon (`p_tournament_id -> organization_id`, kjørt
|
|
|
|
|
|
med skaperens BYPASSRLS-rettigheter, `SET search_path = public` mot
|
|
|
|
|
|
kapring) — samme "løs kontekst-problemet FØR RLS kan håndheve noe"-mønster
|
|
|
|
|
|
som den selvrefererende org-bootstrapen (`app/routers/organizations.py`),
|
|
|
|
|
|
nå for et lese-oppslag i stedet for en innsetting.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** App-laget slår opp org-id via denne funksjonen FØRST, åpner
|
|
|
|
|
|
deretter en vanlig `org_connection(org_id)` og fortsetter med normal
|
|
|
|
|
|
RLS-håndhevelse for alt det faktiske arbeidet. Funksjonen eksponerer KUN en
|
|
|
|
|
|
uuid->uuid-kobling, ingenting annet fra `tournament`-raden — et bevisst
|
|
|
|
|
|
smalt unntak, ikke en generell RLS-omgåelse. Ethvert fremtidig offentlig
|
|
|
|
|
|
(uautentisert) endepunkt som trenger å slå opp org-kontekst fra en kjent
|
|
|
|
|
|
ressurs-id bør gjenbruke akkurat dette mønsteret, ikke finne opp et nytt.
|
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
**Migrasjon:** `007_registration_and_player_fields.sql`. Kontosammenkoblingen
|
|
|
|
|
|
i Beslutning B (e-post → `player.user_id` ved innlogging) fikk sin egen
|
|
|
|
|
|
`SECURITY DEFINER`-bro (`link_player_by_email()`, samme mønster som
|
|
|
|
|
|
Beslutning E) i en oppfølgende migrasjon, `008_link_player_by_email.sql`.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-018 — Landingssider: synlighet, org-profil, delbart innhold
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** Reist av brukeren rett etter at registrerings-API-et
|
|
|
|
|
|
(ADR-017) var live — turneringer og organisasjoner bør ha egne, delbare
|
|
|
|
|
|
landingssider (hero, tekst, program, sponsorer, påmelding for turnering;
|
|
|
|
|
|
klubbprofil + turneringsliste for organisasjon). Brukeren krevde eksplisitt
|
|
|
|
|
|
at synlighet må være et VALG: offentlig, kun org-medlemmer, eller kun
|
|
|
|
|
|
turnering-deltakere.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Trenivås synlighet, ett felt per nivå.**
|
|
|
|
|
|
`tournament.visibility` (`'public'`/`'org'`/`'participants'`, default
|
|
|
|
|
|
`'org'`) og `organization.public_profile` (boolean, default `false`).
|
|
|
|
|
|
Trygg standard: ingen eksisterende eller nyopprettet turnering/org blir
|
|
|
|
|
|
offentlig av seg selv. "Kun deltakere" gir ikke mening på org-nivå (en
|
|
|
|
|
|
organisasjon har ikke "deltakere"), derfor en enklere bryter der, ikke
|
|
|
|
|
|
samme trenivå-enum som turnering.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — RLS beskytter TENANT-grenser, ikke INNHOLDS-synlighet.**
|
|
|
|
|
|
`org_isolation`-policyen håndhever kun at org A aldri ser org B sin data —
|
|
|
|
|
|
den sier ingenting om hvem INNENFOR riktig org-kontekst som får se hva.
|
|
|
|
|
|
Det viste seg at det eksisterende `GET /public/tournaments/{id}` (ADR-017)
|
|
|
|
|
|
allerede leser fullt turneringsinnhold uten noen synlighetssjekk i det
|
|
|
|
|
|
hele tatt, fordi RLS er fornøyd så snart org-konteksten er riktig satt —
|
|
|
|
|
|
uansett hvem (eller om noen) som spør.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** `visibility` MÅ håndheves eksplisitt i applikasjonslaget på
|
|
|
|
|
|
hvert offentlig lese-endepunkt (og på registrering, se Beslutning D), ikke
|
|
|
|
|
|
forventes løst av RLS.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Ny autorisasjonsvei: "deltaker".** For
|
|
|
|
|
|
`visibility='participants'` må leseren enten være org-medlem, ELLER
|
|
|
|
|
|
innlogget med en `player`-rad (koblet via `player.user_id`, ADR-017
|
|
|
|
|
|
Beslutning B) som har en `tournament_registration`- eller
|
|
|
|
|
|
`team_roster`-rad for NØYAKTIG denne turneringen. Ny
|
|
|
|
|
|
`get_current_user_optional`-avhengighet i `app/auth.py` — som
|
|
|
|
|
|
`get_current_user`, men returnerer `None` i stedet for å kaste 401, siden
|
|
|
|
|
|
et offentlig endepunkt skal fungere for en anonym leser også (bare med
|
|
|
|
|
|
`visibility='public'`-tilgang).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Registrering følger SAMME synlighetsgrense som
|
|
|
|
|
|
landingssiden.** Kan du ikke se turneringen, kan du heller ikke melde deg
|
|
|
|
|
|
på den — ingen særbehandling av `/register`-endepunktet.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — Turnering-landingsside: innhold nå, bilder som inerte
|
|
|
|
|
|
felt.** Nye felt: `tournament.description` (presenterende tekst),
|
|
|
|
|
|
`tournament.hero_image_key` (inert til MinIO-runden — kun kolonnen, ingen
|
|
|
|
|
|
opplastingslogikk, sparer en fremtidig migrasjon for null kostnad nå). Ny
|
|
|
|
|
|
tabell `tournament_sponsor` (navn+lenke aktivt, `logo_key` inert av samme
|
|
|
|
|
|
grunn). Program vises via `GET /public/tournaments/{id}/sessions`, som
|
|
|
|
|
|
gjenbruker `_fetch_sessions()` (nytt uttrekk fra `tournaments.py` sin
|
|
|
|
|
|
`list_sessions`, delt mellom den innloggede og den offentlige ruten) —
|
|
|
|
|
|
blind draw-hemmelighold (ADR-013) arves automatisk, ikke reimplementert.
|
|
|
|
|
|
**Bevisst utenfor omfang denne runden:** lag-/roster-visning på
|
|
|
|
|
|
landingssiden (må også respektere blind draw-lås) — egen, senere sak.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning F — Org-landingsside: slug + tredje SECURITY DEFINER-bro.**
|
|
|
|
|
|
`organization.slug` (unik når satt, `CHECK (NOT public_profile OR slug IS
|
|
|
|
|
|
NOT NULL)`). `GET /public/orgs/{slug}` løser org via
|
|
|
|
|
|
`public_org_by_slug(slug)` — samme mønster som `public_tournament_org()`/
|
|
|
|
|
|
`link_player_by_email()` (007/008), nå tredje instans. Returnerer `NULL`
|
|
|
|
|
|
for BÅDE "finnes ikke" og "finnes, men er privat" — samme
|
|
|
|
|
|
anti-enumerering som magic-link (ADR-009), ikke en tilfeldighet at
|
|
|
|
|
|
mønsteret gjentas.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utsatt til egen, senere runde:** hero-/sponsorbilder (krever
|
|
|
|
|
|
MinIO — arkitektur-invarianten finnes fra før, se CLAUDE.md, men er aldri
|
|
|
|
|
|
satt opp i praksis).
|
|
|
|
|
|
|
|
|
|
|
|
**Migrasjon:** `009_landing_pages_and_visibility.sql`.
|
ADR-017 er skrevet inn, og migrasjonen er ferdig og verifisert:
007_registration_and_player_fields.sql — kjørt rent gjennom hele kjeden 001→007 på en fersk scratch-database, test_isolation.sql fortsatt 12/12.
Ett reelt arkitekturproblem løst underveis, ikke bare skjema: et offentlig påmeldingskall kjenner en turnering-id, men ingen org-kontekst — og uten den slipper RLS ingen rader gjennom, heller ikke oppslaget for å finne riktig org. Løst med en snever SECURITY DEFINER-funksjon (public_tournament_org) som kun eksponerer koblingen turnering→org, ingenting annet. Testet presist: kalt som teecup_app-rollen med ingen org-kontekst satt — funksjonen fant riktig org, ga NULL (ikke feil) for en ukjent turnering, og et rått SELECT på samme tilkobling/rolle ga fortsatt 0 rader — beviser at RLS ikke er brutt generelt, bare dette ene smale unntaket finnes.
Ikke gjort ennå (bevisst, dette var kun migrasjonssteget):
Migrasjonen er ikke kjørt mot ekte teecup_db.
Ingen API-endepunkter (offentlig registrerings-router, utvidet players.py).
2026-07-18 08:24:55 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
## ADR-019 — Offisiell banedata: import (kopi), ikke live oppslag
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** ADR-004 vedtok prinsippet (lesende API, ikke delt database) helt
|
|
|
|
|
|
i starten av prosjektet, men ble aldri bygget — kun `source='custom'`-baner
|
|
|
|
|
|
fantes (organisator taster inn banen selv). Brukeren påpekte dette rett etter
|
|
|
|
|
|
program-skjerm-runden (2026-07-18): det er en reell mangel at en organisator
|
|
|
|
|
|
må taste inn en bane manuelt når banen faktisk allerede finnes i teeoff.
|
|
|
|
|
|
|
|
|
|
|
|
**Kartlagt før noe ble besluttet** (lest `/opt/teeoff/backend/main.py`, kun
|
|
|
|
|
|
lesing): `GET /api/facilities/{slug}` er offentlig, uten auth/API-nøkkel, og
|
|
|
|
|
|
returnerer allerede `courses[]` med `holes[]` (`hole_number`, `par`,
|
|
|
|
|
|
`hcp_index`) og `tees[]` (`name`, `cr_men`/`slope_men`, `cr_women`/
|
|
|
|
|
|
`slope_women`) — nøyaktig det `hole`/`tee`/`tee_rating`-tabellene i
|
|
|
|
|
|
`teecup_db` (migrasjon 001) allerede modellerer. `GET /api/facilities?
|
|
|
|
|
|
view=search` gir en lettvekts liste (`id`, `slug`, `name`, `city`, `county`,
|
|
|
|
|
|
…) egnet for søk. Par/hcp_index kan være NULL i teeoffs skjema (ufullstendig
|
|
|
|
|
|
baneregistrering) — teecups `hole`-tabell krever begge NOT NULL.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Import ved organisators eksplisitte valg, ikke live
|
|
|
|
|
|
oppslag ved hver bruk.** Organisator søker blant teeoff sine baner, velger
|
|
|
|
|
|
én, og teecup kopierer da bane+hull+tee+tee_rating INN i `teecup_db` som en
|
|
|
|
|
|
vanlig `course`-rad med `source='official'` og
|
|
|
|
|
|
`external_course_ref = '{facility_slug}:{teeoff_course_id}'`. Etter import
|
|
|
|
|
|
er raden en helt vanlig lokal `course`-rad — økt-/match-/handicap-koden
|
|
|
|
|
|
(som allerede kun kjenner `course_id` som fremmednøkkel) trenger INGEN
|
|
|
|
|
|
endring.
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** Handicap-motoren joiner `tee_rating`/`hole` direkte i SQL
|
|
|
|
|
|
midt i en database-transaksjon (`app/handicap.py`) — et live HTTP-kall til
|
|
|
|
|
|
teeoff derfra ville krevd at hver matchberegning avhenger av teeoffs
|
|
|
|
|
|
oppetid, og ville ikke vært transaksjonssikkert. Import fryser dataene på
|
|
|
|
|
|
importtidspunktet — samme reproduserbarhets-prinsipp som handicap-
|
|
|
|
|
|
snapshotten i ADR-007 (`team_roster.handicap_index_snapshot`): endrer
|
|
|
|
|
|
teeoff en rating i etterkant, skal ikke en allerede opprettet turnering
|
|
|
|
|
|
plutselig regne annerledes. Dette er også det som gjør ADR-004s opprinnelige
|
|
|
|
|
|
"et brudd ett sted skal ikke ta ned begge produktene"-begrunnelse reell: en
|
|
|
|
|
|
teeoff-nedetid blokkerer kun NYE importer, ikke bruk av allerede importerte
|
|
|
|
|
|
baner.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Server-til-server, internt Docker-nettverk, ingen ny
|
|
|
|
|
|
hemmelighet.** `teecup_api` kaller `http://teeoff_api:8000` direkte (samme
|
|
|
|
|
|
`teeoff_default`-nettverk begge allerede deler) — ikke via Caddy/det
|
|
|
|
|
|
offentlige domenet. Teeoff sitt API har ingen auth-mekanisme på disse
|
|
|
|
|
|
endepunktene i det hele tatt, så ingen ny credential trengs. CORS-listen på
|
|
|
|
|
|
teeoff-siden (ekskluderer teecups origin) er irrelevant — CORS gjelder kun
|
|
|
|
|
|
nettleser-fetch, ikke et backend-til-backend-kall. Ny avhengighet: `httpx`
|
|
|
|
|
|
(async HTTP-klient), lagt til i `app/requirements.txt`.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Ufullstendige teeoff-data feiler importen tydelig, importen
|
|
|
|
|
|
skjer aldri delvis.** Mangler et hull `par`/`hcp_index`, eller mangler en
|
|
|
|
|
|
tee både `cr_men` OG `cr_women`, avvises HELE importen med en klar feil
|
|
|
|
|
|
(`code: EXTERNAL_DATA_INCOMPLETE`) FØR noe skrives — ikke en delvis
|
|
|
|
|
|
importert bane med hull som senere feiler i handicap-beregning. Hele
|
|
|
|
|
|
importen kjører i én DB-transaksjon (`org_connection` sin eksisterende
|
|
|
|
|
|
`conn.transaction()`), så en feil midtveis ruller automatisk tilbake.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Kun `full_18`-rating importeres.** Teeoff har ingen egen
|
|
|
|
|
|
front9/back9-rating i sitt skjema (kun én CR/slope per tee, kjønnsdelt).
|
|
|
|
|
|
Samme valg som ADR-008 allerede gjorde bevisst for egendefinerte baner: den
|
|
|
|
|
|
formelle WHS 9-hulls-metoden er ikke i bruk, 18-hulls-tildelingen med uttak
|
|
|
|
|
|
av spilte hull dekker front/back-økter.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — Reimport av samme teeoff-bane er en feil, ikke en
|
|
|
|
|
|
duplikat-rad.** Ny partiell unik indeks `(organization_id,
|
|
|
|
|
|
external_course_ref) WHERE external_course_ref IS NOT NULL` (migrasjon
|
|
|
|
|
|
`010`) — importerer en organisator samme teeoff-bane to ganger, avvises det
|
|
|
|
|
|
med den eksisterende `DUPLICATE`-koden (409), ikke en ny rad med samme
|
|
|
|
|
|
banedata.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** `app/teeoff_client.py` (ny, ren HTTP-klient, ingen
|
|
|
|
|
|
db/RLS-avhengighet — samme isolasjonsprinsipp som `handicap_engine.py`).
|
|
|
|
|
|
`app/routers/courses.py` utvides med `GET .../courses/official-search` og
|
|
|
|
|
|
`POST .../courses/official-import`. Migrasjon `010_official_course_unique_
|
|
|
|
|
|
ref.sql`.
|
|
|
|
|
|
|
2026-08-03 19:15:13 +02:00
|
|
|
|
**Tillegg 2026-08-03 (research, ikke bygget):** samme import-ved-eksplisitt-
|
|
|
|
|
|
valg-prinsipp er identifisert som riktig mal for en fremtidig internasjonal
|
|
|
|
|
|
banekilde (golfapi.io, for baner utenfor Norge/`teeoff_db`s dekning) — en
|
|
|
|
|
|
tredje `course.source`-verdi + `app/golfapi_client.py` ved siden av
|
|
|
|
|
|
`app/teeoff_client.py`, ikke en ny arkitektur. Ingen ADR-runde startet ennå,
|
|
|
|
|
|
kun notert. Full detalj i FEATURE_BACKLOG.md, «Internasjonale baner».
|
|
|
|
|
|
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
|
## ADR-020 — Invitasjonskode (oppdagelse), matchledelse og projisert stilling
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** Reist av brukeren 2026-07-19, rett etter at "bygg i rekkefølgen
|
|
|
|
|
|
ting brukes"-serien var ferdig. Tre relaterte, men separate mangler:
|
|
|
|
|
|
|
|
|
|
|
|
1. Den eneste veien inn til en turnering er en direkte lenke (`/t/[id]`) eller
|
|
|
|
|
|
org-ens klubbside (kun `public`-synlige turneringer). En spiller som bare
|
|
|
|
|
|
har fått muntlig beskjed ("du spiller lørdag") har i dag ingen vei inn i
|
|
|
|
|
|
det hele tatt — dette var reelt ikke gjennomtenkt tidligere.
|
|
|
|
|
|
2. Leaderboardet viser kun FAKTISK opptjente poeng. Ingen visning av hva
|
|
|
|
|
|
stillingen ville blitt om pågående, ikke-avgjorte matcher holder seg som
|
|
|
|
|
|
de står nå (vanlig i profesjonell golf-TV-dekning av Ryder Cup).
|
|
|
|
|
|
3. Ingen visuell indikasjon i matchlister på hvem som leder en pågående
|
|
|
|
|
|
match — brukeren viste et skjermbilde av referanseproduktets fargekodede
|
|
|
|
|
|
matchrader (rød/beige etter ledende side) som ønsket retning.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Kort, menneske-skrivbar invitasjonskode per turnering, som
|
|
|
|
|
|
OVERSTYRER `tournament.visibility`.** Ny `tournament.join_code` (6 tegn, fra
|
|
|
|
|
|
et alfabet uten forvekslingsbare tegn — `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`,
|
|
|
|
|
|
altså uten `0/O/1/I`), generert automatisk ved opprettelse, globalt unik
|
|
|
|
|
|
(kodeoppslag skjer FØR org-kontekst er kjent, samme problem som
|
|
|
|
|
|
`public_tournament_org()` løste for ADR-017). Login-skjermet får et eget
|
|
|
|
|
|
kode-felt (fungerer FØR innlogging) som løser koden til riktig turnering og
|
|
|
|
|
|
sender brukeren til `/t/{id}?code=...`.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst valgt fremfor å la koden respektere synlighet:** en kode gitt
|
|
|
|
|
|
muntlig eller på en lapp ER selve invitasjonen — å likevel kreve org-
|
|
|
|
|
|
medlemskap eller deltakerstatus for en `org`/`participants`-synlig turnering
|
|
|
|
|
|
ville gjort koden verdiløs for akkurat den situasjonen den er ment å løse.
|
|
|
|
|
|
Koden er ikke hemmelig i sikkerhetsforstand (den er MENT å deles), men
|
|
|
|
|
|
6 tegn fra et 33-tegns alfabet (≈1,3 milliarder kombinasjoner) gjør blind
|
|
|
|
|
|
gjetting upraktisk uten separat rate-limiting — akseptabelt for et
|
|
|
|
|
|
tillitsbasert klubb-/vennegjeng-verktøy (samme trusselmodell-resonnement som
|
|
|
|
|
|
`team_authz.py`, se «Brukerroller» i FEATURE_BACKLOG.md).
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** `GET /public/tournaments/{id}` og
|
|
|
|
|
|
`POST /public/tournaments/{id}/register` godtar en valgfri `code`-parameter;
|
|
|
|
|
|
matcher den turneringens `join_code` (case-insensitivt), hoppes den vanlige
|
|
|
|
|
|
`_check_visibility()`-sjekken helt over. Ny SECURITY DEFINER-bro
|
|
|
|
|
|
`public_tournament_by_code(code) RETURNS uuid` (tournament_id) — fjerde
|
|
|
|
|
|
instans av samme mønster som `public_tournament_org()` (007),
|
|
|
|
|
|
`link_player_by_email()` (008), `public_org_by_slug()` (009). Ingen
|
|
|
|
|
|
kode-regenerering bygget denne runden (organisator kan i dag ikke bytte ut
|
|
|
|
|
|
en lekket kode) — egen, senere sak i FEATURE_BACKLOG.md om det blir
|
|
|
|
|
|
etterspurt.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Projisert stilling: pågående matchers nåværende leder får
|
|
|
|
|
|
full poengsum, uavgjort/ikke-startet splittes likt.** For hver IKKE avgjort
|
|
|
|
|
|
match brukes `match.leading_side` (se Beslutning C) til å tildele hele
|
|
|
|
|
|
øktens `points_per_match` til den ledende siden i den PROJISERTE summen —
|
|
|
|
|
|
"AS" (all square) eller en match som ennå ikke har noen registrerte hull
|
|
|
|
|
|
splittes 0,5/0,5, samme regel som en faktisk halvert match. Avgjorte
|
|
|
|
|
|
matcher bidrar likt til både faktisk og projisert sum (de er jo allerede
|
|
|
|
|
|
det de blir). Speiler hvordan TV-dekning av Ryder Cup vanligvis viser
|
|
|
|
|
|
"hvis det sluttet nå"-tavler.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Ny cachet kolonne `match.leading_side`, samme mønster som
|
|
|
|
|
|
`status_text`/`points_side_a/b`.** `recompute_and_cache_match_state()`
|
|
|
|
|
|
(`app/routers/scoring.py`) beregner den allerede tilgjengelige
|
|
|
|
|
|
`MatchState.lead`-verdien (fortegn = ledende side) ved HVER hull-innsending
|
|
|
|
|
|
uansett om matchen er avgjort ennå — lagres nå også i en egen kolonne i
|
|
|
|
|
|
stedet for kun å ligge innbakt i den menneskelesbare `status_text`-strengen
|
|
|
|
|
|
("2 UP (A)"), slik at frontend kan style etter et strukturert felt
|
|
|
|
|
|
(`"a" | "b" | null`) i stedet for å parse norsk/engelsk tekst. Brukes til
|
|
|
|
|
|
BÅDE projisert stilling (Beslutning B) og fargekoding av matchlister
|
|
|
|
|
|
(Beslutning D).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Fargekoding er ren frontend-presentasjon, ingen ny
|
|
|
|
|
|
backend-modell.** Matchlister (blind draw sin avslørte visning, ev. flere
|
|
|
|
|
|
steder senere) farger den ledende sidens kant/bakgrunn med lagets EKSISTERENDE
|
|
|
|
|
|
`team.color` når `leading_side` er satt og matchen ikke er avgjort — samme
|
|
|
|
|
|
fargekilde som resten av appen (leaderboard, roster) allerede bruker, ingen
|
|
|
|
|
|
ny fargemodell innført.
|
|
|
|
|
|
|
|
|
|
|
|
**Migrasjon:** `011_join_code_and_leading_side.sql`.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-19 10:05:42 +02:00
|
|
|
|
## ADR-021 — Passord (valgfritt tillegg) + valgfri 2FA (TOTP eller e-post)
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** Reist av brukeren 2026-07-19, sammen med et konkret sesjons-
|
|
|
|
|
|
problem: dagens magic-link-cookie (ADR-009, 30 dager) fungerte visstnok ikke
|
|
|
|
|
|
som tiltenkt — brukeren måtte be om ny innloggingskode ved HVERT besøk til
|
|
|
|
|
|
`teecup.teeoff.no`. **Diagnostisert og FIKSET samme dag** (ikke en del av
|
|
|
|
|
|
ADR-021s videre omfang, men verdt å nevne her siden det var starten på denne
|
|
|
|
|
|
tråden): kodegjennomgangen av `app/auth.py`/`app/routers/auth.py` fant ingen
|
|
|
|
|
|
feil i selve cookie-settingen, og brukerens ekte, ferske cookie (hentet fra
|
|
|
|
|
|
nettleseren på forespørsel) bekreftet 30 dagers levetid, `Secure`/`HttpOnly`/
|
|
|
|
|
|
`SameSite=Lax` alt korrekt. Rot-årsaken var derfor IKKE en cookie-/backend-
|
|
|
|
|
|
bug, men en manglende sjekk i frontend: `app/page.tsx` (rot-siden) viste
|
|
|
|
|
|
ALLTID innloggingsskjemaet uten noensinne å sjekke om en gyldig sesjon
|
|
|
|
|
|
allerede fantes — `Dashboard` sjekket `/auth/me` og sendte til `/` ved
|
|
|
|
|
|
MANGLENDE sesjon, men ingenting gjorde det motsatte. Fikset med en
|
|
|
|
|
|
server-side sesjonssjekk (leser cookien via `next/headers`, kaller
|
|
|
|
|
|
`/auth/me` direkte mot `TEECUP_API_ORIGIN` server-til-server, samme mønster
|
|
|
|
|
|
som `generateMetadata` i `app/t/[id]/page.tsx`) som sender en allerede
|
|
|
|
|
|
innlogget bruker rett til `/dashboard`. Verifisert med brukerens ekte cookie:
|
|
|
|
|
|
uten cookie → 200 (skjema), med gyldig cookie → 307 til `/dashboard`. Rullet
|
|
|
|
|
|
ut live, kun `teecup_frontend`, ingen migrasjon, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
Utover selve bugen ønsket brukeren eksplisitt: e-post/brukernavn+passord
|
|
|
|
|
|
(må håndtere spesialtegn og mellomrom korrekt) SOM ET TILLEGG til
|
|
|
|
|
|
(ikke erstatning for) den passordløse innloggingen, samt topartsautentisering.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Passord er et valgfritt, sidestilt alternativ, ikke
|
|
|
|
|
|
påkrevd.** Login-skjermet får en tredje modus (ved siden av e-post-magic-link
|
|
|
|
|
|
og invitasjonskode, ADR-020) for e-post+passord. En bruker setter selv et
|
|
|
|
|
|
passord når hen ønsker det (egen "sett passord"-handling, krever en allerede
|
|
|
|
|
|
gyldig sesjon — samme "du må bevise identitet FØRST" som andre sensitive
|
|
|
|
|
|
endringer) — INGEN eksisterende eller ny bruker tvinges til å sette passord.
|
|
|
|
|
|
Magic-link fortsetter å fungere uendret for alle, uavhengig av om et passord
|
|
|
|
|
|
i tillegg er satt.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Passord-hashing: Argon2id, ikke bcrypt.** Bcrypt trunkerer
|
|
|
|
|
|
stille ved 72 BYTES (et kjent fallgruve-mønster — to ulike passord som deler
|
|
|
|
|
|
de første 72 bytene hasher likt) og har historiske NUL-byte-kvirker i enkelte
|
|
|
|
|
|
implementasjoner. Siden brukeren eksplisitt ber om korrekt håndtering av
|
|
|
|
|
|
spesialtegn/mellomrom (dvs. lengre, mer varierte passord/passfraser er
|
|
|
|
|
|
forventet brukt), velges Argon2id (`argon2-cffi`) — minnehardt, OWASPs
|
|
|
|
|
|
anbefalte standard i dag, ingen lengde-fallgruve. Lagres i ny
|
|
|
|
|
|
`app_user.password_hash` (nullable — NULL betyr "ikke satt", faller da
|
|
|
|
|
|
tilbake til kun magic-link).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — 2FA: brukeren velger metode selv, TOTP ELLER e-post-
|
|
|
|
|
|
engangskode.** `app_user.two_factor_method` (nullable, `'totp'`/`'email'`).
|
|
|
|
|
|
- **TOTP:** `app_user.totp_secret` (bare satt når metoden er `'totp'`),
|
|
|
|
|
|
`pyotp` for generering/verifisering (RFC 6238-standard, virker med Google
|
|
|
|
|
|
Authenticator/Authy/1Password etc. uten videre). QR-kode for oppsett
|
|
|
|
|
|
generert server-side (`qrcode`-biblioteket + eksisterende Pillow-
|
|
|
|
|
|
avhengighet, samme mønster som AVIF-konverteringen) — ingen ny ekstern
|
|
|
|
|
|
tjeneste, ingen løpende kostnad.
|
|
|
|
|
|
- **E-post-engangskode:** gjenbruker eksisterende SMTP-oppsett (`app/email.py`,
|
|
|
|
|
|
ADR-009) — ny, kort 6-sifret kode, samme hash-og-utløp-mønster som
|
|
|
|
|
|
`magic_link_token` (ny tabell `two_factor_code`, 5 minutters gyldighet).
|
|
|
|
|
|
Svakere som eneste faktor (e-post-kontoen blir da reelt sett den ENESTE
|
|
|
|
|
|
hemmeligheten), men krever ingen ny infrastruktur og er brukerens eget,
|
|
|
|
|
|
informerte valg mellom de to metodene.
|
|
|
|
|
|
- **Bevisst UTENFOR omfang:** SMS-2FA — krever en betalt tredjeparts
|
|
|
|
|
|
SMS-leverandør (f.eks. Twilio), løpende kostnad per melding, ny ekstern
|
|
|
|
|
|
avhengighet. Ikke bygget denne runden; kan legges til som en tredje metode
|
|
|
|
|
|
senere uten å røre TOTP/e-post-sporene, siden `two_factor_method` allerede
|
|
|
|
|
|
er et åpent tekstfelt, ikke en hardkodet to-verdi-enum i skjemaet.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — 2FA er PÅKREVD for org-eier/admin, valgfritt ellers.**
|
|
|
|
|
|
Ved innlogging (uansett om via magic-link eller passord): har brukeren
|
|
|
|
|
|
`organization_membership.role IN ('owner','admin')` i MINST én organisasjon
|
|
|
|
|
|
OG `two_factor_method IS NULL`, gis IKKE en full sesjon — brukeren tvinges
|
|
|
|
|
|
inn i et "sett opp 2FA nå"-steg først. En vanlig `member`-rolle (eller en
|
|
|
|
|
|
bruker uten noe org-medlemskap ennå) kan fortsette å bruke appen helt uten
|
|
|
|
|
|
2FA om hen ønsker det. **Begrunnelse:** organisator-roller har skrivetilgang
|
|
|
|
|
|
til andre spilleres personopplysninger (ADR-017) og kontroll over hele
|
|
|
|
|
|
turneringer — et kompromittert organisator-passord/magic-link er en langt
|
|
|
|
|
|
alvorligere hendelse enn en kompromittert spillerkonto. Håndheves ved HVER
|
|
|
|
|
|
innlogging (ikke bare første gang), siden en bruker kan BLI eier av en ny
|
|
|
|
|
|
org (`POST /orgs`, ADR ingen restriksjon — se avklaringen under) etter at
|
|
|
|
|
|
kontoen allerede eksisterer uten 2FA.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — To-stegs innlogging via en `stage`-claim i sesjons-JWT-en,
|
|
|
|
|
|
ikke en egen tabell for "pågående innlogging".** Når 2FA kreves (enten fordi
|
|
|
|
|
|
brukeren selv har slått det på, eller fordi Beslutning D tvinger det),
|
|
|
|
|
|
utsteder primær-autentisering (magic-link-verifisering ELLER passord-innlogging)
|
|
|
|
|
|
et KORTLEVD (5 min) JWT med `stage: "pending_2fa"` i stedet for en full
|
|
|
|
|
|
30-dagers sesjon. En ny avhengighet (`get_pending_2fa_user`, speiler
|
|
|
|
|
|
`get_current_user`) godtar KUN denne mellomtilstanden, og eksponeres bare på
|
|
|
|
|
|
2FA-verifiserings-/oppsett-endepunktene — `get_current_user` (brukt av ALLE
|
|
|
|
|
|
andre endepunkter) avviser eksplisitt en `pending_2fa`-claim som ugyldig,
|
|
|
|
|
|
slik at en ufullstendig innlogging ALDRI gir reell tilgang til noe. Først når
|
|
|
|
|
|
riktig TOTP-/e-post-kode verifiseres, byttes denne inn mot den vanlige fulle
|
|
|
|
|
|
sesjonscookien (samme 30-dagers levetid som i dag).
|
|
|
|
|
|
|
|
|
|
|
|
**Avklaring underveis, ikke en ny beslutning:** brukeren spurte samtidig om
|
|
|
|
|
|
«kan en bruker eie flere organisasjoner» er tenkt gjennom. Bekreftet: JA,
|
|
|
|
|
|
dette var alltid en del av modellen (ADR-002 — bruker og org-medlemskap er
|
|
|
|
|
|
bevisst atskilt nettopp for at én bruker skal kunne krysse flere
|
|
|
|
|
|
organisasjoner). `POST /orgs` (`app/routers/organizations.py`) har INGEN
|
|
|
|
|
|
begrensning på hvor mange organisasjoner én bruker kan opprette/eie —
|
|
|
|
|
|
hver ny org gir automatisk `role='owner'` for oppretteren, uavhengig av
|
|
|
|
|
|
eksisterende medlemskap. Allerede testet i praksis: dashbordets org-bytter,
|
|
|
|
|
|
kryss-org-isolasjonstestene, og `/auth/me` sin N+1-oppslagsstrategi
|
|
|
|
|
|
(«N = antall organisasjoner brukeren tilhører») forutsetter alle nettopp
|
|
|
|
|
|
dette. Ingen kodeendring nødvendig — kun bekreftelse.
|
|
|
|
|
|
|
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
ADR-021 (passord/2FA) og ADR-022 (org-eierskap) er live. Kort oppsummert:
Nytt i innloggingen:
Passord (valgfritt tillegg til magic-link) — Argon2id, testet med ekte spesialtegn/mellomrom/æøå
2FA — TOTP eller e-post-engangskode, brukerens eget valg
Påkrevd 2FA for org-eiere/administratorer, valgfritt for andre
Ny /account-skjerm for å sette passord og styre 2FA
Nytt for organisasjoner:
Inviter andre på e-post til enhver rolle (eiere) eller kun medlem (administratorer)
Frasi deg eierskap selv, eller fjern andre medlemmer — med vern mot at en organisasjon står igjen uten eier
Superadmin-flagg (kun manuelt satt i databasen, aldri via API) som kan sette eierskap på hvilken som helst organisasjon
Ny /orgs/[id]/members-skjerm, lenket fra dashbordet
Fant og fikset tre reelle bugs underveis i scratch-testingen (ingen nådde produksjon) — en krasj i selve innloggingen og to tilfeller av en uendelig 2FA-løkke. Alt er nå verifisert grundig og rullet ut, eksisterende sesjoner er upåvirket.
2026-07-19 10:40:15 +02:00
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-19.** Migrasjon `012_password_2fa_and_
|
2026-08-03 06:24:57 +02:00
|
|
|
|
org_invitations.sql` kjørt mot ekte `teecup_db`. Se CHANGELOG.md for
|
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
ADR-021 (passord/2FA) og ADR-022 (org-eierskap) er live. Kort oppsummert:
Nytt i innloggingen:
Passord (valgfritt tillegg til magic-link) — Argon2id, testet med ekte spesialtegn/mellomrom/æøå
2FA — TOTP eller e-post-engangskode, brukerens eget valg
Påkrevd 2FA for org-eiere/administratorer, valgfritt for andre
Ny /account-skjerm for å sette passord og styre 2FA
Nytt for organisasjoner:
Inviter andre på e-post til enhver rolle (eiere) eller kun medlem (administratorer)
Frasi deg eierskap selv, eller fjern andre medlemmer — med vern mot at en organisasjon står igjen uten eier
Superadmin-flagg (kun manuelt satt i databasen, aldri via API) som kan sette eierskap på hvilken som helst organisasjon
Ny /orgs/[id]/members-skjerm, lenket fra dashbordet
Fant og fikset tre reelle bugs underveis i scratch-testingen (ingen nådde produksjon) — en krasj i selve innloggingen og to tilfeller av en uendelig 2FA-løkke. Alt er nå verifisert grundig og rullet ut, eksisterende sesjoner er upåvirket.
2026-07-19 10:40:15 +02:00
|
|
|
|
full byggerunde (backend/frontend-detaljer, tre reelle bugs funnet og
|
|
|
|
|
|
fikset under scratch-testing).
|
2026-07-19 10:05:42 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-022 — Organisasjonseierskap: dele, invitere, frasi seg, superadmin
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** Reist av brukeren 2026-07-19, rett etter ADR-021. Et reelt,
|
|
|
|
|
|
mer FUNDAMENTALT hull ble synlig under gjennomgangen: `organization_
|
|
|
|
|
|
membership` har i dag INGEN vei til å legge til et nytt medlem i det hele
|
|
|
|
|
|
tatt etter at organisasjonen er opprettet — den ENESTE raden som noensinne
|
|
|
|
|
|
settes inn er grunnleggerens egen `owner`-rad (`POST /orgs`,
|
|
|
|
|
|
`app/routers/organizations.py`). Det finnes ingen invitasjon, ingen
|
|
|
|
|
|
rollestyring, ingen måte å fjerne noen på. «Del eierskap»-ønsket er derfor
|
|
|
|
|
|
bare den mest synlige kanten av et bredere manglende felt: hele
|
|
|
|
|
|
medlemskaps-livssyklusen etter opprettelse.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Flere eiere er allerede støttet av SKJEMAET, kun API-et
|
|
|
|
|
|
mangler.** `organization_membership.role` har ingen unikhetsbegrensning som
|
|
|
|
|
|
hindrer flere `owner`-rader for samme organisasjon — dette var aldri en
|
|
|
|
|
|
sperre, bare et ubrukt hull. Ingen skjemaendring nødvendig for selve
|
|
|
|
|
|
flereeiere-støtten, kun nye endepunkter.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — E-post-basert invitasjon, samme mønster som spiller-
|
|
|
|
|
|
sammenkobling (ADR-017 Beslutning B), ikke et helt nytt konsept.** Ny
|
|
|
|
|
|
`POST /orgs/{id}/invitations {email, role}` oppretter en
|
|
|
|
|
|
`organization_invitation`-rad (e-post, rolle, token-hash, utløper, hvem som
|
|
|
|
|
|
inviterte) og sender en e-post via eksisterende SMTP-infrastruktur
|
|
|
|
|
|
(`app/email.py`). Den inviterte trenger IKKE ha en konto fra før — lenken
|
|
|
|
|
|
tar dem til innlogging (magic-link, evt. passord etter ADR-021), og
|
|
|
|
|
|
`verify_magic_link` utvides til å sjekke ventende invitasjoner på e-posten
|
|
|
|
|
|
sin (samme "kjør på hver innlogging, idempotent" mønster som
|
|
|
|
|
|
`link_player_by_email`) og sette inn `organization_membership`-raden da.
|
|
|
|
|
|
**Rolle-grense ved invitasjon:** en `owner` kan invitere til ENHVER rolle
|
|
|
|
|
|
(owner/admin/member); en `admin` kan KUN invitere til `member` — å la en
|
|
|
|
|
|
admin invitere en ny eier ville vært en reell privilegie-eskaleringsvei
|
|
|
|
|
|
(en admin gir seg selv/en alliert eierskap). Dette er en bevisst, ikke
|
|
|
|
|
|
åpen, avgrensning — minste-privilegium-prinsippet, samme resonnement som
|
|
|
|
|
|
`team_authz.py` sin eksisterende owner/admin-splitt.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Rollestyring og fjerning, med et «siste eier»-vern.**
|
|
|
|
|
|
Ny `PATCH /orgs/{id}/memberships/{membership_id} {role}` (kun `owner`,
|
|
|
|
|
|
UNNTATT at en bruker alltid kan senke SIN EGEN rolle selv — «frasi seg
|
|
|
|
|
|
eierskapet» er en selvbetjent handling, ikke noe som må be en annen eier om
|
|
|
|
|
|
lov) og `DELETE /orgs/{id}/memberships/{membership_id}` (kun `owner`, eller
|
|
|
|
|
|
selv for å forlate organisasjonen). Begge avviser handlingen med en klar
|
|
|
|
|
|
409 (`LAST_OWNER`) hvis den ville latt organisasjonen stå igjen med NULL
|
|
|
|
|
|
eiere — samme «TOCTOU-trygg med `FOR UPDATE`»-mønster som ADR-011s
|
|
|
|
|
|
to-lags-grense. En enslig eier må altså enten forfremme noen andre til
|
|
|
|
|
|
eier FØRST, eller be en superadmin om hjelp (Beslutning D) hvis
|
|
|
|
|
|
organisasjonen skal forlates helt.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Superadmin er et manuelt tildelt, ikke selvbetjent, flagg.**
|
|
|
|
|
|
Ny `app_user.is_super_admin` (boolean, default `false`). INGEN API-endepunkt
|
|
|
|
|
|
lar noen sette dette flagget på seg selv ELLER andre — det settes kun
|
|
|
|
|
|
direkte i databasen av en driftsansvarlig (samme tillitsnivå som å kjøre en
|
|
|
|
|
|
migrasjon), bevisst utenfor appens eget autorisasjonssystem. Årsak: et
|
|
|
|
|
|
selvbetjent «bli superadmin»-endepunkt ville vært selve
|
|
|
|
|
|
sikkerhetshullet det er ment å ikke være. Med flagget satt får brukeren en
|
|
|
|
|
|
NY autorisasjonssti (`get_current_user_or_superadmin`, parallell til
|
|
|
|
|
|
`get_authorized_org` — sjekker `is_super_admin` FØR det vanlige
|
|
|
|
|
|
org-medlemskaps-kravet) som lar dem kalle de samme rolle-/medlemskaps-
|
|
|
|
|
|
endepunktene på ENHVER organisasjon, ikke bare de de selv er medlem av.
|
|
|
|
|
|
**Bevisst avgrenset:** superadmin-stien dekker KUN medlemskap/rolle-
|
|
|
|
|
|
styring i denne runden (nøyaktig det brukeren spurte om — «sette hvem som
|
|
|
|
|
|
helst som eiere av hvilken som helst organisasjon»), ikke generell
|
|
|
|
|
|
skriveadgang til turnering-/spiller-data i andres organisasjoner. En
|
|
|
|
|
|
bredere «support/drift kan se alt»-rolle er en egen, senere beslutning om
|
|
|
|
|
|
den blir etterspurt.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — Fjernet/frasigende eiers roster-/spillerdata forblir
|
|
|
|
|
|
URØRT.** Bekreftet av bruker 2026-07-19. Kun `organization_membership`-raden
|
|
|
|
|
|
endres/fjernes ved rollestyring eller fjerning — `team_roster`/`player`-rader
|
|
|
|
|
|
(deltakelse-historikk) røres aldri av disse handlingene. Organisasjons-
|
|
|
|
|
|
STYRING og DELTAKELSE er allerede modellert som separate ting, og forblir
|
|
|
|
|
|
det.
|
|
|
|
|
|
|
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
ADR-021 (passord/2FA) og ADR-022 (org-eierskap) er live. Kort oppsummert:
Nytt i innloggingen:
Passord (valgfritt tillegg til magic-link) — Argon2id, testet med ekte spesialtegn/mellomrom/æøå
2FA — TOTP eller e-post-engangskode, brukerens eget valg
Påkrevd 2FA for org-eiere/administratorer, valgfritt for andre
Ny /account-skjerm for å sette passord og styre 2FA
Nytt for organisasjoner:
Inviter andre på e-post til enhver rolle (eiere) eller kun medlem (administratorer)
Frasi deg eierskap selv, eller fjern andre medlemmer — med vern mot at en organisasjon står igjen uten eier
Superadmin-flagg (kun manuelt satt i databasen, aldri via API) som kan sette eierskap på hvilken som helst organisasjon
Ny /orgs/[id]/members-skjerm, lenket fra dashbordet
Fant og fikset tre reelle bugs underveis i scratch-testingen (ingen nådde produksjon) — en krasj i selve innloggingen og to tilfeller av en uendelig 2FA-løkke. Alt er nå verifisert grundig og rullet ut, eksisterende sesjoner er upåvirket.
2026-07-19 10:40:15 +02:00
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-19**, samme runde som ADR-021 (samme
|
2026-08-03 06:24:57 +02:00
|
|
|
|
migrasjon `012`). Se CHANGELOG.md for full byggerunde.
|
2026-07-19 10:05:42 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-19 11:41:57 +02:00
|
|
|
|
## ADR-023: Brukerroller — kaptein som reell autorisasjon, deltaker-avgrenset scoring
|
|
|
|
|
|
|
|
|
|
|
|
Reist 2026-07-19, direkte oppfølging av det lenge åpne «Brukerroller»-punktet
|
|
|
|
|
|
i FEATURE_BACKLOG.md (der siden prosjektets start beskrevet som turnerings-
|
|
|
|
|
|
admin/lagkaptein/spiller/tilskuer, men aldri fullt ut avgjort). Fire
|
|
|
|
|
|
delspørsmål, alle avklart eksplisitt med bruker før bygging.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Kaptein gir eksklusive rettigheter til å sette opp/låse
|
|
|
|
|
|
laget, ikke lenger «hvem som helst rostret».** `app/team_authz.py` sin
|
|
|
|
|
|
`user_is_team_captain` (erstatter `user_may_act_for_team`) krever
|
|
|
|
|
|
`team_roster.is_captain = true` for brukeren (eller org-eier/admin, uendret
|
|
|
|
|
|
fra 2026-07-18-runden) — brukt av `matches.py` sin `add_participant`/
|
|
|
|
|
|
`remove_participant`/`lock_lineup`. Ny feilkode `NOT_TEAM_CAPTAIN` (403,
|
|
|
|
|
|
erstatter `NOT_ROSTERED_ON_TEAM` for disse tre stedene).
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst unntak, funnet ved å faktisk sjekke ekte produksjonsdata FØR
|
|
|
|
|
|
utrulling, ikke antatt:** har et lag INGEN utpekt kaptein i det hele tatt,
|
|
|
|
|
|
godtas enhver rostret spiller i stedet — uten dette ville en kaptein-only-
|
|
|
|
|
|
regel umiddelbart LÅST ute et helt lag fra å sette opp seg selv. Sjekket mot
|
|
|
|
|
|
ekte `teecup_db`: laget «De Unge» i «De Gamle er Eldst» har i dag 0 av 2
|
|
|
|
|
|
roster-rader merket kaptein — dette er altså ikke et hypotetisk
|
|
|
|
|
|
kantscenario, det ville rammet en reell, allerede opprettet turnering med
|
|
|
|
|
|
en gang. Har laget FØRST fått en kaptein, gjelder utelukkende den.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — «Kun én kaptein per lag» håndheves nå.** `PATCH .../
|
|
|
|
|
|
roster/{id}` og `POST .../roster` (`app/routers/tournaments.py`) fjerner
|
|
|
|
|
|
automatisk kapteinmerket fra enhver annen roster-rad på SAMME lag når en ny
|
|
|
|
|
|
kaptein settes, i samme transaksjon. Nødvendig konsekvens av Beslutning A —
|
|
|
|
|
|
uten dette ville det vært uklart hvem som faktisk har myndighet hvis flere
|
|
|
|
|
|
er merket. Sjekket mot ekte data: ingen eksisterende lag hadde flere
|
|
|
|
|
|
kapteiner (kun opprydningsbehovet «0 kapteiner» fantes reelt), så ingen
|
|
|
|
|
|
data-migrering var nødvendig.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Score-føring/-korrigering begrenses til matchens faktiske
|
|
|
|
|
|
deltakere.** `app/team_authz.py` sin nye `user_is_match_participant` krever
|
|
|
|
|
|
en `match_participant`-rad for brukeren i AKKURAT den matchen (valgfritt
|
|
|
|
|
|
begrenset til én side via `team_side` — brukt for `stroke`-modus sin
|
|
|
|
|
|
side-spesifikke innsending, `None`/hvilken som helst side for `hole_result`-
|
|
|
|
|
|
modus siden begge sider kan rapportere). Erstatter den brede
|
|
|
|
|
|
«rostret på laget»-sjekken i `app/routers/scoring.py` sin
|
|
|
|
|
|
`submit_hole_score`/`submit_hole_result`. UAVHENGIG av kapteinmerket — å
|
|
|
|
|
|
være kaptein gir ikke i seg selv rett til å føre score for en match man
|
|
|
|
|
|
selv ikke spiller; org-eier/admin har samme unntak som før. Ny feilkode
|
|
|
|
|
|
`NOT_MATCH_PARTICIPANT` (403, erstatter `NOT_ROSTERED_ON_TEAM` her).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — «Tilskuer»-rollen utsettes bevisst.** Ingen kode denne
|
|
|
|
|
|
runden. Offentlig/deltaker-lesetilgang finnes allerede
|
|
|
|
|
|
(`tournament.visibility` + `get_current_user_optional`, ADR-018) — et
|
|
|
|
|
|
formelt tilskuer-begrep bør defineres sammen med det ennå ubesluttede
|
|
|
|
|
|
synlighetsspørsmålet for «Banter Board»-feeden (FEATURE_BACKLOG), ikke
|
|
|
|
|
|
isolert her, for å unngå å bygge to overlappende synlighetsmodeller.
|
|
|
|
|
|
|
2026-07-19 11:46:33 +02:00
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-19.** Ingen migrasjon (ren
|
2026-08-03 06:24:57 +02:00
|
|
|
|
autorisasjonslogikk, ingen skjemaendring). Se CHANGELOG.md for
|
2026-07-19 11:46:33 +02:00
|
|
|
|
scratch-verifisering og utrulling.
|
2026-07-19 11:41:57 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-19 21:25:31 +02:00
|
|
|
|
## ADR-024: Walkover/konsesjon
|
|
|
|
|
|
|
|
|
|
|
|
Reist 2026-07-19, direkte oppfølging av det tidligere åpne punktet i
|
|
|
|
|
|
FEATURE_BACKLOG.md: en side som aldri stiller nok spillere får ALDRI
|
|
|
|
|
|
beregnet handicap, og matchen kan derfor ALDRI avgjøres — den henger uendelig.
|
|
|
|
|
|
Ventet tidligere på Brukerroller (ADR-023), som nå er avgjort. Fire
|
|
|
|
|
|
delspørsmål, alle avklart eksplisitt med bruker før bygging.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Kun den TAPENDE siden (eller org-eier/admin) kan erklære,
|
|
|
|
|
|
til fordel for motstanderen.** Speiler ekte golf-etikette: du gir bort DITT
|
|
|
|
|
|
EGET tap, du krever ikke seier på motstanderens vegne. Bruker
|
|
|
|
|
|
`app/team_authz.py` sin eksisterende `user_is_team_captain` (ADR-023) på det
|
|
|
|
|
|
KONSEDERENDE laget spesifikt — ingen ny autorisasjonsfunksjon nødvendig. Rene
|
|
|
|
|
|
no-show-tilfeller (den tapende siden har ingen innlogget/rostret spiller i
|
|
|
|
|
|
det hele tatt) dekkes av org-admin-fallbacken som allerede finnes i
|
|
|
|
|
|
`user_is_team_captain`.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Ensidig erklæring, ingen bekreftelse fra motparten.**
|
|
|
|
|
|
Samme tillitsnivå som all annen scoring i appen (allerede en upsert uten
|
|
|
|
|
|
godkjenning). Ingen ny "venter på bekreftelse"-tilstand eller
|
|
|
|
|
|
varslingsmekanisme bygget.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Både match- og turnering-nivå, i samme runde.**
|
|
|
|
|
|
`POST /orgs/{id}/matches/{id}/concede` (`app/routers/scoring.py`) for én
|
|
|
|
|
|
match. `POST /orgs/{id}/tournaments/{id}/concede`
|
|
|
|
|
|
(`app/routers/tournaments.py`) for å gi opp ALLE ikke-avgjorte matcher laget
|
|
|
|
|
|
har i turneringen, i én operasjon — v1 er låst til nøyaktig to lag
|
|
|
|
|
|
(ADR-011), så det finnes bare ÉN motstander uansett hvor mange
|
|
|
|
|
|
sesjoner/matcher turneringen har, og turnering-nivå-konsesjon er derfor
|
|
|
|
|
|
bare match-nivå-logikken (`apply_concession`) kjørt per ikke-avgjorte match.
|
|
|
|
|
|
Hull-nivå konsesjon er bevisst IKKE en egen mekanisme — `hole_result`-modus
|
|
|
|
|
|
dekker det allerede (rapporter bare hvem som vant hullet).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Kan erklæres uansett hvor mange hull som allerede er
|
|
|
|
|
|
registrert.** I match-play teller kun seier/tap/delt for poeng, ikke
|
|
|
|
|
|
marginen — det er derfor ingen reell forskjell, poengmessig, på "ga opp
|
|
|
|
|
|
etter 5 hull" og "ga opp før start". `apply_concession` overstyrer status/
|
|
|
|
|
|
poeng direkte, uavhengig av `_compute_hole_results`; allerede registrerte
|
|
|
|
|
|
hull forblir urørt i `hole_score`/`match_hole_result` (kun matchens
|
|
|
|
|
|
avgjørelses-felt endres), så scorekortet fortsatt viser nøyaktig hva som ble
|
|
|
|
|
|
spilt før konsesjonen.
|
|
|
|
|
|
|
|
|
|
|
|
**Gjenbruk, ikke duplisering:** `apply_concession` skriver til nøyaktig de
|
|
|
|
|
|
samme fire kolonnene (`status_text`/`points_side_a/b`/`leading_side`) som
|
|
|
|
|
|
`recompute_and_cache_match_state` — samme "matchen er avgjort"-signal
|
|
|
|
|
|
(`points_side_a IS NOT NULL`) som allerede stopper videre hull-innsending
|
|
|
|
|
|
(`submit_hole_score`/`submit_hole_result`), ingen ny låsemekanisme
|
|
|
|
|
|
nødvendig. Status-teksten "Walkover (A)"/"Walkover (B)" følger samme
|
|
|
|
|
|
"(bokstav)"-visningskonvensjon som `compute_match_state.describe()` sine
|
|
|
|
|
|
egne strenger (f.eks. "9&7 (A)").
|
|
|
|
|
|
|
2026-07-19 21:36:33 +02:00
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-19.** Ingen migrasjon (ren applogikk,
|
2026-08-03 06:24:57 +02:00
|
|
|
|
ingen skjemaendring). Se CHANGELOG.md for scratch-verifisering og
|
2026-07-19 21:36:33 +02:00
|
|
|
|
utrulling.
|
2026-07-19 21:25:31 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-19 22:26:45 +02:00
|
|
|
|
## ADR-025: Kommunikasjon — lag-chat + offentlig runde-feed
|
|
|
|
|
|
|
|
|
|
|
|
Reist 2026-07-19, rett etter turnering-status-runden. Dekker to ganske ulike
|
|
|
|
|
|
ting under samme paraply: lag-intern chat («det hemmelige rommet») og en
|
|
|
|
|
|
offentlig runde-feed («Banter Board»). Fire beslutninger avklart eksplisitt
|
|
|
|
|
|
med bruker (AskUserQuestion) før bygging.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Begge bygges i samme runde**, ikke lag-chat først som egen
|
|
|
|
|
|
runde. Deler mye infrastruktur (meldingsmodell, sanntid-levering), så
|
|
|
|
|
|
designes/bygges sammen selv om feeden isolert sett hadde flere åpne
|
|
|
|
|
|
spørsmål.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Sanntid via WebSockets**, ikke polling. Løser samtidig det
|
|
|
|
|
|
tidligere åpne "sanntid vs. polling"-spørsmålet i FEATURE_BACKLOG.md
|
|
|
|
|
|
generelt (samme mekanisme kan gjenbrukes for leaderboard/andre skjermer
|
|
|
|
|
|
senere, selv om denne runden kun kobler den til meldinger).
|
|
|
|
|
|
**Viktig driftsbegrensning, videreført fra et allerede kjent åpent
|
|
|
|
|
|
arkitekturspørsmål (se «Åpne spørsmål» punkt 2 i dette dokumentet):**
|
|
|
|
|
|
tilkoblingsregisteret er en in-memory Python-struktur i `teecup_api`-
|
|
|
|
|
|
prosessen. Med kun én container/prosess (dagens oppsett) er dette trygt;
|
|
|
|
|
|
skaleres API-et til flere prosesser/containere senere, må broadcast flyttes
|
|
|
|
|
|
til noe delt (Redis pub/sub e.l.) — samme klasse begrensning som den
|
|
|
|
|
|
allerede aksepterte in-memory-cachen.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Lag-chat er EKTE privat: kun rostrede spillere på laget,
|
|
|
|
|
|
INGEN unntak for org-eier/admin.** Et bevisst avvik fra appens ellers
|
|
|
|
|
|
gjennomgående mønster (kaptein-/deltaker-sjekkene i `team_authz.py` har
|
|
|
|
|
|
alltid en org-admin-fallback, se ADR-023). Ny, egen autorisasjonsfunksjon
|
|
|
|
|
|
`user_is_rostered_on_team` (uten fallback) brukt KUN her — de eksisterende
|
|
|
|
|
|
funksjonene med org-admin-unntak røres ikke, siden de fortsatt er riktige
|
|
|
|
|
|
for sine egne bruksområder (oppsett/scoring, der en organisator uten
|
|
|
|
|
|
dette ville stått fast tidlig i en turnering).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Bilder med fra start**, ikke utsatt. Gjenbruker
|
|
|
|
|
|
`app/storage.py` sin allerede byggede og bevist MinIO+AVIF-konverterings-
|
|
|
|
|
|
pipeline (samme mønster som turnering-hero-bilder/sponsorlogoer, ADR-018) —
|
|
|
|
|
|
ingen ny opplastingsinfrastruktur trengs, kun en ny `prefix="messages"`.
|
|
|
|
|
|
|
|
|
|
|
|
**Datamodell:** delt `message`-tabell (migrasjon `013_messaging.sql`) med
|
|
|
|
|
|
en `scope`-diskriminator (`team`/`tournament_feed`) i stedet for to separate
|
|
|
|
|
|
tabeller — meldingsformen (tekst + valgfritt bilde) er identisk, kun
|
|
|
|
|
|
synlighet/autorisasjon skiller dem. `author_display_name` FRYSES ved
|
|
|
|
|
|
skrivetidspunkt (samme prinsipp som `handicap_index_snapshot`, ADR-007) —
|
|
|
|
|
|
utledet server-side fra avsenderens `player`-rad i org-en (hvis den finnes),
|
|
|
|
|
|
ellers e-postens lokaldel som fallback (en org-ansatt uten egen spillerprofil
|
|
|
|
|
|
kan fortsatt poste i den offentlige feeden).
|
|
|
|
|
|
|
|
|
|
|
|
**Offentlig feed — synlighet og posterett, to atskilte spørsmål:**
|
|
|
|
|
|
LESING gjenbruker `registration.py` sitt eksisterende trenivå-mønster
|
|
|
|
|
|
(`tournament.visibility` + `get_current_user_optional` + deltaker-sjekk,
|
|
|
|
|
|
ADR-018) uendret — ingen ny synlighetsmekanisme. POSTING er derimot
|
|
|
|
|
|
STRENGERE enn lesing: en anonym leser på en `public`-synlig turnering kan
|
|
|
|
|
|
lese feeden, men må logge inn OG være enten org-medlem eller faktisk
|
|
|
|
|
|
deltaker/registrert i NØYAKTIG denne turneringen for å få poste — hindrer at
|
|
|
|
|
|
en helt urelatert innlogget bruker (konto et helt annet sted i systemet) kan
|
|
|
|
|
|
poste på en fremmed offentlig turnering-side bare fordi den er synlig.
|
|
|
|
|
|
Moderering: forfatteren selv, ELLER org-eier/admin, kan slette et
|
|
|
|
|
|
feed-innlegg. Lag-chat har INGEN moderering utover forfatteren selv (rommet
|
|
|
|
|
|
er privat, org-admin har uansett ikke lesetilgang og kan derfor ikke
|
|
|
|
|
|
moderere det).
|
|
|
|
|
|
|
2026-08-03 06:24:57 +02:00
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-19.** Se CHANGELOG.md for scratch-
|
2026-07-19 22:26:45 +02:00
|
|
|
|
verifisering og utrulling, inkl. en egen Caddy-rute (`/ws/*`) for
|
|
|
|
|
|
WebSocket-trafikk direkte til `teecup_api` (Next.js sin `rewrites()`
|
|
|
|
|
|
proxyer ikke WebSocket-oppgraderinger pålitelig — samme klasse
|
|
|
|
|
|
infrastrukturvalg som media-ruten i MinIO-runden, ADR-018).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-19 22:51:07 +02:00
|
|
|
|
## ADR-026: Tilskuer-rolle — offentlig leaderboard, matcher, scorekort
|
|
|
|
|
|
|
|
|
|
|
|
Reist 2026-07-19, rett etter Kommunikasjon (ADR-025), som gjorde det mulig å
|
|
|
|
|
|
definere "tilskuer" skikkelig (samme begrunnelse ble notert allerede i
|
|
|
|
|
|
FEATURE_BACKLOG.md fra starten av: bør avgjøres sammen med feed-synligheten,
|
|
|
|
|
|
som nå finnes).
|
|
|
|
|
|
|
|
|
|
|
|
**Kjernebeslutning: ingen ny rolle, ingen ny mekanisme.** "Tilskuer" er
|
|
|
|
|
|
IKKE en egen kontotype eller databasetabell — det er ganske enkelt: enhver
|
|
|
|
|
|
som kan SE en turnering (per `tournament.visibility`, ADR-018) kan nå også
|
|
|
|
|
|
følge den LIVE, ikke bare lese info-siden og programtidene. Samme
|
|
|
|
|
|
trenivå-visibility + `get_current_user_optional`-mønster som all annen
|
|
|
|
|
|
offentlig lesing, gjenbrukt helt uendret.
|
|
|
|
|
|
|
|
|
|
|
|
**Hva var det egentlige hullet:** `GET /orgs/.../leaderboard`,
|
|
|
|
|
|
`GET /orgs/.../sessions/{id}/matches` og `GET /orgs/.../matches/{id}/
|
|
|
|
|
|
scorecard` fantes allerede (bygget for organisatorer/spillere), men var
|
|
|
|
|
|
KUN tilgjengelige med org-medlemskap — en spectator med kun `/t/[id]`-
|
|
|
|
|
|
lenken (eller anonym på en `public`-synlig turnering) kunne aldri se dem.
|
|
|
|
|
|
Løst ved å ekstrahere den delte kjernelogikken til gjenbrukbare funksjoner
|
|
|
|
|
|
(`fetch_leaderboard` i tournaments.py, `fetch_matches` i matches.py,
|
|
|
|
|
|
`fetch_scorecard` i scoring.py — samme "gjort delt for gjenbruk"-mønster
|
|
|
|
|
|
som `recompute_and_cache_match_state`/`apply_concession` tidligere), og la
|
|
|
|
|
|
tre nye offentlige endepunkter i `registration.py` kalle dem, etter egen
|
|
|
|
|
|
visibility-sjekk.
|
|
|
|
|
|
|
|
|
|
|
|
**Omfang, valgt av bruker utover anbefalingen:** BÅDE leaderboard+
|
|
|
|
|
|
matchliste OG fullt hull-for-hull-scorekort per match, i samme runde (ikke
|
|
|
|
|
|
kun leaderboard+matchliste som opprinnelig anbefalt).
|
|
|
|
|
|
|
|
|
|
|
|
**`own_team_ids()` (blind_draw.py) gjort null-sikker:** tar nå
|
|
|
|
|
|
`user_id: str | None` — en anonym/ikke-tilknyttet leser har per definisjon
|
|
|
|
|
|
ingen egne lag, og skal derfor (korrekt, ikke en feil) kun se AVSLØRTE
|
|
|
|
|
|
matcher, akkurat som en tilfeldig org-medlem uten roster. Ingen ny
|
|
|
|
|
|
synlighetslogikk, bare eksisterende logikk gjort tilgjengelig for en
|
|
|
|
|
|
`None`-bruker.
|
|
|
|
|
|
|
|
|
|
|
|
**To NYE sikkerhetssjekker lagt til, funnet under design, ikke i etterkant:**
|
|
|
|
|
|
1. `session_id`/`match_id` i URL-en må eksplisitt verifiseres å høre til
|
|
|
|
|
|
NØYAKTIG `tournament_id` i samme URL — `org_connection()` setter kun
|
|
|
|
|
|
TENANT-grensen (RLS), ikke at stiens id-er faktisk henger sammen. Uten
|
|
|
|
|
|
denne sjekken kunne noen med tilgang til én offentlig turnering i en
|
|
|
|
|
|
organisasjon lest en HVILKEN SOM HELST økt/match i SAMME organisasjon
|
|
|
|
|
|
(inkl. en helt privat en) ved å gjette/prøve seg frem på id-er — nøyaktig
|
|
|
|
|
|
samme klasse hull som ADR-018 Beslutning B advarte om generelt
|
|
|
|
|
|
("RLS beskytter kun tenant-grenser, ikke innholds-synlighet").
|
|
|
|
|
|
2. Det offentlige scorekort-endepunktet krever eksplisitt at BEGGE lag har
|
|
|
|
|
|
låst oppstillingen for økten (blind draw, ADR-013) — leaderboard/
|
|
|
|
|
|
matchliste arver reveal-skjuling automatisk via `own_team_ids()`, men
|
|
|
|
|
|
scorekortet har ingen tilsvarende innebygd sjekk (kan i prinsippet
|
|
|
|
|
|
inneholde registrerte hull før reveal, selv om det ikke er normal flyt).
|
|
|
|
|
|
|
Update Todos
Refaktorere get_leaderboard til delt fetch_leaderboard()
Gjøre own_team_ids null-sikker for anonyme/uinnloggede tilskuere
Refaktorere list_matches og get_scorecard til delte funksjoner
Legge til offentlige leaderboard/matches/scorecard-endepunkter i registration.py
Skrive ADR-026 (tilskuer-rolle)
Scratch-verifisere backend
Bygge frontend: offentlig live-visning (leaderboard + matcher + scorekort) på /t/[id]/live
Ekte typesjekket frontend-build
Deploy etter bekreftelse + oppdatere .md-filer
Tilskuer-rollen er live. Kjernen: ingen ny rolle trengtes — «tilskuer» er ganske enkelt enhver som allerede kan se turneringen (per synlighetsnivå), nå utvidet til å dekke leaderboard, matchliste og fullt hull-for-hull-scorekort, ikke bare info-siden og programtider som før. Ny /t/[id]/live-side, lenket fra hovedsiden.
Fant og lukket to sikkerhetshull under design (før noe ble bygget): en manglende sjekk på at økt/match i URL-en faktisk hører til turneringen i samme URL (ellers kunne noen lest en hvilken som helst økt i samme organisasjon ved å gjette id-er), og en manglende reveal-sjekk på scorekortet. Begge verifisert eksplisitt med egne tester — 16 automatiserte sjekker totalt, inkludert en presis test med to ekte turneringer i samme organisasjon.
.md-filene er oppdatert. Gjenværende åpne punkter: PWA, kode-regenerering, korrigering-godkjenning, sanntid for leaderboard-siden (WebSocket-mekanismen finnes allerede, bare ikke koblet til der ennå), og de nye turneringsformatene som ble notert tidligere.
2026-07-19 22:53:33 +02:00
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-19.** Ingen migrasjon (kun nye
|
|
|
|
|
|
endepunkter + refaktorering av eksisterende spørringer til delte
|
2026-08-03 06:24:57 +02:00
|
|
|
|
funksjoner). Se CHANGELOG.md for scratch-verifisering og utrulling.
|
2026-07-19 22:51:07 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-19 23:14:17 +02:00
|
|
|
|
## ADR-027: Sanntid for "Følg live"-siden
|
|
|
|
|
|
|
|
|
|
|
|
Reist 2026-07-19, rett etter ADR-026 (tilskuer-rolle) — brukeren valgte min
|
|
|
|
|
|
egen anbefaling: `/t/[id]/live` krevde omlasting for å se nye resultater,
|
|
|
|
|
|
litt selvmotsigende for en side som heter "Følg live". Løser samtidig det
|
|
|
|
|
|
tidligere åpne "koble leaderboard til sanntid"-punktet i FEATURE_BACKLOG.md.
|
|
|
|
|
|
|
|
|
|
|
|
**Gjenbruker WebSocket-mekanismen fra ADR-025 (meldinger), men "noe endret
|
|
|
|
|
|
seg, hent på nytt"-signal i stedet for å sende selve dataene** — å bygge/
|
|
|
|
|
|
sende hele leaderboard+matchliste+scorekort-formen over WS ville duplisert
|
|
|
|
|
|
betydelig beregningslogikk (leaderboardets projeksjons-regnestykke, blind
|
|
|
|
|
|
draw-filtrering osv.). Klienten reagerer på signalet ved å kalle de samme
|
|
|
|
|
|
REST-endepunktene på nytt (ADR-026), akkurat som ved førstegangslasting —
|
|
|
|
|
|
kun for det som faktisk er synlig/åpent på skjermen (leaderboard alltid,
|
|
|
|
|
|
en økts matcher kun hvis økten er utvidet, et scorekort kun hvis det er
|
|
|
|
|
|
åpnet).
|
|
|
|
|
|
|
|
|
|
|
|
**Ny, RUTEFRI modul `app/realtime.py`** for selve tilkoblingsregisteret og
|
|
|
|
|
|
kringkastingsfunksjonen — verken i `messaging.py`, `scoring.py` eller
|
|
|
|
|
|
`tournaments.py`. Årsak: `registration.py` (som eier selve
|
|
|
|
|
|
`/public/...`-endepunktene) importerer allerede fra `scoring.py`/
|
|
|
|
|
|
`tournaments.py`/`matches.py`, og `messaging.py` importerer fra
|
|
|
|
|
|
`registration.py` — å plassere kringkastingsfunksjonen i noen av routerne
|
|
|
|
|
|
ville skapt en sirkulær import. `app/realtime.py` ligger bevisst BAK alle
|
|
|
|
|
|
routere i importgrafen.
|
|
|
|
|
|
|
|
|
|
|
|
**Kringkastingen er lagt INN I de delte funksjonene selv**
|
|
|
|
|
|
(`recompute_and_cache_match_state`, `apply_concession`), ikke som noe
|
|
|
|
|
|
kallerne må huske å gjøre etterpå — samme selv-ansvarlig-mønster som andre
|
|
|
|
|
|
sentrale funksjoner i prosjektet. `apply_concession` fikk en ny påkrevd
|
|
|
|
|
|
`tournament_id`-parameter kun for dette formålet.
|
|
|
|
|
|
|
|
|
|
|
|
**Samme kjente in-memory-per-prosess-begrensning som ADR-025** (se «Åpne
|
|
|
|
|
|
spørsmål» under) — trygt med dagens ene `teecup_api`-container.
|
|
|
|
|
|
|
2026-07-19 23:16:38 +02:00
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-19.** Ingen migrasjon, ingen ny
|
|
|
|
|
|
Caddy-rute (gjenbruker `/ws/*`-ruten fra ADR-025 uendret). Se CLAUDE.md-
|
|
|
|
|
|
status for scratch-verifisering og utrulling.
|
2026-07-19 23:14:17 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
PWA er bygget og scratch/build-verifisert. Status:
Bygget:
Installerbar app: app/manifest.ts, appleWebApp-metadata for iOS, service worker (public/sw.js, håndskrevet — ingen next-pwa-avhengighet), public/offline.html.
Ikoner: enkelt grønt golf-flagg generert programmatisk (måtte omgå at verken PIL, rsvg-convert eller ImageMagick fantes i miljøet — løste det med et scratch npm-oppsett av sharp). Midlertidig, notert i FEATURE_BACKLOG.md for senere erstatning, slik du ba om. Erstattet samtidig den gamle apple-icon.png som faktisk var v0.app sin generiske plassholderlogo, ikke TeeCup-merkevare.
Full offline scoreregistrering: ny lib/offline-queue.ts (IndexedDB-kø) koblet inn i scorekort-skjermen. Slag/hull-resultat registrert offline vises umiddelbart som "Lagret lokalt · venter på synk", synkes automatisk når nettet er tilbake (pluss en manuell "Synkroniser nå"-knapp). Se ADR-028 for alle designvalgene (bl.a. hvorfor køen ligger i klientkoden og ikke i service workeren, og hvorfor jeg bevisst unngikk Background Sync API — iOS Safari støtter den ikke).
Viktig å være ærlig om: jeg har ingen nettleser-verktøy tilgjengelig i denne økten, så offline-flyten er verifisert med typesjekket produksjonsbuild + en kort container-boot (curl mot manifest/service worker/ikoner — alle svarer riktig), men ikke faktisk testet i en ekte nettleser (Chrome DevTools sin Offline-bryter, ordentlig "Legg til på hjemskjerm"). Anbefaler sterkt at du selv tester scorekort-siden med DevTools offline-modus før dere stoler på det i skarp bruk.
De fire UI-hullene du meldte inn er dokumentert i FEATURE_BACKLOG.md/CLAUDE.md med root cause (ikke bare notert som klager) — den mest presserende er /orgs/{id}/members, som er en reell rewrite/dynamisk-rute-kollisjon: siden er helt utilgjengelig i dag fordi Next.js sin /orgs/:path*-proxy-regel fanger kallet før selve siden noensinne nås.
Ingen migrasjon i denne runden — kun teecup_frontend trenger redeploy.
2026-07-19 23:51:08 +02:00
|
|
|
|
## ADR-028: PWA — installasjon + offline scoreregistrering
|
|
|
|
|
|
|
|
|
|
|
|
Reist 2026-07-19, brukeren valgte å ta fatt på PWA (stod som "neste steg" i
|
|
|
|
|
|
CLAUDE.md siden ADR-006 vedtok prinsippet helt i starten av prosjektet — aldri
|
|
|
|
|
|
bygget). To beslutninger avklart eksplisitt med bruker (AskUserQuestion) før
|
|
|
|
|
|
bygging.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Full offline scoreregistrering i v1, ikke bare
|
|
|
|
|
|
installasjon.** Bruker valgte det mest ambisiøse alternativet: BÅDE
|
|
|
|
|
|
manifest/ikoner/service worker (installerbar app) OG at scorekort-skjermen
|
|
|
|
|
|
skal fungere uten nett — skriv til en lokal IndexedDB-kø, synk automatisk når
|
|
|
|
|
|
nettet er tilbake. Matcher ADR-006s opprinnelige formulering ordrett
|
|
|
|
|
|
("score må kunne registreres uten nett og synkes senere").
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Omfang av selve offline-skrivingen: kun `hole-scores`/
|
|
|
|
|
|
`hole-results`, ingenting annet.** De to eksisterende scoreregistrerings-
|
|
|
|
|
|
endepunktene (ADR-012) er de eneste som køes. Bevisst UTENFOR omfang, ikke
|
|
|
|
|
|
glemt:
|
|
|
|
|
|
- Walkover/konsesjon (ADR-024) — sjeldnere handling, kan kreve nett.
|
|
|
|
|
|
- Chat/feed-posting (ADR-025) — bilder gjør en offline-kø vesentlig mer
|
|
|
|
|
|
komplisert (blob-lagring i IndexedDB), og er ikke "score"-handlingen
|
|
|
|
|
|
ADR-006 faktisk siktet til.
|
|
|
|
|
|
- Alle andre skrive-endepunkter (oppsett, roster, økter osv.) — forutsetter
|
|
|
|
|
|
nett i v1, uendret.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Køen lever i klientkoden (IndexedDB, `lib/offline-
|
|
|
|
|
|
queue.ts`), IKKE i service workeren, og bruker `window` sitt `online`-
|
|
|
|
|
|
event, IKKE Background Sync API.** To bevisste forenklinger:
|
|
|
|
|
|
1. Å gi umiddelbar, presis UI-tilbakemelding ("lagret lokalt, venter på
|
|
|
|
|
|
synk", pending-antall) er enklere og mer testbart fra selve
|
|
|
|
|
|
React-komponenten enn fra en service worker sin `fetch`-handler.
|
|
|
|
|
|
2. Background Sync API (som ville gitt synk selv om appen er lukket) støttes
|
|
|
|
|
|
IKKE av iOS Safari i det hele tatt — en stor andel av klubb-/
|
|
|
|
|
|
vennegjeng-brukerne er trolig på iPhone, og et rent
|
|
|
|
|
|
Background-Sync-avhengig design ville derfor vært brutt for dem. Et
|
|
|
|
|
|
`window.addEventListener("online", ...)`-mønster (pluss en manuell
|
|
|
|
|
|
"Synkroniser nå"-knapp i UI-et som reserve) fungerer overalt, på
|
|
|
|
|
|
bekostning av at synk krever at appen faktisk er åpen når nettet kommer
|
|
|
|
|
|
tilbake — akseptabelt for v1.
|
|
|
|
|
|
|
|
|
|
|
|
`submitStroke`/`submitHoleResult` (`components/session-scorecard.tsx`)
|
|
|
|
|
|
sjekker `navigator.onLine` FØRST (unngår en unødvendig ventetid på et
|
|
|
|
|
|
nettverkskall som uansett vil feile), og fanger ellers en EKTE nettverksfeil
|
|
|
|
|
|
i fetch-kallet separat fra et avvist HTTP-svar (`res.ok === false`, f.eks.
|
|
|
|
|
|
409 "matchen er avgjort") — kun den førstnevnte køordner, sistnevnte viser
|
|
|
|
|
|
fortsatt den vanlige feilteksten uendret. Et lokalt overlay
|
|
|
|
|
|
(`pendingStrokes`/`pendingResults`, ikke persistert i selve
|
|
|
|
|
|
`scorecard`-staten) viser køede verdier umiddelbart i UI-et, merket
|
|
|
|
|
|
"Lagret lokalt · venter på synk", inkludert i hull-navigasjonens
|
|
|
|
|
|
registrert-markering.
|
|
|
|
|
|
|
|
|
|
|
|
**Konfliktmodell: samme tillitsnivå som resten av appen, ingen ny
|
|
|
|
|
|
mekanisme.** Server-siden er allerede en upsert per hull (`ON CONFLICT ...
|
|
|
|
|
|
DO UPDATE`) — siste innsending vinner, ingen audit-trail (kjent, tidligere
|
|
|
|
|
|
dokumentert åpent spørsmål, se «Scoring-autorisasjon» i FEATURE_BACKLOG.md).
|
|
|
|
|
|
Køen sender i rekkefølge (aldri parallelt) for å respektere lokal
|
|
|
|
|
|
innsendingsrekkefølge ved flere endringer av samme hull offline. Et
|
|
|
|
|
|
definitivt HTTP-avvist forsøk ved synk (typisk: matchen ble avgjort på en
|
|
|
|
|
|
ANNEN enhet mens denne var offline) fjernes fra køen og vises som en
|
|
|
|
|
|
feilmelding — blir ALDRI hengende for alltid.
|
|
|
|
|
|
|
|
|
|
|
|
**Service worker-strategi: nettverk først, cache som fallback — bevisst
|
|
|
|
|
|
IKKE stale-while-revalidate.** `public/sw.js` cacher (a) side-navigasjon og
|
|
|
|
|
|
(b) GET-kall under `/orgs/*`. Begge prøver ekte nettverk FØRST og faller
|
|
|
|
|
|
kun tilbake til cache ved reell feil. En stale-while-revalidate-strategi ble
|
|
|
|
|
|
vurdert og avvist: scorekortet gjør et `refetchScorecard()`-kall RETT ETTER
|
|
|
|
|
|
hver innsending, og må da alltid få fersk data — en umiddelbart utdatert
|
|
|
|
|
|
cache ville vist feil matchstatus/hull-tall rett etter en vellykket
|
|
|
|
|
|
innsending. `/auth/*` og `/public/*` caches bevisst ikke — omfanget er
|
|
|
|
|
|
begrenset til akkurat det scoreregistrerings-flyten trenger.
|
|
|
|
|
|
|
|
|
|
|
|
**Kjent, akseptert begrensning (ikke løst i v1):** Cache Storage er nøklet
|
|
|
|
|
|
på URL, ikke på innlogget bruker — deler flere kontoer samme enhet/
|
|
|
|
|
|
nettleser, kan en offline-fallback teoretisk vise data cachet av en
|
|
|
|
|
|
TIDLIGERE innlogget bruker på samme enhet. Ingen cache-tømming ved
|
|
|
|
|
|
utlogging bygget. Samme tillitsnivå/trusselmodell som appen ellers opererer
|
|
|
|
|
|
med (tillitsbasert klubb-/vennegjeng-verktøy, jf. `team_authz.py` sin
|
|
|
|
|
|
begrunnelse i ADR-023/025).
|
|
|
|
|
|
|
|
|
|
|
|
**Ikoner: enkelt, midlertidig sett generert programmatisk (grønt
|
|
|
|
|
|
golf-flagg), ikke endelig design.** Bruker valgte å generere nå fremfor å
|
|
|
|
|
|
vente på ekte design, MED eksplisitt beskjed om at disse skal erstattes
|
|
|
|
|
|
senere — notert i FEATURE_BACKLOG.md. Erstattet samtidig den gamle
|
|
|
|
|
|
`public/apple-icon.png` (v0.app sin generiske plassholder-logo, ikke
|
|
|
|
|
|
TeeCup-merkevare i det hele tatt) med samme nye ikon, av konsistens.
|
|
|
|
|
|
|
|
|
|
|
|
**Ikke testet i ekte nettleser (viktig, ikke bare en formalitet):**
|
|
|
|
|
|
verifisert med typesjekket produksjonsbuild (samme `Dockerfile` som
|
|
|
|
|
|
deployes) og en kort container-boot med `curl` (manifest/service worker/
|
|
|
|
|
|
ikoner/offline.html svarer riktig), men INGEN faktisk browser-basert
|
|
|
|
|
|
offline-test (DevTools "Offline"-modus, "Legg til på hjemskjerm") er
|
|
|
|
|
|
gjennomført denne runden — ingen nettleserverktøy tilgjengelig i denne
|
|
|
|
|
|
økten. Anbefales sterkt at brukeren selv tester scorekort-siden med Chrome
|
|
|
|
|
|
DevTools sin Offline-bryter før tillit legges til flyten i skarp bruk.
|
|
|
|
|
|
|
2026-07-19 23:54:19 +02:00
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-19.** Bruker bekreftet eksplisitt, ingen
|
|
|
|
|
|
migrasjon, kun `teecup_frontend` (og en ren, uendret gjenoppbygging av
|
|
|
|
|
|
`teecup_api` som en bivirkning av `docker compose up --build` sin
|
|
|
|
|
|
avhengighets-oppløsning — ingen backend-kode rørt denne runden). Verifisert:
|
|
|
|
|
|
`/health`/`dashboard` → 200, `/manifest.webmanifest`/`sw.js`/ikoner alle
|
2026-08-03 06:24:57 +02:00
|
|
|
|
200 over ekte https, `teeoff.no` upåvirket. Se CHANGELOG.md for full
|
2026-07-19 23:54:19 +02:00
|
|
|
|
byggerunde.
|
|
|
|
|
|
|
|
|
|
|
|
**Gjenstående, IKKE en del av "ferdig"-vurderingen over:** faktisk
|
|
|
|
|
|
nettleser-basert offline-test (Chrome DevTools Offline-modus, ekte "Legg
|
|
|
|
|
|
til på hjemskjerm") er fortsatt ikke gjort — ingen nettleserverktøy
|
|
|
|
|
|
tilgjengelig i byggeøkten. Brukeren har bedt om at dette noteres eksplisitt
|
|
|
|
|
|
for oppfølging, se FEATURE_BACKLOG.md sin PWA-tabell og CLAUDE.md sin
|
|
|
|
|
|
"Neste steg"-liste.
|
PWA er bygget og scratch/build-verifisert. Status:
Bygget:
Installerbar app: app/manifest.ts, appleWebApp-metadata for iOS, service worker (public/sw.js, håndskrevet — ingen next-pwa-avhengighet), public/offline.html.
Ikoner: enkelt grønt golf-flagg generert programmatisk (måtte omgå at verken PIL, rsvg-convert eller ImageMagick fantes i miljøet — løste det med et scratch npm-oppsett av sharp). Midlertidig, notert i FEATURE_BACKLOG.md for senere erstatning, slik du ba om. Erstattet samtidig den gamle apple-icon.png som faktisk var v0.app sin generiske plassholderlogo, ikke TeeCup-merkevare.
Full offline scoreregistrering: ny lib/offline-queue.ts (IndexedDB-kø) koblet inn i scorekort-skjermen. Slag/hull-resultat registrert offline vises umiddelbart som "Lagret lokalt · venter på synk", synkes automatisk når nettet er tilbake (pluss en manuell "Synkroniser nå"-knapp). Se ADR-028 for alle designvalgene (bl.a. hvorfor køen ligger i klientkoden og ikke i service workeren, og hvorfor jeg bevisst unngikk Background Sync API — iOS Safari støtter den ikke).
Viktig å være ærlig om: jeg har ingen nettleser-verktøy tilgjengelig i denne økten, så offline-flyten er verifisert med typesjekket produksjonsbuild + en kort container-boot (curl mot manifest/service worker/ikoner — alle svarer riktig), men ikke faktisk testet i en ekte nettleser (Chrome DevTools sin Offline-bryter, ordentlig "Legg til på hjemskjerm"). Anbefaler sterkt at du selv tester scorekort-siden med DevTools offline-modus før dere stoler på det i skarp bruk.
De fire UI-hullene du meldte inn er dokumentert i FEATURE_BACKLOG.md/CLAUDE.md med root cause (ikke bare notert som klager) — den mest presserende er /orgs/{id}/members, som er en reell rewrite/dynamisk-rute-kollisjon: siden er helt utilgjengelig i dag fordi Next.js sin /orgs/:path*-proxy-regel fanger kallet før selve siden noensinne nås.
Ingen migrasjon i denne runden — kun teecup_frontend trenger redeploy.
2026-07-19 23:51:08 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Presentere full plan og be om bekreftelse (ekte migrasjon + deploy)
Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer:
1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet.
3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19).
4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes.
2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret.
Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak?
Utrullingsplan — dette er første gang i denne runden noe rører ekte data:
Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
|
|
|
|
## ADR-029: Kjønn hører til tee-RATINGEN, ikke selve utslaget
|
|
|
|
|
|
|
|
|
|
|
|
Reist av brukeren 2026-07-19, som del av en runde med fire rapporterte
|
|
|
|
|
|
UX-/korrekthetshull fra faktisk testing av blind draw og scorekort. Startet
|
|
|
|
|
|
som en antatt frontend-fiks ("tee-valget bør følge spillerens kjønn
|
|
|
|
|
|
automatisk"), men brukeren presiserte at premisset mitt var feil: en
|
|
|
|
|
|
golfbane har IKKE fysisk kjønnsdelte utslag -- begge kjønn kan som regel
|
|
|
|
|
|
spille fra ethvert utslag. Det eneste som faktisk varierer per kjønn er om
|
|
|
|
|
|
klubben har VALGT å slope (rate) et gitt utslag for det respektive kjønnet
|
|
|
|
|
|
(noen klubber sloper bevisst ikke det lengste utslaget for damer).
|
|
|
|
|
|
|
|
|
|
|
|
**Bekreftet problem, ikke antatt:** `tee.gender` (migrasjon 001) la kjønn på
|
|
|
|
|
|
selve utslaget, ikke ratingen. Sjekket mot ekte, importert produksjonsdata
|
|
|
|
|
|
(Tjøme Golfklubb, ADR-019): alle fire utslagene lå som RENE navnepar --
|
|
|
|
|
|
"32"/m + "32"/f, "44"/m + "44"/f, osv. -- to separate `tee`-rader for
|
|
|
|
|
|
akkurat samme fysiske utslag. Blind draw-skjermen viste dette som "velg
|
|
|
|
|
|
Dame- eller Herre-tee", som om det var to ulike steder å slå fra.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A -- `gender` flyttes fra `tee` til `tee_rating`.** Ett fysisk
|
|
|
|
|
|
utslag (`tee`, identifisert kun ved navn) kan ha 0, 1 eller 2
|
|
|
|
|
|
kjønnsspesifikke ratinger. `tee_rating` sin unikhet endres fra
|
|
|
|
|
|
`(tee_id, scope)` til `(tee_id, scope, gender)`. `gender` på `tee_rating`
|
|
|
|
|
|
er `NOT NULL CHECK IN ('m','f')` -- 'x' (gyldig på PLAYER-nivå) gir ikke
|
|
|
|
|
|
mening for en WHS-rating, som alltid er for ett bestemt kjønn.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B -- tee-valget blir helt automatisk, ingen manuell
|
|
|
|
|
|
kjønnsvelger (bekreftet med bruker, valgte det anbefalte alternativet).**
|
|
|
|
|
|
Organisator/kaptein velger KUN fysisk utslag i blind draw
|
|
|
|
|
|
(`session-blind-draw.tsx`, ingen "H"/"D"-suffiks lenger). Riktig
|
|
|
|
|
|
kjønnsspesifikk rating løses AUTOMATISK server-side fra spillerens
|
|
|
|
|
|
registrerte `player.gender` -- `app/handicap.py` sin
|
|
|
|
|
|
`compute_and_store_side_handicaps` joiner nå `tee_rating` på
|
|
|
|
|
|
`(tee_id, scope='full_18', gender = player.gender)` i tillegg til den
|
|
|
|
|
|
tidligere ADR-028-fiksen (alltid full_18, uavhengig av hole_config).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C -- manglende rating/kjønn feiler tydelig FØR innsetting,
|
|
|
|
|
|
ingen stille fallback (bekreftet med bruker, valgte det anbefalte
|
|
|
|
|
|
alternativet).** `matches.py` sin `add_participant` validerer, når
|
|
|
|
|
|
`config.use_handicap` er sann: (1) spilleren har et registrert kjønn --
|
|
|
|
|
|
avvist med `VALIDATION_FAILED` hvis ikke ("sett kjønn på spilleren
|
|
|
|
|
|
først"), (2) valgt utslag har faktisk en `tee_rating` for NØYAKTIG det
|
|
|
|
|
|
kjønnet -- avvist med `VALIDATION_FAILED` hvis ikke ("dette utslaget har
|
|
|
|
|
|
ingen dame/herre-rating -- velg et annet utslag"). Samme "fail loudly"-
|
|
|
|
|
|
prinsipp som ADR-019 sin importvalidering og den eksisterende
|
|
|
|
|
|
handicap_index-sjekken (2026-07-18) rett ved siden av. `_remap_course`
|
|
|
|
|
|
(bane-bytte midt i en økt) fikk samme presisering: matcher fortsatt på
|
|
|
|
|
|
tee-navn, men sjekker nå at den NYE tee-en har en rating for hver berørte
|
|
|
|
|
|
spillers kjønn, ikke bare at navnet finnes.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens for import/opprettelse:** ADR-019 sin teeoff-import
|
|
|
|
|
|
(`import_official_course`) lager nå ÉN `tee`-rad per fysisk teeoff-utslag
|
|
|
|
|
|
(tidligere: to, én per kjønn), med inntil to `tee_rating`-rader under.
|
|
|
|
|
|
Manuell tee-opprettelse (`POST .../courses/{id}/tees`) redesignet
|
|
|
|
|
|
tilsvarende: `TeeCreate.ratings` er nå en liste (1-2 elementer, distinkte
|
|
|
|
|
|
kjønn) i stedet for ett flatt kjønn+rating-sett -- ingen reell
|
|
|
|
|
|
bruker-/produksjonsdata brukte dette endepunktet fra før (kun Tjøme, som
|
|
|
|
|
|
er offisielt importert), så ingen bakoverkompatibilitet var nødvendig.
|
|
|
|
|
|
|
|
|
|
|
|
**Migrasjon `014_tee_gender_to_rating.sql`:** flytter `gender` til
|
|
|
|
|
|
`tee_rating`, slår deretter sammen eksisterende kjønns-par-tee-rader til
|
|
|
|
|
|
én fysisk tee-rad per (bane, navn) -- velger laveste id som "beholder",
|
|
|
|
|
|
flytter alle `tee_rating`- og `match_participant.tee_id`-referanser dit,
|
|
|
|
|
|
sletter duplikatene. Bekreftet TRYGT å kjøre mot ekte data FØR skriving
|
|
|
|
|
|
(read-only sjekk): alle 8 eksisterende tee-rader (Tjøme) er rene m/f-par,
|
|
|
|
|
|
ingen `gender='x'`-rader, ingen gruppe med mer enn to rader.
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert grundig, flere runder:** (1) selve
|
|
|
|
|
|
fletting-migrasjonen kjørt mot syntetisk data som gjenskaper Tjøme-
|
|
|
|
|
|
mønsteret nøyaktig, inkl. én `match_participant`-rad som pekte til
|
|
|
|
|
|
DUPLIKATEN (ikke beholderen) -- bekreftet korrekt reparert til beholderens
|
|
|
|
|
|
id etterpå, og et utslag med KUN én rating (ingen duplikat) forblir
|
|
|
|
|
|
urørt. (2) Manuell tee-opprettelse: to ratinger på ett utslag, kun én
|
|
|
|
|
|
rating (simulerer "klubben har ikke slopet dette kjønnet"), duplikat
|
|
|
|
|
|
kjønn i samme innsending avvist. (3) `GET tees` viser riktig sammenslått
|
|
|
|
|
|
struktur (2 fysiske utslag, ikke 3 rader). (4) `add_participant`: kvinne
|
|
|
|
|
|
+ utslag med dame-rating lykkes; mann + utslag som KUN har dame-rating
|
|
|
|
|
|
avvist tydelig; spiller uten registrert kjønn avvist tydelig; en full
|
|
|
|
|
|
kjønnsblandet singel-match scoret korrekt end-to-end. (5) Offisiell
|
|
|
|
|
|
import kjørt mot EKTE `teeoff_api` (Borregaard Golfklubb) -- bekreftet
|
|
|
|
|
|
4 fysiske utslag importert med begge kjønnsratinger hver, ikke 8 doble
|
|
|
|
|
|
rader. (6) `_remap_course`: bane-bytte til en bane UTEN matchende
|
|
|
|
|
|
kjønnsrating avvist tydelig, bane-bytte til en bane MED matchende
|
|
|
|
|
|
rating lykket.
|
|
|
|
|
|
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Rulle ut mot ekte teecup_db + containere, oppdatere .md-status
Notere PDF-ressursene i CLAUDE.md/FEATURE_BACKLOG.md + minne
Alt er rullet ut og verifisert:
Migrasjon 014 kjørt mot ekte teecup_db: Tjømes 8 tee-rader slått sammen til 4 fysiske utslag, alle ratinger og match_participant-referanser intakte (bekreftet med en direkte spørring — 0 brutte referanser).
Begge containere redeployet (teecup_api, teecup_frontend), boot-et rent, /health/dashboard → 200, teeoff.no upåvirket.
Alle fire tilbakemeldingene dine er dermed løst og live.
PDF-ene er notert i CLAUDE.md sin "Autoritative kilder"-seksjon (leses nå ved starten av hver økt) og i FEATURE_BACKLOG.md sitt punkt om de fire nye turneringsformatene, siden det er der de trolig blir mest relevante først. Jeg la også inn en tilsvarende referanse i min egen tvers-av-økt-hukommelse, slik at jeg husker å lese dem selv i en fremstidig, separat samtale om dette prosjektet.
2026-07-20 09:06:25 +02:00
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-19.** Migrasjon 014 kjørt mot ekte
|
|
|
|
|
|
`teecup_db`, bruker bekreftet eksplisitt: Tjømes 8 tee-rader slått sammen
|
|
|
|
|
|
til 4 fysiske utslag, alle 8 `tee_rating`-rader fikk riktig `gender`,
|
|
|
|
|
|
`match_participant`-referansene forble gyldige (0 brutte fremmednøkler
|
|
|
|
|
|
etter migrasjonen, bekreftet med en direkte spørring). Begge containere
|
|
|
|
|
|
(`teecup_api`, `teecup_frontend`) bygget og redeployet, `/health`/
|
|
|
|
|
|
`/dashboard` → 200, `teeoff.no` upåvirket.
|
Update Todos
Diagnostisere HCP-bug mot ekte data (gjort)
Fiks 1: filtrer valgte spillere bort fra listen (blind draw)
Fiks 3: tallvelger for slagregistrering (scorekort)
Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug)
Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend)
Scratch-verifisere punkt 2 (fletting, import, validering, remap)
Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md
Presentere full plan og be om bekreftelse (ekte migrasjon + deploy)
Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer:
1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet.
3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19).
4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes.
2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret.
Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak?
Utrullingsplan — dette er første gang i denne runden noe rører ekte data:
Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-20 10:01:44 +02:00
|
|
|
|
## ADR-030: Dashbord-datoer utledes fra øktene, ikke et separat felt
|
|
|
|
|
|
|
|
|
|
|
|
Reist av brukeren 2026-07-19/20, direkte oppfølging av det tidligere
|
|
|
|
|
|
dokumenterte UI-hullet ("Dashboard-turneringskortet viser 'Ingen datoer
|
|
|
|
|
|
satt'", notert 2026-07-19). Brukeren viste et faktisk skjermbilde: en
|
|
|
|
|
|
turnering med en økt tydelig planlagt til "lør. 11. juli, 10:50" på
|
|
|
|
|
|
Program-fanen, mens dashbord-kortet fortsatt viste "Ingen datoer satt".
|
|
|
|
|
|
|
|
|
|
|
|
**Root cause, bekreftet ved kodegjennomgang (samme konklusjon som forrige
|
|
|
|
|
|
runde, nå faktisk fikset):** dashbord-kortet leser `tournament.start_date`/
|
|
|
|
|
|
`end_date` — et eget felt på selve turneringen (ADR-015) — som INGEN UI
|
|
|
|
|
|
noensinne har hatt en vei til å sette. Fullstendig atskilt fra
|
|
|
|
|
|
`session.scheduled_at` (som Program-fanen korrekt viser).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning: utled datospennet fra øktenes `scheduled_at` i
|
|
|
|
|
|
`list_tournaments`, ikke bygg et manuelt datofelt-skjema.** To kilder til
|
|
|
|
|
|
sannhet (et manuelt "turnering-dato"-felt OG faktiske økt-tidspunkter) ville
|
|
|
|
|
|
uunngåelig kommet ut av synk — turneringens reelle datoer ER ganske enkelt
|
|
|
|
|
|
når rundene faktisk er satt til å spilles. `GET /orgs/{id}/tournaments`
|
|
|
|
|
|
gjør nå `COALESCE(t.start_date, MIN(økt.scheduled_at))` /
|
|
|
|
|
|
`COALESCE(t.end_date, MAX(økt.scheduled_at))` — et eksplisitt satt
|
|
|
|
|
|
`start_date`/`end_date` (om det noensinne blir gitt en skrivevei senere)
|
|
|
|
|
|
vinner fortsatt over det utledede spennet, men i praksis er det alltid det
|
|
|
|
|
|
utledede spennet som vises i dag. En turnering uten noen tidsplanlagte
|
|
|
|
|
|
økter viser fortsatt riktig "Ingen datoer satt" (ikke en feil).
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** kun `list_tournaments` sin spørring endret (ikke
|
|
|
|
|
|
`_TOURNAMENT_COLUMNS`, som fortsatt brukes uendret av opprett-/
|
|
|
|
|
|
PATCH-endepunktene sine `RETURNING`-klausuler — de trenger ikke
|
|
|
|
|
|
sesjons-utledningen rett etter en skriveoperasjon). Ingen migrasjon.
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert:** turnering uten økter → `null`/`null` (ikke feil);
|
|
|
|
|
|
turnering med to økter (11./12. juli) → riktig utledet spenn; en turnering
|
|
|
|
|
|
med et EKSPLISITT satt `start_date`/`end_date` ved opprettelse beholder
|
|
|
|
|
|
fortsatt sin egen verdi selv med økter senere lagt til (bekrefter
|
|
|
|
|
|
COALESCE-prioriteringen).
|
|
|
|
|
|
|
2026-07-20 10:07:17 +02:00
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-20.** Bruker bekreftet eksplisitt, ingen
|
|
|
|
|
|
migrasjon. Verifisert mot ekte data: "De Gamle er Eldst" viser nå korrekt
|
|
|
|
|
|
11. juli 2026 (utledet fra dens økt) i stedet for "Ingen datoer satt".
|
2026-07-20 10:01:44 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
## ADR-031: Personlig landingsside for enhver registrert bruker + personlig profil
|
|
|
|
|
|
|
|
|
|
|
|
Reist av brukeren 2026-07-20, eksplisitt som en "tenk igjennom og foreslå"-
|
|
|
|
|
|
instruks, deretter et klart "gjør det" med et utvidet omfang (personlig
|
|
|
|
|
|
profil-CRUD: profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP,
|
|
|
|
|
|
hjemmeklubb).
|
|
|
|
|
|
|
|
|
|
|
|
**Bekreftet, reelt hull:** `app/page.tsx` sendte enhver innlogget bruker til
|
|
|
|
|
|
`/dashboard`, som viste "opprett organisasjon" så snart brukeren ikke var
|
|
|
|
|
|
org-medlem — også for en bruker som KUN er spiller (koblet via
|
|
|
|
|
|
`player.user_id`, ADR-017 B), aldri organisator.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — ETT samlet dashboard, ikke to atskilte ruter.** `/dashboard`
|
|
|
|
|
|
viser nå en ny "Mine runder"-seksjon øverst (turneringer brukeren er ROSTRET
|
|
|
|
|
|
i, på tvers av organisasjoner) når den finnes, med organisasjonsseksjonen
|
|
|
|
|
|
uendret under. En bruker som er BÅDE organisator og spiller ser begge deler
|
|
|
|
|
|
— ingen tvungen valg mellom to identiteter.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — personlig profil er ETT sett PER KONTO (`app_user`), atskilt
|
|
|
|
|
|
fra org-scopede `player`-rader.** Ny migrasjon `015_user_profile.sql`:
|
|
|
|
|
|
`app_user` får `first_name`/`last_name`/`birth_date`/`gender`/
|
|
|
|
|
|
`handicap_index`/`home_club`/`avatar_key`. Bevisst IKKE forsøkt slått sammen
|
|
|
|
|
|
med `player`-radene (som fortsatt er per-organisasjon, eid av organisator,
|
|
|
|
|
|
brukt til roster/handicap-snapshot) — en person kan ha flere `player`-rader
|
|
|
|
|
|
i ulike klubber (ulikt hjemmeklubb-medlemsnummer, potensielt ulik registrert
|
|
|
|
|
|
HCP per klubb i den virkelige golfverdenen), mens KONTOENS egen profil er
|
|
|
|
|
|
brukerens EGEN, selvstyrte fremstilling av seg selv — to bevisst atskilte
|
|
|
|
|
|
konsepter, ikke ett duplisert.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** `PATCH /auth/profile` (vanlig `exclude_unset`-PATCH-mønster
|
|
|
|
|
|
— et felt sendt eksplisitt som `null` sletter det, et utelatt felt endres
|
|
|
|
|
|
ikke), `POST`/`DELETE /auth/profile/avatar` (samme ekte multipart→AVIF-
|
|
|
|
|
|
opplastingsmønster som `tournament.hero_image_key`, ADR-018 MinIO-runden —
|
|
|
|
|
|
se `app/storage.py`). Ny seksjon i `/account` (`account-settings.tsx`).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — "Mine runder" krever et NYTT tverr-org-oppslag, samme
|
|
|
|
|
|
mønster som fire tidligere bygde bruksområder.** `player` er RLS-beskyttet
|
|
|
|
|
|
per organisasjon; å finne "hvilke org-er har jeg en spiller-rad i" krever
|
|
|
|
|
|
samme smale `SECURITY DEFINER`-bro som `public_tournament_org()` (007)/
|
|
|
|
|
|
`link_player_by_email()` (008)/`public_org_by_slug()` (009)/
|
|
|
|
|
|
`public_tournament_by_code()` (011) — ny `player_organizations_for_user()`
|
|
|
|
|
|
(migrasjon 015), eksponerer KUN en uuid-liste. `/auth/me` utvidet med
|
|
|
|
|
|
`my_tournaments` (samme N+1-over-`org_connection()`-mønster som allerede
|
|
|
|
|
|
brukes for `organizations`).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — ny sikkerhetsutvidelse funnet UNDER design, ikke antatt på
|
|
|
|
|
|
forhånd: en faktisk deltaker skal aldri stenges ute av sin EGEN turnering,
|
|
|
|
|
|
uansett synlighetsnivå.** Under bygging av "Mine runder" ble det klart at
|
|
|
|
|
|
`check_visibility()` (ADR-018 Beslutning C) kun ga deltaker-tilgang for
|
|
|
|
|
|
`visibility='participants'` — IKKE for `'org'` (som er DEFAULT for enhver
|
|
|
|
|
|
NY turnering). En ren spiller (rostret, men uten organisasjonsmedlemskap)
|
|
|
|
|
|
ville dermed vært stengt ute fra sin egen, helt vanlige turnering (default
|
|
|
|
|
|
`'org'`-synlighet) — nøyaktig den brukergruppen "Mine runder" er bygget
|
|
|
|
|
|
for. Utvidet: deltaker-sjekken gjelder nå for BEGGE ikke-offentlige tiere,
|
|
|
|
|
|
ikke bare `'participants'`. Begrunnelse: `visibility` styrer eksponering
|
|
|
|
|
|
mot UTENFORSTÅENDE, aldri mot folk som faktisk spiller i turneringen — det
|
|
|
|
|
|
finnes intet scenario der en organisator ønsker å skjule en turnering for
|
|
|
|
|
|
sin EGEN spiller. Verifisert presist: en faktisk deltaker (ikke org-medlem)
|
|
|
|
|
|
FÅR nå tilgang til en `'org'`-synlig turnering, mens en helt ubeslektet
|
|
|
|
|
|
FREMMED (innlogget, men ikke deltaker) og en ANONYM leser fortsatt begge
|
|
|
|
|
|
avvises som før (403 `NOT_VISIBLE`) — ren utvidelse, ingen innstramming.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst UTENFOR omfang denne runden, kjent gjenstående begrensning:**
|
|
|
|
|
|
"Mine runder" lenker til den offentlige turnering-siden (`/t/{id}`), IKKE
|
|
|
|
|
|
til lagets private chat eller det organisator-vendte scorekortet — disse
|
|
|
|
|
|
krever fortsatt `get_authorized_org` (ekte organisasjonsmedlemskap), en
|
|
|
|
|
|
strengere sperre enn deltaker-status alene, brukt av dusinvis av
|
|
|
|
|
|
endepunkter på tvers av hele appen. Å utvide DENNE sperren trygt til også å
|
|
|
|
|
|
godta "faktisk deltaker" er en egen, større og mer risikofylt endring
|
|
|
|
|
|
(påvirker autorisasjonsarkitekturen bredt) — bevisst IKKE gjort i denne
|
|
|
|
|
|
runden, notert i FEATURE_BACKLOG.md som naturlig neste steg.
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert, 15 sjekker:** profil-CRUD (sett alle felt, delvis
|
|
|
|
|
|
PATCH lar andre felt stå urørt, eksplisitt `null` sletter et felt, tomt
|
|
|
|
|
|
PATCH avvist, avatar lastet opp med ekte AVIF-URL, avatar slettet, ugyldig
|
|
|
|
|
|
filtype avvist), "Mine runder" for en EKTE ren spiller (null organisasjons-
|
|
|
|
|
|
medlemskap, men `my_tournaments` viser riktig turnering+lag), OG den
|
|
|
|
|
|
kritiske sikkerhetssjekken: samme rene spiller FÅR nå se sin `'org'`-
|
|
|
|
|
|
synlige turnering, mens en fremmed innlogget bruker og en anonym begge
|
|
|
|
|
|
fortsatt avvises. Ekte typesjekket produksjonsbuild kjørt og bekreftet.
|
|
|
|
|
|
|
2026-07-20 10:50:42 +02:00
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-20.** Migrasjon 015 kjørt mot ekte
|
|
|
|
|
|
`teecup_db`, bruker bekreftet eksplisitt. Begge containere redeployet,
|
|
|
|
|
|
`/health`/`/dashboard`/`/account` → 200, `teeoff.no` upåvirket. Se
|
2026-08-03 06:24:57 +02:00
|
|
|
|
CHANGELOG.md for full byggerunde.
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-20 11:32:10 +02:00
|
|
|
|
## ADR-032: Verifisert e-postbytte + mobil på personlig profil
|
|
|
|
|
|
|
|
|
|
|
|
Reist av brukeren 2026-07-20, samme dag og rett etter ADR-031: "identifikatoren"
|
|
|
|
|
|
(e-post) manglet i den nye personlige profilen, og mobil (med landsnummer)
|
|
|
|
|
|
burde være en opsjon.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — mobil er et rent, enkelt tillegg til `ProfileUpdate`
|
|
|
|
|
|
(samme PATCH som resten av profilen), splittet i to felt.**
|
|
|
|
|
|
`mobile_country_code` (f.eks. `"+47"`) og `mobile_number` er separate
|
|
|
|
|
|
kolonner på `app_user` (migrasjon `016_profile_contact.sql`) — ikke én
|
|
|
|
|
|
sammensatt streng — slik at frontend kan tilby en egen landsnummer-
|
|
|
|
|
|
velger uten å måtte parse en fritekststreng i etterkant.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — e-post er IKKE en del av den vanlige profil-PATCH-en, og
|
|
|
|
|
|
kan det aldri bli.** E-post er innloggings-identifikatoren (magic-link-
|
|
|
|
|
|
mål) — en enkel PATCH (som de andre feltene) ville latt en skrivefeil
|
|
|
|
|
|
ELLER en kapret sesjon stjele kontoen for godt, ingen verifisering av at
|
|
|
|
|
|
den NYE adressen faktisk eies av noen. Løst med et eget, to-stegs
|
|
|
|
|
|
bekreftelsesløp, samme `token_hash`+`expires_at`+`consumed_at`-mønster
|
|
|
|
|
|
som `magic_link_token` (004), gjenbrukt for et nytt formål: ny tabell
|
|
|
|
|
|
`email_change_token` (bruker_id, ny e-post, token-hash, utløp).
|
|
|
|
|
|
`POST /auth/profile/email` (krever gyldig sesjon — du må bevise at du
|
|
|
|
|
|
ER kontoen i dag) genererer tokenet og sender en bekreftelseslenke til
|
|
|
|
|
|
DEN NYE adressen (ikke den gamle — beviser eierskap av MÅLET, ikke bare
|
|
|
|
|
|
at avsenderen fortsatt er innlogget). `POST /auth/profile/email/confirm`
|
|
|
|
|
|
(ingen sesjon påkrevd — samme mønster som selve magic-link-verifiseringen,
|
|
|
|
|
|
siden lenken kan åpnes på en annen enhet/nettleser enn den som ba om
|
|
|
|
|
|
byttet) forbruker tokenet atomisk og gjennomfører selve byttet. E-posten
|
|
|
|
|
|
endres IKKE før lenken faktisk åpnes.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** duplikat-sjekk (er den ønskede adressen allerede en annen
|
|
|
|
|
|
kontos?) gjøres TO ganger — én gang ved forespørsel (rask
|
|
|
|
|
|
tilbakemelding), én gang igjen rett før selve `UPDATE`-en ved bekreftelse
|
|
|
|
|
|
(kan ha blitt tatt av noen andre i mellomtiden) — pluss den eksisterende
|
|
|
|
|
|
unike indeksen (`app_user_email_unique`, migrasjon 004) som siste
|
|
|
|
|
|
bakstopper via `translate_db_errors()`.
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert, 10 sjekker:** mobil satt via vanlig PATCH; vanlig
|
|
|
|
|
|
profil-PATCH endrer aldri e-post; bytte til en allerede brukt adresse
|
|
|
|
|
|
avvist (409); e-post FORBLIR uendret helt til lenken bekreftes; ugyldig
|
|
|
|
|
|
bekreftelseskode avvist; gyldig kode fullfører byttet; SAMME kode kan
|
|
|
|
|
|
ikke brukes to ganger; en helt ny innlogging med den GAMLE adressen
|
|
|
|
|
|
oppretter nå en fersk, tom konto (beviser byttet er reelt og fullstendig,
|
|
|
|
|
|
ikke kosmetisk). Ekte typesjekket produksjonsbuild kjørt og bekreftet
|
|
|
|
|
|
(ny `/verify-email`-rute listet).
|
|
|
|
|
|
|
|
|
|
|
|
**Status: ✅ BYGGET OG LIVE 2026-07-20.** Migrasjon 016 kjørt mot ekte
|
|
|
|
|
|
`teecup_db`, bruker bekreftet eksplisitt. Begge containere redeployet,
|
|
|
|
|
|
`/health`/`/dashboard`/`/account`/`/verify-email` → 200, `teeoff.no`
|
|
|
|
|
|
upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
ADR-033 er nå fullt kildebelagt. Kort oppsummert hva som endret seg:
Banedata: bekreftet — live oppslag mot teeoff, ikke import.
HCP: leste hele den offisielle WHS Rules of Handicapping 2024 (Rule 2, 3, 5, 6). To viktige presiseringer av det du selv antok:
«Minst 9 hull» stemmer ikke helt — en 9-hulls-runde må ha alle 9 hull i et faktisk ratet sett (front eller back), mens en 18-hulls-runde bare trenger minst 10 av 18 (resten fylles med en «expected score», ny metode fra 2024 som erstattet den gamle «net par»-metoden). Dette betyr også at fritt starthull (Beslutning E) fungerer fint for vanlig logging, men en runde blir kun HCP-tellende hvis de spilte hullene faktisk samsvarer med banens ratede 18/front-9/back-9 — en vilkårlig 9-hulls-strekning har ingen egen rating.
Fant en presis, tidligere ukjent detalj: en 9-hulls Course Handicap halverer indeksen først (Index÷2 × Slope/113 + (Rating−Par)) — helt annen formel enn 18-hulls-varianten. Viktig: dette gjelder frittstående 9-hulls-runder, ikke det eksisterende front_9/back_9-øktoppsettet i turneringsflyten, som løser et annet problem (slagfordeling internt i en match) og bevisst skal la det være.
Full kjede (Score Differential, Net Double Bogey, beste-8-av-20, Low Handicap Index, soft/hard cap, Course/Playing Handicap) er nå skrevet inn i ADR-033 med eksakte tall fra kilden. PCC og Exceptional-Score-justering er bevisst avgrenset ut av v1 (forstått, men krever data på tvers av alle brukeres runder samme dag — egen, senere runde).
2026-07-22 09:26:08 +02:00
|
|
|
|
## ADR-033: Frittstående rundeføring med detaljert statistikk
|
|
|
|
|
|
|
|
|
|
|
|
Reist av brukeren 2026-07-21 (se FEATURE_BACKLOG.md), utdypet 2026-07-22 med
|
|
|
|
|
|
konkrete statistikk-felt og en eksplisitt ambisjon: **dette skal bli
|
|
|
|
|
|
appens hovedfokus** — når rundeføring med detaljert statistikk er på
|
|
|
|
|
|
plass, skal turneringsoppsett deretter gjøres ekstremt enkelt. Dette er
|
|
|
|
|
|
den største enkeltbeslutningen i prosjektet siden ADR-001, fordi den
|
|
|
|
|
|
direkte utfordrer tenant-invarianten ("organisasjon er isolasjonsenheten")
|
|
|
|
|
|
CLAUDE.md hittil har krevd en ny ADR for å bryte.
|
|
|
|
|
|
|
|
|
|
|
|
Fire load-bærende delbeslutninger ble avklart eksplisitt med bruker
|
|
|
|
|
|
(AskUserQuestion) FØR resten av denne ADR-en ble skrevet. De tre
|
|
|
|
|
|
HCP-PDF-ene (se referanse i CLAUDE.md) ble lest i sin helhet som
|
|
|
|
|
|
forberedelse til Beslutning F/G, i tråd med prosjektets stående instruks
|
|
|
|
|
|
om å ikke anta HCP-regler fra hukommelse.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning A — Eierskapsmønster: nytt, parallelt, IKKE en skjult organisasjon
|
|
|
|
|
|
|
|
|
|
|
|
En frittstående runde eies av en BRUKER (`app_user.id`), ikke en
|
|
|
|
|
|
organisasjon. Ingen ny RLS-policy-familie trengs for dette — prosjektet
|
|
|
|
|
|
har ALLEREDE et etablert, bevist mønster for nøyaktig denne typen data:
|
|
|
|
|
|
personlig profil (migrasjon 015), sekundær e-post (017) og
|
|
|
|
|
|
HCP-historikk (018) bruker alle `plain_connection()` (ingen
|
|
|
|
|
|
`app.current_org` satt) med eksplisitt `WHERE user_id = $1`-filtrering i
|
|
|
|
|
|
hver spørring, i stedet for RLS. Frittstående runder gjenbruker dette
|
|
|
|
|
|
mønsteret uendret — nye tabeller (`round`, `round_participant`,
|
|
|
|
|
|
`round_hole_stat`, se Beslutning B) har en `owner_user_id`-kolonne, INGEN
|
|
|
|
|
|
`organization_id`, og INGEN RLS-policy — autorisasjon håndheves i
|
|
|
|
|
|
app-laget (eier ser/redigerer egne runder; en deltaker uten konto har
|
|
|
|
|
|
ingen egen tilgang, se Beslutning D).
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse for å avvise "usynlig personlig organisasjon"-alternativet:**
|
|
|
|
|
|
en skjult organisasjon måtte for alltid filtreres bort fra ENHVER
|
|
|
|
|
|
org-listing/-bytter/-medlemsside/fremtidig fakturering — en varig
|
|
|
|
|
|
lekkasjerisiko som vokser for hver ny org-scopet skjerm som bygges
|
|
|
|
|
|
fremover. Det parallelle mønsteret er ikke bare konseptuelt riktigere,
|
|
|
|
|
|
det er også ALLEREDE bygget og bevist for personlig data — dette er
|
|
|
|
|
|
mindre nytt arbeid enn først antatt i brainstorm-runden.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning B — Statistikk-datamodell: fast sett navngitte felt, ikke fri slag-for-slag-logg
|
|
|
|
|
|
|
|
|
|
|
|
Per hull, i tillegg til slagtall (som i dag): kølle brukt ved utslag,
|
|
|
|
|
|
utslags-resultat (fairway/høyre/venstre — kun relevant på par 4/5),
|
|
|
|
|
|
innspill-resultat (traff/lang/kort/høyre/venstre), antall putter, lengde
|
|
|
|
|
|
på første putt, antall chip, antall bunkerslag, antall straffeslag.
|
|
|
|
|
|
|
|
|
|
|
|
**Presis definisjon av "innspillsslaget"** (nødvendig for at GIR skal
|
|
|
|
|
|
kunne beregnes automatisk, uavhengig av hullets par): SISTE slag før
|
|
|
|
|
|
første putt. På en par 3 er dette utslaget selv; på en par 4 normalt
|
|
|
|
|
|
2. slag; på en par 5 kan det være 2. ELLER 3. slag (f.eks. ved en
|
|
|
|
|
|
layup) — modellen trenger ikke vite hvilket slagnummer det var, kun at
|
|
|
|
|
|
det faktisk er det siste FØR putting startet.
|
|
|
|
|
|
|
|
|
|
|
|
**GIR er DERIVERT, ikke tastet inn direkte:** Green In Regulation = sant
|
|
|
|
|
|
hvis innspill-resultat er "traff" OG antall slag brukt til da er ≤
|
|
|
|
|
|
(hullets par − 2). Utslag-resultat og innspill-resultat er derimot
|
|
|
|
|
|
OBSERVERTE felt spilleren selv taster inn — appen har ingen GPS og kan
|
|
|
|
|
|
ikke oppdage dette selv.
|
|
|
|
|
|
|
|
|
|
|
|
**UX-prinsipp, ikke bare et skjema-valg:** ALLE detalj-felt er valgfrie
|
|
|
|
|
|
per hull. Rask "bare slagtall"-registrering skal alltid fungere uendret
|
|
|
|
|
|
— dette er bevisst for å unngå at "hovedfokus" i praksis blir for
|
|
|
|
|
|
tungvint til daglig bruk (samme klasse avveining som gjorde at
|
|
|
|
|
|
detaljerte felt ble utsatt fra scorekort-rundene tidligere i prosjektet).
|
|
|
|
|
|
|
|
|
|
|
|
**Avvist:** fri slag-for-slag-logging (hvert slag = egen rad). Ville
|
|
|
|
|
|
dekket samme behov, men med vesentlig tyngre registrering og et mer
|
|
|
|
|
|
komplekst skjema fra dag én, uten at brukerens beskrevne behov faktisk
|
|
|
|
|
|
krever det.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning C — Banedata for frittstående runder: LIVE oppslag mot teeoff, bekreftet av bruker
|
|
|
|
|
|
|
|
|
|
|
|
Custom-baner er i dag org-scopet (`course.organization_id`) — dette
|
|
|
|
|
|
fungerer ikke uten organisasjon. **Bekreftet med bruker 2026-07-22:**
|
|
|
|
|
|
offisielle baner slås opp LIVE mot teeoff sitt API ved behov (samme
|
|
|
|
|
|
lesende API som ADR-019, IKKE en importert/kopiert kopi som i
|
|
|
|
|
|
turnering-flyten). ADR-019 sin "importer, ikke slå opp live"-beslutning
|
|
|
|
|
|
var begrunnet i turneringers behov for reproduserbarhet (et resultat skal
|
|
|
|
|
|
ikke endre seg retroaktivt hvis banedata oppdateres) — en frittstående
|
|
|
|
|
|
statistikk-runde har ikke samme behov, og live oppslag unngår i tillegg
|
|
|
|
|
|
dagens duplisering (hver org som importerer "Tjøme Golfklubb" i dag lager
|
|
|
|
|
|
sin egen kopi). Egendefinerte baner som ikke finnes i teeoff: global
|
|
|
|
|
|
banekatalog uten organisasjonstilknytning, søk-før-opprett for å begrense
|
|
|
|
|
|
duplikater (denne delen — selve den globale katalogen for CUSTOM baner —
|
|
|
|
|
|
er fortsatt kun min anbefaling, ikke eksplisitt bekreftet punkt for
|
|
|
|
|
|
punkt, men følger naturlig av at live-oppslag-beslutningen er tatt).
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning D — Deltakere i flighten uten TeeCup-konto: gjenbruk eksisterende mønster
|
|
|
|
|
|
|
|
|
|
|
|
`round_participant` får en nullable `user_id` (ekte TeeCup-bruker som
|
|
|
|
|
|
spiller med) og et `guest_name`-fritekstfelt (ingen konto). Samme
|
|
|
|
|
|
konsept som `player` uten `user_id` i org-sammenheng. E-post-basert
|
|
|
|
|
|
kobling i etterkant (samme mønster som `link_player_by_email`,
|
|
|
|
|
|
migrasjon 008) er en naturlig, men IKKE besluttet, senere utvidelse —
|
|
|
|
|
|
notert som åpent punkt under, ikke bygget i første omgang.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning E — Hullantall og starthull: ingen tvang, kun standardvalg
|
|
|
|
|
|
|
|
|
|
|
|
18/første ni/siste ni er standardvalg i UI-et, ikke en database-
|
|
|
|
|
|
begrensning. Spilleren velger starthull fritt (gjenbruk av samme
|
|
|
|
|
|
`start_hole`-konsept som `session.start_hole`, ADR-015). En runde kan
|
|
|
|
|
|
avsluttes etter et hvilket som helst antall hull uten et forhånds-
|
|
|
|
|
|
deklarert mål — statistikk og par-sum regnes alltid fra hullene FAKTISK
|
|
|
|
|
|
spilt.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning F — HCP-tellende runde: presist kildebelagt fra WHS Rules of Handicapping 2024
|
|
|
|
|
|
|
|
|
|
|
|
Brukeren lastet opp den offisielle kilden 2026-07-22 ("WHS Rules of
|
|
|
|
|
|
Handicapping 2024", USGA/R&A — IKKE en tredjeparts-blogg som de to andre
|
|
|
|
|
|
PDF-ene). Dette erstatter den tidligere, mer omtrentlige "minst 9
|
|
|
|
|
|
hull"-antakelsen med presise regler (**Rule 2.2**):
|
|
|
|
|
|
|
|
|
|
|
|
- **18-hulls-type runde:** minimum **10 av 18 hull** må spilles for at
|
|
|
|
|
|
scoren skal være akseptabel. De resterende (inntil 8) uspilte hullene
|
|
|
|
|
|
fylles med en "expected score" (se under), IKKE net par (net par-
|
|
|
|
|
|
metoden ble erstattet av expected-score-metoden i 2024-revisjonen —
|
|
|
|
|
|
et prinsipielt skifte fra tidligere WHS-versjoner, verdt å merke seg
|
|
|
|
|
|
siden eldre kilder/hukommelse fortsatt kan referere net par).
|
|
|
|
|
|
- **9-hulls-type runde:** ALLE 9 hull i det spesifikke, ratede 9-hulls-
|
|
|
|
|
|
settet (front ELLER back, de eneste to som normalt har egen Course/
|
|
|
|
|
|
Slope Rating) må spilles. Færre enn 9 hull totalt → scoren er IKKE
|
|
|
|
|
|
akseptabel for HCP-formål i det hele tatt, uansett grunn.
|
|
|
|
|
|
- **Konsekvens for Beslutning E (fritt valgt starthull):** den frie
|
|
|
|
|
|
starthull-friheten gjelder fullt ut for CASUAL, ikke-HCP-tellende
|
|
|
|
|
|
logging. For at en runde skal telle mot HCP, må de spilte hullene
|
|
|
|
|
|
derimot samsvare med enten (a) et sammenhengende 10-18-hulls-utsnitt av
|
|
|
|
|
|
banens 18-hulls-rating, eller (b) nøyaktig banens ratede front-9 eller
|
|
|
|
|
|
back-9 — en vilkårlig 9-hulls-strekning (f.eks. hull 5-13) har ingen
|
|
|
|
|
|
egen Course/Slope Rating og kan derfor aldri bli HCP-tellende. Dette må
|
|
|
|
|
|
kommuniseres tydelig i UI-et, ikke bare håndheves stille i motoren.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning G — HCP-indeksberegning bygges NÅ, presist kildebelagt (WHS Rules of Handicapping 2024)
|
|
|
|
|
|
|
|
|
|
|
|
Full kjede, alle tall/formler hentet direkte fra kilden (Rule 3/5/6),
|
|
|
|
|
|
ikke hukommelse:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Net Double Bogey** (maks hull-score for HCP-formål) = hullets par +
|
|
|
|
|
|
2 + spillerens handicapslag på det hullet (Rule 3.1b) — allerede
|
|
|
|
|
|
dekket av eksisterende `allocate_strokes_by_index`/
|
|
|
|
|
|
`allocate_over_played_holes`, ingen endring.
|
Første byggesteg ferdig: HCP-indeks-motoren.
Bygget og testet i handicap_engine.py — ren Python, ingen database/API rørt ennå (samme "test i isolasjon først"-prinsipp som resten av motoren). 9 nye funksjoner dekker hele kjeden fra rå hullscore til en ferdig, cap-justert HCP-indeks: Net Par/Net Double Bogey, Adjusted Gross Score, Score Differential, Handicap Index (beste-8-av-20 med opptrappingstabell for nye spillere), Low Handicap Index, soft/hard cap, og den avvikende 9-hulls Course Handicap-formelen.
41/41 tester bestått, flere verifisert direkte mot regelbokens egne tallregneeksempler (ikke bare intern konsistens) — blant annet begge Rule 5.2a-eksemplene (13,2 / 34,1 / 37,4), alle tre Rule 5.1c-avrundingseksemplene, og soft/hard cap-oppførselen fra Diagram 5.8. For Diagram 3.1b (Net Double Bogey-eksempelet) var jeg åpen om en reell begrensning: front-9 stemte eksakt mot bildet, men jeg kunne ikke garantere hvert enkelt siffer i back-9-scorerekken fra bilde-oppløsningen — testen bruker derfor kun det ene tydelig annoterte, kildebelagte tallet (hull 17: brutto 9, capped til 7) og egenkomponerte tall for resten, dokumentert i kommentaren.
Dokumentasjonen (ADR-033, CLAUDE.md, FEATURE_BACKLOG.md) er oppdatert til å reflektere at bygging er påbegynt.
2026-07-22 10:28:01 +02:00
|
|
|
|
2. **Uspilte hull — LØST 2026-07-22 med et bevisst, kildebelagt avvik
|
|
|
|
|
|
fra "Expected Score":** WHS sin offisielle "Expected Score"-mekanisme
|
|
|
|
|
|
(Rule 3.2b) er eksplisitt beskrevet som automatisk beregnet av
|
|
|
|
|
|
sertifisert WHS-programvare, UTEN at selve formelen er publisert i
|
|
|
|
|
|
regelboken (samme mønster som PCC) — kan derfor ikke bygges presist.
|
|
|
|
|
|
**Brukeren instruerte eksplisitt** å bruke WHS sin egen, presist
|
|
|
|
|
|
DEFINERTE "Net Par"-term i stedet (Rule 3.2b/2 — normalt reservert for
|
|
|
|
|
|
spesielle godkjente tilfeller, men her vedtatt som TeeCups generelle
|
|
|
|
|
|
policy): for hvert uspilt hull antas spilleren å ha skåret sin Net Par
|
|
|
|
|
|
= hullets par + mottatte handicapslag på det hullet (samme formel som
|
|
|
|
|
|
Net Double Bogey, uten +2-leddet) — tilsvarer 2 Stableford-poeng per
|
|
|
|
|
|
uspilt hull. Summeres inn i Adjusted Gross Score FØR standard 18-hulls
|
|
|
|
|
|
Score Differential-formelen brukes. **Viktig konsekvens:** dette gjør
|
|
|
|
|
|
at en 9-hulls-runde nå KAN telle fullt mot HCP-indeksen (de resterende
|
|
|
|
|
|
9 hullene fylles med Net Par, hele runden går gjennom SAMME 18-hulls-
|
|
|
|
|
|
formel) — det tidligere spørsmålet om en egen 9-hulls-differensial-
|
|
|
|
|
|
formel (Rule 5.1b) er dermed ikke lenger nødvendig å bygge separat.
|
|
|
|
|
|
Fortsatt gyldig kun når minimumsantallet (Beslutning F) er oppfylt.
|
ADR-033 er nå fullt kildebelagt. Kort oppsummert hva som endret seg:
Banedata: bekreftet — live oppslag mot teeoff, ikke import.
HCP: leste hele den offisielle WHS Rules of Handicapping 2024 (Rule 2, 3, 5, 6). To viktige presiseringer av det du selv antok:
«Minst 9 hull» stemmer ikke helt — en 9-hulls-runde må ha alle 9 hull i et faktisk ratet sett (front eller back), mens en 18-hulls-runde bare trenger minst 10 av 18 (resten fylles med en «expected score», ny metode fra 2024 som erstattet den gamle «net par»-metoden). Dette betyr også at fritt starthull (Beslutning E) fungerer fint for vanlig logging, men en runde blir kun HCP-tellende hvis de spilte hullene faktisk samsvarer med banens ratede 18/front-9/back-9 — en vilkårlig 9-hulls-strekning har ingen egen rating.
Fant en presis, tidligere ukjent detalj: en 9-hulls Course Handicap halverer indeksen først (Index÷2 × Slope/113 + (Rating−Par)) — helt annen formel enn 18-hulls-varianten. Viktig: dette gjelder frittstående 9-hulls-runder, ikke det eksisterende front_9/back_9-øktoppsettet i turneringsflyten, som løser et annet problem (slagfordeling internt i en match) og bevisst skal la det være.
Full kjede (Score Differential, Net Double Bogey, beste-8-av-20, Low Handicap Index, soft/hard cap, Course/Playing Handicap) er nå skrevet inn i ADR-033 med eksakte tall fra kilden. PCC og Exceptional-Score-justering er bevisst avgrenset ut av v1 (forstått, men krever data på tvers av alle brukeres runder samme dag — egen, senere runde).
2026-07-22 09:26:08 +02:00
|
|
|
|
3. **Ikke fullført hull (spilleren plukker opp)** (Rule 3.3): laveste av
|
|
|
|
|
|
"most likely score" (allerede tatte slag + sannsynlig antall til
|
|
|
|
|
|
fullføring, tabell basert på ballens avstand fra hullet + eventuelle
|
|
|
|
|
|
straffeslag) eller net double bogey.
|
|
|
|
|
|
4. **18-hulls Score Differential** (Rule 5.1a) = `(113 ÷ Slope Rating) ×
|
|
|
|
|
|
(Adjusted Gross Score − Course Rating − PCC-justering)`, avrundet til
|
|
|
|
|
|
nærmeste tidel (,5 rundes opp).
|
Første byggesteg ferdig: HCP-indeks-motoren.
Bygget og testet i handicap_engine.py — ren Python, ingen database/API rørt ennå (samme "test i isolasjon først"-prinsipp som resten av motoren). 9 nye funksjoner dekker hele kjeden fra rå hullscore til en ferdig, cap-justert HCP-indeks: Net Par/Net Double Bogey, Adjusted Gross Score, Score Differential, Handicap Index (beste-8-av-20 med opptrappingstabell for nye spillere), Low Handicap Index, soft/hard cap, og den avvikende 9-hulls Course Handicap-formelen.
41/41 tester bestått, flere verifisert direkte mot regelbokens egne tallregneeksempler (ikke bare intern konsistens) — blant annet begge Rule 5.2a-eksemplene (13,2 / 34,1 / 37,4), alle tre Rule 5.1c-avrundingseksemplene, og soft/hard cap-oppførselen fra Diagram 5.8. For Diagram 3.1b (Net Double Bogey-eksempelet) var jeg åpen om en reell begrensning: front-9 stemte eksakt mot bildet, men jeg kunne ikke garantere hvert enkelt siffer i back-9-scorerekken fra bilde-oppløsningen — testen bruker derfor kun det ene tydelig annoterte, kildebelagte tallet (hull 17: brutto 9, capped til 7) og egenkomponerte tall for resten, dokumentert i kommentaren.
Dokumentasjonen (ADR-033, CLAUDE.md, FEATURE_BACKLOG.md) er oppdatert til å reflektere at bygging er påbegynt.
2026-07-22 10:28:01 +02:00
|
|
|
|
5. **(Rule 5.1b, WHS sin egen 9-hulls-differensial-formel) — IKKE brukt.**
|
|
|
|
|
|
Erstattet av Net-Par-tilnærmingen i punkt 2: en 9-hulls-runde regnes nå
|
|
|
|
|
|
som en 18-hulls-runde med 9 Net-Par-fylte hull, gjennom SAMME formel
|
|
|
|
|
|
som punkt 4. Nevnt her kun for å dokumentere at det bevisst er valgt
|
|
|
|
|
|
bort, ikke oversett.
|
ADR-033 er nå fullt kildebelagt. Kort oppsummert hva som endret seg:
Banedata: bekreftet — live oppslag mot teeoff, ikke import.
HCP: leste hele den offisielle WHS Rules of Handicapping 2024 (Rule 2, 3, 5, 6). To viktige presiseringer av det du selv antok:
«Minst 9 hull» stemmer ikke helt — en 9-hulls-runde må ha alle 9 hull i et faktisk ratet sett (front eller back), mens en 18-hulls-runde bare trenger minst 10 av 18 (resten fylles med en «expected score», ny metode fra 2024 som erstattet den gamle «net par»-metoden). Dette betyr også at fritt starthull (Beslutning E) fungerer fint for vanlig logging, men en runde blir kun HCP-tellende hvis de spilte hullene faktisk samsvarer med banens ratede 18/front-9/back-9 — en vilkårlig 9-hulls-strekning har ingen egen rating.
Fant en presis, tidligere ukjent detalj: en 9-hulls Course Handicap halverer indeksen først (Index÷2 × Slope/113 + (Rating−Par)) — helt annen formel enn 18-hulls-varianten. Viktig: dette gjelder frittstående 9-hulls-runder, ikke det eksisterende front_9/back_9-øktoppsettet i turneringsflyten, som løser et annet problem (slagfordeling internt i en match) og bevisst skal la det være.
Full kjede (Score Differential, Net Double Bogey, beste-8-av-20, Low Handicap Index, soft/hard cap, Course/Playing Handicap) er nå skrevet inn i ADR-033 med eksakte tall fra kilden. PCC og Exceptional-Score-justering er bevisst avgrenset ut av v1 (forstått, men krever data på tvers av alle brukeres runder samme dag — egen, senere runde).
2026-07-22 09:26:08 +02:00
|
|
|
|
6. **Handicap Index** (Rule 5.2) = gjennomsnitt av de beste 8 av de siste
|
|
|
|
|
|
20 Score Differentials, avrundet til nærmeste tidel. For færre enn 20
|
|
|
|
|
|
runder i historikken brukes en egen opptrappingstabell (f.eks. 3
|
|
|
|
|
|
runder → laveste 1 med justering −2,0; 9-11 runder → snitt av laveste
|
|
|
|
|
|
3, ingen justering; osv. — full tabell i Rule 5.2a, IKKE en enkel
|
|
|
|
|
|
"gjennomsnitt av alt"-tilnærming for nye spillere).
|
|
|
|
|
|
7. **Low Handicap Index** (Rule 5.7): laveste indeks siste 365 dager —
|
|
|
|
|
|
nødvendig referansepunkt for cap-mekanismen under, må lagres/spores
|
|
|
|
|
|
per bruker.
|
|
|
|
|
|
8. **Soft cap / hard cap** (Rule 5.8): øker en oppdatert indeks mer enn
|
|
|
|
|
|
3,0 slag over Low Handicap Index, begrenses overskytende beløp til
|
|
|
|
|
|
50 %; mer enn 5,0 slag over Low Handicap Index er et absolutt tak.
|
|
|
|
|
|
Ingen nedre grense på hvor mye indeksen kan SYNKE.
|
|
|
|
|
|
9. **Maksimal indeks** (Rule 5.3) = 54,0 — samsvarer med det allerede
|
|
|
|
|
|
satte `le=54`-taket i `ProfileUpdate` fra profil-fullførings-runden
|
|
|
|
|
|
(2026-07-22), god konsistens-bekreftelse.
|
|
|
|
|
|
10. **9-hulls Course Handicap** (Rule 6.1b) — **NY, presis detalj,
|
|
|
|
|
|
AVVIKER fra 18-hulls-formelen:** `(Index ÷ 2, avrundet til nærmeste
|
|
|
|
|
|
tidel) × (9-hulls Slope ÷ 113) + (9-hulls Course Rating − 9-hulls
|
|
|
|
|
|
Par)`. Dette er IKKE det samme som å bruke full indeks mot en
|
|
|
|
|
|
9-hulls rating — indeksen halveres først. **Denne formelen gjelder
|
|
|
|
|
|
frittstående 9-hulls-RUNDER (denne ADR-en), IKKE det eksisterende
|
|
|
|
|
|
front_9/back_9-øktoppsettet i turnering-flyten** (ADR-008/2026-07-19-
|
|
|
|
|
|
fiksen, som bevisst bruker full_18-rating for HELT ANDRE grunner —
|
|
|
|
|
|
slagfordeling innad i en turneringsmatch, ikke offisiell HCP-
|
|
|
|
|
|
runde-innsending. De to må IKKE forveksles eller slås sammen uten en
|
|
|
|
|
|
egen vurdering.)
|
|
|
|
|
|
11. **18-hulls Course Handicap** (Rule 6.1a) = `Index × (Slope ÷ 113) +
|
|
|
|
|
|
(Course Rating − Par)` — allerede korrekt implementert
|
|
|
|
|
|
(`course_handicap_raw`, verifisert ved grep), ingen endring.
|
|
|
|
|
|
12. **Playing Handicap** (Rule 6.2) — uendret, allerede korrekt dekket av
|
|
|
|
|
|
eksisterende allowance-strategier (ADR-014).
|
|
|
|
|
|
|
|
|
|
|
|
**Dette er i hovedsak en HELT NY komponent i `handicap_engine.py`**
|
|
|
|
|
|
(punktene 4-9), ikke en utvidelse av det eksisterende — dagens motor tar
|
|
|
|
|
|
alltid indeksen som et KJENT input; den har aldri regnet UT en indeks fra
|
|
|
|
|
|
en historie av runder. Punkt 10 er en presisering/utvidelse av
|
|
|
|
|
|
eksisterende kode for 9-hulls-tilfellet. Holdes ren og testet isolert,
|
|
|
|
|
|
som resten av `handicap_engine.py` (ADR-005).
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst UTENFOR omfang i første byggerunde, til tross for at kilden nå
|
|
|
|
|
|
finnes** (for å holde v1 håndterbar — presist avgrenset, ikke bare
|
|
|
|
|
|
utsatt i vage vendinger):
|
|
|
|
|
|
- **Playing Conditions Calculation (PCC)** (Rule 5.6) — full prosedyre
|
|
|
|
|
|
funnet og forstått (statistisk sammenligning av dagens faktiske scorer
|
|
|
|
|
|
mot forventet, justering −1,0 til +3,0, krever minst 8 aksepterte
|
|
|
|
|
|
scorer på banen samme dag blant spillere med indeks ≤36,0). Ikke
|
|
|
|
|
|
bygget nå — krever et helt annet datagrunnlag (ALLE spilte runder på
|
|
|
|
|
|
en bane en gitt dag på tvers av ALLE brukere) enn det en enkelt
|
|
|
|
|
|
frittstående runde naturlig gir. Runder telles inn UTEN PCC-justering
|
|
|
|
|
|
til dette tas som egen, senere runde.
|
|
|
|
|
|
- **Exceptional Score-reduksjon** (Rule 5.9) — funnet og forstått (en
|
|
|
|
|
|
differensial 7,0-9,9 slag bedre enn gjeldende indeks gir automatisk
|
|
|
|
|
|
−1,0 på de siste 20 differensialene, 10,0+ gir −2,0). Ikke bygget i
|
|
|
|
|
|
v1, notert for senere presisjon.
|
|
|
|
|
|
- **Initial indeks fra færre enn 3 runder, Handicap Committee-skjønn**
|
|
|
|
|
|
(Rule 5.2a) — TeeCup har ingen "Handicap Committee"-rolle; en
|
|
|
|
|
|
forenklet, automatisk variant av opptrappingstabellen brukes i stedet,
|
|
|
|
|
|
uten menneskelig overstyring i v1.
|
|
|
|
|
|
|
|
|
|
|
|
**Åpent, ikke besluttet:** når en reell WHS-indeks kan beregnes fra
|
|
|
|
|
|
runder, skal manuell redigering av `app_user.handicap_index`
|
|
|
|
|
|
(eksisterende `PATCH /auth/profile`, ADR-031) fortsatt tillates ved
|
|
|
|
|
|
siden av (f.eks. for en spiller uten noen TeeCup-runder ennå), eller skal
|
|
|
|
|
|
feltet bli read-only/auto-beregnet så snart minst én HCP-tellende runde
|
|
|
|
|
|
finnes? Påvirker om `handicap_history` (018) skal gjenbrukes uendret
|
|
|
|
|
|
eller trenger en ny kolonne som skiller "manuelt satt" fra "beregnet fra
|
|
|
|
|
|
runde".
|
|
|
|
|
|
|
2026-07-22 10:46:10 +02:00
|
|
|
|
**Skjemaet (migrasjon `020_personal_rounds.sql`) er ✅ SKREVET OG
|
|
|
|
|
|
SCRATCH-VERIFISERT 2026-07-22,** som andre byggesteg (etter motoren).
|
|
|
|
|
|
Sju nye tabeller: `round` (header, `owner_user_id`-eid, ingen RLS),
|
|
|
|
|
|
`round_participant` (deltakere — lenket bruker ELLER gjestenavn, XOR-
|
|
|
|
|
|
håndhevet via CHECK; maks én markert eier per runde via partiell unik
|
|
|
|
|
|
indeks), `round_hole` (rating-SNAPSHOT + statistikk per deltaker per
|
|
|
|
|
|
hull — GIR er bevisst IKKE en egen kolonne, kun deriverbar ved lesing:
|
|
|
|
|
|
`approach_result='hit' AND (score-putts) <= par-2`, verifisert eksakt
|
|
|
|
|
|
mot ekte testdata), pluss fire tabeller for den globale banekatalogen
|
|
|
|
|
|
(`personal_course`/`_hole`/`_tee`/`_tee_rating` — sistnevnte BEVISST uten
|
|
|
|
|
|
`scope`-kolonne, ulikt org-tabellen, siden Net-Par-tilnærmingen gjør
|
|
|
|
|
|
9-hulls-spesifikk rating overflødig). `round`/`personal_course_*` har
|
|
|
|
|
|
INGEN RLS (Beslutning A) — autorisasjon i app-laget.
|
|
|
|
|
|
**Scratch-verifisert, 9 sjekker** (kjørt som `teecup_app_scratch`, ikke
|
|
|
|
|
|
superbruker): duplikat kjønn på samme utslag avvist, teeoff+custom-felt
|
|
|
|
|
|
samtidig avvist (CHECK), verken/begge user_id+guest_name avvist (XOR-
|
|
|
|
|
|
CHECK), to markerte eiere på samme runde avvist, duplikat hullnummer per
|
|
|
|
|
|
deltaker avvist, ugyldig approach_result-verdi avvist, kaskade-sletting
|
|
|
|
|
|
av en runde fjerner alle dens deltakere+hull men lar ANDRE runder stå
|
|
|
|
|
|
urørt. `test_isolation.sql` fortsatt 12/12 (ingen RLS-regresjon på
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
eksisterende tabeller). **Rullet ut mot ekte `teecup_db` 2026-07-22,**
|
|
|
|
|
|
bruker bekreftet eksplisitt: alle sju tabeller bekreftet opprettet,
|
|
|
|
|
|
`test_isolation.sql` fortsatt 12/12 mot ekte database. **Gjenstår:**
|
|
|
|
|
|
API-lag og frontend — ingen av disse er startet.
|
|
|
|
|
|
|
|
|
|
|
|
**API-laget er ✅ BYGGET, SCRATCH-VERIFISERT OG RULLET UT LIVE 2026-07-22,**
|
|
|
|
|
|
som tredje byggesteg. Ny `app/routers/rounds.py` (registrert i `main.py`):
|
|
|
|
|
|
`POST/GET /rounds` (opprett/list egne runder), `GET/DELETE /rounds/{id}`,
|
|
|
|
|
|
`POST/DELETE /rounds/{id}/participants` (kun gjester i v1, se moduldoc),
|
|
|
|
|
|
`PATCH /rounds/{id}/participants/{pid}/holes/{n}` (hull-for-hull-
|
|
|
|
|
|
registrering), `POST /rounds/{id}/complete` (kjører hele motor-kjeden:
|
|
|
|
|
|
Adjusted Gross Score → Score Differential → `counts_for_handicap` via
|
|
|
|
|
|
`round_counts_for_handicap`), pluss `GET/POST /personal-courses` for den
|
|
|
|
|
|
globale banekatalogen. Ny motor-funksjon lagt til underveis:
|
|
|
|
|
|
`round_counts_for_handicap(played_holes_count, holes_planned)` — to
|
|
|
|
|
|
distinkte terskler (Rule 2.2a: min 10/18 ved 18-hulls-intensjon; Rule
|
|
|
|
|
|
2.2b: ALLE 9 ved 9-hulls-intensjon, ikke "minst 9"), testet (2 nye
|
|
|
|
|
|
tester, 43/43 totalt i `handicap_engine.py`).
|
|
|
|
|
|
**Reelt hull funnet OG fikset FØR API-et kunne fullføres:**
|
|
|
|
|
|
`round_participant` manglet kolonner for selve rating-tallene (Course
|
|
|
|
|
|
Rating/Slope Rating/Par) brukt til å beregne Course Handicap — kun
|
|
|
|
|
|
`round.tee_name_snapshot` (navn) fantes, ikke tallene. Ny migrasjon
|
|
|
|
|
|
`021_round_participant_rating_snapshot.sql` (tre nye nullable kolonner)
|
|
|
|
|
|
skrevet, scratch-verifisert sammen med resten, og rullet ut.
|
|
|
|
|
|
**Scratch-verifisert grundig, 26 sjekker** (isolert scratch-rolle+MinIO+
|
|
|
|
|
|
engangs API-container): full livssyklus for en custom-bane-runde
|
|
|
|
|
|
(course_handicap_snapshot regnet riktig — Index 15/Slope 128/Rating
|
|
|
|
|
|
71.5/Par 72 → 16, verifisert for hånd), en gjest UTEN HCP (ingen
|
|
|
|
|
|
snapshot/differensial, teller aldri), 18/18 spilt → tellende med korrekt
|
|
|
|
|
|
differensial (16.3, verifisert for hånd), 9 av 18 spilt ved 18-hulls-
|
|
|
|
|
|
intensjon → IKKE tellende, 9 av 9 spilt ved 9-hulls-intensjon → TELLENDE
|
|
|
|
|
|
(Net Par fyller resten av de 18, se Beslutning G punkt 2), full
|
|
|
|
|
|
autorisasjons-isolasjon (en fremmed bruker avvist 403 fra både lesing og
|
|
|
|
|
|
hull-oppdatering, egen runde-liste tom), kan ikke fjerne eieren, slett-
|
|
|
|
|
|
runde-kaskade, ufullstendig profil avvist fra å opprette runde. **Egen,
|
|
|
|
|
|
separat verifisering av teeoff-LIVE-oppslaget** (Beslutning C) mot den
|
|
|
|
|
|
ekte kjørende `teeoff_api`-containeren (Borregaard Golfklubb, samme
|
|
|
|
|
|
anlegg som ADR-019s opprinnelige verifisering) — bekreftet at INGEN
|
|
|
|
|
|
`course`/`hole`/`tee`-rad skrives noe sted, kun et navn-snapshot
|
|
|
|
|
|
("Borregaard Golfklubb – Hovedbanen") og et rating-snapshot; en andre
|
|
|
|
|
|
deltaker lagt til samme runde utløste et FERSK, uavhengig live-oppslag
|
|
|
|
|
|
mot teeoff (ikke gjenbruk av cachet data). `test_isolation.sql` fortsatt
|
|
|
|
|
|
12/12.
|
|
|
|
|
|
**Rullet ut mot ekte systemer 2026-07-22:** migrasjon 021 kjørt mot ekte
|
|
|
|
|
|
`teecup_db` (kolonner bekreftet, `test_isolation.sql` fortsatt 12/12),
|
|
|
|
|
|
`docker compose up -d --build teecup_api` (kun backend — ingen frontend-
|
|
|
|
|
|
skjerm bygget for dette ennå), boot-et rent, `/health`/`/dashboard` → 200,
|
|
|
|
|
|
`teeoff.no` upåvirket. `frontend/next.config.mjs` sin `rewrites()` fikk
|
|
|
|
|
|
`/rounds/*` og `/personal-courses/*` lagt til proaktivt (samme lærdom som
|
|
|
|
|
|
ADR-016/medlemsside-hendelsen — enhver ny API-prefiks MÅ inn her FØR en
|
|
|
|
|
|
frontend-side bygges) — denne ENDRINGEN ligger IKKE deployet ennå (ingen
|
|
|
|
|
|
frontend-kode bruker den), tas med i neste frontend-runde.
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for frittstående rundeføring er nå live på teecup.teeoff.no. Oppsummert:
/rounds — liste over egne runder, med «Ny runde»-knapp
/rounds/new — velg bane (søk i teeoff live, eller søk/opprett egen bane), utslag (filtrert til ditt registrerte kjønn), dato, starthull, 9/18 hull
/rounds/[id] — deltaker-faner (deg + gjester), hull-for-hull-registrering med slag/putt-tallvelgere, utvidbare detaljer (kølle, utslagsretning, innspill, chip, bunker, straffeslag, putt-avstand), automatisk GIR-visning, og «Fullfør runde» som beregner HCP-differensial
Lenket fra dashbordet som «Egne runder» — bevisst adskilt fra det eksisterende «Mine runder» (turnering-deltakelse) for å unngå forveksling.
Underveis fant jeg og fikset et reelt kontraktshull: hull-PATCH-endepunktet skriver alle felt ved hvert kall, ikke bare det som sendes — jeg bekreftet dette eksplisitt i scratch (et PATCH med kun score nullstiller stille putts) og bygget derfor inn en merge-før-send i frontend-koden.
Alt er scratch-verifisert (22 sjekker + en egen test av hele teeoff-baserte oppretteflyten mot ekte teeoff_api), typesjekket med ekte produksjonsbuild, og rullet ut uten migrasjon. teeoff.no upåvirket gjennom hele runden. Status oppdatert i CLAUDE.md, FEATURE_BACKLOG.md og ADR-033.
2026-07-23 06:06:41 +02:00
|
|
|
|
**Frontend BYGGET OG RULLET UT 2026-07-23** — fjerde og siste lag
|
|
|
|
|
|
(engine → skjema → API → frontend). Tre nødvendige tillegg til
|
|
|
|
|
|
`app/routers/rounds.py` funnet og bygget UNDER frontend-designet, ikke
|
|
|
|
|
|
antatt på forhånd: `GET /rounds/official-search`/`{slug}` (samme
|
|
|
|
|
|
teeoff-søkemønster som `courses.py`, men uten org-kontekst — frittstående
|
|
|
|
|
|
runder har ingen), `GET /personal-courses/{id}` (detalj med
|
|
|
|
|
|
utslag+kjønn — søk-endepunktet returnerte kun navn), og
|
|
|
|
|
|
`GET .../participants/{id}/holes` (et reelt hull: `RoundOut` bar aldri
|
|
|
|
|
|
hull-nivå-data, så ingen skjerm kunne vise gjeldende tilstand ved
|
|
|
|
|
|
gjenlasting). Hull-PATCH endret til å returnere hele den oppdaterte raden
|
|
|
|
|
|
i stedet for `{"ok": true}`.
|
|
|
|
|
|
**Reelt kontraktsfunn, bekreftet i scratch FØR frontend stolte på det:**
|
|
|
|
|
|
hull-PATCH-endepunktet er IKKE et ekte delvis-PATCH — det skriver ALLE
|
|
|
|
|
|
felt ved hvert kall (arvet fra hvordan `HoleUpdate`-modellen alltid har
|
|
|
|
|
|
defaultverdier for utelatte felt). Et PATCH som kun sender `score` ville
|
|
|
|
|
|
derfor stille NULLSTILT `putts` og alle andre allerede lagrede felt.
|
|
|
|
|
|
Løst ved at `round-detail.tsx` alltid slår sammen med gjeldende
|
|
|
|
|
|
hull-data før hver PATCH, aldri sender et isolert feltnavn alene —
|
|
|
|
|
|
verifisert eksplisitt i en egen scratch-test som FØRST beviste
|
|
|
|
|
|
nullstillings-oppførselen uten merge, DERETTER beviste at
|
|
|
|
|
|
merge-mønsteret unngår den.
|
|
|
|
|
|
**Nye sider:** `/rounds` (liste over egne runder), `/rounds/new`
|
|
|
|
|
|
(bane-kilde teeoff vs. egen — for egen bane: søk-før-opprett, samme idé
|
|
|
|
|
|
som org-banenes gjenbrukbare katalog; utslag filtrert til kun de som har
|
|
|
|
|
|
rating for brukerens registrerte kjønn, ADR-029s automatikk-prinsipp
|
|
|
|
|
|
gjenbrukt her selv om HCP-motoren er en helt annen), `/rounds/[id]`
|
|
|
|
|
|
(deltaker-faner — eier + gjester, ingen ekte kontokobling i v1 per
|
|
|
|
|
|
Beslutning D; hull-navigasjon fra runde-ens starthull; slag/putt-
|
|
|
|
|
|
tallvelgere i samme visuelle stil som `session-scorecard.tsx` sin
|
|
|
|
|
|
`StrokePicker`; kølle/retning/innspill/chip/bunker/straffeslag/
|
|
|
|
|
|
putt-avstand bak en «flere detaljer»-utvidelse; GIR utledet og vist
|
|
|
|
|
|
KLIENTSIDE ved lesing, aldri lagret — nøyaktig Beslutning B sitt prinsipp;
|
|
|
|
|
|
fullfør-runde med HCP-differensial-sammendrag, låser videre redigering).
|
|
|
|
|
|
Lenket fra dashbordet som «Egne runder» (header-lenke + en egen kort på
|
|
|
|
|
|
forsiden) — bevisst adskilt navn fra det eksisterende «Mine runder»
|
|
|
|
|
|
(ADR-031, turnering-deltakelse) for å unngå at de to konseptene blandes
|
|
|
|
|
|
sammen i UI-et, selv om begge bokstavelig talt handler om "runder".
|
|
|
|
|
|
**Scratch-verifisert grundig** (isolert `teecup_app_scratch`-rolle +
|
|
|
|
|
|
isolert scratch-MinIO + engangs API-container, samme mønster som resten
|
|
|
|
|
|
av ADR-033): 22 automatiserte sjekker (egendefinert-bane-opprettelse med
|
|
|
|
|
|
to utslag/to kjønn, søk, detalj, PATCH-kontraktsbeviset over,
|
|
|
|
|
|
GIR-derivering for et konstruert par4/score3/putt1-tilfelle, gjest-
|
|
|
|
|
|
fjerning, kryss-bruker-autorisasjon 403, full fullføring med differensial)
|
|
|
|
|
|
PLUSS en separat, egen test av HELE teeoff-baserte opprettelsesløpet mot
|
|
|
|
|
|
den ekte kjørende `teeoff_api`-containeren (Borregaard Golfklubb, samme
|
|
|
|
|
|
anlegg som tidligere ADR-019/033-verifiseringer) som bekreftet
|
|
|
|
|
|
`course_handicap_snapshot` ble beregnet riktig fra live-hentet
|
|
|
|
|
|
rating. Ekte typesjekket PRODUKSJONSBUILD kjørt via
|
|
|
|
|
|
`docker build --target builder` (nøyaktig samme steg `Dockerfile` bruker
|
|
|
|
|
|
i prod, ikke `next dev`) — kompilerte rent, alle nye ruter listet.
|
|
|
|
|
|
**Rullet ut live 2026-07-23**, bruker bekreftet eksplisitt: ingen
|
|
|
|
|
|
migrasjon i denne del-runden, `docker compose up -d --build teecup_api
|
|
|
|
|
|
teecup_frontend`, begge containere boot-et rent, `/health`/`/dashboard`/
|
|
|
|
|
|
`/rounds` → 200 over ekte https, `teeoff.no` upåvirket.
|
|
|
|
|
|
**Bevisst utenfor omfang, ikke bygget denne runden:** ekte kontokobling
|
|
|
|
|
|
for gjeste-deltakere (Beslutning D), automatisk oppdatering av
|
|
|
|
|
|
`app_user.handicap_index`/`handicap_history` ved fullført tellende runde
|
|
|
|
|
|
(åpent spørsmål i Beslutning G, fortsatt ubesvart), shotgun-start
|
|
|
|
|
|
(egen, separat ADR-034), GPS/avstandsmåling (se eget notat i
|
|
|
|
|
|
FEATURE_BACKLOG.md, krever data ingen kilde har i dag).
|
2026-07-22 10:46:10 +02:00
|
|
|
|
|
Update Todos
Pakke ut og diffe alle tre V0-eksporter mot levende tre
Lese V0s round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx grundig
Lese V0s app/rounds*/page.tsx routing-filer
Ta inn round-card.tsx uendret
Skrive om datalaget i own-rounds.tsx (fjerne mock/toggle, ekte fetch)
Skrive om datalaget i new-round.tsx (ekte søk/opprett/bekreft)
Skrive om datalaget i round-detail.tsx (ekte runde/deltakere/hull, fjerne preview-toggle)
Oppdatere page.tsx-rutene og slette gamle hånd-bygde komponentfiler
Typesjekket produksjonsbuild + slette V0-zip-filene
Rulle ut live etter bekreftelse
Rullet ut og dokumentert. Oppsummert:
Frontend for frittstående rundeføring bruker nå V0s design på alle tre skjermene (/rounds, /rounds/new, /rounds/[id]), med ekte data koblet inn på samme måte som resten av appen. Verifisert med typesjekket produksjonsbuild, ingen backend-endring, teeoff.no upåvirket gjennom hele utrullingen. Status oppdatert i CLAUDE.md, FEATURE_BACKLOG.md og ADR-033.
Gjenstår fortsatt fra tidligere: ekte kontokobling for gjestedeltakere, automatisk HCP-oppdatering ved fullført runde, shotgun-start (egen ADR-034), og GPS/avstandsmåling.
2026-07-23 11:31:50 +02:00
|
|
|
|
**Frontend ERSTATTET med V0-designet versjon 2026-07-23, samme dag som
|
|
|
|
|
|
den hånd-bygde frontend-en over ble rullet ut:** brukeren påpekte
|
|
|
|
|
|
(berettiget) at de tre nye skjermene var hånd-kodet av meg direkte i
|
|
|
|
|
|
stedet for designet i V0 -- et avvik fra prosjektets etablerte mønster
|
|
|
|
|
|
gjennom HELE resten av appen. Bekreftet med bruker at arbeidsmåten
|
|
|
|
|
|
fortsatt skal være: JEG skriver V0-prompten, BRUKEREN kjører den i
|
|
|
|
|
|
v0.app og sender koden tilbake, JEG integrerer (samme flyt som alltid,
|
|
|
|
|
|
ikke endret). Skrev tre detaljerte prompter (liste/opprett/hull-
|
|
|
|
|
|
registrering, inkl. eksplisitt tilgjengelighetskrav i hver) -- bruker
|
|
|
|
|
|
lastet opp tre zip-eksporter (`tee-cup-login-screen (10/11/12).zip`).
|
|
|
|
|
|
**Samme "full re-eksport hver gang"-mønster som ALLE tidligere V0-runder:**
|
|
|
|
|
|
diffet mot levende tre FØR noe ble tatt inn -- kun fire filer var reelt
|
|
|
|
|
|
nye/relevante (`round-card.tsx`, `own-rounds.tsx`, `new-round.tsx`,
|
|
|
|
|
|
`round-detail.tsx` + tre `app/rounds*/page.tsx`-ruter), resten
|
|
|
|
|
|
(login-form, dashboard, config, ui/*) var forventede full-reverts og ble
|
|
|
|
|
|
IKKE tatt inn. **Samme kjente V0-feil dukket opp igjen, hoppet bevisst
|
|
|
|
|
|
over:** `app/clubs/[id]/page.tsx` med feilnavngitt `[id]`-parameter
|
|
|
|
|
|
(egentlig en slug) -- identisk feil som ble rettet i klubbside-runden
|
|
|
|
|
|
under ADR-018, V0 gjenskaper den tydeligvis når prosjektet re-genereres.
|
|
|
|
|
|
**Mine tre hånd-bygde komponenter (`personal-rounds.tsx`, `round-new.tsx`,
|
|
|
|
|
|
og min opprinnelige `round-detail.tsx`) er ERSTATTET, ikke supplert** --
|
|
|
|
|
|
V0s presentasjon beholdt, datalaget skrevet om fra mock til ekte fetch
|
|
|
|
|
|
(samme "V0 leverte mock, jeg kabler ekte data"-mønster som enhver annen
|
|
|
|
|
|
skjerm i appen). Reelle tilpasninger utover ren om-kabling:
|
|
|
|
|
|
1. V0s teeoff-søkemodell antok hele bane+utslag-lista lå ferdig i
|
|
|
|
|
|
søkeresultatet -- det ekte API-et er et to-stegs oppslag (søk →
|
|
|
|
|
|
facility-detalj). Løst med en `resolving`-tilstand i
|
|
|
|
|
|
`OfficialSearchStep` (eneste strukturelle tillegg til V0s JSX).
|
|
|
|
|
|
2. La til et tredje kjønnsvalg "Annet" i gjeste-skjemaets `ChoiceRow`
|
|
|
|
|
|
(V0 hadde kun Mann/Kvinne) -- matcher appens ellers etablerte
|
|
|
|
|
|
Herre/Dame/Annet-konvensjon (`account-settings.tsx` m.fl.), og
|
|
|
|
|
|
API-ets `GuestParticipantCreate.gender` støtter allerede `x`.
|
|
|
|
|
|
3. Beholdt merge-før-PATCH-sikringen fra forrige rundes kontraktsfunn
|
|
|
|
|
|
(hull-PATCH skriver alle felt, ikke bare det endrede) -- V0 kjente
|
|
|
|
|
|
naturligvis ikke til denne kontraktsdetaljen, lagt inn i
|
|
|
|
|
|
`updateStat()` uendret i prinsipp fra min opprinnelige versjon.
|
|
|
|
|
|
4. Fjernet V0s dev-only forhåndsvisningskontroller (`ReviewToggle` i
|
|
|
|
|
|
`own-rounds.tsx`, den nederste "Forhåndsvis: Pågår/Fullført"-linjen i
|
|
|
|
|
|
`round-detail.tsx`) -- samme opprydningsmønster som ALLE tidligere
|
|
|
|
|
|
V0-runder i prosjektet.
|
|
|
|
|
|
Ingen backend-endring i denne del-runden (samme API-kontrakt som allerede
|
|
|
|
|
|
var scratch-verifisert). **Verifisert:** ekte typesjekket
|
|
|
|
|
|
produksjonsbuild (`docker build --target builder`) kompilerte rent, alle
|
|
|
|
|
|
ruter listet. Ingen ny interaktiv nettleser-test utført (intet slikt
|
|
|
|
|
|
verktøy tilgjengelig i denne økten) -- kun kodegjennomgang + typesjekk,
|
|
|
|
|
|
flagget eksplisitt til bruker.
|
|
|
|
|
|
**Rullet ut live 2026-07-23**, bruker bekreftet eksplisitt: `docker
|
|
|
|
|
|
compose up -d --build teecup_frontend` (gjenskapte også `teecup_api` som
|
|
|
|
|
|
vanlig compose-bivirkning, ingen backend-kode rørt), begge containere
|
|
|
|
|
|
boot-et rent, `/health`/`/dashboard`/`/rounds`/`/rounds/new` → 200 over
|
|
|
|
|
|
ekte https, `teeoff.no` upåvirket. De tre V0-zip-ene slettet fra
|
|
|
|
|
|
prosjektroten etter fullført integrering.
|
|
|
|
|
|
|
2026-07-23 12:19:31 +02:00
|
|
|
|
**Reell produksjonsbug funnet OG FIKSET 2026-07-23, rapportert av bruker
|
|
|
|
|
|
med to skjermbilder (`/rounds` ga "Klarte ikke å hente rundene dine",
|
|
|
|
|
|
`/rounds/new` sitt siste steg ga "Klarte ikke å opprette runden"):**
|
|
|
|
|
|
rot-årsak var en EKSAKT navnekollisjon mellom frontend-sidens toppnivå-
|
|
|
|
|
|
prefiks (`/rounds`) og backend-APIets ressursprefiks (samme `/rounds`,
|
|
|
|
|
|
`app/routers/rounds.py`) -- en verre variant av den allerede dokumenterte
|
|
|
|
|
|
"afterFiles"-fellen fra ADR-016s medlemsside-hendelse, denne gangen
|
|
|
|
|
|
rammende BEGGE retninger samtidig:
|
|
|
|
|
|
- `/rounds` (eksakt sti): en STATISK frontend-side. Statiske sider
|
|
|
|
|
|
sjekkes FØR rewrites, så siden vant presedens -- klientens
|
|
|
|
|
|
`fetch("/rounds")`/`POST /rounds` traff ALDRI backend, fikk Next sin
|
|
|
|
|
|
egen HTML tilbake i stedet for JSON (stille `res.json()`-parsefeil,
|
|
|
|
|
|
fanget av try/catch, viste den generiske feilteksten).
|
|
|
|
|
|
- `/rounds/[id]` (DYNAMISK side): her sjekkes rewrites FØR dynamiske
|
|
|
|
|
|
sider, så rewrite-regelen vant presedens i stedet -- selve
|
|
|
|
|
|
rundedetalj-SIDEN var dermed fullstendig UOPPNÅELIG (ville vist rå
|
|
|
|
|
|
backend-JSON i stedet for UI-et), bekreftet direkte med `curl` FØR
|
|
|
|
|
|
fiksen (anonymt `GET /rounds/00000000-...` ga ekte backend-JSON i
|
|
|
|
|
|
stedet for Next sin HTML).
|
|
|
|
|
|
Det andre skjermbildets utslagsnavn "55/50/44/32" ble UNDERSØKT OG
|
|
|
|
|
|
BEKREFTET Å IKKE VÆRE EN BUG -- lest direkte fra ekte `teeoff_api` (`GET
|
|
|
|
|
|
tjome-golfklubb`): Tjøme Golfklubb sine faktiske utslagsnavn i teeoff ER
|
|
|
|
|
|
bokstavelig talt disse tallene (lengde i hundremeter, ikke fargenavn) --
|
|
|
|
|
|
data gjengitt korrekt, ingen kode-endring nødvendig for dette punktet.
|
|
|
|
|
|
**Fikset ved samme prinsipp som medlemsside-hendelsen: flytt siden, ikke
|
|
|
|
|
|
APIet.** Alle tre frontend-rutene flyttet til et helt nytt, ikke-
|
|
|
|
|
|
overlappende toppnivå-prefiks `/my-rounds/*` (`app/rounds/` →
|
|
|
|
|
|
`app/my-rounds/`), API-et (`/rounds/*`) uendret. Åtte interne
|
|
|
|
|
|
navigasjonsreferanser oppdatert på tvers av `round-card.tsx`/
|
|
|
|
|
|
`own-rounds.tsx`/`new-round.tsx`/`round-detail.tsx`/`dashboard.tsx` --
|
|
|
|
|
|
ekte `fetch()`-kall til API-et (samme filer) bevisst latt urørt, kun
|
|
|
|
|
|
`<Link href>`/`router.push`/`router.replace` endret. Utvidet
|
|
|
|
|
|
`next.config.mjs` sin allerede eksisterende advarselskommentar med denne
|
|
|
|
|
|
nye, verre varianten av samme fellesklasse, som en fremtidig påminnelse:
|
|
|
|
|
|
et rewrite-prefiks og en frontend-sides toppnivå-segment må ALDRI være
|
|
|
|
|
|
identisk streng.
|
|
|
|
|
|
**Verifisert presist FØR og ETTER utrulling** (ikke bare "bygget uten
|
|
|
|
|
|
feil"): et `curl` mot ekte produksjon FØR fiksen bekreftet nøyaktig
|
|
|
|
|
|
mekanismen i begge retninger (se over). Ekte typesjekket
|
|
|
|
|
|
produksjonsbuild etterpå viste selv at rutetreet nå lister `/my-rounds`,
|
|
|
|
|
|
`/my-rounds/[id]`, `/my-rounds/new` i stedet for de gamle `/rounds`-
|
|
|
|
|
|
rutene. **Rullet ut live 2026-07-23**, bruker bekreftet eksplisitt: kun
|
|
|
|
|
|
`teecup_frontend` (gjenskapte `teecup_api` som vanlig bivirkning, ingen
|
|
|
|
|
|
backend-kode rørt). Verifisert ETTERPÅ med et nytt sett `curl`-kall mot
|
|
|
|
|
|
ekte https: anonymt `GET /rounds` ga nå korrekt backend-JSON
|
|
|
|
|
|
(`NOT_AUTHENTICATED`, IKKE Next sin HTML som før), anonymt `GET
|
|
|
|
|
|
/rounds/<uuid>` fortsatt korrekt backend-JSON (uendret, som forventet),
|
|
|
|
|
|
og -- den avgjørende nye sjekken -- anonymt `GET /my-rounds/<uuid>` ga nå
|
|
|
|
|
|
faktisk `text/html` (selve React-siden, ikke lenger uoppnåelig).
|
|
|
|
|
|
`/dashboard`/`/my-rounds`/`/my-rounds/new` → 200, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
**Nok en runde brukerrapporterte punkter, ALLE BYGGET OG LIVE 2026-07-24,
|
|
|
|
|
|
samme dag:** (1) Utslagstidspunkt (`round.started_at`, migrasjon
|
|
|
|
|
|
`023_round_start_time.sql`, valgfritt) + tidsbruk beregnet klientside
|
|
|
|
|
|
som `completed_at − started_at` når runden fullføres. Bekreftet
|
|
|
|
|
|
eksplisitt med bruker: "Ferdig"-tidspunktet er den ALLEREDE eksisterende
|
|
|
|
|
|
"Fullfør runde"-knappen, ingen ny handling/kolonne. (2) "Idx" i hull-
|
|
|
|
|
|
overskriften byttet til "Hcp". (3) Slag-tastaturets numpad merker nå
|
|
|
|
|
|
knappen som tilsvarer hullets par med en liten "par"-bildetekst.
|
|
|
|
|
|
(4) Hull-navigasjonens "Forrige"/"Neste" respekterte tidligere ALLTID
|
|
|
|
|
|
18 hull uansett `holes_planned` -- en 9-hulls runde 10-18 hoppet feilaktig
|
|
|
|
|
|
til hull 9 ved "Forrige" fra hull 10. Fikset: navigasjonsrekkefølgen
|
|
|
|
|
|
bygges nå med `holes_planned` som lengde, ikke hardkodet 18.
|
|
|
|
|
|
(5) Putter/Chip/Bunker/Straffeslag/Anywayslag kan nå aldri velges høyere
|
|
|
|
|
|
enn antall registrerte slag på hullet (`NumberPicker` fikk en
|
|
|
|
|
|
`maxValue`-prop, `Stepper` en `max`-prop) -- ingen vits i å tilby et
|
|
|
|
|
|
selvmotsigende tall. (6) "Slett runde" bygget i UI-et (backendens
|
|
|
|
|
|
`DELETE /rounds/{id}` fantes fra før, men hadde aldri fått en
|
|
|
|
|
|
frontend-knapp) -- bekreftelsesdialog, sletter for alle (kaskade
|
|
|
|
|
|
fjerner automatisk alle deltakere/hull, guest-spillere har uansett ingen
|
|
|
|
|
|
egen konto å bevare noe for).
|
|
|
|
|
|
(7) **Ny `PATCH /rounds/{id}`** -- retter opp feil bane/utslag eller
|
|
|
|
|
|
feil antall hull ETTER opprettelse, uten å røre allerede registrerte
|
|
|
|
|
|
slag/putter/etc. Speiler samme filosofi som turnering-øktenes
|
|
|
|
|
|
`_remap_course` (ADR-tidligere runde), men enklere: ingen tee-navn-
|
|
|
|
|
|
matching på tvers av kjønn siden hver deltaker valideres eksplisitt mot
|
|
|
|
|
|
den NYE banens rating for sitt eget kjønn FØR noe skrives (hele byttet
|
|
|
|
|
|
avvises 400 hvis ÉN deltaker ville mistet HCP-sporing). Bevisst
|
|
|
|
|
|
AVVIST (409) etter at runden er fullført -- ulikt turnering-øktenes
|
|
|
|
|
|
bane-bytte, som bevisst tillater dette selv etter avgjørelse; her er
|
|
|
|
|
|
omfanget mindre (ingen re-beregning av differensial bygget for dette
|
|
|
|
|
|
tilfellet). Frontend: ny "Rediger runde"-seksjon på rundesiden (antall
|
|
|
|
|
|
hull som enkelt 9/18-valg, "Bytt bane" som en kompakt søke-flyt --
|
|
|
|
|
|
teeoff-søk ELLER egen-bane-søk, samme mønster som ved opprettelse, bare
|
|
|
|
|
|
kondensert). Etter et vellykket bytte hentes hull-data på nytt for
|
|
|
|
|
|
ALLE allerede lastede deltakere (par/stroke-index kan ha endret seg for
|
|
|
|
|
|
alle, ikke bare aktiv spiller).
|
|
|
|
|
|
**Scratch-verifisert grundig** (isolert `teecup_app_scratch`-rolle +
|
|
|
|
|
|
isolert scratch-MinIO + engangs API-container): 22 sjekker for
|
|
|
|
|
|
rediger/slett-rundene (bane-bytte med FAKTISK ulike par/stroke-index-
|
|
|
|
|
|
verdier mellom to egendefinerte baner, bekreftet at allerede registrerte
|
|
|
|
|
|
slag på hull 1-3 var UENDRET etter byttet mens par/stroke-index OG
|
|
|
|
|
|
course_handicap_snapshot var oppdatert; avvist bytte til en bane uten
|
|
|
|
|
|
rating for deltakerens kjønn, bekreftet at INGENTING ble endret ved
|
|
|
|
|
|
avvisning; avvist bane/hull-endring etter fullføring, 409; slett-runde
|
|
|
|
|
|
+ idempotent 404 + kryss-bruker-autorisasjon 403) pluss 8 sjekker for
|
|
|
|
|
|
utslagstid/tidsbruk. `test_isolation.sql` 12/12. Ekte typesjekket
|
|
|
|
|
|
produksjonsbuild kompilerte rent begge ganger.
|
|
|
|
|
|
**Rullet ut mot ekte systemer 2026-07-24**, bruker bekreftet eksplisitt:
|
|
|
|
|
|
migrasjon 023 kjørt mot ekte `teecup_db` (kolonne bekreftet,
|
|
|
|
|
|
`test_isolation.sql` fortsatt 12/12), deretter `docker compose up -d
|
|
|
|
|
|
--build teecup_api teecup_frontend`, begge containere boot-et rent,
|
|
|
|
|
|
`/health`/`/dashboard`/`/my-rounds` → 200 over ekte https, `teeoff.no`
|
|
|
|
|
|
upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
**To til brukerpunkter, ALLE BYGGET OG LIVE 2026-07-25, samme dag:**
|
|
|
|
|
|
(1) `PATCH /rounds/{id}` utvidet med `start_hole`/`started_at`/
|
|
|
|
|
|
`completed_at` -- start_hole kan rettes uansett (ren metadata, aldri
|
|
|
|
|
|
sperret av fullført-status), started_at kan justeres når som helst,
|
|
|
|
|
|
completed_at kan KUN justeres på en runde som allerede ER fullført
|
|
|
|
|
|
(avvist 400 ellers -- denne PATCH-en fullfører aldri runden selv, kun
|
|
|
|
|
|
korrigerer et allerede satt tidspunkt fra "Fullfør runde"). Ny
|
|
|
|
|
|
validering: completed_at må være etter started_at, ellers 400. Løser
|
|
|
|
|
|
brukerens konkrete case: glemmer å trykke "Fullfør runde" i flere timer,
|
|
|
|
|
|
vil rette opp tidsbruken i etterkant. `EditRoundPanel` sin "Bane/antall
|
|
|
|
|
|
hull"-blokk forblir sperret post-fullføring (uendret fra forrige runde),
|
|
|
|
|
|
mens et nytt tidspunkt-skjema (Utslagstid alltid, Fullført-tidspunkt kun
|
|
|
|
|
|
hvis fullført) nå vises uansett fullført-status.
|
|
|
|
|
|
(2) **Nærmeste offisielle baner** i "Ny runde"-flyten -- nytt
|
|
|
|
|
|
`GET /rounds/official-search/nearby?lat=&lng=&limit=` (Haversine-formel,
|
|
|
|
|
|
sortert stigende, MÅ registreres FØR `/rounds/official-search/{slug}` i
|
|
|
|
|
|
routeren, samme presedens-lærdom som ADR-020s "by-code"). Henter ALLE
|
|
|
|
|
|
174 teeoff-anleggenes lat/lng via samme `search_facilities("")`-kall som
|
|
|
|
|
|
allerede finnes (ingen ny teeoff-avhengighet), beregner avstand i Python
|
|
|
|
|
|
per forespørsel (ingen cache -- datasettet er lite nok). Frontend: ny
|
|
|
|
|
|
`NearbyClubs`-komponent i `OfficialSearchStep`, ber om
|
|
|
|
|
|
`navigator.geolocation` ved mount, viser inntil 5 nærmeste med
|
|
|
|
|
|
nærmeste tydelig merket + avstand (m under 1 km, ellers km) -- rett
|
|
|
|
|
|
FØR søkefeltet, som bedt om. Avslått/manglende posisjon feiler helt
|
|
|
|
|
|
stille (ingen feilmelding), søket fungerer uendret som fallback.
|
|
|
|
|
|
**Scratch-verifisert:** 14 sjekker (start_hole-endring, tidspunkt-
|
|
|
|
|
|
korreksjon i begge retninger inkl. de to nye valideringsreglene,
|
|
|
|
|
|
`holes_planned` fortsatt sperret post-fullføring uendret, OG et ekte
|
|
|
|
|
|
`nearby`-kall mot den kjørende `teeoff_api`-containeren fra Tjømes egne
|
|
|
|
|
|
koordinater som korrekt fant Tjøme selv som nærmeste/nest-nærmeste
|
|
|
|
|
|
treff). `test_isolation.sql` 12/12 (ingen skjemaendring). Ekte
|
|
|
|
|
|
typesjekket produksjonsbuild kompilerte rent.
|
|
|
|
|
|
**Rullet ut live 2026-07-25**, bruker bekreftet eksplisitt: ingen
|
|
|
|
|
|
migrasjon, `docker compose up -d --build teecup_api teecup_frontend`,
|
|
|
|
|
|
begge containere boot-et rent, `/health`/`/dashboard`/`/my-rounds/new`
|
|
|
|
|
|
→ 200, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
**Reell UX-bug funnet OG fikset, samme dag 2026-07-25, rapportert av
|
|
|
|
|
|
bruker med skjermbilder av en konkurrerende golf-app (Golf Game Book)
|
|
|
|
|
|
som referanse:** brukeren rapporterte at "All statistikk" ikke viste
|
|
|
|
|
|
noe utover slag/putter under selve registreringen. **Bekreftet direkte
|
|
|
|
|
|
mot ekte `teecup_db` (kun lesing) at dette IKKE var en datafeil** --
|
|
|
|
|
|
brukerens faktiske runde hadde `stat_level='full'` lagret korrekt. Rot-
|
|
|
|
|
|
årsaken var et REELT presentasjonsproblem: alle detaljfeltene (kølle,
|
|
|
|
|
|
utslag, innspill, chip, bunker, straffeslag, putt-avstand, anywayslag)
|
|
|
|
|
|
lå bak en "Score"/"Statistikk"-fane (V0-runden 2026-07-24s bevisste
|
|
|
|
|
|
designvalg) som brukeren aldri oppdaget -- "aktivert full statistikk"
|
|
|
|
|
|
ga i praksis ingen synlig endring uten et ekstra, ikke-annonsert
|
|
|
|
|
|
tastetrykk. **Fikset ved å fjerne fane-løsningen helt:** alle feltene
|
|
|
|
|
|
vises nå ALLTID samlet under slag/putter i én sammenhengende scroll når
|
|
|
|
|
|
`statLevel==="full"` (samme prinsipp brukeren opprinnelig ba om FØR
|
|
|
|
|
|
V0-rundens sveip/fane-vurdering) -- fjerner enhver tvetydighet om
|
|
|
|
|
|
hvorvidt statistikken faktisk er slått på. `PanelTabs`-komponenten og
|
|
|
|
|
|
`panelTab`-state fjernet som død kode.
|
|
|
|
|
|
**Samtidig bygget: "Så langt i runden"-oversikt**, direkte etterspurt
|
|
|
|
|
|
("jeg trenger et grensesnitt som viser scoren min så langt") med
|
|
|
|
|
|
referansebildene som inspirasjon for FUNKSJONEN (ikke kopiert
|
|
|
|
|
|
utseendemessig). Ny komponent `ScoreSoFar` i `round-detail.tsx`: en
|
|
|
|
|
|
kompakt alltid-synlig linje ("Så langt: X hull · Y slag · +Z til par")
|
|
|
|
|
|
rett under spillerfanene, pluss en "Vis full oversikt"-knapp (samme
|
|
|
|
|
|
etablerte mønster som `session-scorecard.tsx` sin `HoleSummaryTable`
|
|
|
|
|
|
for turnering-scoring) som åpner en tabell: hull, par, score, netto,
|
|
|
|
|
|
løpende sum. **Ny backend-beregning for å muliggjøre netto-kolonnen:**
|
|
|
|
|
|
`RoundHoleOut` fikk et nytt felt `strokes_received` (`list_holes` i
|
|
|
|
|
|
`app/routers/rounds.py`) -- utledet fra deltakerens
|
|
|
|
|
|
`course_handicap_snapshot` + hullenes `stroke_index` via den
|
|
|
|
|
|
ALLEREDE eksisterende `allocate_strokes_by_index()` fra
|
|
|
|
|
|
`handicap_engine.py` (samme allokeringsalgoritme som brukes overalt
|
|
|
|
|
|
ellers i appen, ingen ny logikk) -- `None` for en deltaker uten
|
|
|
|
|
|
beregnet HCP (f.eks. en gjest uten oppgitt handicap), aldri lagret,
|
|
|
|
|
|
kun beregnet ved lesing.
|
|
|
|
|
|
**Scratch-verifisert grundig** (isolert `teecup_app_scratch`-rolle +
|
|
|
|
|
|
isolert scratch-MinIO + engangs API-container): 11 sjekker, inkl. et
|
|
|
|
|
|
presist talleksempel (course rating 72.0/slope 113/HCP 10.0 → course
|
|
|
|
|
|
handicap nøyaktig 10, bekreftet at de 10 laveste stroke-indeksene fikk
|
|
|
|
|
|
nøyaktig 1 slag hver og de resterende 8 fikk 0, sum(strokes_received)
|
|
|
|
|
|
== course_handicap), et registrert hull som ga korrekt netto (score 5 −
|
|
|
|
|
|
1 mottatt slag = 4), og en gjest UTEN HCP som korrekt fikk
|
|
|
|
|
|
`strokes_received: null` på alle 18 hull. `test_isolation.sql` 12/12
|
|
|
|
|
|
(ingen skjemaendring). Ekte typesjekket produksjonsbuild kompilerte
|
|
|
|
|
|
rent.
|
|
|
|
|
|
**Rullet ut live 2026-07-25**, bruker bekreftet eksplisitt: ingen
|
|
|
|
|
|
migrasjon, kun `teecup_api`+`teecup_frontend` redeployet.
|
|
|
|
|
|
`/health`/`/dashboard` → 200, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
**Reell produksjonsregresjon rapportert AV BRUKEREN samme dag, rett
|
|
|
|
|
|
etter utrulling over, funnet og fikset umiddelbart:** "Ingenting er
|
|
|
|
|
|
klikkbart i den avanserte statistikken." Første antagelse (frontend-
|
|
|
|
|
|
CSS/event-håndtering) ble IKKE bekreftet ved kodegjennomgang -- all
|
|
|
|
|
|
onClick-kabling i `DirectionCross`/`Stepper`/`ChoiceRow`/`NumberPicker`
|
|
|
|
|
|
var korrekt. Root cause funnet ved å faktisk GJENSKAPE brukerens
|
|
|
|
|
|
klikk-sekvens mot en fersk, isolert scratch-container (samme mønster
|
|
|
|
|
|
som resten av runden) i stedet for å gjette videre: `update_hole`
|
|
|
|
|
|
(PATCH-endepunktet for hull-registrering) konstruerte fortsatt
|
|
|
|
|
|
`RoundHoleOut(**dict(row))` UTEN det nye påkrevde
|
|
|
|
|
|
`strokes_received`-feltet fra runden rett over -- en Pydantic
|
|
|
|
|
|
`ValidationError` (500) på HVER ENESTE hull-lagring, ikke bare de
|
|
|
|
|
|
"avanserte" feltene. Frontend sin `updateStat()` svelger `!res.ok`
|
|
|
|
|
|
stille uten feilmelding, så symptomet fremsto nøyaktig som "ingenting
|
|
|
|
|
|
skjer når jeg trykker" for ALLE felt (Slag/Putter inkludert) -- brukeren
|
|
|
|
|
|
merket det trolig først på de avanserte feltene siden Slag/Putter fra
|
|
|
|
|
|
TIDLIGERE runder allerede hadde lagrede verdier som så riktige ut ved
|
|
|
|
|
|
åpning.
|
|
|
|
|
|
**Fikset:** `update_hole` beregner nå `strokes_received` for akkurat det
|
|
|
|
|
|
oppdaterte hullet (henter deltakerens `course_handicap_snapshot` +
|
|
|
|
|
|
ALLE 18 sine `stroke_index` -- samme allokeringsalgoritme som
|
|
|
|
|
|
`list_holes`, siden fordelingen avhenger av hele rundens
|
|
|
|
|
|
stroke-indeks-rekkefølge, ikke bare ett hull) og sender den med i
|
|
|
|
|
|
responsen.
|
|
|
|
|
|
**Scratch-verifisert på nytt, presist mot akkurat denne regresjonen:**
|
|
|
|
|
|
14 sjekker som gjenskaper brukerens EKSAKTE klikk-rekkefølge (Slag →
|
|
|
|
|
|
kølle → Utslag-retning → Innspill-retning → Chip → første putt-bøtte →
|
|
|
|
|
|
Anywayslag, pluss et klikk på et avansert felt FØR Slag i det hele tatt
|
|
|
|
|
|
er satt, og en gjest uten HCP) -- alle 200 med riktig ekko, `GET` etterpå
|
|
|
|
|
|
bekrefter faktisk lagring, gjest uten HCP gir korrekt `strokes_received:
|
|
|
|
|
|
null` uten å krasje. `test_isolation.sql` uendret (ingen skjemaendring,
|
|
|
|
|
|
ren Python-fiks). **Rullet ut live 2026-07-25**, bruker bekreftet
|
|
|
|
|
|
eksplisitt: kun `teecup_api` redeployet, `/health`/`/dashboard` → 200,
|
|
|
|
|
|
`teeoff.no` upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
**Rediger/Fullfør/Slett tonet ned og flyttet til toppen + auto-scroll ved
|
|
|
|
|
|
hull-bytte, LIVE samme dag (2026-07-25):** brukeren rapporterte at
|
|
|
|
|
|
"Fullfør runde"/"Slett runde" lå som store, fremtredende knapper RETT
|
|
|
|
|
|
under "Neste hull"-navigasjonen nederst i hull-panelet -- altfor lett å
|
|
|
|
|
|
trykke feil ved et uhell mens man bare skulle bla mellom hull. Flyttet
|
|
|
|
|
|
alle tre (Rediger/Fullfør/Slett) til en samlet rad ØVERST på siden,
|
|
|
|
|
|
tonet ned til samme nøytrale "trigger"-stil (ikke lenger store,
|
|
|
|
|
|
fargede knapper) -- brukeren må nå aktivt scrolle OPP forbi hele
|
|
|
|
|
|
hull-registreringen for å nå dem. Samtidig rapportert, i samme runde:
|
|
|
|
|
|
"Neste hull"/"Forrige" lastet nytt innhold, men brukeren ble stående
|
|
|
|
|
|
scrollet nede der knappene er, med det nye hullets Slag-felt utenfor
|
|
|
|
|
|
skjermen. Løst med `holePanelRef` + `scrollIntoView({block:"start"})`
|
|
|
|
|
|
kalt fra `goPrev`/`goNext` OG fra hull-navigasjonens direkte hull-valg,
|
|
|
|
|
|
med `scroll-mt-28` på panelet for å unngå at den sticky headeren dekker
|
|
|
|
|
|
toppen. Ren frontend-endring, ingen backend/migrasjon. Ekte typesjekket
|
|
|
|
|
|
build kjørt og bekreftet (alle 19 ruter), rullet ut, `teeoff.no`
|
|
|
|
|
|
upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
**"Så langt i runden" utvidet med netto/stableford-sum, putt-/kølle-
|
|
|
|
|
|
statistikk-totaler og grafisk fairway-/innspill-fordeling, LIVE samme
|
|
|
|
|
|
dag (2026-07-25):** brukeren etterspurte flere detaljer i
|
|
|
|
|
|
rundeoversikten: netto- og stableford-sum, hvor man kan se totalt antall
|
|
|
|
|
|
putter/chip/bunker/straffeslag/anywayslag for runden, grafisk fremstilt
|
|
|
|
|
|
prosentvis fordeling av fairwaytreff og innspillstreff, og gjennomsnittlig
|
|
|
|
|
|
score til par (to desimaler) splittet på fairwaytreff-vs-bom og
|
|
|
|
|
|
innspill-treff-vs-bom.
|
|
|
|
|
|
**Ingen backend-endring nødvendig** -- alt beregnes klientside fra data
|
|
|
|
|
|
`GET .../holes` allerede returnerer (score/par/strokes_received/putts/
|
|
|
|
|
|
chip_count/bunker_shot_count/penalty_strokes/tee_shot_result/
|
|
|
|
|
|
approach_result/anyway_strokes). `ScoreSoFar`-komponenten
|
|
|
|
|
|
(round-detail.tsx) utvidet med: en `StatPill`-rutenett (Slag/Til par/
|
|
|
|
|
|
Netto/Stableford/Putt/Chip/Bunker/Straffeslag/Anywayslag -- hver vist
|
|
|
|
|
|
kun hvis det faktisk finnes registrert data for akkurat det feltet), en
|
|
|
|
|
|
ny Stableford-kolonne i hull-for-hull-tabellen (kun når netto er
|
|
|
|
|
|
beregnbart), og to nye `DistributionBar`-seksjoner (Fairwaytreff/
|
|
|
|
|
|
Innspill) -- generalisert N-kategori-variant av samme visuelle idé som
|
|
|
|
|
|
`SegmentedBar` i tournament-leaderboard.tsx (ett fargesegmentert
|
|
|
|
|
|
rektangel + prosent-legend), ikke noe chart-bibliotek lagt til.
|
|
|
|
|
|
**Presisert i kode-kommentar, bevisst valg:** appen har ingen egen
|
|
|
|
|
|
"spilleform"-innstilling for frittstående runder -- stableford beregnes
|
|
|
|
|
|
derfor alltid ut fra netto score når det er mulig (krever registrert
|
|
|
|
|
|
HCP, samme forutsetning som netto), uavhengig av om brukeren
|
|
|
|
|
|
"egentlig" spiller slagspill eller stableford. Standard stableford-
|
|
|
|
|
|
poengtabell brukt (netto par = 2 poeng, ett poeng mer/mindre per slag
|
|
|
|
|
|
bedre/dårligere enn par, gulv på 0).
|
|
|
|
|
|
Gjennomsnittlig score-til-par (fairway/innspill, treff-vs-bom) bruker
|
|
|
|
|
|
BRUTTO score (ikke netto) relativt til par, formatert med fortegn og to
|
|
|
|
|
|
desimaler (`formatSignedAvg`), matcher brukerens eksplisitte
|
|
|
|
|
|
spesifikasjon. Fairwaytreff ekskluderer par-3-hull (samme regel som
|
|
|
|
|
|
registrerings-skjemaet, som aldri viser Utslag-retning der).
|
|
|
|
|
|
**Logikken verifisert manuelt mot et regnet eksempel** (4 hull, blandet
|
|
|
|
|
|
par 3/4/5, ulike utslag-/innspillresultater) FØR utrulling -- stableford-
|
|
|
|
|
|
sum, netto-sum og begge gjennomsnitts-splittene stemte med
|
|
|
|
|
|
håndregning. Ekte typesjekket produksjonsbuild kjørt og bekreftet (alle
|
|
|
|
|
|
19 ruter). **Rullet ut live 2026-07-25**, bruker bekreftet eksplisitt:
|
|
|
|
|
|
ren frontend-endring, ingen migrasjon, `/health`/`/my-rounds` → 200,
|
|
|
|
|
|
`teeoff.no` upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
**Rundestatistikk-skjerm (dypdykk), inspirert av en konkurrent-video,
|
|
|
|
|
|
BYGGET OG LIVE samme dag (2026-07-25):** brukeren lastet opp en 26
|
|
|
|
|
|
sekunders skjermopptaksvideo av en KONKURRENT-apps statistikkskjerm og
|
|
|
|
|
|
ba eksplisitt om et V0-prompt inspirert av innholdet, IKKE et plagiat.
|
|
|
|
|
|
Video analysert bilde for bilde (ffmpeg kjørt i en engangs Docker-
|
|
|
|
|
|
container, ikke installert på verten -- ryddet opp etterpå). Innhold
|
|
|
|
|
|
identifisert: score-fordeling (par/bogey/dobbel bogey/verre),
|
|
|
|
|
|
snitt-til-par per hulltype, fairwayfordeling + score-splitt, GIR-donut +
|
|
|
|
|
|
per-hulltype + kryss med fairway + score-splitt, bom-retning på green
|
|
|
|
|
|
som et kompass-diagram, putt-fordeling (1/2/3-putt) + snitt per hulltype
|
|
|
|
|
|
+ med/uten GIR, én-putt% etter puttlengde + lengdefordeling hit/miss,
|
|
|
|
|
|
chip-fordeling, scrambling%/sand save%, bunker/straffeslag per runde +
|
|
|
|
|
|
score-splitt.
|
|
|
|
|
|
**Bevisste valg for å unngå plagiat, skrevet inn i selve V0-promptet:**
|
|
|
|
|
|
egen visuell identitet (TeeCups presise grønn/oransje, ikke konkurrentens
|
|
|
|
|
|
fargekoding), fritt valg av chart-type/layout/rekkefølge til V0 selv,
|
|
|
|
|
|
INGEN kopiering av konkurrentens eksakte ordlyd/fargekoding/ikonografi.
|
|
|
|
|
|
Puttlengde-bøttene i promptet er TeeCups EGNE seks bøtter
|
|
|
|
|
|
(<1m/<2m/<3m/<5m/<8m/8m+, samme som ADR-033 allerede lagrer) -- bevisst
|
|
|
|
|
|
ANDRE enn videoens fem bøtter (<1m/1-2/2-4/4-8/+), både fordi det unngår
|
|
|
|
|
|
en direkte kopi og fordi det er det datamodellen faktisk allerede
|
|
|
|
|
|
produserer. "Lengste drive" (krever GPS/avstandsmåling appen ikke har)
|
|
|
|
|
|
utelatt fra promptet.
|
|
|
|
|
|
**Zip 14 mottatt og integrert samme dag:** diffet mot live-treet FØR noe
|
|
|
|
|
|
ble tatt inn (samme rutine som alltid) -- kun to reelt nye filer
|
|
|
|
|
|
(`components/round-stats.tsx`, en ny `RoundStats`-skjerm bygget med
|
|
|
|
|
|
kollapsbare `StatCard`-seksjoner, conic-gradient-donuter med sentertall,
|
|
|
|
|
|
avviks-stolper fra en null-linje, og et kompass-rutenett for bom-retning
|
|
|
|
|
|
-- en tydelig ANNEN visuell løsning enn konkurrentens skjermbilder, ikke
|
|
|
|
|
|
en klone). Resten av eksporten var V0s vanlige uvitende reverts, inkl.
|
|
|
|
|
|
sin egen `/rounds/[id]`-ruteversjon fra FØR `/my-rounds`-omdøpingen
|
|
|
|
|
|
(se lenger opp, "Reell produksjonsbug...") -- korrekt hoppet over. Ny
|
|
|
|
|
|
rute lagt til som `app/my-rounds/[id]/stats/page.tsx` (ikke
|
|
|
|
|
|
`app/rounds/[id]/stats` som eksporten foreslo), samme
|
|
|
|
|
|
kollisjon-unngåelse. `app/globals.css` sin nye `--chart-1..6`
|
|
|
|
|
|
data-viz-fargeskala (god->dårlig-skala, brukt sammen med
|
|
|
|
|
|
tekst-/tall-etiketter) slått sammen inn i alle tre temablokkene;
|
|
|
|
|
|
V0s egne, mer omtrentlige `--primary`/`--ring`/`--brand-orange`-verdier
|
|
|
|
|
|
i SAMME diff ble bevisst IKKE tatt inn -- beholdt de presise OKLCH-
|
|
|
|
|
|
verdiene utregnet i ADR-016.
|
|
|
|
|
|
**Datalag skrevet fullstendig om fra mock:** ny `computeStats()`-
|
|
|
|
|
|
funksjon i `round-stats.tsx` regner ut alt fra rå `GET .../holes`-data
|
|
|
|
|
|
(samme endepunkt round-detail.tsx allerede bruker -- ingen ny backend).
|
|
|
|
|
|
Hver seksjon skjules helt når det ikke finnes nok data for den (samme
|
|
|
|
|
|
"vis kun det som faktisk finnes"-prinsipp som `ScoreSoFar`). Ny enkel
|
|
|
|
|
|
spillervelger (pill-rad) lagt til når runden har flere deltakere,
|
|
|
|
|
|
default til eieren. `CompletedBanner` i round-detail.tsx fikk en "Se
|
|
|
|
|
|
full rundestatistikk"-lenke (fantes i V0s egen, ellers reverterte fil --
|
|
|
|
|
|
portert manuelt inn i vår live versjon i stedet for å ta hele filen).
|
|
|
|
|
|
**Matematikken verifisert FØR utrulling, ikke bare "kompilerer":**
|
|
|
|
|
|
`computeStats()`-logikken portert til et frittstående Node-script og
|
|
|
|
|
|
kjørt mot et hånd-etterregnet 6-hulls syntetisk datasett (blandet par
|
|
|
|
|
|
3/4/5, fairwaytreff/-bom, GIR-treff/-bom, bunkerslag) -- alle utledede
|
|
|
|
|
|
tall (score-kategorier, snitt-til-par per hulltype, fairway-splitt,
|
|
|
|
|
|
GIR%/kryss-fairway/score-splitt, bom-retning, putt-fordeling,
|
|
|
|
|
|
scrambling%, sand save%) stemte med manuell utregning. Ekte typesjekket
|
|
|
|
|
|
produksjonsbuild kjørt og bekreftet (`/my-rounds/[id]/stats` listet som
|
|
|
|
|
|
ny rute).
|
|
|
|
|
|
**Rullet ut live 2026-07-25**, ren frontend-endring, ingen migrasjon,
|
|
|
|
|
|
`/health`/`/my-rounds` → 200, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
**Rundeliste-info + scorekort-redesign, BYGGET OG LIVE samme dag
|
|
|
|
|
|
(2026-07-25):** brukeren delte en ny skjermopptaksvideo (video 2, 18
|
|
|
|
|
|
frames analysert ved 1,5 fps) av to problemer: "Egne runder"-listen
|
|
|
|
|
|
manglet ALL score-informasjon (kun tee/hull/dato/spillerantall/status),
|
|
|
|
|
|
og selve scorekort-registreringen (rapportert "veldig dårlig
|
|
|
|
|
|
designet") stablet fem ulike kontrolltyper (numpad, retningskors,
|
|
|
|
|
|
steppere, pill-rutenett, pill-rad) uten hierarki, pluss at "Runde
|
|
|
|
|
|
fullført"-siden gjentok NØYAKTIG samme tall tre ganger (CompletedBanner-
|
|
|
|
|
|
differensial, "Så langt"-oppsummeringslinjen, og StatPill-rutenettet
|
|
|
|
|
|
under). Bedt om å reflektere kort og eventuelt skrive V0-prompt(er).
|
|
|
|
|
|
**Reflektert og handlet i samme runde** (ikke bare skrevet ned): pekte ut
|
|
|
|
|
|
at score/til-par mangler helt fra listekortet som det klareste,
|
|
|
|
|
|
konkrete hullet; anbefalte å samle alt utover Slag/Putter bak én
|
|
|
|
|
|
kollapsbar "Flere detaljer"-seksjon som den viktigste enkeltfiksen for
|
|
|
|
|
|
scorekort-rotet; og fikset duplikat-problemet PÅ "Runde fullført"-siden
|
|
|
|
|
|
DIREKTE (ikke via V0) siden det bare er snakk om å SKJULE innhold, ikke
|
|
|
|
|
|
designe noe nytt: `ScoreSoFar` skjules nå helt når runden er fullført
|
|
|
|
|
|
(`{!completed && <ScoreSoFar .../>}`) -- CompletedBanner sin "Se full
|
|
|
|
|
|
rundestatistikk"-lenke dekker akkurat det samme, langt grundigere.
|
|
|
|
|
|
**Ny backend-beregning FØR V0-prompten** (slik at prompten kunne
|
|
|
|
|
|
referere til ekte, tilgjengelig data): `_load_round_out`
|
|
|
|
|
|
(`app/routers/rounds.py`) kjører nå en liten aggregatspørring mot
|
|
|
|
|
|
`round_hole` for eierens egen deltaker-rad ved hver lasting --
|
|
|
|
|
|
`owner_holes_played`/`owner_total_score`/`owner_score_to_par` (null
|
|
|
|
|
|
til minst ett hull er registrert). Verifisert med 10 scratch-sjekker,
|
|
|
|
|
|
inkl. at en gjests egen score IKKE lekker inn i eierens aggregat.
|
|
|
|
|
|
**To V0-prompter skrevet** (round-card-redesign med prominent
|
|
|
|
|
|
score-flis/hull-fremdrift-bar/differensial; scorekort-redesign med
|
|
|
|
|
|
Slag+Putter+Avstand-første-putt alltid synlig og alt annet bak "Flere
|
|
|
|
|
|
detaljer", pluss en bevisst ikke-bygget reservasjon av layout-plass for
|
|
|
|
|
|
en fremtidig avstandsmåling-funksjon brukeren bekreftet er planlagt).
|
|
|
|
|
|
**Zip 15 og 16 mottatt samme dag** (samme v0.app-prosjekt, kontinuerlig
|
|
|
|
|
|
-- `round-card.tsx`/`own-rounds.tsx` byte-for-byte identiske i begge
|
|
|
|
|
|
zip-ene; zip 16s `round-detail.tsx` var den nyeste med selve
|
|
|
|
|
|
kollaps-redesignet, zip 15s tilsvarende fil manglet det og ble derfor
|
|
|
|
|
|
forkastet til fordel for zip 16).
|
|
|
|
|
|
`round-card.tsx`: ny `ScoreTile` -- prominent resultat+til-par-flis
|
|
|
|
|
|
(fargekodet under/over par uten å stole på farge alene, tekst+tall
|
|
|
|
|
|
alltid med) for fullførte/scorede runder, en hull-fremdrift-progressbar
|
|
|
|
|
|
("6/18 hull spilt") for runder som fortsatt pågår, pluss en HCP-
|
|
|
|
|
|
differensial-chip i metadata-raden når runden faktisk telte. Fullt
|
|
|
|
|
|
`aria-label` på hele kortet for skjermlesere. `own-rounds.tsx` sitt
|
|
|
|
|
|
datalag skrevet om fra mock til ekte fetch, kobler de nye `owner_*`-
|
|
|
|
|
|
feltene fra backend + eierens `score_differential` (kun vist når
|
|
|
|
|
|
`counts_for_handicap`).
|
|
|
|
|
|
`round-detail.tsx`: ny kollapsbar "Flere detaljer"-seksjon (lukket som
|
|
|
|
|
|
default, `ChevronDown`-rotasjon på toggle) som nå rommer kølle/utslag-
|
|
|
|
|
|
retning/innspill-retning/chip-bunker-straffeslag/anywayslag -- Slag,
|
|
|
|
|
|
Putter og Avstand første putt forblir alltid synlig over kollapsen,
|
|
|
|
|
|
uendret rekkefølge fra den forrige runden. **Viktig integrasjonsdisiplin:**
|
|
|
|
|
|
V0s eksport hadde reversert til en MYE eldre mock-baseline (manglet
|
|
|
|
|
|
StatLevel-gating, Anywayslag, bøtte-basert puttlengde, kølle-bag fra
|
|
|
|
|
|
profil, maxValue-capping, "Hullet er spilt"-avkrysningen som bevisst BLE
|
|
|
|
|
|
FJERNET en tidligere runde, "Tid brukt" i CompletedBanner) -- kun selve
|
|
|
|
|
|
kollaps-mekanismen og plasseringen av "Flere detaljer" ble hentet ut og
|
|
|
|
|
|
lagt oppå den fullt oppdaterte, LIVE koden. Ingen av de tidligere
|
|
|
|
|
|
byggede funksjonene gikk tapt. Lagt til en kort kode-kommentar (ikke
|
|
|
|
|
|
noe bygget UI) som reserverer plass ved siden av hull-headeren til en
|
|
|
|
|
|
fremtidig avstandsmåling-indikator.
|
|
|
|
|
|
**Verifisert:** ekte typesjekket produksjonsbuild kjørt og bekreftet
|
|
|
|
|
|
(alle 19 ruter). Ingen ny scratch-backend-runde nødvendig utover
|
|
|
|
|
|
owner-score-testen over (ingen ny domenelogikk i selve rundeliste-/
|
|
|
|
|
|
scorekort-visningen, kun lesing av allerede-testede felt).
|
|
|
|
|
|
**Rullet ut live 2026-07-25**, ren frontend-endring + den lille
|
|
|
|
|
|
backend-tilføyelsen over, ingen migrasjon, `/health`/`/my-rounds` →
|
|
|
|
|
|
200, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
**Manglende scorekort på statistikk-siden, funnet og fikset SAMME dag
|
|
|
|
|
|
(2026-07-25):** brukeren spurte rett etter forrige runde: "Hvor er
|
|
|
|
|
|
scorekortet? (Gjerne også med statistikk?)" -- et ekte, uforutsett hull
|
|
|
|
|
|
i forrige rundes endring. Da `ScoreSoFar` ("Så langt i runden") ble
|
|
|
|
|
|
skjult for fullførte runder (for å fjerne dobbel informasjon, se over),
|
|
|
|
|
|
forsvant OGSÅ det eneste stedet den rå hull-for-hull-tabellen (Hull/
|
|
|
|
|
|
Par/Score/Netto/Sum) fantes -- ingen tilsvarende tabell ble noensinne
|
|
|
|
|
|
lagt til på den nye `round-stats.tsx`-siden, som kun inneholdt UTLEDET/
|
|
|
|
|
|
aggregert statistikk (donuter, kategori-stolper, avviks-visualiseringer),
|
|
|
|
|
|
aldri de faktiske rå tallene per hull. Med andre ord: brukeren fikk
|
|
|
|
|
|
riktignok fjernet duplikatet, men mistet samtidig tilgang til selve
|
|
|
|
|
|
scorekortet -- en reell regresjon, ikke bare en presentasjonsdetalj.
|
|
|
|
|
|
**Fikset ved å legge til en ny "Scorekort"-seksjon FØRST på
|
|
|
|
|
|
`round-stats.tsx`** (før "Scorer"-kategoriseksjonen), åpen som default
|
|
|
|
|
|
(i motsetning til resten av seksjonene som starter kollapsbare men
|
|
|
|
|
|
åpne -- denne er det brukeren eksplisitt spurte etter, så den skal ikke
|
|
|
|
|
|
kreve et ekstra trykk). Samme tabellmønster som `ScoreSoFar` sin
|
|
|
|
|
|
tidligere tabell hadde (Hull/Par/Score/Netto/Stableford/Sum, Stableford-
|
|
|
|
|
|
kolonnen vist kun når netto er beregnbart). To type-tilføyelser var
|
|
|
|
|
|
nødvendig: `start_hole: number` lagt til `round-stats.tsx` sin
|
|
|
|
|
|
`ApiRound`-type (manglet fra før, siden ingen tidligere seksjon på
|
|
|
|
|
|
denne siden trengte rundens faktiske start-/rekkefølge) og
|
|
|
|
|
|
`strokes_received: number | null` lagt til `ApiHole`-typen (feltet kom
|
|
|
|
|
|
allerede fra backend -- lagt til i forrige runde for `list_holes` --
|
|
|
|
|
|
men ble aldri lest av denne siden før nå). Ny `holeOrder`/
|
|
|
|
|
|
`orderedHoles`-beregning i `RoundStats` gjenbruker EKSAKT samme
|
|
|
|
|
|
sirkulære start_hole-formel som round-detail.tsx, slik at en 9-hulls
|
|
|
|
|
|
runde som starter på hull 10 vises i riktig spillerekkefølge (10-18),
|
|
|
|
|
|
ikke bare rå hullnummer 1-9.
|
|
|
|
|
|
**Lærdom notert for fremtidige runder:** når et helt panel flyttes/
|
|
|
|
|
|
skjules for å fjerne duplisert informasjon, må hver del av det gamle
|
|
|
|
|
|
panelet spores til et nytt hjem FØR det fjernes -- ikke bare de delene
|
|
|
|
|
|
som åpenbart var "statistikk". Denne runden ble oppdaget kun fordi
|
|
|
|
|
|
brukeren faktisk lette etter scorekortet rett etterpå, ikke ved egen
|
|
|
|
|
|
verifisering før utrulling.
|
|
|
|
|
|
Ekte typesjekket produksjonsbuild kjørt og bekreftet. **Rullet ut live
|
|
|
|
|
|
2026-07-25**, ren frontend-endring, ingen backend/migrasjon,
|
|
|
|
|
|
`teeoff.no` upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
**Scorekort-visning: ny presentasjonsregel + V0-prompt skrevet, IKKE
|
|
|
|
|
|
bygget ennå (2026-07-25):** brukeren delte et referansebilde av et
|
|
|
|
|
|
tradisjonelt fysisk golf-scorekort (horisontal layout, hull 1-9/10-18
|
|
|
|
|
|
som KOLONNER, Slope/Par/Score/Net som rader, "Ut"/"Inn"-sum-kolonne
|
|
|
|
|
|
YTTERST TIL HØYRE for hver ni-hulls-halvdel) og formulerte en generell
|
|
|
|
|
|
presentasjonsregel: **listes hullene horisontalt (som kolonner), skal
|
|
|
|
|
|
summeringen stå TIL HØYRE; listes hullene vertikalt (som rader), skal
|
|
|
|
|
|
summeringen stå UNDER.** Den nylig byggede "Scorekort"-seksjonen på
|
|
|
|
|
|
`round-stats.tsx` (se punktet rett over) lister hull VERTIKALT (én rad
|
|
|
|
|
|
per hull) med en løpende sum-KOLONNE innimellom hver rad -- ikke i tråd
|
|
|
|
|
|
med regelen (en vertikal liste burde hatt en avsluttende sumrad
|
|
|
|
|
|
UNDERST, ikke en kolonne). Bedt om å tenke gjennom dette og skrive et
|
|
|
|
|
|
V0-prompt for et "fabelaktig" scorekort inspirert av (ikke et plagiat
|
|
|
|
|
|
av) referansebildet.
|
|
|
|
|
|
**Bevisste avvik fra referansebildet for å unngå plagiat, skrevet inn i
|
|
|
|
|
|
selve promptet:** egen visuell identitet (TeeCups grønn/oransje, ikke
|
|
|
|
|
|
bildets rød-sirkel/blå-firkant-fargekoding for birdie/bogey), egen
|
|
|
|
|
|
term ("Hcp" for hullets slag-fordelings-rangering -- IKKE "Slope", som
|
|
|
|
|
|
i golf-terminologi betyr banens/tee-ens helhetlige vanskelighetsgrad,
|
|
|
|
|
|
et helt annet tall enn det bildet faktisk viser per hull), en ekstra
|
|
|
|
|
|
Stableford-rad (finnes ikke i referansen, men vi beregner det allerede
|
|
|
|
|
|
andre steder), og INGEN kopiering av bildets spiller-header-komposisjon
|
|
|
|
|
|
(navn/HCP/posisjon-boksen) -- kun selve tabell-strukturen er
|
|
|
|
|
|
inspirasjonskilden.
|
|
|
|
|
|
**Presist håndtert i promptet, IKKE triviell:** appen støtter allerede
|
|
|
|
|
|
vilkårlig `start_hole` + `holes_planned` (9 ELLER 18, sirkulær
|
|
|
|
|
|
rekkefølge, se "Manglende scorekort"-punktet over) -- en STIV
|
|
|
|
|
|
fysisk "hull 1-9 er alltid Ut" ville vært feil for en 9-hulls runde som
|
|
|
|
|
|
starter på hull 10. Promptet ber derfor om ÉN 9-kolonners blokk når
|
|
|
|
|
|
`holes_planned=9`, TO blokker (første/andre halvdel AV SPILLEREKKEFØLGEN,
|
|
|
|
|
|
ikke nødvendigvis fysisk hull 1-9/10-18) når `holes_planned=18` -- den
|
|
|
|
|
|
faktiske hull-til-blokk-tildelingen løses av meg i datalaget ved
|
|
|
|
|
|
integrering, som med alle tidligere V0-runder.
|
|
|
|
|
|
**Venter på V0-eksport** før noe bygges. Når den kommer, erstatter den
|
|
|
|
|
|
den eksisterende vertikale "Scorekort"-tabellen på `round-stats.tsx`
|
|
|
|
|
|
(bygget rett over samme dag) -- ikke en ny, separat skjerm.
|
|
|
|
|
|
**Notert som en generell prinsipp-lærdom, relevant utover akkurat dette
|
|
|
|
|
|
scorekortet:** samme horisontal-til-høyre/vertikal-til-under-regel bør
|
|
|
|
|
|
vurderes senere for turnering-scorekortet (`session-scorecard.tsx`,
|
|
|
|
|
|
match-play) den dagen det scorekortet også skal pusses -- ikke i
|
|
|
|
|
|
omfang nå, bare notert for konsistens.
|
|
|
|
|
|
|
|
|
|
|
|
**Presisering av promptet samme dag, FØR noe ble sendt til V0:**
|
|
|
|
|
|
brukeren spurte eksplisitt om et "if REALLY necessary"-unntak for
|
|
|
|
|
|
horisontal scroll (opprinnelig formulering) faktisk fanget opp målet
|
|
|
|
|
|
om ALDRI å måtte scrolle. Vurdert og svart nei -- reell breddekonflikt,
|
|
|
|
|
|
ikke bare en formulering-detalj: 9 hull + 1 sum-kolonne (+ en
|
|
|
|
|
|
label-kolonne) får ikke plass på en telefonskjerm med normal
|
|
|
|
|
|
"lesbar uten briller"-tekststørrelse, og et unntak formulert som en
|
|
|
|
|
|
myk fallback ville sannsynligvis latt V0 falle tilbake til scroll uansett,
|
|
|
|
|
|
siden tilgjengelighetskravet rett under ga et påskudd. **Løst ved å
|
|
|
|
|
|
gjøre "ingen scroll" til et HARDT krav i promptet, OG eksplisitt fortelle
|
|
|
|
|
|
V0 hvordan det oppnås** (kompakte, fete, høykontrast-siffer i selve
|
|
|
|
|
|
rutenettet -- samme konvensjon som et fysisk scorekort bruker, også
|
|
|
|
|
|
synlig i brukerens eget referansebilde -- mens "lesbar uten
|
|
|
|
|
|
briller"-kravet eksplisitt avgrenses til labels/knapper/løpetekst, ikke
|
|
|
|
|
|
hvert enkelt rutenett-siffer). Smale forkortede rad-labels ("Hcp",
|
|
|
|
|
|
"Par", "Score", "Netto") i en trang venstre-gutter i stedet for en bred
|
|
|
|
|
|
tekstkolonne, for å frigjøre bredde til de 9 hull-kolonnene.
|
|
|
|
|
|
|
|
|
|
|
|
**Zip 17 mottatt, horisontalt scorekort BYGGET OG LIVE samme dag
|
|
|
|
|
|
(2026-07-25):** V0 leverte akkurat det det reviderte promptet ba om --
|
|
|
|
|
|
en EKTE HTML `<table>` med `<colgroup>` (fast `w-9`-label-kolonne, auto
|
|
|
|
|
|
for hver hull-kolonne, `w-[11%]` sum-kolonne) og `table-fixed`, ingen
|
|
|
|
|
|
scroll-container noe sted. Score-cellene bruker FORM (sirkel = under
|
|
|
|
|
|
par, firkant = over par) + fylt/ufylt (fylt = 2+ slag av) i stedet for
|
|
|
|
|
|
farge alene, pluss en egen liten symbolforklaring under rundesammendraget
|
|
|
|
|
|
-- tilfredsstiller "aldri stole på farge alene" uten å kopiere
|
|
|
|
|
|
referansebildets rød-sirkel/blå-firkant-konvensjon.
|
|
|
|
|
|
**Egen, ny dedikert side** `/my-rounds/[id]/scorecard`
|
|
|
|
|
|
(`components/round-scorecard.tsx`, ny `app/my-rounds/[id]/scorecard/
|
|
|
|
|
|
page.tsx`) -- IKKE slått sammen inn i `round-stats.tsx`, siden V0
|
|
|
|
|
|
designet komponenten med sin egen fulle side-chrome (sticky header,
|
|
|
|
|
|
rundesammendrag-strip), ikke som et innebygd tabell-fragment. Dette gir
|
|
|
|
|
|
en klar arbeidsdeling: `round-scorecard.tsx` = det rå scorekortet,
|
|
|
|
|
|
`round-stats.tsx` = utledet/aggregert statistikk -- samme prinsipp som
|
|
|
|
|
|
allerede etablert for `ScoreSoFar` vs. `round-stats.tsx` tidligere denne
|
|
|
|
|
|
økten.
|
|
|
|
|
|
**Bevisst forkastet fra V0s eksport:** V0s egen `strokesReceived()`-
|
|
|
|
|
|
funksjon var en generisk `Math.floor(hcp/18) + (index <= hcp%18 ? 1 :
|
|
|
|
|
|
0)`-modulo-formel -- byttet ut med backend sin ALLEREDE beregnede
|
|
|
|
|
|
`strokes_received` per hull (samme `allocate_strokes_by_index()` som
|
|
|
|
|
|
resten av appen bruker), for å unngå to parallelle, potensielt
|
|
|
|
|
|
avvikende implementasjoner av HCP-slagfordeling i samme app. Samme
|
|
|
|
|
|
sirkulære `start_hole`-rekkefølge som `round-detail.tsx`/
|
|
|
|
|
|
`round-stats.tsx` bruker for Ut/Inn-blokkene. Ny spillervelger
|
|
|
|
|
|
(pill-rad) lagt til for runder med flere deltakere, samme mønster som
|
|
|
|
|
|
`round-stats.tsx`.
|
|
|
|
|
|
**Opprydning i `round-stats.tsx`:** den midlertidige vertikale
|
|
|
|
|
|
"Scorekort"-tabellen (bygget tidligere samme dag som en rask fiks for
|
|
|
|
|
|
"Hvor er scorekortet?") er FJERNET og erstattet med en enkel, prominent
|
|
|
|
|
|
"Se scorekort"-lenke til den nye siden, rett under rundesammendraget --
|
|
|
|
|
|
`round-stats.tsx` er dermed nå rendyrket aggregert/utledet statistikk,
|
|
|
|
|
|
det rå scorekortet finnes kun ett sted. `stablefordPoints()`-
|
|
|
|
|
|
hjelpefunksjonen og `holeOrder`/`orderedHoles`-beregningen i
|
|
|
|
|
|
`round-stats.tsx`, som kun eksisterte for den fjernede tabellen, fjernet
|
|
|
|
|
|
som dødt kode.
|
|
|
|
|
|
**V0 la selv til en to-knappers layout i `CompletedBanner`**
|
|
|
|
|
|
(round-detail.tsx) -- "Se scorekort" (primær, fylt) ved siden av den
|
|
|
|
|
|
eksisterende "Se full rundestatistikk" (sekundær, omrisset) -- tatt inn
|
|
|
|
|
|
uendret bortsett fra å rette `/rounds/...`-hrefs til `/my-rounds/...`.
|
|
|
|
|
|
**Samtidig, urelatert, rapportert av bruker midt i integreringen:**
|
|
|
|
|
|
Anywayslag manglet helt fra `round-stats.tsx` sin "Chip, bunker og
|
|
|
|
|
|
straffeslag"-seksjon (verken `anyway_strokes` i `ApiHole`-typen eller
|
|
|
|
|
|
noe utledet total fantes). Lagt til `anyway_strokes` i typen, ny
|
2026-07-25 05:32:23 +02:00
|
|
|
|
`anywayPerRound`-aggregat i `computeStats()`.
|
|
|
|
|
|
**Feilrettet SAMME dag, rett etterpå:** min første fiks slo sammen
|
|
|
|
|
|
Anywayslag-tallet INN I den eksisterende "Chip, bunker og
|
|
|
|
|
|
straffeslag"-seksjonen og omdøpte HELE seksjonen til "Annet" -- feil,
|
|
|
|
|
|
påpekt av bruker. Rettet: "Chip, bunker og straffeslag" beholder sitt
|
|
|
|
|
|
opprinnelige navn og innhold UENDRET (Bunkerslag/Straffeslag-flisene
|
|
|
|
|
|
tilbake til to, ikke tre). Ny, EGEN "Annet"-seksjon lagt til RETT ETTER
|
|
|
|
|
|
den (ikke slått sammen), med kun Anywayslag-statistikk: total per runde
|
|
|
|
|
|
+ andel hull med minst ett anywayslag (samme "andel hull med..."-mønster
|
|
|
|
|
|
som straffeslag-statistikken). Ny `pctHolesWithAnyway`-beregning i
|
|
|
|
|
|
`computeStats()`. Den midlertidige `StatTileRow`-komponenten (bygget for
|
|
|
|
|
|
den feilaktig sammenslåtte tre-flis-varianten) fjernet igjen som dødt
|
|
|
|
|
|
kode -- `StatTilePair` (fast to fliser) er tilstrekkelig for begge
|
|
|
|
|
|
seksjonene nå. Notert for senere: brukeren ser for seg at et
|
|
|
|
|
|
fritekst-notatfelt havner i "Annet"-seksjonen etter hvert.
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
**Verifisert:** ekte typesjekket produksjonsbuild kjørt og bekreftet,
|
|
|
|
|
|
ny rute `/my-rounds/[id]/scorecard` listet. Ingen ny backend-endring
|
|
|
|
|
|
(`anyway_strokes`/`strokes_received` fantes allerede i `RoundHoleOut`).
|
2026-07-25 05:32:23 +02:00
|
|
|
|
**Rullet ut live 2026-07-25** (to runder, feilrettingen rullet ut rett
|
|
|
|
|
|
etter originalen), ren frontend-endring, ingen migrasjon, `teeoff.no`
|
|
|
|
|
|
upåvirket.
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
|
Første byggesteg ferdig: HCP-indeks-motoren.
Bygget og testet i handicap_engine.py — ren Python, ingen database/API rørt ennå (samme "test i isolasjon først"-prinsipp som resten av motoren). 9 nye funksjoner dekker hele kjeden fra rå hullscore til en ferdig, cap-justert HCP-indeks: Net Par/Net Double Bogey, Adjusted Gross Score, Score Differential, Handicap Index (beste-8-av-20 med opptrappingstabell for nye spillere), Low Handicap Index, soft/hard cap, og den avvikende 9-hulls Course Handicap-formelen.
41/41 tester bestått, flere verifisert direkte mot regelbokens egne tallregneeksempler (ikke bare intern konsistens) — blant annet begge Rule 5.2a-eksemplene (13,2 / 34,1 / 37,4), alle tre Rule 5.1c-avrundingseksemplene, og soft/hard cap-oppførselen fra Diagram 5.8. For Diagram 3.1b (Net Double Bogey-eksempelet) var jeg åpen om en reell begrensning: front-9 stemte eksakt mot bildet, men jeg kunne ikke garantere hvert enkelt siffer i back-9-scorerekken fra bilde-oppløsningen — testen bruker derfor kun det ene tydelig annoterte, kildebelagte tallet (hull 17: brutto 9, capped til 7) og egenkomponerte tall for resten, dokumentert i kommentaren.
Dokumentasjonen (ADR-033, CLAUDE.md, FEATURE_BACKLOG.md) er oppdatert til å reflektere at bygging er påbegynt.
2026-07-22 10:28:01 +02:00
|
|
|
|
**Motor-komponenten (punkt 1-11) er ✅ BYGGET OG TESTET 2026-07-22,**
|
|
|
|
|
|
som første, isolerte byggesteg (ren Python, ingen DB/API/frontend ennå —
|
|
|
|
|
|
matcher ADR-005s "test i isolasjon FØR resten"). Nye funksjoner i
|
|
|
|
|
|
`handicap_engine.py`: `net_par`, `max_hole_score_for_handicap`,
|
|
|
|
|
|
`adjusted_gross_score`, `round_half_up_decimal`, `score_differential`,
|
|
|
|
|
|
`handicap_index_from_differentials`, `low_handicap_index`,
|
|
|
|
|
|
`apply_index_caps`, `course_handicap_9_raw`/`course_handicap_9`.
|
|
|
|
|
|
**Bevisst avvik fra opprinnelig plan, instruert av bruker 2026-07-22:**
|
|
|
|
|
|
"Expected Score" (Rule 3.2b) sin upubliserte formel erstattes gjennomgående
|
|
|
|
|
|
av WHS sin egen, presist definerte "Net Par"-term (par + mottatte
|
|
|
|
|
|
handicapslag = 2 Stableford-poeng) for uspilte hull — gjelder BÅDE
|
|
|
|
|
|
ufullstendige 18-hulls-runder og konvertering av en 9-hulls-runde til
|
|
|
|
|
|
18-hulls-ekvivalent (Rule 5.1b sin egen separate 9-hulls-differensial-
|
|
|
|
|
|
formel er dermed bevisst IKKE implementert, se punkt 5 over).
|
|
|
|
|
|
**41/41 tester bestått** (`test_handicap_engine.py`, kjørt uten pytest —
|
|
|
|
|
|
ikke installert i miljøet, kun den innebygde selvsjekk-runneren), 17 nye
|
|
|
|
|
|
i tillegg til de 24 eksisterende. Flere verifisert mot regelbokens EGNE
|
|
|
|
|
|
tallregneeksempler, ikke bare intern konsistens: Rule 5.2a sine to
|
|
|
|
|
|
initial-indeks-eksempler (13,2 og 34,1, samt oppfølgingen til 37,4),
|
|
|
|
|
|
Rule 5.1c sine tre avrundingseksempler, Diagram 5.8 sin soft-/hard-cap-
|
|
|
|
|
|
oppførsel, og Diagram 3.1b sin Net-Double-Bogey-capping (der front-9 ble
|
|
|
|
|
|
verifisert eksakt mot diagrammet, mens back-9 sine åtte ikke-annoterte
|
|
|
|
|
|
scorer bevisst ble egenkomponert pga. usikker bilde-lesing av akkurat de
|
|
|
|
|
|
sifrene — se testens egen kommentar for full transparens om hva som er
|
|
|
|
|
|
kildebelagt og hva som ikke er det).
|
|
|
|
|
|
**Gjenstår:** migrasjon (nye tabeller for runde/deltaker/statistikk),
|
|
|
|
|
|
API-lag, frontend — ingen av disse er startet.
|
|
|
|
|
|
|
ADR-033 er nå fullt kildebelagt. Kort oppsummert hva som endret seg:
Banedata: bekreftet — live oppslag mot teeoff, ikke import.
HCP: leste hele den offisielle WHS Rules of Handicapping 2024 (Rule 2, 3, 5, 6). To viktige presiseringer av det du selv antok:
«Minst 9 hull» stemmer ikke helt — en 9-hulls-runde må ha alle 9 hull i et faktisk ratet sett (front eller back), mens en 18-hulls-runde bare trenger minst 10 av 18 (resten fylles med en «expected score», ny metode fra 2024 som erstattet den gamle «net par»-metoden). Dette betyr også at fritt starthull (Beslutning E) fungerer fint for vanlig logging, men en runde blir kun HCP-tellende hvis de spilte hullene faktisk samsvarer med banens ratede 18/front-9/back-9 — en vilkårlig 9-hulls-strekning har ingen egen rating.
Fant en presis, tidligere ukjent detalj: en 9-hulls Course Handicap halverer indeksen først (Index÷2 × Slope/113 + (Rating−Par)) — helt annen formel enn 18-hulls-varianten. Viktig: dette gjelder frittstående 9-hulls-runder, ikke det eksisterende front_9/back_9-øktoppsettet i turneringsflyten, som løser et annet problem (slagfordeling internt i en match) og bevisst skal la det være.
Full kjede (Score Differential, Net Double Bogey, beste-8-av-20, Low Handicap Index, soft/hard cap, Course/Playing Handicap) er nå skrevet inn i ADR-033 med eksakte tall fra kilden. PCC og Exceptional-Score-justering er bevisst avgrenset ut av v1 (forstått, men krever data på tvers av alle brukeres runder samme dag — egen, senere runde).
2026-07-22 09:26:08 +02:00
|
|
|
|
### Beslutning H — Shotgun- vs. fortløpende start: EGEN, separat ADR (ADR-034)
|
|
|
|
|
|
|
|
|
|
|
|
Bekreftet med bruker: dette er et turnering/økt-konsept (start_hole per
|
|
|
|
|
|
match i stedet for per økt, samme klokkeslett for alle grupper), uten
|
|
|
|
|
|
reell avhengighet til rundeførings-arkitekturen over. Ikke behandlet
|
|
|
|
|
|
videre her.
|
|
|
|
|
|
|
|
|
|
|
|
### Ikke besluttet, gjenstår før bygging kan starte
|
|
|
|
|
|
|
|
|
|
|
|
- Den globale banekatalogen for EGENDEFINERTE (ikke-teeoff) baner
|
|
|
|
|
|
(del av Beslutning C) — naturlig konsekvens av live-oppslag-
|
|
|
|
|
|
beslutningen, men ikke bekreftet punkt for punkt ennå.
|
|
|
|
|
|
- Gjest-e-post-kobling (Beslutning D) — notert, ikke designet i detalj.
|
|
|
|
|
|
- Manuell vs. auto-beregnet HCP-indeks etter at motoren finnes
|
|
|
|
|
|
(Beslutning G) — notert, ikke avgjort.
|
|
|
|
|
|
- Skal turnering-scoring til slutt bruke SAMME statistikk-modell (reist i
|
|
|
|
|
|
brainstorm-runden 2026-07-22, FEATURE_BACKLOG.md) — ikke avgjort, ingen
|
|
|
|
|
|
konsekvens for denne ADR-ens omfang uansett.
|
|
|
|
|
|
- Eksakte tabellnavn/skjema (`round`/`round_participant`/
|
|
|
|
|
|
`round_hole_stat` er arbeidsnavn i denne ADR-en, ikke endelig
|
|
|
|
|
|
fastlagt) — avgjøres ved migrasjonsskriving.
|
|
|
|
|
|
|
Første byggesteg ferdig: HCP-indeks-motoren.
Bygget og testet i handicap_engine.py — ren Python, ingen database/API rørt ennå (samme "test i isolasjon først"-prinsipp som resten av motoren). 9 nye funksjoner dekker hele kjeden fra rå hullscore til en ferdig, cap-justert HCP-indeks: Net Par/Net Double Bogey, Adjusted Gross Score, Score Differential, Handicap Index (beste-8-av-20 med opptrappingstabell for nye spillere), Low Handicap Index, soft/hard cap, og den avvikende 9-hulls Course Handicap-formelen.
41/41 tester bestått, flere verifisert direkte mot regelbokens egne tallregneeksempler (ikke bare intern konsistens) — blant annet begge Rule 5.2a-eksemplene (13,2 / 34,1 / 37,4), alle tre Rule 5.1c-avrundingseksemplene, og soft/hard cap-oppførselen fra Diagram 5.8. For Diagram 3.1b (Net Double Bogey-eksempelet) var jeg åpen om en reell begrensning: front-9 stemte eksakt mot bildet, men jeg kunne ikke garantere hvert enkelt siffer i back-9-scorerekken fra bilde-oppløsningen — testen bruker derfor kun det ene tydelig annoterte, kildebelagte tallet (hull 17: brutto 9, capped til 7) og egenkomponerte tall for resten, dokumentert i kommentaren.
Dokumentasjonen (ADR-033, CLAUDE.md, FEATURE_BACKLOG.md) er oppdatert til å reflektere at bygging er påbegynt.
2026-07-22 10:28:01 +02:00
|
|
|
|
**Status: 🔨 ADR skrevet OG kildebelagt 2026-07-22, BYGGING PÅBEGYNT.**
|
|
|
|
|
|
Alle tre store åpne punktene fra første utkast (banedata, WHS 9-hulls-
|
|
|
|
|
|
regel, full HCP-indeksformel) er enten eksplisitt bekreftet med bruker
|
ADR-033 er nå fullt kildebelagt. Kort oppsummert hva som endret seg:
Banedata: bekreftet — live oppslag mot teeoff, ikke import.
HCP: leste hele den offisielle WHS Rules of Handicapping 2024 (Rule 2, 3, 5, 6). To viktige presiseringer av det du selv antok:
«Minst 9 hull» stemmer ikke helt — en 9-hulls-runde må ha alle 9 hull i et faktisk ratet sett (front eller back), mens en 18-hulls-runde bare trenger minst 10 av 18 (resten fylles med en «expected score», ny metode fra 2024 som erstattet den gamle «net par»-metoden). Dette betyr også at fritt starthull (Beslutning E) fungerer fint for vanlig logging, men en runde blir kun HCP-tellende hvis de spilte hullene faktisk samsvarer med banens ratede 18/front-9/back-9 — en vilkårlig 9-hulls-strekning har ingen egen rating.
Fant en presis, tidligere ukjent detalj: en 9-hulls Course Handicap halverer indeksen først (Index÷2 × Slope/113 + (Rating−Par)) — helt annen formel enn 18-hulls-varianten. Viktig: dette gjelder frittstående 9-hulls-runder, ikke det eksisterende front_9/back_9-øktoppsettet i turneringsflyten, som løser et annet problem (slagfordeling internt i en match) og bevisst skal la det være.
Full kjede (Score Differential, Net Double Bogey, beste-8-av-20, Low Handicap Index, soft/hard cap, Course/Playing Handicap) er nå skrevet inn i ADR-033 med eksakte tall fra kilden. PCC og Exceptional-Score-justering er bevisst avgrenset ut av v1 (forstått, men krever data på tvers av alle brukeres runder samme dag — egen, senere runde).
2026-07-22 09:26:08 +02:00
|
|
|
|
(Beslutning C) eller presist kildebelagt fra den offisielle WHS Rules of
|
|
|
|
|
|
Handicapping 2024 (Beslutning F/G) — ikke lenger antatt eller tilnærmet.
|
Migrasjonen er kjørt, og jeg har fortsatt helt til et komplett, verifisert API-lag.
Underveis fant og fikset jeg et reelt hull: round_participant manglet kolonner for selve rating-tallene (kun tee-navnet var lagret) — løst med migrasjon 021, kjørt mot ekte database sammen med resten.
Bygget og rullet ut:
app/routers/rounds.py — opprett/liste/hent/slett runde, legg til/fjern gjestedeltakere, registrer statistikk hull for hull, og en fullfør-endepunkt som kjører hele kjeden (Adjusted Gross Score → Score Differential → tellende-vurdering).
26 scratch-sjekker grønt, pluss en egen test mot den ekte kjørende teeoff-tjenesten som bekreftet at live-oppslaget fungerer nøyaktig som tiltenkt (ingen bane-kopi skrives noe sted).
Begge migrasjoner + ny kode er nå live mot ekte teecup_db/teecup_api. teeoff.no upåvirket.
Jeg la også inn /rounds og /personal-courses i frontendens proxy-oppsett proaktivt, siden prosjektet har blitt bitt av glemte proxy-ruter før — ikke deployet ennå, tas med neste gang frontend faktisk bygges.
Notatet om slaglengde-måling og avstand-til-punkter-på-banen er lagret i CLAUDE.md og FEATURE_BACKLOG.md, med en tydelig flagget avhengighet: dette krever geografiske/GPS-data som ingen nåværende kilde (verken teeoff eller den nye banekatalogen) har.
2026-07-22 11:31:08 +02:00
|
|
|
|
**Tre byggesteg ferdig samme dag, alle rullet ut mot ekte systemer:**
|
|
|
|
|
|
(1) hele HCP-indeks-motor-komponenten (Beslutning G, punkt 1-11) bygget
|
|
|
|
|
|
og testet i `handicap_engine.py` (43/43 tester), (2) full
|
|
|
|
|
|
databasemigrasjon (`020_personal_rounds.sql` + rettefiksen `021_round_
|
|
|
|
|
|
participant_rating_snapshot.sql`, åtte tabeller/utvidelser) kjørt mot
|
|
|
|
|
|
ekte `teecup_db`, (3) fullt API-lag (`app/routers/rounds.py`) bygget,
|
|
|
|
|
|
scratch-verifisert (26 sjekker + egen teeoff-live-oppslag-test) og
|
|
|
|
|
|
redeployet (`teecup_api`). **Gjenstår: HELE frontend-en** — ingenting
|
|
|
|
|
|
bygget ennå. Notat fra bruker 2026-07-22 (IKKE designet): planer om
|
|
|
|
|
|
slaglengde-måling + avstand-til-punkter-på-banen (golf-GPS/rangefinder),
|
|
|
|
|
|
krever geografiske data ingen kilde har i dag — se FEATURE_BACKLOG.md.
|
ADR-033 er nå fullt kildebelagt. Kort oppsummert hva som endret seg:
Banedata: bekreftet — live oppslag mot teeoff, ikke import.
HCP: leste hele den offisielle WHS Rules of Handicapping 2024 (Rule 2, 3, 5, 6). To viktige presiseringer av det du selv antok:
«Minst 9 hull» stemmer ikke helt — en 9-hulls-runde må ha alle 9 hull i et faktisk ratet sett (front eller back), mens en 18-hulls-runde bare trenger minst 10 av 18 (resten fylles med en «expected score», ny metode fra 2024 som erstattet den gamle «net par»-metoden). Dette betyr også at fritt starthull (Beslutning E) fungerer fint for vanlig logging, men en runde blir kun HCP-tellende hvis de spilte hullene faktisk samsvarer med banens ratede 18/front-9/back-9 — en vilkårlig 9-hulls-strekning har ingen egen rating.
Fant en presis, tidligere ukjent detalj: en 9-hulls Course Handicap halverer indeksen først (Index÷2 × Slope/113 + (Rating−Par)) — helt annen formel enn 18-hulls-varianten. Viktig: dette gjelder frittstående 9-hulls-runder, ikke det eksisterende front_9/back_9-øktoppsettet i turneringsflyten, som løser et annet problem (slagfordeling internt i en match) og bevisst skal la det være.
Full kjede (Score Differential, Net Double Bogey, beste-8-av-20, Low Handicap Index, soft/hard cap, Course/Playing Handicap) er nå skrevet inn i ADR-033 med eksakte tall fra kilden. PCC og Exceptional-Score-justering er bevisst avgrenset ut av v1 (forstått, men krever data på tvers av alle brukeres runder samme dag — egen, senere runde).
2026-07-22 09:26:08 +02:00
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
**Sju punkter fra faktisk bruk, BYGGET OG LIVE 2026-07-24** (rapportert av
|
|
|
|
|
|
bruker som selv testet scorekort-skjermen): (1) `currentHole` respekterte
|
|
|
|
|
|
aldri `round.start_hole` (`useState<number>(1)` + `prev || start_hole` —
|
|
|
|
|
|
`1` er truthy i JS, så `||` ble en no-op), fikset ved å bruke `null` som
|
|
|
|
|
|
"ikke satt ennå"-tilstand og en sirkulær 18-hulls navigasjonsrekkefølge
|
|
|
|
|
|
fra starthullet. Trolig rot-årsak til det samtidig rapporterte GIR-
|
|
|
|
|
|
avviket: selve formelen (`slag − putter ≤ par − 2`) var allerede
|
|
|
|
|
|
matematisk identisk med regelen brukeren beskrev, men feil hull i fokus
|
|
|
|
|
|
ga feil par inn i en ellers korrekt formel. (2) Kølle-bag på personlig
|
|
|
|
|
|
profil — 28 faste kølletyper (`BAG_CLUBS` i `app/routers/auth.py`,
|
|
|
|
|
|
speilet i frontend), maks 14 (den ekte golfregelen, håndhevet i både
|
|
|
|
|
|
Pydantic og en CHECK-constraint), brukt som knapp-utvalg for "kølle
|
|
|
|
|
|
brukt ved utslaget" for runde-eieren (gjester har ingen profil, beholder
|
|
|
|
|
|
fritekst). (3) Nytt statistikkfelt "Anywayslag", siste punkt i "Flere
|
|
|
|
|
|
detaljer", samme tallvelger-stil som slag/putter (ikke en liten
|
|
|
|
|
|
stepper). (4) Valgfritt statistikknivå per deltaker
|
|
|
|
|
|
(`strokes_only`/`strokes_and_putts`/`full`, ny kolonne `round_participant.
|
|
|
|
|
|
stat_level`, default `strokes_only` — brukerens egen presisering: kun
|
|
|
|
|
|
slag er strengt tatt nødvendig for resultat/HCP). Nytt PATCH-endepunkt
|
|
|
|
|
|
`/rounds/{id}/participants/{id}` for å endre nivået underveis. (5) Putt-
|
|
|
|
|
|
avstand endret fra fritekst-tall til seks faste bøtter (`<1m`…`8m+`) —
|
|
|
|
|
|
`round_hole.first_putt_distance_m` (numeric) erstattet med
|
|
|
|
|
|
`first_putt_distance_bucket` (text+CHECK) i migrasjon
|
|
|
|
|
|
`022_round_stats_and_bag.sql`, ingen produksjonsdata å bevare (0 rader
|
|
|
|
|
|
hadde verdi). (6) "Hullet er spilt"-avkrysningen FJERNET helt — reelt
|
|
|
|
|
|
overflødig, `played` settes allerede automatisk når et slagtall velges.
|
|
|
|
|
|
(7) Direkte spørsmål om avkrysningens funksjon avdekket at brukeren
|
|
|
|
|
|
egentlig ville vite om `played` (bevisst enkel avledning, uendret) — i
|
|
|
|
|
|
samme svar reiste brukeren "plukket opp"-behovet for et fremtidig
|
|
|
|
|
|
Stableford-format (fanget i FEATURE_BACKLOG.md, IKKE bygget — krever en
|
|
|
|
|
|
helt egen scoring-format-designrunde).
|
|
|
|
|
|
**Punkt 6 (numpad-layout + retningskors + sveip-vurdering) BYGGET OG
|
|
|
|
|
|
LIVE 2026-07-24, samme dag** — V0-prompten (se FEATURE_BACKLOG.md)
|
|
|
|
|
|
kjørt av bruker, zip 13 mottatt og integrert. **V0s egen designbeslutning
|
|
|
|
|
|
på sveip-spørsmålet:** IKKE et sveip-panel, men trykk-baserte
|
|
|
|
|
|
"Score"/"Statistikk"-faner (`PanelTabs`) inni samme kort — begrunnet
|
|
|
|
|
|
med at skjermen allerede har to horisontalt scrollende rader
|
|
|
|
|
|
(spillerfaner, hull-navigasjon), så en tredje sveiperetning ville vært
|
|
|
|
|
|
forvirrende. Vurdert som et godt, veloverveid valg, beholdt uendret.
|
|
|
|
|
|
Retningskors (`DirectionCross`/`DirButton`, piler + `Target`-ikon)
|
|
|
|
|
|
brukt for Utslag (venstre/senter/høyre) og Innspill (fullt 5-veis
|
|
|
|
|
|
kors), tekstlabel beholdt på hver knapp (ikke ikon-only). Tallvelgerne
|
|
|
|
|
|
fikk ny numpad-layout (3 kolonner, `h-16`-knapper, fyller bredden).
|
|
|
|
|
|
**Reelt integreringsarbeid, ikke ren om-kabling:** siden V0 ikke kjente
|
|
|
|
|
|
til dagens datalag (bygget i en tidligere, separat runde samme dag),
|
|
|
|
|
|
måtte `PanelTabs`/`DirectionCross` flettes inn i EKSISTERENDE, allerede
|
|
|
|
|
|
fungerende kode — ikke erstatte den. Konkret: `PanelTabs` vises kun når
|
|
|
|
|
|
`statLevel==="full"` (ingen "Statistikk"-fane å bytte til ellers),
|
|
|
|
|
|
"Score"-innholdet (Slag, evt. Putter) vises direkte uten fane-UI når
|
|
|
|
|
|
nivået er lavere. `DirectionCross` erstattet kun de to `ChoiceRow`-
|
|
|
|
|
|
kallene for Utslag/Innspill — `ChoiceRow` selv beholdt uendret til
|
|
|
|
|
|
resten (Kjønn, Statistikknivå, putt-avstand-bøtter). Kølle-bag-picker,
|
|
|
|
|
|
anywayslag, putt-bøtter, `stat_level`-gating, merge-før-PATCH,
|
|
|
|
|
|
starthull-fiksen og alle `/my-rounds`-lenker fra tidligere samme dag
|
|
|
|
|
|
ALLE bevart uendret -- kun presentasjonslaget for tallvelgere/retning
|
|
|
|
|
|
byttet ut. V0s egen `PlayedToggle` (som den ikke visste var fjernet)
|
|
|
|
|
|
ble bevisst IKKE tatt inn igjen. Ekte typesjekket produksjonsbuild
|
|
|
|
|
|
kompilerte rent. **Rullet ut live**, kun `teecup_frontend` (+ vanlig
|
|
|
|
|
|
`teecup_api`-bivirkning), ingen migrasjon, `teeoff.no` upåvirket.
|
|
|
|
|
|
Zip 13 slettet fra prosjektroten.
|
|
|
|
|
|
**Scratch-verifisert grundig** (isolert `teecup_app_scratch`-rolle +
|
|
|
|
|
|
isolert scratch-MinIO + engangs API-container): 18 sjekker — kølle-bag
|
|
|
|
|
|
lagret/hentet riktig, ukjent kølletype avvist (422), mer enn 14 køller
|
|
|
|
|
|
avvist (422), `stat_level`-default og eksplisitt verdi ved
|
|
|
|
|
|
opprettelse/gjest-tilføyelse, PATCH av `stat_level` vedvarer etter
|
|
|
|
|
|
refetch, `anyway_strokes`/`first_putt_distance_bucket` lagret riktig,
|
|
|
|
|
|
ugyldig bøtte-verdi avvist (422), OG en bevisst re-bekreftelse av
|
|
|
|
|
|
merge-før-PATCH-kontrakten (et PATCH uten `anyway_strokes` nuller den
|
|
|
|
|
|
fortsatt — samme grunnkontrakt som forrige runde, ikke endret av denne
|
|
|
|
|
|
utvidelsen). `test_isolation.sql` fortsatt 12/12. Ekte typesjekket
|
|
|
|
|
|
produksjonsbuild kompilerte rent.
|
|
|
|
|
|
**Rullet ut mot ekte systemer 2026-07-24**, bruker bekreftet eksplisitt:
|
|
|
|
|
|
migrasjon 022 kjørt mot ekte `teecup_db` (alle nye kolonner/CHECK-er
|
|
|
|
|
|
bekreftet, `test_isolation.sql` fortsatt 12/12), deretter `docker
|
|
|
|
|
|
compose up -d --build teecup_api teecup_frontend`, begge containere
|
|
|
|
|
|
boot-et rent, `/health`/`/dashboard`/`/my-rounds`/`/account` → 200 over
|
|
|
|
|
|
ekte https, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
Alle tre punktene er live og verifisert:
Enkeltbane-anlegg: Tjøme Golfklubb og de fleste andre (kun 15 av 174 teeoff-anlegg har mer enn én bane) viser nå bare klubbnavnet, uten det overflødige "– Hovedbanen"-suffikset. Flerbane-anlegg (f.eks. Ålesund) beholder det kombinerte navnet.
Navngi runder: nytt valgfritt navnefelt på runder, settbart ved opprettelse og redigerbart/fjernbart etterpå, vist konsekvent på tvers av rundeliste, rundeside, scorekort og statistikk.
Land før hjemmeklubb: Land er nå en nedtrekksliste (Norge, klargjort for flere), Hjemmeklubb er en søkbar liste mot teeoffs ekte klubbregister.
Migrasjon 024 kjørt mot ekte teecup_db, begge containere redeployet og verifisert (/health, /dashboard, /my-rounds, /account → 200), teeoff.no upåvirket. Status- og beslutningsdokumentene er oppdatert.
2026-07-25 06:33:38 +02:00
|
|
|
|
**Tre nye brukerpunkter, ALLE BYGGET, SCRATCH-VERIFISERT OG LIVE
|
|
|
|
|
|
2026-07-25:**
|
|
|
|
|
|
1. **Enkeltbane-anlegg dropper det overflødige banenavnet.** Brukeren
|
|
|
|
|
|
påpekte at Tjøme Golfklubb (og de aller fleste andre norske anlegg —
|
|
|
|
|
|
kartlagt ved å skanne alle 174 teeoff-anlegg: kun 15 har mer enn én
|
|
|
|
|
|
bane) fikk et unødvendig sammensatt navn ("Tjøme Golfklubb –
|
|
|
|
|
|
Hovedbanen") siden anlegget uansett bare har ÉN bane. Fikset i BEGGE
|
|
|
|
|
|
stedene navnet bygges (`app/routers/courses.py` sin
|
|
|
|
|
|
`import_official_course`, `app/routers/rounds.py` sin
|
|
|
|
|
|
`_resolve_teeoff_course`): har `facility["courses"]` nøyaktig ett
|
|
|
|
|
|
element, brukes kun anleggsnavnet; har det flere (bekreftet fortsatt
|
|
|
|
|
|
riktig for f.eks. Ålesund Golfklubb, som har "Solnør Gaard" og "Moa
|
|
|
|
|
|
Golfsenter"), beholdes det kombinerte "Anlegg – Bane"-navnet uendret.
|
|
|
|
|
|
Ingen migrasjon (ren navnelogikk, ingen lagret data endret av seg selv
|
|
|
|
|
|
— kun FREMTIDIGE importer/runde-opprettelser får det nye navnet).
|
|
|
|
|
|
2. **Runder kan nå navngis** (`round.name`, migrasjon
|
|
|
|
|
|
`024_round_name.sql`, nullable). Valgfritt felt lagt til
|
|
|
|
|
|
`RoundCreate`/`RoundUpdate`/`RoundOut` i `app/routers/rounds.py` — PATCH-
|
|
|
|
|
|
kontrakten følger samme "tom streng = fjern, utelatt = ikke rør"-mønster
|
|
|
|
|
|
som resten av `RoundUpdate` sine felt (ikke et ekte `exclude_unset`,
|
|
|
|
|
|
konsistent med hvordan `holes_planned`/`start_hole` allerede
|
|
|
|
|
|
håndteres i samme endepunkt). Vises på tvers av `round-card.tsx`
|
|
|
|
|
|
(rundeliste), `round-detail.tsx` (header + redigerbart i "Rediger
|
|
|
|
|
|
runde"-panelet), `round-scorecard.tsx` og `round-stats.tsx` — alle
|
|
|
|
|
|
fire faller tilbake til `course_name_snapshot` når navn ikke er satt,
|
|
|
|
|
|
og viser banenavnet som sekundær informasjon når et eget navn ER satt
|
|
|
|
|
|
(ikke bare erstattet det, siden banen fortsatt er relevant
|
|
|
|
|
|
informasjon).
|
|
|
|
|
|
3. **Land+hjemmeklubb i profilen, redesignet (ADR-031/032s
|
|
|
|
|
|
`ProfileOnboarding`/`ProfileSection`).** Brukeren ba om at Land skal stå
|
|
|
|
|
|
FØR Hjemmeklubb, og at Hjemmeklubb skal være en nedtrekksliste fra
|
|
|
|
|
|
teeoff, filtrert på valgt land. Avklart eksplisitt med bruker
|
|
|
|
|
|
(AskUserQuestion, begge anbefalte valg): "Land" er nå en ekte
|
|
|
|
|
|
`<select>` (`COUNTRIES = ["Norge"]`, ett element foreløpig — bevisst
|
|
|
|
|
|
klargjøring for fremtidig flerspråklighet, ingen backend-endring
|
|
|
|
|
|
trengs når flere land legges til, kun listen utvides), og "Hjemmeklubb"
|
|
|
|
|
|
er en søkbar kombinasjons-boks (`HomeClubField`, debounce 250ms) som
|
|
|
|
|
|
gjenbruker det ALLEREDE EKSISTERENDE `/rounds/official-search`-
|
|
|
|
|
|
endepunktet (org-uavhengig, tilgjengelig for enhver innlogget bruker
|
|
|
|
|
|
siden ADR-033) — ingen ny backend-kode i det hele tatt. Teeoff filtrerer
|
|
|
|
|
|
allerede bort upubliserte/nedlagte anlegg server-side (`is_published`),
|
|
|
|
|
|
så "nedlagte og ikke operative klubber skal ikke inkluderes" er dekket
|
|
|
|
|
|
uten egen filtrering på teecup-siden. Begge komponentene delt mellom
|
|
|
|
|
|
`ProfileOnboarding` (obligatorisk profil-fullføring) og `ProfileSection`
|
|
|
|
|
|
(kontoinnstillinger) i `account-settings.tsx`, ingen duplisert
|
|
|
|
|
|
implementasjon.
|
|
|
|
|
|
**Bevisst IKKE bygget:** hard validering som avviser fritekst utenfor
|
|
|
|
|
|
søkeresultatene — eksisterende, allerede lagrede `home_club`-verdier
|
|
|
|
|
|
(fritekst fra før denne runden) forblir gyldige og redigerbare, feltet
|
|
|
|
|
|
oppfører seg som "skriv for å filtrere, klikk for å velge" fremfor en
|
|
|
|
|
|
strengt låst `<select>`, for å unngå å gjøre eksisterende profiler
|
|
|
|
|
|
utilgjengelige.
|
|
|
|
|
|
**Scratch-verifisert grundig** (isolert `teecup_app_scratch`-rolle +
|
|
|
|
|
|
isolert scratch-MinIO + engangs API-container, samt et direkte
|
|
|
|
|
|
engangs-oppslag mot ekte `teeoff_api` for å kartlegge hvilke anlegg som
|
|
|
|
|
|
faktisk har mer enn én bane): 22 sjekker — enkeltbane-navn uten suffiks
|
|
|
|
|
|
(Tjøme), flerbane-navn med suffiks uendret (Ålesund), rundenavn lagret
|
|
|
|
|
|
ved opprettelse, rundenavn med i rundeliste, PATCH omdøper, PATCH med
|
|
|
|
|
|
tom streng fjerner navnet, PATCH uten navn-felt lar det urørt,
|
|
|
|
|
|
`official-search`-endepunktet (brukt av det nye profilfeltet) fortsatt
|
|
|
|
|
|
fungerer identisk. `test_isolation.sql` fortsatt 12/12. Ekte
|
|
|
|
|
|
typesjekket produksjonsbuild kompilerte rent, alle 21 ruter listet.
|
|
|
|
|
|
**Rullet ut mot ekte systemer 2026-07-25**, bruker bekreftet
|
|
|
|
|
|
eksplisitt: migrasjon 024 kjørt mot ekte `teecup_db` (kolonne
|
|
|
|
|
|
bekreftet, `test_isolation.sql` fortsatt 12/12), deretter `docker
|
|
|
|
|
|
compose up -d --build teecup_api teecup_frontend`, begge containere
|
|
|
|
|
|
boot-et rent, `/health`/`/dashboard`/`/my-rounds`/`/account` → 200 over
|
|
|
|
|
|
ekte https, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
### Oppdatering 2026-07-26: utslag/HCP per deltaker, gjeste-kontaktinfo, og scoringsflyten redesignet (v2)
|
|
|
|
|
|
|
|
|
|
|
|
Tre beslutninger denne dagen utvider ADR-033 uten å endre grunn-
|
|
|
|
|
|
arkitekturen (Beslutning A-G står uendret).
|
|
|
|
|
|
|
|
|
|
|
|
**1. Utslagssted er nå PER DELTAKER, ikke per runde.** Reelt hull
|
|
|
|
|
|
oppdaget ved bruk: alle deltakere delte tidligere `round.
|
|
|
|
|
|
tee_name_snapshot` uansett kjønn/faktisk valg — en medspiller kunne
|
|
|
|
|
|
aldri spille fra et annet utslag enn eieren. Migrasjon `028_round_
|
|
|
|
|
|
participant_tee.sql` gir `round_participant` sitt eget `tee_name_
|
|
|
|
|
|
snapshot`, backfylt fra rundens eksisterende utslag for eksisterende
|
|
|
|
|
|
rader. `ParticipantCreate`/`ParticipantUpdate` fikk et valgfritt
|
|
|
|
|
|
`tee_name`; et bane-bytte (eksisterende `RoundUpdate`-mekanisme)
|
|
|
|
|
|
nullstiller nå eksplisitt ALLE deltakeres per-deltaker-utslag til det
|
|
|
|
|
|
nye standardutslaget (et bane-bytte gjør individuelle valg fra den
|
|
|
|
|
|
gamle banen meningsløse). HCP kan nå også redigeres per deltaker for
|
|
|
|
|
|
KUN denne runden (samme reproduserbarhets-unntak som resten av
|
|
|
|
|
|
`round_participant` — endrer aldri spillerens faktiske profil),
|
|
|
|
|
|
avvist etter at runden er fullført (samme presedens som bane-bytte).
|
|
|
|
|
|
|
|
|
|
|
|
**2. Gjester (`user_id IS NULL`) fikk et valgfritt kontaktfelt +
|
|
|
|
|
|
redigerbart navn/kjønn.** Migrasjon `029_round_participant_guest_
|
|
|
|
|
|
email.sql` (`guest_email`, nullable). `ParticipantUpdate` fikk `guest_
|
|
|
|
|
|
name`/`gender`/`guest_email` — eksplisitt avvist (400) for en LENKET
|
|
|
|
|
|
deltaker (disse feltene gir ingen mening for noen med egen konto).
|
|
|
|
|
|
`gender`-endring inngår i samme rating-rekalkulering som utslag/HCP
|
|
|
|
|
|
(påvirker hvilken rating som er gyldig).
|
|
|
|
|
|
|
|
|
|
|
|
**3. Scoringsflyten redesignet til en skjermovertagende veiviser --
|
|
|
|
|
|
to iterasjoner, andre erstattet den første samme dag.** Brukeren delte
|
|
|
|
|
|
en skjermopptaksvideo av en konkurrentapp (Golf GameBook) sin
|
|
|
|
|
|
scoreregistrering og spurte om prinsippene kunne forbedre TeeCup.
|
|
|
|
|
|
Videoen ble analysert bilde for bilde (ffmpeg i en engangs Docker-
|
|
|
|
|
|
container) og avdekket et "samlebånd"-mønster: taltastatur med
|
|
|
|
|
|
kontekstuelle golf-termer (Eagle/Birdie/Par/Bogey ut fra hullets par),
|
|
|
|
|
|
og at fullført registrering for én spiller automatisk åpner NESTE
|
|
|
|
|
|
spillers registrering for samme hull.
|
|
|
|
|
|
**Første forsøk (v1), BEVISST avvist fra en full modal:** vurdert som
|
|
|
|
|
|
unødvendig risikofylt å bygge en skjermovertagende steg-for-steg-flyt
|
|
|
|
|
|
korrekt uten visuell testing (intet nettleserverktøy tilgjengelig i
|
|
|
|
|
|
denne økten). I stedet: golf-term-taltastatur lagt til der det allerede
|
|
|
|
|
|
var (`NumberPicker`s nye `showGolfTerms`-modus), pluss en "Neste:
|
|
|
|
|
|
{navn}"/"Neste hull"-knapp nederst på den EKSISTERENDE, lange
|
|
|
|
|
|
inline-siden.
|
|
|
|
|
|
**Brukeren testet v1 live og avviste den eksplisitt:** "ingen forbedring
|
|
|
|
|
|
i det hele tatt", "visuelt like overveldende og rotete" — den
|
|
|
|
|
|
forsiktige, risikoreduserte tilnærmingen var utilstrekkelig; en ekte
|
|
|
|
|
|
skjermovertagende veiviser var det som faktisk krevdes, sammen med et
|
|
|
|
|
|
nytt, eksplisitt krav: akkumulert score-så-langt for RUNDEN synlig for
|
|
|
|
|
|
HVER spiller samtidig (ikke bare én valgt), matchende konkurrentappens
|
|
|
|
|
|
vedvarende "E"/"+1"-visning ved siden av hvert navn.
|
|
|
|
|
|
**v2 bygget samme dag, ERSTATTER v1 helt:** ny `ScoringWizard`-komponent
|
|
|
|
|
|
(fullskjerm, egen DOM-stack utenfor `<main>`), tre steg maks drevet av
|
|
|
|
|
|
`stat_level` ("strokes_only" → kun Slag; "strokes_and_putts" → + Putter/
|
|
|
|
|
|
avstand første putt; "full" → + ett samlet detalj-steg). Bevisst FÆRRE,
|
|
|
|
|
|
grovere steg enn konkurrentens egne 5-6 skjermer — en risikoreduksjon
|
|
|
|
|
|
uten visuell testing, og TeeCups stat_level-modell gjør en finere
|
|
|
|
|
|
oppdeling mindre naturlig uansett. Alle spillerne vises som en fast,
|
|
|
|
|
|
ikke-trykkbar kontekst-rad øverst i veiviseren (speiler konkurrent-
|
|
|
|
|
|
appens "mist aldri oversikten"-prinsipp — bevisst IKKE trykkbar, siden
|
|
|
|
|
|
GameBooks egen video heller ikke viste spillerbytte midt i et steg).
|
|
|
|
|
|
Hovedsiden forenklet radikalt: det gamle Slag/Putter/"flere detaljer"-
|
|
|
|
|
|
skjemaet er fjernet, erstattet med én kompakt liste (navn, "HCP X ·
|
|
|
|
|
|
{til-par} så langt (N hull)", en stor rund knapp som åpner veiviseren).
|
|
|
|
|
|
Datalasting utvidet til å hente ALLE deltakeres hull med det samme
|
|
|
|
|
|
(ikke lenger lat lasting kun for aktiv spiller), nødvendig for at
|
|
|
|
|
|
akkumulert score kan vises for alle samtidig.
|
|
|
|
|
|
**Reell driftsfeil funnet OG rettet SAMME dag, rapportert av bruker som
|
|
|
|
|
|
testet live:** den gamle `ScoreSoFar`("Så langt i runden")-boksen ble
|
|
|
|
|
|
ved en feil IKKE fjernet/flyttet i selve v2-bygget — den lå fortsatt
|
|
|
|
|
|
øverst, uendret, og fikk brukeren til å rapportere "ingen synlig
|
|
|
|
|
|
endring i det hele tatt" (toppen av siden var reelt uendret). Bekreftet
|
|
|
|
|
|
presist ved å hente den FAKTISK kjørende JS-bunten i produksjon og
|
|
|
|
|
|
søke i den — begge de gamle OG de nye tekst-strengene lå i samme bunt,
|
|
|
|
|
|
som beviste dette var en ekte plasseringsfeil, ikke en cache-/
|
|
|
|
|
|
utrullingsfeil. Rettet ved å flytte `ScoreSoFar` til UNDER scorings-
|
|
|
|
|
|
seksjonen (fortsatt tilgjengelig, bare ikke lenger i veien for
|
|
|
|
|
|
hovedoppgaven) — samme klasse feil hele denne rundens redesign forsøkte
|
|
|
|
|
|
å løse.
|
|
|
|
|
|
**Bevisst utenfor omfang, IKKE bygget:** de mest granulære trekkene fra
|
|
|
|
|
|
konkurrentappen (separate skjermer per detalj-felt, en illustrert
|
|
|
|
|
|
"bommet green"-grafikk, spillerbytte midt i et steg) — vurdert som for
|
|
|
|
|
|
stor risiko å bygge presist uten visuell testing, og TeeCups egen
|
|
|
|
|
|
stat_level-gruppering gjør en så fin oppdeling mindre naturlig uansett.
|
|
|
|
|
|
**Ingen ekte nettleser-interaksjonstest utført av noen av iterasjonene**
|
|
|
|
|
|
(intet slikt verktøy tilgjengelig denne økten) — kun logikk (Node-
|
|
|
|
|
|
simulering av stegmaskinen, rangering, akkumulert-score-beregning) og
|
|
|
|
|
|
et ekte API-rundtur-script (nøyaktig samme feltkombinasjon som
|
|
|
|
|
|
veiviseren sender) er verifisert. Full detalj i CLAUDE.md sin
|
|
|
|
|
|
statuslogg (2026-07-26).
|
|
|
|
|
|
|
ADR-033 er nå fullt kildebelagt. Kort oppsummert hva som endret seg:
Banedata: bekreftet — live oppslag mot teeoff, ikke import.
HCP: leste hele den offisielle WHS Rules of Handicapping 2024 (Rule 2, 3, 5, 6). To viktige presiseringer av det du selv antok:
«Minst 9 hull» stemmer ikke helt — en 9-hulls-runde må ha alle 9 hull i et faktisk ratet sett (front eller back), mens en 18-hulls-runde bare trenger minst 10 av 18 (resten fylles med en «expected score», ny metode fra 2024 som erstattet den gamle «net par»-metoden). Dette betyr også at fritt starthull (Beslutning E) fungerer fint for vanlig logging, men en runde blir kun HCP-tellende hvis de spilte hullene faktisk samsvarer med banens ratede 18/front-9/back-9 — en vilkårlig 9-hulls-strekning har ingen egen rating.
Fant en presis, tidligere ukjent detalj: en 9-hulls Course Handicap halverer indeksen først (Index÷2 × Slope/113 + (Rating−Par)) — helt annen formel enn 18-hulls-varianten. Viktig: dette gjelder frittstående 9-hulls-runder, ikke det eksisterende front_9/back_9-øktoppsettet i turneringsflyten, som løser et annet problem (slagfordeling internt i en match) og bevisst skal la det være.
Full kjede (Score Differential, Net Double Bogey, beste-8-av-20, Low Handicap Index, soft/hard cap, Course/Playing Handicap) er nå skrevet inn i ADR-033 med eksakte tall fra kilden. PCC og Exceptional-Score-justering er bevisst avgrenset ut av v1 (forstått, men krever data på tvers av alle brukeres runder samme dag — egen, senere runde).
2026-07-22 09:26:08 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
## ADR-035: Dashbord-redesign — organisasjon blir implisitt
|
|
|
|
|
|
|
|
|
|
|
|
Direkte oppfølging av en refleksjonsrunde 2026-07-25: brukeren spurte
|
|
|
|
|
|
eksplisitt hvorfor organisasjon i det hele tatt trengs, gitt at en enkelt
|
|
|
|
|
|
bruker som vil sette opp ÉN turnering for en vennegjeng ikke får noe igjen
|
|
|
|
|
|
for opprettelses-seremonien. To alternativer ble veid opp mot hverandre:
|
|
|
|
|
|
|
|
|
|
|
|
- **A — bruker-eide turneringer** (samme mønster som ADR-033 sine
|
|
|
|
|
|
frittstående runder, `user_id`-eierskap, ingen RLS): AVVIST. Praktisk
|
|
|
|
|
|
talt HELE turnering-apparatet (lag/roster/blind draw/scoring/chat/
|
|
|
|
|
|
leaderboard/offentlig side, se CLAUDE.md sin arkitektur-invariant om
|
|
|
|
|
|
`organization_id` på alle domenetabeller) er bygget rundt RLS og
|
|
|
|
|
|
org-medlemskap. Å gjøre turneringer bruker-eide ville krevd enten en
|
|
|
|
|
|
full duplisering av dette apparatet i en parallell, RLS-fri variant,
|
|
|
|
|
|
eller å gjøre `organization_id` valgfri overalt — et brudd på
|
|
|
|
|
|
invarianten CLAUDE.md eksplisitt krever en ny ADR for, med en
|
|
|
|
|
|
reverseringskostnad som går GALT begge veier (migrere eksisterende
|
|
|
|
|
|
bruker-eide turneringer inn i organisasjoner senere, eller gjenoppfinne
|
|
|
|
|
|
medadministrasjon fra bunnen om den viser seg nødvendig uansett).
|
|
|
|
|
|
- **B — organisasjon beholdes, men opprettelsen gjøres usynlig/automatisk**:
|
|
|
|
|
|
VALGT. Rører verken skjema, RLS eller noen av de ni routerne som
|
|
|
|
|
|
allerede er bygget rundt organisasjon — alt fortsetter å virke uendret.
|
|
|
|
|
|
Kun UX-seremonien fjernes.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning A — Hva "usynlig organisasjon" betyr konkret
|
|
|
|
|
|
|
|
|
|
|
|
`POST /orgs` (`app/routers/organizations.py`) krever i dag KUN `name` —
|
|
|
|
|
|
verifisert direkte i koden: `slug`/`public_profile` settes separat via
|
|
|
|
|
|
`PATCH /orgs/{id}` (ADR-018), ikke ved opprettelse. Dette betyr at
|
|
|
|
|
|
"usynlig opprettelse" krever **null backend-endring** — kun en endret
|
|
|
|
|
|
frontend-orkestrering:
|
|
|
|
|
|
|
|
|
|
|
|
- Trykker en bruker "Ny turnering" og har PRESIS ÉN organisasjon fra før:
|
|
|
|
|
|
den gjenbrukes direkte, ingen synlig org-steg.
|
|
|
|
|
|
- Har brukeren INGEN organisasjon ennå: `POST /orgs` kalles automatisk med
|
|
|
|
|
|
et generert navn (`"{fornavn} {etternavn}s turneringer"`, eller
|
|
|
|
|
|
`"Mine turneringer"` som fallback hvis navn mangler) FØR turnering-
|
|
|
|
|
|
opprettelsen vises — brukeren ser aldri et eget "opprett organisasjon"-
|
|
|
|
|
|
skjema.
|
|
|
|
|
|
- Har brukeren FLERE organisasjoner (f.eks. fordi de også er invitert inn
|
|
|
|
|
|
i en ekte klubb, ADR-022): et lett org-valg vises FØRST da — samme
|
|
|
|
|
|
`OrganizationView`-mønster som i dag, men kun synlig i dette ene
|
|
|
|
|
|
tilfellet, ikke som standard.
|
|
|
|
|
|
- Alt annet er UENDRET: en bruker som ønsker en ekte klubb-identitet
|
|
|
|
|
|
(navn, slug, offentlig side, medadministratorer) kan fortsatt navngi
|
|
|
|
|
|
orgen sin og bygge den ut senere — B fjerner kun ceremonien ved
|
|
|
|
|
|
FØRSTE opprettelse, ikke noen av de eksisterende org-funksjonene.
|
|
|
|
|
|
|
|
|
|
|
|
### Reversibilitet
|
|
|
|
|
|
|
|
|
|
|
|
B kan reverseres på en ettermiddag: legg navnesteget tilbake i UI-flyten
|
|
|
|
|
|
FØR "Ny turnering" fullføres. Ingen datamigrering — eksisterende org-rader
|
|
|
|
|
|
er identiske uansett om de ble navngitt av brukeren eller auto-generert.
|
|
|
|
|
|
|
|
|
|
|
|
**Status: 📋 DESIGNET 2026-07-25, IKKE BYGGET.** Se
|
|
|
|
|
|
"Dashbord: tom-tilstand"-seksjonen i FEATURE_BACKLOG.md for det fulle
|
|
|
|
|
|
blokk-forslaget og V0-prompten som ble skrevet i samme runde.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-036: Venner, kategorisert deling av runder, og tiered personsøk for medspillere
|
|
|
|
|
|
|
|
|
|
|
|
Reist av brukeren 2026-07-25, samme runde som ADR-035. To sammenhengende
|
|
|
|
|
|
behov: (1) når man legger til en medspiller på en frittstående runde, skal
|
|
|
|
|
|
det søkes opp EKTE personer — venner FØRST, deretter samme hjemmeklubb,
|
|
|
|
|
|
deretter samme land, til slutt globalt, uavhengig av om for- eller
|
|
|
|
|
|
etternavn skrives først, i samme UI-mønster som bane-/klubbsøket; (2)
|
|
|
|
|
|
appen skal ha et fullverdig vennekonsept, der venner (med mindre runden er
|
|
|
|
|
|
satt til "Privat") kan se livescoren din, og der DU kan kategorisere hver
|
|
|
|
|
|
venn i én eller flere av et fast sett grupper — uten at vennen selv vet
|
|
|
|
|
|
hvilke grupper du har puttet dem i.
|
|
|
|
|
|
|
|
|
|
|
|
**Direkte oppfølging av et allerede notert, men avvist punkt:** ADR-033
|
|
|
|
|
|
Beslutning D satte `round_participant.user_id` i skjemaet, men avgrenset
|
|
|
|
|
|
bevisst til KUN gjeste-deltakere i v1 ("en deltaker med `user_id` satt får
|
|
|
|
|
|
IKKE egen tilgang til runden"), med e-post-kobling notert som en mulig,
|
|
|
|
|
|
ikke-designet senere utvidelse. Denne ADR-en erstatter e-post-kobling-
|
|
|
|
|
|
ideen med noe som passer bedre til det brukeren faktisk ba om: ekte
|
|
|
|
|
|
personsøk (samme mønster som bane/klubb), ikke en e-postadresse man må
|
|
|
|
|
|
kjenne på forhånd.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning A — Vennskap er gjensidig; kategorisering er privat og ensidig
|
|
|
|
|
|
|
|
|
|
|
|
Et vennskap krever en forespørsel + aksept (samme grunnmønster som
|
|
|
|
|
|
organisasjon-invitasjoner, ADR-022) — "venner ser livescoren din" gir kun
|
|
|
|
|
|
mening som et GJENSIDIG, bekreftet forhold, ikke en ensidig følging.
|
|
|
|
|
|
Kategoriseringen ("Make", "Golfvenner" osv.) er derimot et HELT SEPARAT,
|
|
|
|
|
|
privat attributt EIET av den som kategoriserer — A kan sette B i
|
|
|
|
|
|
"Golfvenner" uten at B noensinne får vite det, og B kategoriserer A helt
|
|
|
|
|
|
uavhengig (kan sette A i en annen gruppe, eller ingen). Dette er bevisst
|
|
|
|
|
|
samme idé som Facebooks "nære venner"-lister: vennskapet er symmetrisk,
|
|
|
|
|
|
men grupperingen er det ikke.
|
|
|
|
|
|
|
|
|
|
|
|
**Datamodell (arbeidsnavn, avgjøres ved migrasjonsskriving):**
|
|
|
|
|
|
- `friendship`: `requester_user_id`, `addressee_user_id`, `status`
|
|
|
|
|
|
(`pending`/`accepted`/`declined`), tidsstempler. Et UNIK
|
|
|
|
|
|
uttrykks-indeks på `(LEAST(requester_user_id, addressee_user_id),
|
|
|
|
|
|
GREATEST(...))` hindrer duplikate forespørsler i begge retninger
|
|
|
|
|
|
samtidig.
|
|
|
|
|
|
- Kategori er IKKE en egen tabell — et fast, CHECK-constrained sett
|
|
|
|
|
|
(samme mønster som `BAG_CLUBS`/`gender`/`stat_level` ellers i appen),
|
|
|
|
|
|
ikke brukerdefinerbart: `spouse` (Make), `close_family` (Nær familie),
|
|
|
|
|
|
`extended_family` (Storfamilie), `close_friends` (Nære venner),
|
|
|
|
|
|
`golf_friends` (Golfvenner), `colleagues` (Kollegaer), `business`
|
|
|
|
|
|
(Forretningsforbindelser), `classmates` (Studiekamerater),
|
|
|
|
|
|
`acquaintances` (Perifere bekjente), `other` (Ymse).
|
|
|
|
|
|
- `friend_categorization`: `owner_user_id`, `friend_user_id`, `category`
|
|
|
|
|
|
— én rad PER kategori en venn er satt i (en venn kan ha flere rader,
|
|
|
|
|
|
"de skal kunne være tilknyttet forskjellige kategorier"). Håndheves i
|
|
|
|
|
|
app-laget (skriving krever en `accepted`-vennskapsrad mellom de to),
|
|
|
|
|
|
ikke en direkte FK til `friendship` (unngår å måtte holde styr på
|
|
|
|
|
|
requester/addressee-retning to steder).
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning B — Rundevisibilitet: tre nivåer, samme struktur som turnering, men egen mekanisme
|
|
|
|
|
|
|
|
|
|
|
|
`round` får `visibility_mode` (`public`/`private`/`friends`, default
|
|
|
|
|
|
`private` — trygg standard, samme filosofi som RLS-policyenes "se
|
|
|
|
|
|
ingenting" ved manglende kontekst). Når `friends` er valgt: hvilke
|
|
|
|
|
|
KATEGORIER som får se runden velges eksplisitt (`round_visible_category`,
|
|
|
|
|
|
`round_id`+`category`) — ikke "alle venner", siden brukeren eksplisitt ba
|
|
|
|
|
|
om å velge blant gruppene sine ved rundestart.
|
|
|
|
|
|
|
|
|
|
|
|
**Viktig presisering, unngår en reell forvekslingsfelle:** dette er
|
|
|
|
|
|
ADSKILT fra om noen er lagt til som faktisk MEDSPILLER (Beslutning C
|
|
|
|
|
|
under). En medspiller du har lagt til ser ALLTID runden dere spiller
|
|
|
|
|
|
sammen, uavhengig av `visibility_mode` — visibility-nivået styrer kun
|
|
|
|
|
|
TREDJEPARTS innsyn (venner/offentligheten), ikke de som faktisk er med i
|
|
|
|
|
|
flighten.
|
|
|
|
|
|
|
|
|
|
|
|
**Autorisasjonssjekk** (app-lag, `plain_connection()`-mønsteret fra
|
|
|
|
|
|
ADR-033 Beslutning A — INGEN RLS, samme begrunnelse som der):
|
|
|
|
|
|
1. `viewer == round.owner_user_id` → alltid tilgang.
|
|
|
|
|
|
2. `viewer` er en lenket medspiller (`round_participant.user_id ==
|
|
|
|
|
|
viewer`) → alltid tilgang til DEN runden.
|
|
|
|
|
|
3. `visibility_mode == 'public'` → alle, også anonyme (samme mønster som
|
|
|
|
|
|
turneringers offentlige side).
|
|
|
|
|
|
4. `visibility_mode == 'private'` → kun 1+2.
|
|
|
|
|
|
5. `visibility_mode == 'friends'` → krever innlogget bruker MED en
|
|
|
|
|
|
`accepted`-vennskapsrad til eieren OG minst én
|
|
|
|
|
|
`friend_categorization`-rad (`owner_user_id=eier, friend_user_id=
|
|
|
|
|
|
viewer`) hvor kategorien finnes i `round_visible_category` for akkurat
|
|
|
|
|
|
denne runden. Legg merke til retningen: det er EIERENS kategorisering
|
|
|
|
|
|
av VIEWER som brukes, ikke omvendt — riktig, siden det er eieren som
|
|
|
|
|
|
begrenser hvem som får se, basert på eierens egen gruppering.
|
|
|
|
|
|
|
|
|
|
|
|
**"Livescore"** — forstått som sanntidsoppdatering mens runden pågår,
|
|
|
|
|
|
samme idé som turnering sin `/t/[id]/live` (ADR-027). Gjenbruker samme
|
|
|
|
|
|
kringkastingsmønster (`app/realtime.py`, `broadcast_live_update`) — en
|
|
|
|
|
|
tilsvarende funksjon for runder, kringkastet ved hver hull-PATCH når
|
|
|
|
|
|
`visibility_mode != 'private'`, med et WS-endepunkt gated av samme
|
|
|
|
|
|
autorisasjonssjekk som over.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning C — Tiered personsøk: delt mellom "finn venn" og "legg til medspiller"
|
|
|
|
|
|
|
|
|
|
|
|
Samme underliggende endepunkt brukes til BEGGE formål (finn en venn å
|
|
|
|
|
|
sende forespørsel til, OG søk opp en medspiller å legge til på en runde)
|
|
|
|
|
|
— begge er i essens "finn en person", kun hva som skjer ETTER valg
|
|
|
|
|
|
skiller dem. Foreslått: `GET /people/search?q=...`.
|
|
|
|
|
|
|
|
|
|
|
|
**Navnerekkefølge-uavhengig matching:** spørringen splittes i tokens
|
|
|
|
|
|
(mellomrom-separert). For HVER token må minst ett av `first_name`/
|
|
|
|
|
|
`last_name` matche (`ILIKE token||'%'`) — ALLE tokens må matche (AND på
|
|
|
|
|
|
tvers av tokens, OR på tvers av feltene per token). Dette gjør at "Erol
|
|
|
|
|
|
Haagenrud" og "Haagenrud Erol" gir identisk treff, uten noen spesiell
|
|
|
|
|
|
"gjett rekkefølgen"-logikk — det faller naturlig ut av at hvert ord kan
|
|
|
|
|
|
matche HVILKET SOM HELST av de to feltene.
|
|
|
|
|
|
|
|
|
|
|
|
**Tiered rangering, én spørring:** en beregnet prioritet per kandidat —
|
|
|
|
|
|
0 hvis venn (uavhengig av kategorisering — ALLE venner, ikke bare de i
|
|
|
|
|
|
en bestemt gruppe, siden dette er søk-for-å-legge-til, ikke visibility-
|
|
|
|
|
|
sjekken over), 1 hvis samme `home_club` som søkeren (nå en pålitelig
|
|
|
|
|
|
eksakt streng siden ADR-025-runden 2026-07-25 gjorde Hjemmeklubb til en
|
|
|
|
|
|
ekte dropdown-verdi i stedet for fritekst — retroaktivt en god
|
|
|
|
|
|
begrunnelse for den endringen), 2 hvis samme `country`, 3 ellers.
|
|
|
|
|
|
`ORDER BY prioritet, fornavn, etternavn LIMIT 20` gir venner-først uten
|
|
|
|
|
|
behov for separate spørringer per nivå.
|
|
|
|
|
|
|
|
|
|
|
|
**Personvernhensyn, bevisst innebygd i designet, ikke tilføyd i
|
|
|
|
|
|
etterkant:**
|
|
|
|
|
|
- Svaret inneholder KUN navn, avatar, hjemmeklubb — ALDRI e-post/mobil/
|
|
|
|
|
|
fødselsdato.
|
|
|
|
|
|
- Foreslått minimum 2 tegn i søket før noe returneres i det hele tatt
|
|
|
|
|
|
(hindrer triviell enumerering av alle brukere via ett enkelt
|
|
|
|
|
|
bokstavsøk) — MIN anbefaling, ikke bekreftet med bruker.
|
|
|
|
|
|
- Kun innloggede brukere kan søke (`get_current_user`, ikke anonymt).
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning D — Byggerekkefølge (foreslått, IKKE bekreftet)
|
|
|
|
|
|
|
|
|
|
|
|
Gitt omfanget (nytt vennekonsept + ny rundevisibilitet + nytt delt
|
|
|
|
|
|
søkeendepunkt + sanntidsutvidelse) foreslås tre uavhengig leverbare faser,
|
|
|
|
|
|
samme "bygg i rekkefølgen ting brukes"-prinsipp som resten av prosjektet:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Venner-kjernen**: `friendship`+`friend_categorization`, forespørsel/
|
|
|
|
|
|
aksept/fjern-endepunkter, `/people/search`, en ny `/friends`-side
|
|
|
|
|
|
(søk, forespørsler, kategoriser). Leverbar og nyttig helt alene.
|
|
|
|
|
|
2. **Rundevisibilitet**: `round.visibility_mode`+`round_visible_category`,
|
|
|
|
|
|
valg ved rundestart, `can_view_round`-gatede lese-endepunkter for
|
|
|
|
|
|
ikke-eiere, sanntidsutvidelse for live-visning. Avhenger av fase 1.
|
|
|
|
|
|
3. **Ekte medspillere** (ikke bare gjester): utvid `POST .../participants`
|
|
|
|
|
|
til å godta et søkt `user_id` i tillegg til `guest_name`. Avhenger av
|
|
|
|
|
|
fase 1 (samme søk). **Avklart med bruker 2026-07-25: JA** — en
|
|
|
|
|
|
lagt-til ekte medspiller skal se runden i SIN EGEN "Egne runder"-liste,
|
|
|
|
|
|
ikke bare eieren.
|
|
|
|
|
|
**Konkret teknisk konsekvens, presisert her siden det ikke er
|
|
|
|
|
|
opplagt:** `list_rounds` filtrerer i dag KUN på `owner_user_id`, og
|
|
|
|
|
|
`RoundOut` sine `owner_holes_played`/`owner_total_score`/
|
|
|
|
|
|
`owner_score_to_par`-felt (lagt til 2026-07-25, se status) er alltid
|
|
|
|
|
|
utledet fra EIERENS `round_participant`-rad. For en medspiller som
|
|
|
|
|
|
ser runden i SIN liste må disse tallene i stedet vise DERES EGEN
|
|
|
|
|
|
score i runden, ikke eierens — feltene må derfor bli
|
|
|
|
|
|
"viewer-relative" (utledet fra HVILKEN SOM HELST deltaker-rad som
|
|
|
|
|
|
matcher innlogget bruker, enten `is_owner` eller lenket `user_id`),
|
|
|
|
|
|
ikke hardkodet til `is_owner=true`-raden. `list_rounds`-spørringen må
|
|
|
|
|
|
utvides til `WHERE owner_user_id = $1 OR id IN (SELECT round_id FROM
|
|
|
|
|
|
round_participant WHERE user_id = $1)`. Ingen endring i selve
|
|
|
|
|
|
eierskapet (`round.owner_user_id` er fortsatt entydig én person).
|
|
|
|
|
|
**Skrivetilgang avklart med bruker 2026-07-25: en medspiller skal
|
|
|
|
|
|
kunne registrere score for ALLE i flighten**, ikke bare sin egen rad
|
|
|
|
|
|
— samme skrivetilgang som eieren allerede har i dag (via
|
|
|
|
|
|
`_get_owned_round_or_404`), nå utvidet til å gjelde ENHVER lenket
|
|
|
|
|
|
ekte deltaker (`round_participant.user_id`), ikke kun eieren. Praktisk
|
|
|
|
|
|
presisering: dette gjelder KUN hull-registrering
|
|
|
|
|
|
(`PATCH .../participants/{id}/holes/{hole}`) og legg-til-deltaker —
|
|
|
|
|
|
sletting av selve runden og bane-/utslagsbytte (`DELETE /rounds/{id}`,
|
|
|
|
|
|
`PATCH /rounds/{id}` sine bane-felt) forblir eier-eksklusivt, siden
|
|
|
|
|
|
disse er destruktive/strukturelle handlinger uavhengig av hvem som
|
|
|
|
|
|
fører score. Autorisasjonssjekken for hull-PATCH blir dermed:
|
|
|
|
|
|
`viewer == owner_user_id OR viewer IN (SELECT user_id FROM
|
|
|
|
|
|
round_participant WHERE round_id = $1 AND user_id IS NOT NULL)`.
|
|
|
|
|
|
|
2026-07-25 08:37:07 +02:00
|
|
|
|
**Status: 🔨 FASE 1 (venner-kjernen) BACKEND + FRONTEND BYGGET
|
|
|
|
|
|
2026-07-25.** Backend: migrasjon `025_friends.sql` + `app/routers/
|
|
|
|
|
|
friends.py` (`GET /people/search`, `POST /friends`, `POST /friends/{id}/
|
|
|
|
|
|
accept`, `DELETE /friends/{id}`, `GET /friends`, `PUT /friends/
|
|
|
|
|
|
{friend_user_id}/categories`) — 31 scratch-sjekker bestått, inkl.
|
|
|
|
|
|
presist bevist tiered rangering (venn→klubb→land→globalt) og at
|
|
|
|
|
|
kategorisering faktisk er PRIVAT (B ser aldri kategoriene A har satt B
|
|
|
|
|
|
i). `test_isolation.sql` fortsatt 12/12. Rullet ut mot ekte `teecup_db`/
|
|
|
|
|
|
`teecup_api` samme dag, bruker bekreftet eksplisitt.
|
|
|
|
|
|
Frontend: V0-zip (zip 19) integrert som `components/friends.tsx` + ny
|
|
|
|
|
|
rute `app/my-friends/page.tsx` (IKKE `/friends`, som V0 selv foreslo —
|
|
|
|
|
|
det er API-prefikset, samme kollisjonsklasse som `/rounds` unngått fra
|
|
|
|
|
|
start denne gangen). Dashbordets "Venner"-blokk koblet til ekte data i
|
2026-07-26 08:22:41 +02:00
|
|
|
|
samme runde. Ekte typesjekket produksjonsbuild kompilerte rent. Rullet
|
|
|
|
|
|
ut live 2026-07-25, bruker bekreftet eksplisitt.
|
|
|
|
|
|
|
|
|
|
|
|
**Fase 3 (ekte medspillere på en frittstående runde) BYGGET OG SCRATCH-
|
|
|
|
|
|
VERIFISERT 2026-07-26**, utløst av at brukeren rapporterte at "+ Gjest"-
|
|
|
|
|
|
skjemaet ikke søkte etter spillere i det hele tatt (rent tekstfelt,
|
|
|
|
|
|
`/people/search` var aldri koblet på). Bygget den delen av fase 3 som
|
|
|
|
|
|
faktisk var etterspurt: søk-og-legg-til en ekte bruker (gjenbruker
|
|
|
|
|
|
tiered `/people/search`, samme endepunkt som vennesøket) PLUSS den
|
|
|
|
|
|
tidligere avklarte "skriv for hele flighten"-regelen (bekreftet av
|
|
|
|
|
|
brukeren 2026-07-26 — "full tilgang nå", ikke bare rask utfylling).
|
|
|
|
|
|
**IKKE** bygget i denne runden: selve rundevisibilitet (fase 2,
|
|
|
|
|
|
offentlig/privat/delt-med-venner-i-grupper) — det er fortsatt et
|
|
|
|
|
|
separat, senere steg.
|
|
|
|
|
|
|
|
|
|
|
|
Konkret: `round_participant.user_id` (fantes i skjemaet siden ADR-033,
|
|
|
|
|
|
aldri eksponert via API) er nå skrivbar via `POST /rounds/{id}/
|
|
|
|
|
|
participants` (kjønn/HCP hentes automatisk fra den valgte personens
|
|
|
|
|
|
egen profil, ikke tastet manuelt). Ny migrasjon `027_round_participant_
|
|
|
|
|
|
user_unique.sql` (partiell unik indeks, hindrer dobbel-lenking). Rundens
|
|
|
|
|
|
tilgang delt i to nivåer: `_get_accessible_round_or_404` (eier ELLER en
|
|
|
|
|
|
lenket medspiller — lesing, hull-scoring for HELE flighten, fullføring)
|
|
|
|
|
|
vs. `_get_owned_round_or_404` (fortsatt strengt eier-only — rediger/
|
|
|
|
|
|
slett runde, legg til/fjern medspillere). `RoundOut` sine `owner_*`-
|
|
|
|
|
|
statistikkfelt omdøpt til `my_*` og gjort VIEWER-relative (regnes nå fra
|
|
|
|
|
|
den spørrende brukerens egen deltaker-rad, ikke alltid eierens — løser
|
|
|
|
|
|
et teknisk konsekvens-punkt notert allerede 2026-07-25 da "medspiller
|
|
|
|
|
|
ser runden i egen liste" ble bekreftet). `RoundParticipantOut` fikk et
|
|
|
|
|
|
nytt, alltid utfylt `display_name`-felt (levende oppslått, ikke
|
|
|
|
|
|
snapshot) — fanget OG fikset en reell latent bug i samme runde:
|
|
|
|
|
|
leaderboard-endepunktet (bygget 2026-07-26, samme dag) ville vist et
|
|
|
|
|
|
tomt navn for enhver lenket medspiller, siden dets `display_name`-
|
|
|
|
|
|
utledning den gang kun sjekket `is_owner`.
|
|
|
|
|
|
**Scratch-verifisert grundig (39/39 sjekker i to testløp):** søk-og-
|
|
|
|
|
|
legg-til (kjønn/HCP auto-fylt, avvist duplikat/selv/ukjent bruker/
|
|
|
|
|
|
ufullstendig profil), lenket medspiller kan se runden+registrere score
|
|
|
|
|
|
for BÅDE egen OG andres rad (whole-flight), men KAN IKKE forvalte runden
|
|
|
|
|
|
(rediger/slett/legge til/fjerne/endre stat_level — alle 403), lenket
|
|
|
|
|
|
medspiller KAN fullføre runden, `/rounds`-listen viser nå runden for en
|
|
|
|
|
|
lenket medspiller (med DERES egen fremdrift, ikke eierens), en helt
|
|
|
|
|
|
urelatert bruker fortsatt 403/ikke i listen, leaderboardets navn stemmer
|
|
|
|
|
|
for alle tre deltakertyper (eier/medspiller/gjest). `test_isolation.sql`
|
|
|
|
|
|
12/12 uendret. Ekte typesjekket produksjonsbuild av frontend (ny
|
|
|
|
|
|
søk-UI i `round-detail.tsx`, "Gjest" omdøpt til "Medspiller" gjennomgående,
|
|
|
|
|
|
samme viewer-relative "Deg"-fiks portert til `round-stats.tsx`/
|
|
|
|
|
|
`round-scorecard.tsx`, som hadde samme latente bug).
|
2026-07-25 07:51:32 +02:00
|
|
|
|
|
2026-08-08 12:37:09 +02:00
|
|
|
|
### Beslutning E — Kategori-match ved rundevisibilitet: MINST ÉN, ikke ALLE
|
|
|
|
|
|
|
|
|
|
|
|
Beslutning B punkt 5 (over) spesifiserte fra start "minst én
|
|
|
|
|
|
`friend_categorization`-rad ... hvor kategorien finnes i
|
|
|
|
|
|
`round_visible_category`". En senere presisering 2026-07-29 (kun
|
|
|
|
|
|
dokumentert i kode-kommentarer, aldri i denne ADR-en — selve
|
|
|
|
|
|
dokumentasjonshullet som gjorde denne avviket vanskelig å spore) strammet
|
|
|
|
|
|
dette til at ALLE en venns kategorier måtte være i rundens synlige sett,
|
|
|
|
|
|
ikke bare én.
|
|
|
|
|
|
|
|
|
|
|
|
**Reversert til original Beslutning B-regel 2026-08-08**, etter at
|
|
|
|
|
|
brukeren rapporterte at en runde delt med kategorien "Make" ikke ble
|
|
|
|
|
|
synlig for vedkommendes ektefelle. Årsak: ektefellen var i tillegg
|
|
|
|
|
|
kategorisert "Storfamilie" (Beslutning A tillater flere kategorier per
|
|
|
|
|
|
venn), og siden "Storfamilie" ikke var huket av på DENNE runden, blokkerte
|
|
|
|
|
|
den strenge ALLE-regelen hele synligheten -- til tross for at den
|
|
|
|
|
|
relevante kategorien ("Make") faktisk var valgt. I praksis gjorde dette
|
|
|
|
|
|
det nærmest umulig å dele pålitelig med noen som var tagget i mer enn én
|
|
|
|
|
|
kategori, uten å huke av samtlige av dem hver gang.
|
|
|
|
|
|
|
|
|
|
|
|
**Ny/gjeninnført regel:** en venn ser runden hvis MINST ÉN av kategoriene
|
|
|
|
|
|
eieren har satt dem i, er i rundens synlige sett -- flere kategorier på
|
|
|
|
|
|
samme venn er bare flere sjanser til å matche, aldri en ekstra
|
|
|
|
|
|
begrensning. En venn med INGEN kategorier vises fortsatt aldri (uendret).
|
|
|
|
|
|
|
|
|
|
|
|
Rettet i fire duplikate SQL-steder (`app/routers/rounds.py`:
|
|
|
|
|
|
`_can_view_round`, `_friends_who_can_see_round`,
|
|
|
|
|
|
`list_friends_on_course`; `app/routers/round_messages.py`: feed-
|
|
|
|
|
|
listingen) -- samme sted-for-sted-duplisering som opprinnelig omtalt i
|
|
|
|
|
|
Beslutning B, nå alle konsistent på MINST ÉN-varianten.
|
|
|
|
|
|
|
2026-07-25 07:51:32 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-26 07:54:00 +02:00
|
|
|
|
## ADR-037: Individuelle turneringer, flerrunde-turneringer — grunnstruktur
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** reist 2026-07-26, som følge av en avklaringsrunde om et
|
|
|
|
|
|
ønsket turnering-leaderboard (se FEATURE_BACKLOG.md sin "Utvidelse
|
|
|
|
|
|
2026-07-26"-seksjon for hele forhistorien). TeeCup skal etter hvert
|
|
|
|
|
|
kunne arrangere turneringer for INDIVIDUELLE spillere (f.eks.
|
|
|
|
|
|
«Københavner» — se FEATURE_BACKLOG.md sitt eget punkt om flere
|
|
|
|
|
|
turneringsformater), ikke bare dagens Ryder Cup-lagformat (ADR-011),
|
|
|
|
|
|
og disse skal kunne gå over FLERE RUNDER med sammenlagt resultat. Et
|
|
|
|
|
|
fremtidig Order of Merit (sesong-sammenlagt på tvers av flere
|
|
|
|
|
|
turneringer, brukerens eksempel: "klubbdager") er identifisert som et
|
|
|
|
|
|
beslektet, men separat, SISTE steg — se eget avsnitt nederst, ikke
|
|
|
|
|
|
designet i denne runden.
|
|
|
|
|
|
|
|
|
|
|
|
Denne ADR-en dekker KUN grunnstrukturen (datamodell-plassering,
|
|
|
|
|
|
turnering-type, flerrunde-støtte, poengmodell). Selve de fem konkrete
|
|
|
|
|
|
formatene (Københavner, High-low-high, Robbins, Try all,
|
|
|
|
|
|
Flaggturnering) fra FEATURE_BACKLOG.md designes hver for seg OVENPÅ
|
|
|
|
|
|
denne grunnstrukturen, ikke i denne runden.
|
|
|
|
|
|
|
|
|
|
|
|
**Viktig presisering underveis, som endrer en tidligere antakelse
|
|
|
|
|
|
(2026-07-25-runden om "flere flighter i én frittstående runde"):** i en
|
|
|
|
|
|
formell, org-arrangert individuell turnering er "flight" KUN en
|
|
|
|
|
|
tee-tid-/spilletempo-gruppering (som i dag for lag-turneringer) — IKKE
|
|
|
|
|
|
en leaderboard-grense. Leaderboardet spenner alltid HELE feltet,
|
|
|
|
|
|
uavhengig av hvem som spilte sammen. Dette er strukturelt ulikt den ad
|
|
|
|
|
|
hoc "flere flighter i en frittstående runde"-ideen (der leaderboardet
|
|
|
|
|
|
bevisst skal avgrenses til det man selv satte opp) — de to
|
|
|
|
|
|
"flight"-begrepene ligner i UI, men er IKKE samme konsept. Holdes
|
|
|
|
|
|
bevisst adskilt, ikke forent, selv om forrige runde antydet det motsatte.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning A — Ny, parallell org-scopet datamodell (IKKE gjenbruk av `round`)
|
|
|
|
|
|
|
|
|
|
|
|
En individuell/flerrunde-turnering får sin EGEN, RLS-beskyttede
|
|
|
|
|
|
tabellstruktur under `organization_id` — IKKE en utvidelse av de
|
|
|
|
|
|
frittstående rundetabellene (`round`/`round_participant`/`round_hole`,
|
|
|
|
|
|
ADR-033).
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** ADR-033 Beslutning A var et BEVISST valg om at
|
|
|
|
|
|
frittstående runder er 100 % org-uavhengige (`plain_connection()`,
|
|
|
|
|
|
ingen RLS i det hele tatt — autorisasjon håndheves med
|
|
|
|
|
|
`WHERE owner_user_id = $1` i app-laget). Å gi `round` en valgfri
|
|
|
|
|
|
`organization_id`/`tournament_id` ville krevd HYBRID RLS (håndhevet kun
|
|
|
|
|
|
når organisasjon er satt) — en helt ny klasse sikkerhetslogikk som
|
|
|
|
|
|
IKKE finnes noe sted ellers i systemet i dag (`organization_id på ALLE
|
|
|
|
|
|
domenetabeller, håndhevet av RLS` er en ufravikelig invariant i
|
|
|
|
|
|
CLAUDE.md — en betinget/nullbar variant ville vært det FØRSTE
|
|
|
|
|
|
unntaket). Dette prosjektet har allerede én dokumentert hendelse
|
2026-08-03 06:24:57 +02:00
|
|
|
|
(RLS-tomstreng-bugen, se CHANGELOG.md 2026-07-16) som kom av
|
2026-07-26 07:54:00 +02:00
|
|
|
|
nettopp denne typen RLS-finesse — vi unngår bevisst å introdusere en
|
|
|
|
|
|
ny variant av samme risikoklasse. Noe skjema dupliseres (hull-for-hull-
|
|
|
|
|
|
registrering ligner mye på `round_hole`), men den delte regnelogikken
|
|
|
|
|
|
(`handicap_engine.py`) er allerede rammeverk-uavhengig og gjenbrukes
|
|
|
|
|
|
uendret uansett hvilken tabell dataene ligger i.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning B — Samme `tournament`-tabell, ny type-diskriminator
|
|
|
|
|
|
|
|
|
|
|
|
`tournament` får en ny kolonne, f.eks. `format_type` (`'team'` |
|
|
|
|
|
|
`'individual'`, default `'team'` for bakoverkompatibilitet med alle
|
|
|
|
|
|
eksisterende rader).
|
|
|
|
|
|
|
|
|
|
|
|
**Begrunnelse:** en individuell turnering trenger fortsatt navn, status,
|
|
|
|
|
|
datoer, synlighet (ADR-018), invitasjonskode (ADR-020), org-eierskap —
|
|
|
|
|
|
ALT dette er allerede bygget og fungerer uendret på `tournament`-raden,
|
|
|
|
|
|
uavhengig av format. Å lage en helt ny toppnivå-entitet ville betydd å
|
|
|
|
|
|
gjenoppbygge synlighet/join-kode/landingssider fra bunnen for et andre
|
|
|
|
|
|
system — ren duplisering uten reell gevinst. Lag-tabellene
|
|
|
|
|
|
(`team`/`team_roster`) blir ganske enkelt ubrukte for
|
|
|
|
|
|
`format_type = 'individual'` — håndheves i app-laget (samme mønster som
|
|
|
|
|
|
ADR-011s to-lags-grense), ikke med en tung DB-constraint på tvers av
|
|
|
|
|
|
tabeller.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning C — Flerrunde via en ny, økt-lignende tabell
|
|
|
|
|
|
|
|
|
|
|
|
Individuelle turneringer kan ha FLERE runder fra start, via en ny
|
|
|
|
|
|
`tournament_round`-tabell (org+tournament-scopet, samme rolle som
|
|
|
|
|
|
`session` har for lag-turneringer i dag — dato/bane/hullomfang per
|
|
|
|
|
|
runde). Sammenlagt resultat på tvers av rundene i turneringen summeres
|
|
|
|
|
|
ved lesing, SAMME "summer på ekte deltaker-id, aldri på en løs
|
|
|
|
|
|
side-label"-prinsipp som det eksisterende lag-leaderboardet allerede
|
|
|
|
|
|
bruker (`fetch_leaderboard`, `app/routers/tournaments.py`) for å unngå
|
|
|
|
|
|
å blande sammen feil rader.
|
|
|
|
|
|
|
|
|
|
|
|
**Bane-referanse:** `tournament_round.course_id` peker til den
|
|
|
|
|
|
EKSISTERENDE org-scopede `course`-tabellen (samme som `session` bruker
|
|
|
|
|
|
i dag) — INGEN snapshot-mekanisme som i frittstående runder
|
|
|
|
|
|
(ADR-033s `course_name_snapshot`). Organisasjonens egen banedata er
|
|
|
|
|
|
allerede stabil, admin-kontrollert data; snapshot-behovet i ADR-033 kom
|
|
|
|
|
|
av at frittstående runder leser LIVE fra en ekstern, ikke-org-kontrollert
|
|
|
|
|
|
kilde (teeoff) — samme begrunnelse gjelder ikke her.
|
|
|
|
|
|
|
|
|
|
|
|
**Deltaker-identitet:** en ny `tournament_participant`-tabell (org+
|
|
|
|
|
|
turnering-scopet, samme rolle som `team_roster` — refererer til
|
|
|
|
|
|
EKSISTERENDE `player`-tabellen, fryser `handicap_index_snapshot` ved
|
|
|
|
|
|
uttak, samme reproduserbarhetsprinsipp som ADR-007) — IKKE en ny,
|
|
|
|
|
|
frittstående gjeste-modell slik `round_participant` har. En turnering
|
|
|
|
|
|
har alltid en organisator som administrerer en kjent spillerpool
|
|
|
|
|
|
(samme som lag-turneringer i dag), ulikt en privatpersons frittstående
|
|
|
|
|
|
runde.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning D — Rå slag lagres, poeng caches per format (samme mønster som match-play)
|
|
|
|
|
|
|
|
|
|
|
|
Hver `tournament_round`-deltaker sin hull-for-hull-registrering lagrer
|
|
|
|
|
|
RÅ BRUTTO SLAGTALL per hull (samme grunnform som `round_hole.score`) —
|
|
|
|
|
|
scorekortet trenger dette uansett, og det gir handlefrihet til å vise
|
|
|
|
|
|
flere ulike visninger (brutto/netto/stableford/Københavner-poeng) av
|
|
|
|
|
|
SAMME underliggende data.
|
|
|
|
|
|
|
|
|
|
|
|
I TILLEGG caches et FERDIG UTREGNET poengtall per format — samme
|
|
|
|
|
|
etablerte mønster som `match.status_text`/`points_side_a/b` i dag:
|
|
|
|
|
|
motoren (`handicap_engine.py`, ny funksjon per scoringsmetode) regner
|
|
|
|
|
|
poeng ved HVER hull-innsending, resultatet lagres i en egen kolonne
|
|
|
|
|
|
(f.eks. på en ny `tournament_round_score`-rad, én per deltaker per
|
|
|
|
|
|
runde) — rask leaderboard-lesing uten å måtte regne ut alt på nytt for
|
|
|
|
|
|
hele feltet ved hver visning, konsistent med hvordan matchstatus
|
|
|
|
|
|
allerede caches og regnes på nytt ved hver innsending
|
|
|
|
|
|
(`recompute_and_cache_match_state`).
|
|
|
|
|
|
|
|
|
|
|
|
`tournament.scoring_method` (eller ev. per `tournament_round`, hvis et
|
|
|
|
|
|
fremtidig format trenger å blande metoder — ikke avklart, default: fast
|
|
|
|
|
|
per turnering) avgjør HVILKEN motorfunksjon som brukes til å regne
|
|
|
|
|
|
poeng fra de rå slagene — ny CHECK-verdi + ny ren funksjon i
|
|
|
|
|
|
`handicap_engine.py` per format som legges til (`stroke_play_gross`,
|
|
|
|
|
|
`stroke_play_net`, `stableford`, `copenhagen_points`, ...). Hvert nytt
|
|
|
|
|
|
format fra FEATURE_BACKLOG.md sin liste blir dermed i hovedsak: én ny
|
|
|
|
|
|
motorfunksjon (testet isolert, samme "test i isolasjon FØR resten"-
|
|
|
|
|
|
prinsipp som ADR-005) + én ny CHECK-verdi, ikke en skjemaendring.
|
|
|
|
|
|
|
|
|
|
|
|
### Konsekvens — hva denne ADR-en IKKE avgjør ennå
|
|
|
|
|
|
|
|
|
|
|
|
- **De fem konkrete formatene** (Københavner, High-low-high, Robbins,
|
|
|
|
|
|
Try all, Flaggturnering) — hver trenger sin egen, mindre design-runde
|
|
|
|
|
|
oppå denne grunnstrukturen (poengformel, evt. spesielle
|
|
|
|
|
|
paringsregler for High-low-high/Robbins som IKKE er rent individuelle
|
|
|
|
|
|
poeng-per-hull).
|
|
|
|
|
|
- **Order of Merit** (sesong-sammenlagt på tvers av FLERE separate
|
|
|
|
|
|
turneringer). Bekreftet av brukeren som et beslektet, men SISTE steg
|
|
|
|
|
|
— forutsetter at individuelle turneringer med et poengresultat
|
|
|
|
|
|
finnes først. Krever sannsynligvis et helt nytt overordnet konsept
|
|
|
|
|
|
(en "sesong"/"serie", org-scopet, RLS som ellers) som grupperer flere
|
|
|
|
|
|
`tournament`-rader og akkumulerer poeng — IKKE designet i denne
|
|
|
|
|
|
runden.
|
Backend-laget (migrasjon 040 + motor + API) er ferdig bygget og grundig scratch-verifisert — 63/63 motor-tester, 52/52 API-sjekker, test_isolation.sql fortsatt 12/12. Dokumentasjonen er oppdatert (CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md).
Bevisst utenfor denne runden: frontend (ingen skjerm ennå — samme "motor → skjema → API → frontend"-rekkefølge som tidligere ADR-er), og en senere innstramming av autorisasjon (i dag bredt org-medlemskap for alt, inkl. scoring — analogt med at ADR-023s kaptein-only kom som egen, senere runde for lagturneringer).
Før jeg ruller ut mot ekte teecup_db, her er planen:
Kjør migrasjon 040_individual_tournaments.sql mot ekte teecup_db — rent additivt: to nye kolonner på tournament (format_type default 'team', scoring_method nullable) + fem nye tabeller (tournament_round, tournament_participant, tournament_round_participant, tournament_round_hole, tournament_round_score), full RLS. Ingen eksisterende rader røres.
Verifiser at kolonnene/tabellene ble opprettet riktig, og kjør test_isolation.sql mot ekte database (forventer fortsatt 12/12).
Redeploy kun teecup_api (docker compose up -d --build teecup_api) — ren backend-endring, ingen frontend-kode denne runden.
Verifiser at containeren booter rent, /health//dashboard fortsatt 200, den nye API-stien faktisk når FastAPI (f.eks. anonymt kall gir riktig 401, ikke en rå 404), og teeoff.no er upåvirket.
2026-07-30 07:12:05 +02:00
|
|
|
|
- **Migrasjon/kode — ✅ BYGGET OG SCRATCH-VERIFISERT 2026-07-30, IKKE ENNÅ
|
|
|
|
|
|
RULLET UT.** Migrasjon `040_individual_tournaments.sql` (alle fem nye
|
|
|
|
|
|
tabeller + `tournament.format_type`/`scoring_method`), nye motorfunksjoner
|
|
|
|
|
|
i `handicap_engine.py` (`stroke_play_gross_total`/`stroke_play_net_total`/
|
|
|
|
|
|
`stableford_points_for_hole`/`stableford_total`, 8 nye tester), og et
|
|
|
|
|
|
fullt CRUD-API (`app/routers/individual_tournaments.py`: runder/
|
|
|
|
|
|
turnering-deltakere/rundedeltakere/hull-for-hull-scoring/leaderboard).
|
|
|
|
|
|
Se FEATURE_BACKLOG.md ("Utvidelse 2026-07-26"-seksjonens oppdatering
|
|
|
|
|
|
2026-07-30) for full detalj om design, funn under bygging og
|
|
|
|
|
|
verifisering (52 API-sjekker + 63 motor-tester). Frontend er bevisst
|
|
|
|
|
|
IKKE bygget i denne runden — neste steg i samme "motor → skjema → API →
|
|
|
|
|
|
frontend"-rekkefølge som ADR-033/038/039.
|
2026-07-26 07:54:00 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
## ADR-038: Faktisk (beregnet) HCP vs. manuelt satt HCP
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** reist 2026-07-28, rett etter en gjennomgang av frittstående
|
|
|
|
|
|
rundeføring (ADR-033) som avdekket et reelt, ubesluttet hull: hele
|
|
|
|
|
|
WHS-indeksmotoren (`handicap_index_from_differentials`/
|
|
|
|
|
|
`low_handicap_index`/`apply_index_caps` i `handicap_engine.py`, testet
|
|
|
|
|
|
41/41) kalles ALDRI fra noe API-endepunkt — `round_participant.
|
|
|
|
|
|
score_differential` regnes og lagres per runde, men `app_user.
|
|
|
|
|
|
handicap_index` endres kun manuelt (`PATCH /auth/profile`). Bekreftet
|
|
|
|
|
|
eksplisitt i `rounds.py` sin egen moduldoc som "eksplisitt uavklart punkt
|
|
|
|
|
|
i ADR-033".
|
|
|
|
|
|
|
|
|
|
|
|
Brukeren avklarte at TeeCup skal operere med TO tall, ikke ett:
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning A — To atskilte HCP-verdier
|
|
|
|
|
|
|
|
|
|
|
|
- **Manuelt satt HCP** (eksisterende `app_user.handicap_index`,
|
|
|
|
|
|
ADR-031) — UENDRET betydning og bruk: dette er tallet som brukes til
|
|
|
|
|
|
Course Handicap-beregning i enhver runde/deltaker med mindre
|
|
|
|
|
|
eksplisitt overstyrt per runde (allerede slik i dag via
|
|
|
|
|
|
`handicap_index_snapshot`). Redigeres fritt av brukeren selv, som i
|
|
|
|
|
|
dag.
|
|
|
|
|
|
- **Faktisk HCP** (ny `app_user.computed_handicap_index` +
|
|
|
|
|
|
`computed_handicap_index_updated_at`) — WHS Handicap Index (Rule 5.2),
|
|
|
|
|
|
regnet automatisk fra beste 8 av de ≤20 nyeste TELLENDE Score
|
|
|
|
|
|
Differential-ene, med den offisielle opptrappingstabellen for færre
|
|
|
|
|
|
enn 20 runder (`_INDEX_TABLE_UNDER_20`, allerede implementert og
|
|
|
|
|
|
testet — ingen ny motorkode trengs, kun at den faktisk kalles). Aldri
|
|
|
|
|
|
direkte redigerbar — kun avledet.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning B — Uttrykkelig "overfør til manuelt HCP"-handling
|
|
|
|
|
|
|
|
|
|
|
|
Golferen får en egen, eksplisitt knapp ("Bruk som mitt HCP") i
|
|
|
|
|
|
`/account` som kopierer gjeldende `computed_handicap_index` inn i
|
|
|
|
|
|
`handicap_index` (samme skrivevei/historikk-logging som en vanlig
|
|
|
|
|
|
manuell PATCH). INGEN automatisk synkronisering noen vei — de to
|
|
|
|
|
|
tallene lever bevisst uavhengig av hverandre helt til brukeren selv
|
|
|
|
|
|
trykker knappen.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning C — Eksplisitt eksklusjon per deltaker, kontrollert av HVER innlogget deltaker selv
|
|
|
|
|
|
|
|
|
|
|
|
Ny `round_participant.exclude_from_handicap boolean DEFAULT false` —
|
|
|
|
|
|
uavhengig av `counts_for_handicap` (som fortsatt er den AUTOMATISKE
|
|
|
|
|
|
WHS-kvalifiseringen, minimum spilte hull). Endelig medregning i faktisk
|
|
|
|
|
|
HCP krever BEGGE: `counts_for_handicap AND NOT exclude_from_handicap`.
|
|
|
|
|
|
|
|
|
|
|
|
**Avklart eksplisitt med bruker (AskUserQuestion), IKKE gjettet:** hver
|
|
|
|
|
|
INNLOGGET deltaker (eier ELLER en lenket medspiller, ADR-036 fase 3)
|
|
|
|
|
|
styrer SIN EGEN rads eksklusjon — ikke bare runde-eieren. Autorisasjon i
|
|
|
|
|
|
`PATCH .../participants/{id}` utvidet presist til akkurat dette ene
|
|
|
|
|
|
feltet: en ikke-eier kan KUN sende `exclude_from_handicap` (ethvert
|
|
|
|
|
|
annet felt i samme kall avvises 403) og KUN på sin egen rad (`rp.user_id
|
|
|
|
|
|
== requester`) — alle andre felt (tee/HCP/kjønn/navn/stat_level) forblir
|
|
|
|
|
|
strengt eier-only, uendret. Feltet er bevisst IKKE en del av
|
|
|
|
|
|
`rating_changed`-sperren i `update_participant` — kan endres uansett
|
|
|
|
|
|
fullført-status, siden det ikke påvirker AGS/differensial-matematikken
|
|
|
|
|
|
i seg selv, kun om resultatet TELLER i det HELE tatt.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning D — Spilleform (slagspill/matchspill), selvdeklarert
|
|
|
|
|
|
|
|
|
|
|
|
Ny `round.play_format text DEFAULT 'stroke' CHECK (IN ('stroke',
|
|
|
|
|
|
'match'))`. Frittstående runder har i dag INGEN uavhengig måte å
|
|
|
|
|
|
"oppdage" matchspill på (ingen egen match-motor, ulikt turnering-siden)
|
|
|
|
|
|
— feltet er derfor selvdeklarert av brukeren ved oppsett (og fritt
|
|
|
|
|
|
redigerbart senere, som ren metadata, uendret av fullført-status).
|
|
|
|
|
|
|
|
|
|
|
|
**WHS-kilde lest og lagt til grunn** (`WHS_Rules_of_Handicapping_2024.
|
|
|
|
|
|
pdf`, Rule 3.3 "When a Hole is Started But Player Does Not Hole Out"):
|
|
|
|
|
|
matchspill-scorer ER teknisk et gyldig HCP-grunnlag under WHS (Rule
|
|
|
|
|
|
2.1a), MEN et hull som avgjøres/konsederes før utspilt krever en
|
|
|
|
|
|
subjektiv "most likely score" (avgrenset til netto dobbel bogey) — noe
|
|
|
|
|
|
TeeCups rene tallregistrering ikke har noen UI-vei til å representere
|
|
|
|
|
|
presist. Dette begrunner brukerens instinkt ("man bruker vanligvis ikke
|
|
|
|
|
|
matchspill til å beregne hcp") uten å hardkode et forbud: WHS tillater
|
|
|
|
|
|
det teknisk, men TeeCup kan ikke garantere et pålitelig grunnlag for
|
|
|
|
|
|
det i dag.
|
|
|
|
|
|
|
|
|
|
|
|
**Derfor: spør, ikke tving.** Når `play_format = 'match'` velges (ved
|
|
|
|
|
|
opprettelse, ELLER ved senere tillegg av en lenket medspiller), foreslår
|
|
|
|
|
|
frontend `exclude_from_handicap = true` som FORHÅNDSVALGT verdi med en
|
|
|
|
|
|
forklarende tekst — brukeren kan uansett overstyre til `false` (f.eks.
|
|
|
|
|
|
en fullstendig utspilt vennskapelig 18-hulls "matchspill" der alle hull
|
|
|
|
|
|
faktisk ble spilt ut). Ingen server-side tvang — `play_format` og
|
|
|
|
|
|
`exclude_from_handicap` er to uavhengige felt i skjemaet; kun
|
|
|
|
|
|
frontendens forhåndsutfylling kobler dem sammen.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning E — Datagrunnlag: designet for BEGGE kilder, bygget for én
|
|
|
|
|
|
|
|
|
|
|
|
**Avklart eksplisitt med bruker:** "faktisk HCP" skal på sikt kunne
|
|
|
|
|
|
telle scorer fra BÅDE frittstående runder OG organisasjons-/
|
|
|
|
|
|
turneringsscoring, men v1 bygger KUN kilden for frittstående runder.
|
|
|
|
|
|
Løst med ett tynt, bevisst uabstrahert skjøtepunkt — IKKE en ny,
|
|
|
|
|
|
generell "scoring record"-tabell (ville vært for tidlig abstraksjon for
|
|
|
|
|
|
en kilde som ikke finnes ennå, se CLAUDE.md sin regel mot design for
|
|
|
|
|
|
hypotetiske fremtidige krav): en enkelt funksjon
|
|
|
|
|
|
`_gather_qualifying_differentials(conn, user_id)` i `rounds.py` henter
|
|
|
|
|
|
i dag KUN fra `round_participant JOIN round`, med en kommentar som
|
|
|
|
|
|
peker ut nøyaktig dette som skjøtepunktet for en fremtidig UNION mot
|
|
|
|
|
|
turnering-siden (som uansett trenger sin egen ADR-037-baserte
|
|
|
|
|
|
poeng-/differensial-motor FØRST, se ADR-037 Beslutning D).
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning F — Low Handicap Index / cap-historikk gjenbruker `handicap_history`, ikke en ny tabell
|
|
|
|
|
|
|
|
|
|
|
|
Rule 5.7 (Low Handicap Index, laveste indeks i 365 dager) og Rule 5.8
|
|
|
|
|
|
(soft/hard cap) krever en historikk av TIDLIGERE beregnede indeksverdier
|
|
|
|
|
|
å sammenligne mot — samme grunnform som den eksisterende
|
|
|
|
|
|
`handicap_history`-tabellen (ADR-031-oppfølging, migrasjon 018), men
|
|
|
|
|
|
den logger i dag KUN manuelle profilendringer. Løst med én ny kolonne
|
|
|
|
|
|
`handicap_history.source text DEFAULT 'manual' CHECK (IN ('manual',
|
|
|
|
|
|
'computed'))` fremfor en parallell tabell — samme tabell, to kilder,
|
|
|
|
|
|
`low_handicap_index()` filtrerer på `source = 'computed'`. Det aller
|
|
|
|
|
|
første beregnede indekstallet for en bruker har ingen tidligere
|
|
|
|
|
|
`computed`-historikk å låses mot — capping hoppes bevisst over da
|
|
|
|
|
|
(Rule 5.8 sitt "Low HI" er per definisjon udefinert før en første indeks
|
|
|
|
|
|
er etablert), ikke behandlet som en feil.
|
|
|
|
|
|
|
|
|
|
|
|
### Konsekvens — hva denne ADR-en IKKE dekker
|
|
|
|
|
|
|
|
|
|
|
|
- Turnering-/organisasjonsscoring teller fortsatt IKKE mot faktisk HCP
|
|
|
|
|
|
(Beslutning E) — venter på ADR-037s videre arbeid.
|
|
|
|
|
|
- Ingen automatisk periodisk "aging" av gamle differensialer utover de
|
|
|
|
|
|
20 nyeste (Rule 5.5) — løst implisitt ved at spørringen alltid henter
|
|
|
|
|
|
KUN de 20 nyeste, ikke en egen bakgrunnsjobb.
|
|
|
|
|
|
- Stableford-format for frittstående runder (se FEATURE_BACKLOG.md) er
|
|
|
|
|
|
fortsatt ikke bygget — påvirker ikke denne ADR-en, siden Score
|
|
|
|
|
|
Differential regnes fra Adjusted Gross Score uavhengig av
|
|
|
|
|
|
poengformat.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
## ADR-039: Ekte spillformer for frittstående runder (match/skins/par-lag)
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** reist 2026-07-28, rett etter ADR-038: "Når en singlerunde
|
|
|
|
|
|
settes opp: Er dette en slagspillsrunde, en match mellom to spillere,
|
|
|
|
|
|
skins, eller en par- eller lag-konkurranse. Avhengig av svaret så må hcp
|
|
|
|
|
|
beregnes forskjellig, og også scorekortet vil se annerledes ut." Viktig
|
|
|
|
|
|
presisering: `round.play_format` (ADR-038) styrer i dag KUN et
|
|
|
|
|
|
HCP-eksklusjonsforslag — ikke selve scorings- eller scorekort-modellen.
|
|
|
|
|
|
Dette utvider `play_format` til å faktisk STYRE begge deler.
|
|
|
|
|
|
|
|
|
|
|
|
Fire load-bærende beslutninger avklart eksplisitt med bruker
|
|
|
|
|
|
(AskUserQuestion, to runder) før denne ADR-en ble skrevet.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning A — To sider per runde, gjenbruker ADR-011s antakelse
|
|
|
|
|
|
|
|
|
|
|
|
Match/fourball/foursome/greensome/scramble trenger et «hvem spiller
|
|
|
|
|
|
mot hvem»-konsept `round_participant` ikke har i dag (en flat liste
|
|
|
|
|
|
uten gruppering). Løst med nøyaktig TO SIDER per runde (ny
|
|
|
|
|
|
`round_side`-tabell), håndhevet i app-laget — samme mønster som
|
|
|
|
|
|
ADR-011s to-lags-grense for organisasjons-turneringer, ikke en
|
|
|
|
|
|
DB-constraint. En side er 1 spiller (match) eller 2+ (fourball/
|
|
|
|
|
|
foursome/greensome/scramble_2/scramble_4). `round_participant.
|
|
|
|
|
|
round_side_id` (nullable — kun satt for to-sidede formater) knytter
|
|
|
|
|
|
deltakeren til sin side.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning B — Alle fire par-/lag-underformater fra start, samme motor som turneringer
|
|
|
|
|
|
|
|
|
|
|
|
Bruker valgte bredt omfang: fourball, foursome, greensome OG begge
|
|
|
|
|
|
scramble-variantene (`scramble_2`/`scramble_4`) — ikke bare fourball
|
|
|
|
|
|
(som alene ville passet uendret inn i dagens skjema).
|
|
|
|
|
|
|
|
|
|
|
|
**Gjenbruker den EKSISTERENDE, testede match-play-motoren fullstendig
|
|
|
|
|
|
uendret** (`handicap_engine.py` sin `Format`-enum,
|
|
|
|
|
|
`AllowanceStrategy`-familien, `match_play_strokes`,
|
|
|
|
|
|
`allocate_over_played_holes`, `compute_match_state`, `HoleResult`) —
|
|
|
|
|
|
ingen ny motorkode for disse fire formatene. Selve API-laget
|
|
|
|
|
|
(`app/routers/rounds.py`) porterer det allerede bevisste,
|
|
|
|
|
|
produksjonskjørte mønsteret fra `app/handicap.py`/`app/routers/
|
|
|
|
|
|
scoring.py` (`compute_and_store_side_handicaps` →
|
|
|
|
|
|
`relative_strokes_for_match` → `_side_net`/`_compute_hole_results` →
|
|
|
|
|
|
`recompute_and_cache_match_state`), tilpasset `round`/`round_side` i
|
|
|
|
|
|
stedet for `match`/`team_side`. Samme `_SIDE_IS_UNIT`/`FORMAT_UNIT_SIZE`-
|
|
|
|
|
|
tabeller (hvor mange spillere som kreves per side før handicap kan
|
|
|
|
|
|
beregnes) gjenbrukes identisk.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning C — Delt-ball-formater: `round_hole` gjenbruker `hole_score`s nullable-nøkkel-mønster
|
|
|
|
|
|
|
|
|
|
|
|
Foursome/greensome/scramble har ÉN kombinert score per SIDE per hull,
|
|
|
|
|
|
ikke én per spiller — akkurat samme situasjon som org-scopet
|
|
|
|
|
|
`hole_score` allerede løser (`match_participant_id` NULLABLE, delt-ball-
|
|
|
|
|
|
rader identifisert av `team_side` alene, to partielle unike indekser).
|
|
|
|
|
|
`round_hole.round_participant_id` gjøres NULLABLE, ny
|
|
|
|
|
|
`round_hole.round_side_id` lagt til, med samme
|
|
|
|
|
|
`(participant XOR side)`-CHECK og to partielle unike indekser som
|
|
|
|
|
|
originalen. For fourball/match (individuell ball) forblir `round_hole`
|
|
|
|
|
|
uendret, én rad per DELTAKER per hull, akkurat som i dag.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens for statistikk (ADR-033 Beslutning B):** de detaljerte
|
|
|
|
|
|
per-hull-feltene (kølle/retning/chip/bunker/straffeslag/putt-lengde) gir
|
|
|
|
|
|
ingen entydig mening for en DELT ball (hvem sitt slag var det?) — for
|
|
|
|
|
|
delt-ball-formater lagres derfor KUN selve slagtallet på siderraden,
|
|
|
|
|
|
ingen av de andre detalj-feltene (samme "et fast sett felt, ikke fri
|
|
|
|
|
|
logg"-prinsipp, bare at feltsettet er tynnere for denne raden).
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning D — Skins: konfigurerbar netto/brutto OG rullerer/deles, ny motorfunksjon
|
|
|
|
|
|
|
|
|
|
|
|
Ingen eksisterende motorstøtte for skins i det hele tatt (ikke et
|
|
|
|
|
|
turnering-format). Bruker valgte at DEN SOM SETTER OPP runden skal
|
|
|
|
|
|
kunne velge BEGGE akser uavhengig — ikke ett fastlåst standardvalg:
|
|
|
|
|
|
- `round.skins_scoring`: `'net'` (HCP-slag trekkes fra, WHS-vanlig) |
|
|
|
|
|
|
`'gross'`.
|
|
|
|
|
|
- `round.skins_tie_handling`: `'carry'` (uavgjort hulls "skin" ruller
|
|
|
|
|
|
til neste hull, vinneren der tar hele potten) | `'split'` (uavgjort
|
|
|
|
|
|
hulls skin deles likt mellom de tied spillerne i stedet).
|
|
|
|
|
|
|
|
|
|
|
|
Ny, ren, testbar motorfunksjon `compute_skins(scores_by_hole,
|
|
|
|
|
|
tie_handling)` i `handicap_engine.py` — tar allerede netto-ELLER-brutto-
|
|
|
|
|
|
avgjorte per-hull-scorer (kalleren bestemmer hvilket FØR kall, motoren
|
|
|
|
|
|
vet ikke selv om input er netto eller brutto), returnerer
|
|
|
|
|
|
`{participant_id: antall skins vunnet}` (float, siden 'split' kan gi
|
|
|
|
|
|
brøkdeler). Ingen sider trengs for skins — rent individuelt, som
|
|
|
|
|
|
slagspill.
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning E — HCP-telling per format
|
|
|
|
|
|
|
|
|
|
|
|
- **Slagspill, fourball, skins** (individuell ball — hver spiller har
|
|
|
|
|
|
en FULL, ekte brutto-score per hull): teller normalt mot faktisk HCP,
|
|
|
|
|
|
samme pipeline som i dag (Score Differential fra AGS). Bekreftet
|
|
|
|
|
|
eksplisitt av bruker for skins — "krever fullt utspilte hull (ingen
|
|
|
|
|
|
konsedering som i match) -- samme grunnlagsproblem som gjorde match
|
|
|
|
|
|
upålitelig gjelder ikke her."
|
|
|
|
|
|
- **Match** (uendret fra ADR-038): fortsatt "spør, anbefal
|
|
|
|
|
|
ekskludering" — Rule 3.3-problemet (konsederte hull) gjelder fortsatt
|
|
|
|
|
|
uendret.
|
|
|
|
|
|
- **Foursome/greensome/scramble** (delt ball — INGEN individuell score
|
|
|
|
|
|
finnes å telle): `counts_for_handicap` forblir usann for disse
|
|
|
|
|
|
deltakerne i disse rundene, alltid, uansett antall spilte hull.
|
|
|
|
|
|
Bekreftet eksplisitt av bruker som riktig løsning — matcher for
|
|
|
|
|
|
øvrig hvordan WHS i praksis også behandler delt-ball-formater (ikke
|
|
|
|
|
|
indeks-byggende på samme måte som individuell/fourball-spill).
|
|
|
|
|
|
|
|
|
|
|
|
### Beslutning F — Scorekort-presentasjon (frontend, ikke bygget i denne runden)
|
|
|
|
|
|
|
|
|
|
|
|
Notert for senere: match/fourball/foursome/greensome/scramble trenger
|
|
|
|
|
|
en løpende matchstatus-visning (gjenbruk av `describe()`-språket fra
|
|
|
|
|
|
`MatchState`, samme som `session-scorecard.tsx` allerede viser for
|
|
|
|
|
|
turnering-matcher) i stedet for rå slagsummer. Skins trenger en
|
|
|
|
|
|
skins-tavle (skins vunnet per spiller + hvilke hull som fortsatt er
|
|
|
|
|
|
"i potten"). Presist UI-design ikke gjort i denne runden — kun
|
|
|
|
|
|
backend/motor/skjema.
|
|
|
|
|
|
|
|
|
|
|
|
### Konsekvens — hva som bygges i DENNE runden vs. senere
|
|
|
|
|
|
|
|
|
|
|
|
Bruker valgte å gå rett til migrasjon + motor (ikke bare dokumentere).
|
|
|
|
|
|
Bygget: skjema (migrasjon 031), `compute_skins`-motorfunksjon + tester,
|
|
|
|
|
|
og backend-API for sider/delt-ball-scoring/format-resultat. Frontend
|
|
|
|
|
|
(nye skjermer for sideoppsett og de nye scorekort-presentasjonene,
|
|
|
|
|
|
Beslutning F) er en egen, senere runde — samme lagdeling som ADR-038
|
|
|
|
|
|
(backend/motor FØR frontend).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-08-02 16:34:54 +02:00
|
|
|
|
## ADR-040: «Det store grepet» — sammenhengende rundeoppsett + delt
|
|
|
|
|
|
Score/Scorekort/Leaderboard-navigasjon
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** reist 2026-08-01/02, som en direkte oppfølging av flere
|
|
|
|
|
|
visuelle redesign-runder (login-skjerm og dashbord, begge inspirert av et
|
|
|
|
|
|
eksternt design-verktøy, "Stitch"). Brukeren pekte på to strukturelle
|
|
|
|
|
|
problemer som selve fargeredesignet ikke løser: (1) rundeoppsettet er i
|
|
|
|
|
|
dag spredt over ETT langt skjema (`new-round.tsx`) OG en helt annen skjerm
|
|
|
|
|
|
(`round-detail.tsx`, etter at runden allerede er opprettet, for sider/
|
|
|
|
|
|
Money Ball-rekkefølge), (2) Score/Scorekort/Leaderboard for en frittstående
|
|
|
|
|
|
runde deler ingen fast navigasjon — hver side bygger sin egen fulle
|
|
|
|
|
|
side-chrome, ulikt Stitchs skisse.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — rundeoppsettet blir én sammenhengende veiviser, 5 steg:**
|
|
|
|
|
|
Bane & tid → Spilleform (inkl. HCP-innstillinger) → Spillere → Lag/
|
|
|
|
|
|
rekkefølge (kun to-sidede formater/Money Ball) → Deling. Erstatter dagens
|
|
|
|
|
|
ett-langt-skjema-pluss-en-helt-annen-skjerm-mønster.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — HCP-prosent er ALLTID justerbar, Match-HCP er en egen
|
|
|
|
|
|
bryter:** brukeren presiserte eksplisitt at prosenten (ADR-014s
|
|
|
|
|
|
`strategy`) aldri skal være låst til formatets standardverdi, og at
|
|
|
|
|
|
to-sidede formater i tillegg skal kunne skru `use_matchplay_handicap`
|
|
|
|
|
|
av/på — differensial-fordeling mellom sidene (laveste settes til 0
|
|
|
|
|
|
mottatte, resten mottar differansen fordelt fra laveste stroke index) i
|
|
|
|
|
|
stedet for ren brutto når det ikke er noen forskjell. **Funnet ved
|
|
|
|
|
|
verifisering, ikke antatt:** begge deler fantes allerede FERDIGBYGD i
|
|
|
|
|
|
motoren (`match_play_strokes()` i `handicap_engine.py`, ADR-014s "fire
|
|
|
|
|
|
brytere" i `app/handicap.py`) — kun brukt av org-scopede turneringer
|
|
|
|
|
|
(`session.allowance_override`). Frittstående runder (`round`-tabellen)
|
|
|
|
|
|
manglet bare selve kolonnen; ingen ny regnelogikk.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — "Spillere og runde" (inkl. flighter) blir værende på
|
|
|
|
|
|
rundeoppsettsiden**, flytter IKKE inn i den nye delte fane-raden — dette
|
|
|
|
|
|
er forvaltning, ikke visning. Score/Scorekort/Leaderboard trenger i
|
|
|
|
|
|
stedet en kompakt, alltid tilgjengelig vei tilbake til oppsettsiden
|
|
|
|
|
|
(sammen med "Fullfør runde"/"Slett runde") — plassering (toppmeny vs.
|
|
|
|
|
|
footer vs. noe annet) er bevisst overlatt til V0 å foreslå ut fra en
|
|
|
|
|
|
presist beskrevet kontekst, ikke forhåndsbestemt her.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Scorekort og Statistikk slås sammen til ÉN side,**
|
|
|
|
|
|
statistikk RETT UNDER scorekortet — den delte fane-raden blir dermed tre
|
|
|
|
|
|
faner (Score / Scorekort / Leaderboard), ikke fire. `/my-rounds/[id]/
|
|
|
|
|
|
stats` forsvinner som egen rute, innholdet flytter inn i `/my-rounds/
|
|
|
|
|
|
[id]/scorecard`.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — reelt, tidligere udokumentert funn: spillere i
|
|
|
|
|
|
scorekortet var ALDRI gruppert lagvis.** Brukeren rapporterte det
|
|
|
|
|
|
konkret (en ekte fourball-runde der Erol+Kåre var ett lag, Ellen
|
|
|
|
|
|
Anette+Christer det andre, men vist interleaved i rekkefølgen de ble
|
|
|
|
|
|
lagt til). Bekreftet i koden på BEGGE steder spillere vises for et
|
|
|
|
|
|
to-sidet format: `ScorecardGrid` (`round-detail.tsx`) og
|
|
|
|
|
|
`MatchScorecardGrid` (`round-scorecard.tsx`) sorterte ingen av dem på
|
|
|
|
|
|
`round_side_id` — ren innsettingsrekkefølge. Fikset med en liten,
|
|
|
|
|
|
presis sortering (stabil sort på side, side A samlet FØR side B) i
|
|
|
|
|
|
begge, IKKE en ny funksjon. Om celle-nivå-fargelegging (hvem som vant
|
|
|
|
|
|
hullet) alene er nok til å skille lagene visuelt, eller om det trengs
|
|
|
|
|
|
en tydeligere lag-blokk-markering, er et åpent spørsmål overlatt til V0
|
|
|
|
|
|
(samme "spør V0"-mønster som Beslutning C).
|
|
|
|
|
|
|
|
|
|
|
|
**Bygget og verifisert denne runden (2026-08-02):**
|
|
|
|
|
|
- Ny migrasjon `051_round_allowance_override.sql` — `round.allowance_
|
|
|
|
|
|
override jsonb`, rent additivt.
|
|
|
|
|
|
- `app/routers/rounds.py`: ny `_round_allowance_override()`-hjelpe-
|
|
|
|
|
|
funksjon (henter/parser friskt fra `round`-raden, samme mønster som
|
|
|
|
|
|
`matches.py`/`scoring.py` sin `session.allowance_override` — IKKE
|
|
|
|
|
|
cachet i selve config-objektet), koblet inn i `_recompute_side_
|
|
|
|
|
|
handicaps`/`_relative_strokes_for_round`. `RoundCreate`/`RoundUpdate`/
|
|
|
|
|
|
`RoundOut` fikk `allowance_override`. PATCH gjenberegner `playing_
|
|
|
|
|
|
handicap` for allerede tildelte sider ved endring (samme mønster som
|
|
|
|
|
|
`tournaments.py` sin `_recompute_session_matches`).
|
|
|
|
|
|
- Scratch-verifisert presist: 100%→50%-prosent ga `playing_handicap`
|
|
|
|
|
|
24/10 → 12/5 (nøyaktig som beregnet for hånd fra HCP 20,0/10,0 mot en
|
|
|
|
|
|
slope/rating-satt egendefinert bane), `use_matchplay_handicap:false`
|
|
|
|
|
|
ga rå 12/5 i stedet for differensial 7/0 (`match_play_strokes([12,5])`
|
|
|
|
|
|
→ laveste=5 til 0, differanse 7). Slagspill upåvirket (regresjon
|
|
|
|
|
|
bekreftet).
|
|
|
|
|
|
- Lag-grupperingsfiksen (Beslutning E) browserverifisert mot en fersk
|
|
|
|
|
|
scratch-fourball-runde som gjenskapte brukerens eget rapporterte
|
|
|
|
|
|
scenario nøyaktig (fire spillere lagt til i interleaved rekkefølge
|
|
|
|
|
|
A→Rødt, B→Blått, C→Rødt, D→Blått) — bekreftet visuelt i BÅDE
|
|
|
|
|
|
`ScorecardGrid` (live) og `MatchScorecardGrid` (post-runde) at Rødt
|
|
|
|
|
|
lag nå vises samlet, deretter Blått.
|
|
|
|
|
|
- Rullet ut mot ekte systemer 2026-08-02, bruker bekreftet eksplisitt:
|
|
|
|
|
|
migrasjon 051 kjørt mot ekte `teecup_db` (kolonne bekreftet,
|
|
|
|
|
|
`test_isolation.sql` fortsatt 12/12), deretter `docker compose up -d
|
|
|
|
|
|
--build teecup_api teecup_frontend`. Begge containere boot-et rent,
|
|
|
|
|
|
`/health`/`/dashboard` → 200, anonym `PATCH /rounds/{ukjent-id}` ga
|
|
|
|
|
|
korrekt `401` (ikke en rå 404 — bekrefter ruten når FastAPI),
|
|
|
|
|
|
`teeoff.no` upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
**Steg 3 (V0-prompt 1, rundeoppsett-veiviseren) ✅ BYGGET, GRUNDIG
|
|
|
|
|
|
VERIFISERT OG LIVE (2026-08-02), samme dag:** V0-prompten (Beslutning A/B)
|
|
|
|
|
|
skrevet og kjørt av bruker (zip 25), `frontend/components/new-round.tsx`
|
|
|
|
|
|
skrevet fullstendig om — V0s 5-stegs veiviser-skall flettet med ekte
|
|
|
|
|
|
logikk: offisiell/egen bane-søk (inkl. nearby-geolokasjon), ekte egen-
|
|
|
|
|
|
bane-opprettelse (portert fra tidligere versjon), ekte `/people/search`,
|
|
|
|
|
|
ny `allowance_override`-konstruksjon i steg 2 (samme mønster som
|
|
|
|
|
|
`CreateSessionCard`s `buildAllowanceOverride`), og en helt ny
|
|
|
|
|
|
innsendingsorkestrering — runden (+ deltakere + sider/lineup) finnes
|
|
|
|
|
|
FØRST når HELE veiviseren er fullført (runde → sider → medspillere med
|
|
|
|
|
|
`round_side_id` satt direkte på opprettelse → eierens egen side-
|
|
|
|
|
|
tildeling via PATCH → Money Ball-rekkefølge via PATCH → naviger til
|
|
|
|
|
|
runden), ulikt den gamle "opprett runde først, legg til spillere
|
|
|
|
|
|
etterpå"-flyten.
|
|
|
|
|
|
**Reelle feil funnet og rettet under integrering:** V0s egen `Tee`-type
|
|
|
|
|
|
manglet kjønnsfelt (ville vist ufiltrerte utslag for både eier og
|
|
|
|
|
|
gjester) — lagt til `genders: ApiGender[]`, filtrert riktig per kjent
|
|
|
|
|
|
kjønn. To TS-feil (prop-spredning som overskrev `course`/`ownGender`
|
|
|
|
|
|
tilbake til `null`, og en `"x"`-kjønn-mismatch i tee-filtreringen).
|
|
|
|
|
|
**Bevisst, ærlig begrensning:** en spiller funnet via `/people/search`
|
|
|
|
|
|
har ukjent kjønn før de faktisk er lagt til runden (samme begrensning
|
|
|
|
|
|
som `PersonMatch`-modellen alltid har hatt) — utslagslisten for en slik
|
|
|
|
|
|
spiller er derfor UFILTRERT i veiviseren, med en forklarende tekst;
|
|
|
|
|
|
server-validering gir en klar feil ved et faktisk uforenlig valg.
|
|
|
|
|
|
**Verifisert grundig i isolert scratch:** full produksjonsbuild (alle
|
|
|
|
|
|
ruter listet), og to komplette nettleser-gjennomkjøringer mot en fersk
|
|
|
|
|
|
scratch-backend — én Fourball-runde (egen bane, gjest, to sider
|
|
|
|
|
|
opprettet og tildelt interleaved, 90 % HCP-prosent) og én Slagspill-
|
|
|
|
|
|
runde (rene standardvalg). Bekreftet direkte mot databasen at
|
|
|
|
|
|
`allowance_override` ble bygget nøyaktig riktig i begge tilfeller
|
|
|
|
|
|
(`{"strategy":{"type":"per_player","percentage":0.9},...}` for
|
|
|
|
|
|
Fourball — riktig `per_player`, ikke `combined`, siden fourball ikke er
|
|
|
|
|
|
en side-enhet — og `null` for Slagspill, ingen unødvendig payload),
|
|
|
|
|
|
sidene/tildelingene stemte, og HCP ble beregnet korrekt fra ekte
|
|
|
|
|
|
profildata (kryssjekket mot hånd-regnet Course Handicap for begge
|
|
|
|
|
|
testspillerne).
|
|
|
|
|
|
**Rullet ut live 2026-08-02**, bruker bekreftet eksplisitt: ingen
|
|
|
|
|
|
migrasjon (ren frontend), `docker compose up -d --build teecup_frontend`
|
|
|
|
|
|
(gjenskapte også `teecup_api` som vanlig bivirkning, ingen backend-kode
|
|
|
|
|
|
rørt). Begge containere boot-et rent, `/health`/`/dashboard`/
|
|
|
|
|
|
`/my-rounds/new` → 200, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
**Steg 4-5 (V0-prompt 2, den delte Score/Scorekort/Leaderboard-fane-raden
|
|
|
|
|
|
+ integrering) ✅ BYGGET, GRUNDIG VERIFISERT OG LIVE (2026-08-02), samme
|
|
|
|
|
|
dag:** V0-prompten (Beslutning C/D) skrevet — bevisst SMALERE i omfang
|
|
|
|
|
|
enn prompt 1: kun den delte header/fane-raden, ikke en redesign av selve
|
|
|
|
|
|
innholdet på Scorekort-siden (den sammenslåingen er ren sammenstabling av
|
|
|
|
|
|
to allerede ferdige komponenter, løst direkte i kode uten V0). Kjørt av
|
|
|
|
|
|
bruker (zip 26) — leverte `components/round-header.tsx` (kontekstblokk
|
|
|
|
|
|
med tilbakeknapp/bane/tee/dato/tid/Matchspill-merke, tre-fanet
|
|
|
|
|
|
segmentert kontroll, og en "Administrer"-knapp som åpner en bunnsheet-
|
|
|
|
|
|
dialog med lenke til "Spillere og runde" + "Fullfør runde" + to-stegs
|
|
|
|
|
|
"Slett runde") — tatt inn UENDRET (ren presentasjon, ingen logikk å
|
|
|
|
|
|
bytte ut).
|
|
|
|
|
|
**Ekte datalag bygget rundt den:** ny `components/round-page-shell.tsx`
|
|
|
|
|
|
— henter kun de feltene headeren trenger via `/rounds/{id}`, bygger
|
|
|
|
|
|
`tabHrefs` til de tre ekte rutene, og implementerer `onFinishRound`/
|
|
|
|
|
|
`onDeleteRound` som egne, selvstendige handlinger (samme
|
|
|
|
|
|
`POST .../complete`/`DELETE ...`-endepunkter som `round-detail.tsx` sin
|
|
|
|
|
|
opprinnelige logikk, men uavhengig av den siden komponenten også må
|
|
|
|
|
|
fungere fra Scorekort- og Leaderboard-siden).
|
|
|
|
|
|
**Wiret inn i alle tre destinasjonene:** Score (`round-detail.tsx`sin
|
|
|
|
|
|
gamle egne header fjernet, erstattet med `RoundPageShell` — den
|
|
|
|
|
|
INTERNE "Score"/"Spillere og runde"-fanevekslingen beholdt UENDRET som
|
|
|
|
|
|
et bevisst lavrisiko-valg, nås nå i tillegg via headerens
|
|
|
|
|
|
"Administrer"-dialog med en ny `?tab=manage`-URL-parameter lest ved
|
|
|
|
|
|
mount), Scorekort (`round-scorecard.tsx` + `round-stats.tsx` slått
|
|
|
|
|
|
sammen til ÉN side via en ny `embedded`-prop på begge, statistikk RETT
|
|
|
|
|
|
UNDER scorekortet som spec-et i Beslutning D — gammel `/stats`-rute er
|
|
|
|
|
|
nå en ren redirect dit), Leaderboard.
|
|
|
|
|
|
**Reelle feil funnet og rettet under integrering:** to `<main>`-
|
|
|
|
|
|
landemerker på samme side etter sammenslåingen av Scorekort+Statistikk
|
|
|
|
|
|
(ugyldig HTML/tilgjengelighet) — rettet med en `ContentTag`-switch
|
|
|
|
|
|
(`div` når embedded, `main` ellers) i `round-stats.tsx`. Fjernet nå
|
|
|
|
|
|
overflødige kryss-lenker mellom de to sammenslåtte sidene, kollapset
|
|
|
|
|
|
"Se scorekort"/"Se full rundestatistikk" til én "Se scorekort og
|
|
|
|
|
|
statistikk"-knapp i `round-detail.tsx` sitt fullført-banner.
|
|
|
|
|
|
**Verifisert grundig i isolert scratch:** full produksjonsbuild (alle
|
|
|
|
|
|
ruter listet), full nettleser-gjennomkjøring gjennom hele "Administrer"-
|
|
|
|
|
|
flyten — headeren konsistent på tvers av alle tre fanene, dialogens
|
|
|
|
|
|
lenke til "Spillere og runde" bekreftet å faktisk åpne riktig intern
|
|
|
|
|
|
fane via `?tab=manage`, ekte "Fullfør runde" (bekreftet `completed_at`
|
|
|
|
|
|
satt via et direkte API-kall etterpå) og ekte to-stegs "Slett runde"
|
|
|
|
|
|
(bekreftet navigerte korrekt til tom-tilstand på `/my-rounds`
|
|
|
|
|
|
etterpå). Traff en Turbopack-flakighet i selve scratch-dev-serveren
|
|
|
|
|
|
underveis (stuck spinner, ingen nettverkskall i det hele tatt) — løst
|
|
|
|
|
|
med en frisk containerstart, bekreftet IKKE en kodefeil (samme
|
|
|
|
|
|
kjente ustabilitetsklasse som tidligere dokumentert i dette
|
|
|
|
|
|
prosjektet, ikke noe nytt).
|
|
|
|
|
|
**Rullet ut live 2026-08-02**, bruker bekreftet eksplisitt: ingen
|
|
|
|
|
|
migrasjon (ren frontend), `docker compose up -d --build
|
|
|
|
|
|
teecup_frontend` (gjenskapte også `teecup_api` som vanlig bivirkning,
|
|
|
|
|
|
ingen backend-kode rørt). Begge containere boot-et rent,
|
|
|
|
|
|
`/health`/`/dashboard` → 200, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
**ADR-040 («det store grepet») er dermed HELT FERDIG, alle 5 steg,
|
|
|
|
|
|
backend + frontend, live:**
|
|
|
|
|
|
1. ~~Backend: `allowance_override`-migrasjon + kobling~~ ✅
|
|
|
|
|
|
2. ~~Kode-fiks: lag-sortering~~ ✅
|
|
|
|
|
|
3. ~~V0-prompt 1: den samlede rundeoppsett-veiviseren~~ ✅
|
|
|
|
|
|
4. ~~V0-prompt 2: den delte Score/Scorekort/Leaderboard-fane-raden~~ ✅
|
|
|
|
|
|
5. ~~Integrering + browserverifisering + utrulling~~ ✅
|
|
|
|
|
|
|
|
|
|
|
|
Bevisst utenfor omfang, egen fremtidig vurdering om ønskelig: den
|
|
|
|
|
|
INTERNE "Score"/"Spillere og runde"-fanevekslingen på selve Score-siden
|
|
|
|
|
|
ble bevisst beholdt (ikke fjernet/erstattet av `?tab=manage`-mekanismen
|
|
|
|
|
|
alene) som et lavrisiko-valg — kan forenkles videre senere om ønsket,
|
|
|
|
|
|
ikke noe brukeren har bedt om.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-08-04 03:49:41 +02:00
|
|
|
|
## ADR-041: Konkurranseklasser ("Damer fra 44, Herrer fra 50+")
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** Brukeren spurte om det var mulig å sette opp runder i en
|
|
|
|
|
|
turnering slik at f.eks. damer spiller fra utslag 44 mens herrer spiller
|
|
|
|
|
|
fra 50+. Undersøkelse viste at DELER allerede virket (hvert
|
|
|
|
|
|
`match_participant`/`round_participant` har alltid hatt sitt eget
|
|
|
|
|
|
`tee_id`, uavhengig av de andre, med server-side validering av at
|
|
|
|
|
|
utslaget har rating for spillerens kjønn, ADR-029) — men det var et
|
|
|
|
|
|
manuelt valg per spiller hver gang, ingen gjenbrukbar "klasse"-
|
|
|
|
|
|
gruppering. Ingen "klasse"/kategori-konsept for konkurranse fantes fra
|
|
|
|
|
|
før noe sted i skjemaet (kun den urelaterte venne-kategoriseringen,
|
|
|
|
|
|
ADR-036).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Klasser er fritt navngitte, ikke en fast taksonomi.**
|
|
|
|
|
|
Organisator oppretter klasser med eget navn (f.eks. "Damer", "Herrer A",
|
|
|
|
|
|
"Junior") og et VALGFRITT standardutslag — ikke en kjønns- eller
|
|
|
|
|
|
alders-spesifikk datamodell. Bekreftet eksplisitt med bruker
|
|
|
|
|
|
(`AskUserQuestion`) framfor en fast kjønn+alder-kombinasjon: dekker
|
|
|
|
|
|
kjønn/alder/HCP eller annet uten at systemet må forstå forskjellen,
|
|
|
|
|
|
samme fleksibilitets-prinsipp som `friend_categorization` (ADR-036).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Delt tabell, ikke duplisert per turneringstype.** Ny
|
|
|
|
|
|
`tournament_class`-tabell (migrasjon 053) FK'et til `tournament`, ikke
|
|
|
|
|
|
en utvidelse av selve `tournament`-raden — samme sidetabell-mønster som
|
|
|
|
|
|
`tournament_sponsor`. Siden BÅDE lagturneringer (ADR-011) og
|
|
|
|
|
|
individuelle turneringer (ADR-037) peker til samme delte `tournament`-
|
|
|
|
|
|
tabell (`format_type`-diskriminator), dekker ÉN tabell begge uten
|
|
|
|
|
|
duplisering. Klassetilhørighet lever på `team_roster.class_id`
|
|
|
|
|
|
(lagturneringer) og `tournament_participant.class_id` (individuelle
|
|
|
|
|
|
turneringer) — begge nullable, `ON DELETE SET NULL` (samme prinsipp som
|
|
|
|
|
|
`tournament_round_bbb_hole`s FK-er, migrasjon 044): sletter man en
|
|
|
|
|
|
klasse, forsvinner ikke roster-/deltaker-radene, de mister bare
|
|
|
|
|
|
merkelappen.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Standardutslag er en frontend-bekvemmelighet, ikke en
|
|
|
|
|
|
ny backend-mekanisme.** `tee_id` er ALLEREDE required og satt per
|
|
|
|
|
|
deltaker (ikke per økt/runde) ved både `POST .../matches/{id}/
|
|
|
|
|
|
participants` og `POST .../rounds/{id}/participants` — klassens
|
|
|
|
|
|
standardutslag forhåndsvelger bare dette feltet i UI-et (fortsatt fritt
|
|
|
|
|
|
overstyrbart), backend-kontrakten er uendret. Samme mønster som
|
|
|
|
|
|
`new-round.tsx`s eksisterende kjønnsfilter/-default for personlige
|
|
|
|
|
|
runder. Forhåndsvelgingen sjekker eksplisitt at utslaget faktisk finnes
|
|
|
|
|
|
på DEN aktuelle øktens/rundens bane før det brukes — en klasses
|
|
|
|
|
|
standardutslag kan tilhøre en annen bane enn den som er i bruk akkurat
|
|
|
|
|
|
da.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Egen resultatliste KUN i individuelle turneringer,
|
|
|
|
|
|
aldri i lagturneringer.** Den mest substansielle avklaringen
|
|
|
|
|
|
(`AskUserQuestion`, med begrunnelse presentert først): i lagturneringer
|
|
|
|
|
|
er poeng knyttet til hele KAMPER (lag mot lag, `match.points_side_a/b`),
|
|
|
|
|
|
ikke enkeltspillere — å dele opp dette per klasse gir ikke naturlig
|
|
|
|
|
|
mening, og brukeren bekreftet eksplisitt at klasse der KUN skal foreslå
|
|
|
|
|
|
utslag. Det finnes fra før et smalt, PER-ØKT individuelt leaderboard for
|
|
|
|
|
|
singles/fourball-slagspill (`fetch_individual_leaderboard`,
|
|
|
|
|
|
`tournaments.py`) — bevisst IKKE utvidet eller gjenbrukt av denne runden,
|
|
|
|
|
|
det er en annen, allerede eksisterende funksjon med annet formål
|
|
|
|
|
|
(sesjonsscoped, format-begrenset). I individuelle turneringer (flatt
|
|
|
|
|
|
felt, ADR-037) er splitting derimot en naturlig match: `individual_
|
|
|
|
|
|
leaderboard`-endepunktet fikk `class_id`/`class_name` lagt til i
|
|
|
|
|
|
SELECT+respons, men selve summerings-/sorteringslogikken er UENDRET —
|
|
|
|
|
|
frontend grupperer den allerede korrekt sorterte listen visuelt (stabil
|
|
|
|
|
|
gruppering bevarer riktig rangering per klasse), ingen ny motorlogikk i
|
|
|
|
|
|
`handicap_engine.py` trengtes (Copenhagen/BBB var allerede scope-
|
|
|
|
|
|
agnostiske på deltaker-mengde).
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens:** `053_tournament_classes.sql`. Ny klasse-CRUD i
|
|
|
|
|
|
`tournaments.py` (delt, siden `tournament`-tabellen er delt). `Roster
|
|
|
|
|
|
EntryUpdate` (tidligere KUN `is_captain: bool` påkrevd) lagt om til
|
|
|
|
|
|
`exclude_unset`-PATCH-semantikk. Ny `PATCH .../participants/{id}` i
|
|
|
|
|
|
`individual_tournaments.py` (fantes ikke før). Frontend: "Klasser"-kort
|
|
|
|
|
|
i BÅDE `tournament-detail.tsx` og `individual-tournament-detail.tsx`,
|
|
|
|
|
|
utslag-forhåndsutfylling i `session-blind-draw.tsx`s `AddSlotForm` og
|
|
|
|
|
|
`individual-tournament-detail.tsx`s `AssignRoundParticipantControl`,
|
|
|
|
|
|
klasse-gruppert `LeaderboardTab` (individuelle turneringer). Se
|
|
|
|
|
|
CHANGELOG.md 2026-08-04 for full bygge-/verifiseringsdetalj (inkl. tall-
|
|
|
|
|
|
for-tall-bekreftet leaderboard-rangering per klasse i ekte nettleser).
|
|
|
|
|
|
**Rullet ut live 2026-08-04**, bruker bekreftet eksplisitt.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-08-04 11:34:28 +02:00
|
|
|
|
## ADR-042: Delt, plattform-omfattende bane-mal-bibliotek (`personal_course`)
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** Brukeren oppdaget at turneringsmodulens "opprett manuell
|
|
|
|
|
|
bane" i praksis var ubrukelig (kun et navnefelt, ingen vei til hull/
|
|
|
|
|
|
utslag), og ba om at BÅDE turneringsmodulen og single-runde-modulen skal
|
|
|
|
|
|
tilby "bruk en eksisterende bane (TeeOff eller andres custom-bane) som
|
|
|
|
|
|
mal" ved manuell baneoppretting. Et oppfølgingsspørsmål presiserte at
|
|
|
|
|
|
disse custom-banene "bør lagres, og de må være offentlige" — synlige og
|
|
|
|
|
|
gjenbrukbare av andre.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Gjenbruk `personal_course`, ikke en ny tabell.**
|
|
|
|
|
|
`personal_course` (020_personal_rounds.sql, opprinnelig bygget for
|
|
|
|
|
|
frittstående runder) er, til tross for navnet og til tross for at ingen
|
|
|
|
|
|
tidligere runde eksplisitt utnyttet det, ALLEREDE en global,
|
|
|
|
|
|
plattform-omfattende katalog — `GET /personal-courses` søker på tvers av
|
|
|
|
|
|
ALLE brukeres baner uten eier- eller organisasjonsfiltrering, siden
|
|
|
|
|
|
tabellen (bevisst, fra migrasjon 020) aldri har vært RLS-/org-scopet.
|
|
|
|
|
|
Fremfor å bygge en ny `community_course`-tabell (vurdert og forkastet)
|
|
|
|
|
|
gjenbrukes denne eksisterende, allerede-globale tabellen direkte som det
|
|
|
|
|
|
delte mal-biblioteket — eneste tilføyelse er én kolonne,
|
|
|
|
|
|
`forked_from_id` (migrasjon 054, selvreferende FK, `ON DELETE SET NULL`)
|
|
|
|
|
|
for proveniens/attribusjon. Dette er også den ENESTE farbare veien for
|
|
|
|
|
|
at single-runde-modulen (som ikke har noe organisasjons-begrep i det
|
|
|
|
|
|
hele tatt) kan dele samme bibliotek som org-turneringsmodulen.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — "Offentlig" betyr hele plattformen, ikke bare egen
|
|
|
|
|
|
organisasjon.** Bekreftet eksplisitt med bruker (`AskUserQuestion`) etter
|
|
|
|
|
|
at jeg flagget spenningen: skal en publisert custom-bane være synlig for
|
|
|
|
|
|
ALLE organisasjoner + alle frittstående brukere, eller kun innad i én
|
|
|
|
|
|
organisasjon? Svaret var plattform-omfattende — organisasjonsgrensen
|
|
|
|
|
|
gjelder domenedata (turneringer, spillere, resultater), ikke dette
|
|
|
|
|
|
delte, lavsensitive bane-referansebiblioteket. En org-`course`-rad
|
|
|
|
|
|
(brukt til faktisk spill i en turnering) forblir like fullt org-scopet
|
|
|
|
|
|
og RLS-beskyttet som før — publisering til `personal_course` skjer som
|
|
|
|
|
|
en SEPARAT, samtidig innsetting i samme transaksjon (`courses.py`), ikke
|
|
|
|
|
|
en endring av `course`-tabellens egen skoping.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Eierskap håndheves i applikasjonslaget, ikke RLS.**
|
|
|
|
|
|
Siden `personal_course` bevisst aldri har vært org-scopet, finnes det
|
|
|
|
|
|
ingen `app.current_org`-kontekst å håndheve mot. De nye
|
|
|
|
|
|
endre-/dupliser-/slette-endepunktene (`rounds.py`) sjekker eksplisitt
|
|
|
|
|
|
`created_by_user_id == innlogget bruker` i handler-koden — samme
|
|
|
|
|
|
`plain_connection()`-uten-RLS-mønster som resten av `personal_course`/
|
|
|
|
|
|
`round`-familien allerede bruker (ADR-033 Beslutning A).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Rediger forker automatisk hvis du ikke eier raden;
|
|
|
|
|
|
Dupliser er en egen, eksplisitt handling.** `PATCH /personal-courses/{id}`:
|
|
|
|
|
|
eier den innloggede brukeren raden, oppdateres den i sted; eier
|
|
|
|
|
|
brukeren IKKE raden, opprettes automatisk en NY rad (eid av innlogget
|
|
|
|
|
|
bruker, `forked_from_id` satt til originalen) — originalen selv røres
|
|
|
|
|
|
aldri. Ett endepunkt dekker begge casene uten at frontend selv må
|
|
|
|
|
|
forgrene på eierskap. `POST .../duplicate` er en SEPARAT, eksplisitt
|
|
|
|
|
|
handling tilgjengelig uansett eierskap på kilden — dekker f.eks. å lage
|
|
|
|
|
|
en variant av DIN EGEN bane uten å miste originalen, noe fork-ved-
|
|
|
|
|
|
redigering alene ikke gjør (den trigges kun når raden IKKE er din).
|
|
|
|
|
|
Bekreftet eksplisitt med bruker at begge mekanismene skulle beholdes
|
|
|
|
|
|
side om side.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — Mal-bruk er en engangs-kopi, ingen vedvarende kobling.**
|
|
|
|
|
|
Når en org-`course` opprettes med en `personal_course` (eller en
|
|
|
|
|
|
TeeOff-bane) som utgangspunkt, kopieres dataene inn i den nye raden ved
|
|
|
|
|
|
opprettelsestidspunktet — ingen fremmednøkkel eller synk-mekanisme
|
|
|
|
|
|
knytter dem sammen etterpå. Samme filosofi som offisiell TeeOff-import
|
|
|
|
|
|
allerede etablerte (ADR-019): en mal er et utgangspunkt å redigere fritt
|
|
|
|
|
|
fra, ikke en levende referanse. Konsekvens: sletting av en
|
|
|
|
|
|
`personal_course`-mal påvirker ALDRI en org-`course`-rad som tidligere
|
|
|
|
|
|
ble opprettet fra den.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekvens/bevisst utenfor omfang:** ingen moderasjon/vetting av
|
|
|
|
|
|
offentlige baner (tillitsbasert, matcher appens øvrige
|
|
|
|
|
|
lukkede-plattform-antagelser); ingen privat/kun-min-org-synlighet per
|
|
|
|
|
|
bane (alt publisert via denne veien er plattform-offentlig, ingen
|
|
|
|
|
|
per-rad-innstilling bygget). Se CHANGELOG.md 2026-08-04 for full
|
|
|
|
|
|
bygge-/verifiseringsdetalj (34/34 håndregnede API-sjekker, ekte
|
|
|
|
|
|
nettleser-verifisering på tvers av to brukere/to organisasjoner).
|
|
|
|
|
|
**Rullet ut live 2026-08-04**, bruker bekreftet eksplisitt.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
Bygg Order of Merit: sesong-sammenlagt spillerrangering på tvers av turneringer
Ny funksjon (ADR-043, migrasjon 055): organisatorer kan opprette en
Order of Merit, lenke individuelle turneringer til den, og få en
sesong-sammenlagt rangering regnet ut ved lesing -- fem resultattyper
(poeng etter plassering, Stableford-, brutto- og nettoscore-sum,
pengeliste), sum/snitt-aggregering, med valgfri behold-N-beste,
minimum antall resultater og aldersgrense. Motor i handicap_engine.py
(9 nye enhetstester), ny app/routers/order_of_merit.py, og to nye
håndkodede frontend-sider under /organizations/[id]/order-of-merit.
Eclectic-aggregering og lag-OOM sin faktiske resultatberegning er
bevisst ikke bygget denne runden (skjema/CRUD for lag finnes, men
leaderboardet avviser kind='team' med en tydelig feilmelding).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 14:19:30 +02:00
|
|
|
|
## ADR-043: Order of Merit — sesong-sammenlagt rangering [OOM]
|
|
|
|
|
|
|
|
|
|
|
|
**Kontekst:** Brukeren ba om en vurdering av GolfBox sin "Order of
|
|
|
|
|
|
Merit"-funksjon (opplastet PDF) og om noe var verdt å adoptere. Svaret
|
|
|
|
|
|
var "ja, delvis": skillet mellom HVA som telles per turnering
|
|
|
|
|
|
(`result_type`) og HVORDAN det summeres over en sesong
|
|
|
|
|
|
(`aggregation_mode`) er et genuint godt, gjenbrukbart mønster —
|
|
|
|
|
|
GolfBox sin lag-kobling (strengmatching på lagnavn på tvers av
|
|
|
|
|
|
turneringer) ble eksplisitt flagget som noe å IKKE kopiere, siden
|
|
|
|
|
|
TeeCup allerede har ekte `player_id`-identitet på tvers av hele
|
|
|
|
|
|
organisasjonen.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Kun individuelle turneringer kan lenkes.** En OOM
|
|
|
|
|
|
(`order_of_merit`, migrasjon 055) lenker KUN `tournament.format_type =
|
|
|
|
|
|
'individual'`-turneringer (ADR-037) — ikke org-lagturneringer (ADR-011),
|
|
|
|
|
|
av samme grunn som konkurranseklassenes leaderboard-splitting (ADR-041):
|
|
|
|
|
|
poeng i en lagturnering er knyttet til hele kamper, ikke enkeltspillere.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — To uavhengige akser, fem resultattyper.** `result_type`
|
|
|
|
|
|
(poeng-etter-plassering/Stableford-sum/brutto/netto/pengeliste) utleder
|
|
|
|
|
|
HVA som telles fra en lenket turnerings EGEN, allerede beregnede
|
|
|
|
|
|
leaderboard (gjenbruker `_compute_individual_standings`, faktorisert ut
|
|
|
|
|
|
av `individual_tournaments.py` sin `individual_leaderboard` 2026-08-04 —
|
|
|
|
|
|
ingen duplisering av posisjon-/til-par-beregningen).
|
|
|
|
|
|
`aggregation_mode` (sum/snitt/eclectic) avgjør HVORDAN dette summeres
|
|
|
|
|
|
over flere turneringer, uavhengig av `result_type`. Poeng/pengeliste
|
|
|
|
|
|
trenger en per-lenke konfigurasjon (`order_of_merit_link.points_table`/
|
|
|
|
|
|
`money_payout_table`, JSONB) siden ulike turneringer i samme OOM kan gi
|
|
|
|
|
|
ulik poengverdi for samme plassering.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — "Lag" i en OOM er et sesong-par satt opp DIREKTE i
|
|
|
|
|
|
OOM-en, ikke hentet fra noen turnering.** Avklart eksplisitt med bruker
|
|
|
|
|
|
etter at jeg flagget en strukturell floke (individuelle turneringer har
|
|
|
|
|
|
per definisjon ingen lag, ADR-037): `order_of_merit_team`/
|
|
|
|
|
|
`_team_member` (migrasjon 055) er en REN gruppering av spillere oppå
|
|
|
|
|
|
deres allerede beregnede individuelle OOM-resultater — ingen
|
|
|
|
|
|
strengmatching, ingen ny turnering-struktur. Bevisst IKKE bygget i denne
|
|
|
|
|
|
runden (se FEATURE_BACKLOG.md).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — `kind` og `result_type` låses etter opprettelse.**
|
|
|
|
|
|
Matcher GolfBox sin egen tilsvarende låsing, strukket til også å dekke
|
|
|
|
|
|
`result_type` her: å endre den i etterkant ville gjort eksisterende
|
|
|
|
|
|
lenkers poeng-/pengetabell-konfig meningsløs uten en egen
|
|
|
|
|
|
migreringssti. `PATCH`-endepunktet eksponerer bevisst ikke disse to
|
|
|
|
|
|
feltene i det hele tatt (ikke bare app-lags-validering) — opprett en ny
|
|
|
|
|
|
OOM i stedet.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — "Regn ut ved lesing", ingen cache-tabell.** Samme
|
|
|
|
|
|
filosofi som Nassau/High-low-high/`individual_leaderboard` selv —
|
|
|
|
|
|
OOM-leaderboardet beregnes helt på nytt ved hvert `GET`-kall (henter
|
|
|
|
|
|
alle lenkede turneringers standings, utleder bidrag per spiller, filtrerer
|
|
|
|
|
|
på aldersgrense/minimum-resultater, aggregerer med
|
|
|
|
|
|
`order_of_merit_aggregate`, rangerer). Ingen ytelsesbekymring i praksis
|
|
|
|
|
|
gitt forventet skala (en sesong har typisk noen få til noen titalls
|
|
|
|
|
|
lenkede turneringer).
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang v1 (se FEATURE_BACKLOG.md for full liste):**
|
|
|
|
|
|
Eclectic-aggregering (per-hull "drømmerunde" på tvers av lenkede
|
|
|
|
|
|
turneringer — krever egen datainnsamlingslogikk, ikke bare en ny
|
|
|
|
|
|
aggregeringsmodus); Lag-OOM sin faktiske leaderboard-beregning (CRUD-
|
|
|
|
|
|
skjema finnes fra migrasjon 055, men `GET .../leaderboard` avviser
|
|
|
|
|
|
eksplisitt `kind='team'` med en tydelig feilmelding inntil videre).
|
|
|
|
|
|
|
|
|
|
|
|
**Motor** (`handicap_engine.py`): `order_of_merit_points_for_position`
|
|
|
|
|
|
(uavgjort deler SAMME poengverdi ved sin felles plassering, ikke
|
|
|
|
|
|
gjennomsnitt) og `order_of_merit_aggregate` (sum/snitt, valgfri
|
|
|
|
|
|
behold-N-beste — "best" er alltid størst-er-bedre, kalleren negerer
|
|
|
|
|
|
brutto/netto-verdier før kall). 9 nye enhetstester, alle grønne.
|
|
|
|
|
|
|
|
|
|
|
|
**Frontend:** to nye sider (`/organizations/[id]/order-of-merit` liste
|
|
|
|
|
|
+ opprett, `.../[oomId]` detalj: lenkede turneringer → innstillinger →
|
|
|
|
|
|
resultatliste, i den rekkefølgen). Håndkodet (ikke V0 — ingen
|
|
|
|
|
|
V0-credits tilgjengelig denne runden), bevisst bygget til samme
|
|
|
|
|
|
visuelle presisjon som appens V0-eksporter (samme tokens/kort-mønster
|
|
|
|
|
|
som `org-members.tsx`, samme gull-ledertrøye-mønster som
|
|
|
|
|
|
`stroke-play-leaderboard.tsx`). Ny inngangslenke fra `org-members.tsx`.
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert grundig** (isolert `teecup_app_scratch`-rolle +
|
|
|
|
|
|
isolert scratch-MinIO + engangs API-/frontend-container): 63/63
|
|
|
|
|
|
håndregnede API-sjekker (fem resultattyper, uavgjort-håndtering,
|
|
|
|
|
|
behold-N-beste, minimum-resultater, aldersgrense, låst `kind`/
|
|
|
|
|
|
`result_type`), RLS-isolasjon eksplisitt testet på de fire nye
|
|
|
|
|
|
tabellene (kryss-org-lesing gir 0 rader, kryss-org-skriving blokkeres av
|
|
|
|
|
|
`WITH CHECK`), `test_isolation.sql` 12/12 uendret, ekte
|
|
|
|
|
|
nettleser-gjennomgang (liste, opprettelse, lenke turnering inkl.
|
|
|
|
|
|
poeng-tabell-skjemaet, innstillinger, gull-ledertrøye, ikke-kvalifisert-
|
|
|
|
|
|
seksjon). Full detalj i CHANGELOG.md 2026-08-04.
|
|
|
|
|
|
**Rullet ut live 2026-08-04**, bruker bekreftet eksplisitt.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-08-06 07:01:22 +02:00
|
|
|
|
## ADR-044: Kommentarer/bilder på frittstående runder + samlet feed
|
|
|
|
|
|
|
|
|
|
|
|
Reist av brukeren 2026-08-06: samme "Banter Board"-mulighet (bilde+tekst)
|
|
|
|
|
|
som org-turneringer allerede har (ADR-025), men for en enkelt, frittstående
|
|
|
|
|
|
runde — pluss en sentral feed-side som samler dette på tvers av runder.
|
|
|
|
|
|
Underveis avdekket research at rundevisibilitet (ADR-036 fase 2 —
|
|
|
|
|
|
`round.visibility_mode`/`round_visible_category`) allerede var bygget og
|
|
|
|
|
|
live siden 2026-07-28 (en eldre, aldri oppdatert CHANGELOG-seksjon sa
|
|
|
|
|
|
fortsatt "ikke bygget") — denne ADR-en gjenbruker den infrastrukturen
|
|
|
|
|
|
uendret, den handler kun om selve kommentarene og feed-aggregeringen.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Skriverett: kan-se-kan-poste.** Alle som kan SE en runde
|
|
|
|
|
|
(eier, lenket medspiller, `public`-synlig, eller riktig-kategorisert
|
|
|
|
|
|
`friends`-venn — samme `_can_view_round` som ADR-036) kan også POSTE et
|
|
|
|
|
|
bilde/kommentar til den. Samme "kan se = kan bidra"-modell som org-feeden,
|
|
|
|
|
|
IKKE begrenset til eier+medspillere. Avklart eksplisitt med bruker
|
|
|
|
|
|
(AskUserQuestion) — overstyrer et tidligere, snevrere "eier+medspillere"-
|
|
|
|
|
|
utkast notert i FEATURE_BACKLOG.md 2026-08-06.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Moderering: forfatter ELLER rundeeier.** Samme
|
|
|
|
|
|
"forfatter-eller-admin"-modell som org-feedens `delete_feed_message`
|
|
|
|
|
|
(ADR-025 Beslutning C) — rundens eier er her analogen til org-admin. Ulikt
|
|
|
|
|
|
lag-chattens strengere "kun forfatter, ingen unntak" (den er ekte privat,
|
|
|
|
|
|
en runde-kommentarseksjon åpen for tredjeparter trenger en reell
|
|
|
|
|
|
moderasjonsvei).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Egen `round_message`-tabell (migrasjon 058), ikke
|
|
|
|
|
|
gjenbruk av org sin `message`-tabell.** `round` har verken
|
|
|
|
|
|
`organization_id` eller RLS (ADR-033 Beslutning A) — en delt tabell ville
|
|
|
|
|
|
krevd enten en kunstig organisasjon rundt hver frittstående runde eller et
|
|
|
|
|
|
RLS-unntak. Samme feltform (forfatter-snapshot, valgfri tekst, valgfri
|
|
|
|
|
|
bilde-nøkkel) og samme MinIO+AVIF-pipeline (`app/storage.py`, ADR-018)
|
|
|
|
|
|
gjenbrukt uendret, egen `prefix="round_messages"`. Autorisasjon håndheves
|
|
|
|
|
|
utelukkende i app-laget (`app/routers/round_messages.py`,
|
|
|
|
|
|
`plain_connection()`), ikke database-policy — samme mønster som
|
|
|
|
|
|
`round`/`round_participant`.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Sanntid: ingen ny WebSocket-kanal.** Gjenbruker den
|
|
|
|
|
|
eksisterende `broadcast_round_update`/`/ws/rounds/{id}/live`-kringkastingen
|
|
|
|
|
|
(allerede trigget ved hver hull-endring) fremfor et nytt, payload-bærende
|
|
|
|
|
|
kanal-mønster (slik org-feeden bruker). `round-detail.tsx` sin eksisterende
|
|
|
|
|
|
WS-lytter fikk ett nytt tick-signal (`messagesRefreshTick`) som
|
|
|
|
|
|
`RoundMessages` reagerer på med en vanlig REST-refetch — ingen egen socket
|
|
|
|
|
|
for kommentarer. Bekreftet fungerende i ekte nettleser (se
|
|
|
|
|
|
CHANGELOG.md 2026-08-06) — MERK at dette KUN virker gjennom Caddy sin
|
|
|
|
|
|
dedikerte `handle /ws/* { reverse_proxy teecup_api:8000 }`-rute
|
|
|
|
|
|
(`/opt/teeoff/deploy/Caddyfile`), IKKE gjennom Next.js sitt eget
|
|
|
|
|
|
rewrite-lag (som ikke pålitelig proxyer WebSocket-oppgraderinger i
|
|
|
|
|
|
standalone-modus) — samme forutsetning som all annen runde-/turnering-
|
|
|
|
|
|
sanntid i appen.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — Ny, dedikert `/my-feed`-side, IKKE en dashbord-seksjon.**
|
|
|
|
|
|
Avklart eksplisitt med bruker. Aggregerer på tvers av egne runder (uansett
|
|
|
|
|
|
`visibility_mode`) + venners synlige runder, modellert på
|
|
|
|
|
|
`list_friends_on_course` (`GET /friends/on-course`) sin reverserte retning
|
|
|
|
|
|
(viewer → venners synlige runder), men uten "spiller nå/siste 24t"-
|
|
|
|
|
|
filteret — en all-time, paginert (keyset, `?before=`) liste av
|
|
|
|
|
|
`round_message`-rader. Nås fra dashbordets "Venner på banen"-seksjon
|
|
|
|
|
|
(`SeeAllLink`) og en ny lenke i `/more`.
|
|
|
|
|
|
|
|
|
|
|
|
**KRITISK, funnet under full-stack-verifisering: rute-/rewrite-kollisjon,
|
|
|
|
|
|
samme klasse som `/rounds`→`/my-rounds` og `/friends`→`/my-friends`
|
|
|
|
|
|
(dokumentert i `frontend/next.config.mjs`).** Frontend-siden ble først lagt
|
|
|
|
|
|
på `/feed` — nøyaktig samme streng som det flate API-endepunktet `GET
|
|
|
|
|
|
/feed`. Uten en eksplisitt rewrite-regel vant Next.js sin egen side-rute
|
|
|
|
|
|
over ethvert forsøk på server-side proxy, så `fetch("/feed")` fra klienten
|
|
|
|
|
|
fikk appens egen HTML tilbake i stedet for JSON (stille feilet
|
|
|
|
|
|
JSON-parsing). Rettet ved (1) å flytte frontend-siden til `/my-feed`
|
|
|
|
|
|
(samme navnemønster som de tre presedensene over) OG (2) legge til en
|
|
|
|
|
|
manglende, eksplisitt `{ source: "/feed", destination: ... }`-regel i
|
|
|
|
|
|
`next.config.mjs` (det flate mønsteret, uten `:path*`, siden `/feed` ikke
|
|
|
|
|
|
har understier) — retting nummer to var nødvendig uavhengig av
|
|
|
|
|
|
sideflyttingen, siden regelen rett og slett manglet helt fra start.
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert grundig** (isolert scratch-DB/rolle/MinIO/API-/
|
|
|
|
|
|
frontend-container, PLUSS en minimal scratch-Caddy satt opp spesifikt for
|
|
|
|
|
|
å faktisk bevise WS-refetchen — ikke bare anta at den ville virke i
|
|
|
|
|
|
produksjon): 30/30 håndregnede API-sjekker (hele autorisasjonsmatrisen —
|
|
|
|
|
|
eier poster/sletter egen, medspiller poster, en annen ikke-eier kan IKKE
|
|
|
|
|
|
slette andres, eier KAN moderere andres, fremmed uten innsyn avvist på
|
|
|
|
|
|
GET+POST, `friends`-synlighet riktig inkludert/ekskludert per kategori,
|
|
|
|
|
|
feed-aggregering korrekt per bruker, paginering uten hopp/duplikat, anonym
|
|
|
|
|
|
`/feed`-tilgang korrekt 401). Ekte bildeopplasting bekreftet konvertert til
|
|
|
|
|
|
AVIF og lagret riktig. Ekte nettleser: kommentar+bilde postet og vist,
|
|
|
|
|
|
sletting fungerte, `/my-feed` viste riktig aggregert sett (egne + andres
|
|
|
|
|
|
offentlige runder), WS-refetch bekreftet fungerende GJENNOM Caddy (en
|
|
|
|
|
|
kommentar postet i én "økt" dukket opp automatisk i en åpen fane uten
|
|
|
|
|
|
interaksjon). Ekte typesjekket produksjonsbuild av frontend kjørt og
|
|
|
|
|
|
bekreftet (fanget rute-kollisjonen over ved kjøretid, ikke ved bygging —
|
|
|
|
|
|
typesjekk alene fanger ikke denne klassen feil). `test_isolation.sql`
|
|
|
|
|
|
uendret 12/12. Scratch-miljøet (DB/rolle/MinIO/API-/frontend-/Caddy-
|
|
|
|
|
|
containere/images, V0-eksport-zip-er) ryddet opp fullstendig etter bruk.
|
|
|
|
|
|
|
|
|
|
|
|
**Frontend bygget via V0** (brukerens eksplisitte, stående arbeidsfordeling
|
|
|
|
|
|
— Claude skriver V0-prompten, bruker limer inn selv, Claude integrerer
|
|
|
|
|
|
eksporten): to prompter (kommentarseksjon + feed-side), begge levert med
|
|
|
|
|
|
eksakte data-kontrakter/design-tokens fra `DESIGN_SYSTEM.md` forhåndsutfylt
|
|
|
|
|
|
i prompten. Begge eksportene (zip 32/33) diffet ordrett mot hverandre og
|
|
|
|
|
|
mot ingenting-fra-før (nye filer) før integrering — ingen utilsiktet drift.
|
|
|
|
|
|
To små, bevisste integrasjonsjusteringer utover ren copy-inn: fjernet en
|
|
|
|
|
|
ubrukt `cn`-import, og lot `Feed`-komponenten selv hente `/auth/me` (samme
|
|
|
|
|
|
mønster som `round-detail.tsx`) i stedet for å kreve `currentUserId` som
|
|
|
|
|
|
ekstern prop, siden alle andre sider i appen er tynne wrappere som selv
|
|
|
|
|
|
henter egen brukeridentitet.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** egen payload-bærende WS-kanal for kommentarer,
|
|
|
|
|
|
redigering av innlegg (kun slett, samme som org-feeden), liker/reaksjoner,
|
|
|
|
|
|
varsler ved ny kommentar, sanntid på selve `/my-feed`-siden, inline
|
|
|
|
|
|
slett-knapp på `/my-feed` (kun fra runde-siden).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-08-07 22:02:56 +02:00
|
|
|
|
## ADR-045: Tilskuer-visning (`/watch/[id]`) — tre faner + "Spillere og runde", full paritet med eiersiden
|
|
|
|
|
|
|
|
|
|
|
|
Reist av brukeren 2026-08-06 (video-illustrasjon av bugen): en tilskuer som
|
|
|
|
|
|
klikker seg inn på en venns pågående runde via "Venner på banen" endte på
|
|
|
|
|
|
`/my-rounds/{id}` (eier-/medspiller-only, `_get_accessible_round_or_404`)
|
|
|
|
|
|
i stedet for `/watch/{id}` (den faktiske tredjepartsvisningen, ADR-036
|
|
|
|
|
|
fase 2) — rettet samme dag som en liten, isolert lenke-bug (se CHANGELOG
|
|
|
|
|
|
2026-08-06, punkt 26). I samme runde ba brukeren om at `/watch/[id]`
|
|
|
|
|
|
(fram til da ÉN enkelt side, kun kompakt status + leaderboard/matchstatus)
|
|
|
|
|
|
skulle få samme tre faner som eiersiden (Score/Scorekort/Leaderboard) og
|
|
|
|
|
|
vise "Spillere og runde" (deltakerliste, HCP/utslag/tildelte slag,
|
|
|
|
|
|
flight-gruppering), uten eier-kun-handlingene (Rediger/Fullfør/Slett
|
|
|
|
|
|
runde, +Medspiller).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Full formatparitet via delt komponent + `publicMode`-
|
|
|
|
|
|
bryter, IKKE nye, forenklede tilskuer-komponenter.** `round-scorecard.tsx`
|
|
|
|
|
|
(`RoundScorecard`) og `round-leaderboard.tsx` (`RoundLeaderboard`) sin
|
|
|
|
|
|
faktiske rutenett-/tavle-rendring var ALLEREDE ren, skrivefri visning —
|
|
|
|
|
|
redigering skjer et helt annet sted (Score-fanens `ScoringWizard` i
|
|
|
|
|
|
`round-detail.tsx`). Det eneste som hindret gjenbruk for tilskuere var at
|
|
|
|
|
|
alle interne data-hentinger (flere hooks: `useFormatResult`,
|
|
|
|
|
|
`useFlightTeamStandings`, `useLeaderboardData`, pluss selve
|
|
|
|
|
|
`RoundScorecard`/`RoundLeaderboard` sine egne fetch-effekter) hardkodet
|
|
|
|
|
|
`/rounds/*`-prefikset og en autentisert WebSocket. Løsning: en ny,
|
|
|
|
|
|
valgfri `publicMode`-prop (default `false`, INGEN endring i eiersidens
|
|
|
|
|
|
oppførsel) som bytter alle interne URL-er til `/public/rounds/*` og
|
|
|
|
|
|
WS-kanalen til `/ws/public/rounds/{id}/live`. Dette gir ekte full
|
|
|
|
|
|
formatparitet (alle formater, ikke et utvalg) for null ekstra
|
|
|
|
|
|
vedlikeholdshold -- samme kode, to datakilder -- fremfor å bygge og
|
|
|
|
|
|
vedlikeholde en parallell, forenklet visning. Bekreftet eksplisitt med
|
|
|
|
|
|
bruker (AskUserQuestion) at full detalj var ønsket, ikke en forenklet
|
|
|
|
|
|
fellesvisning.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Ingen gjenbruk av `RoundHeader`/`RoundPageShell` for
|
|
|
|
|
|
selve fane-navigasjonen.** `RoundPageShell` gjør sitt eget autentiserte
|
|
|
|
|
|
`fetch('/rounds/{id}')`-kall og hardkoder `/my-rounds/{id}`-hrefs.
|
|
|
|
|
|
`RoundHeader` er i og for seg props-styrt, men "Administrer"-tannhjulet
|
|
|
|
|
|
render ALLTID og åpner `ManageRoundDialog` (Rediger/Fullfør/Slett-runde)
|
|
|
|
|
|
uansett om handlere er gitt inn — ingen eksisterende prop slår av selve
|
|
|
|
|
|
admin-seksjonen. Bygget i stedet en egen, liten `WatchTabs`-komponent
|
|
|
|
|
|
(`frontend/components/watch-tabs.tsx`) uten noen admin-vei i det hele
|
|
|
|
|
|
tatt -- ingen risiko for at en eier-handling vises ved et uhell, siden
|
|
|
|
|
|
koden rett og slett ikke inneholder den.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Ny, egen "Spillere og runde"-komponent
|
|
|
|
|
|
(`watch-players.tsx`), IKKE et forsøk på å gjøre `round-detail.tsx` sin
|
|
|
|
|
|
lokale `PlayerList`-funksjon gjenbrukbar via `canManage=false`.**
|
|
|
|
|
|
`PlayerList` er ikke eksportert, tett koblet til eier-autentiserte
|
|
|
|
|
|
handlere, og selv `canManage`-proppen dekker ikke alt konsekvent i dag
|
|
|
|
|
|
("Fullfør runde"-knappen der er kun gated på `!completed`, ikke
|
|
|
|
|
|
`isOwnerViewer` -- en eksisterende, urelatert kvalitetsbrist, ikke
|
|
|
|
|
|
arvet inn her). Den nye komponenten inneholder ingen +Medspiller-/
|
|
|
|
|
|
Rediger-/Fullfør-/Slett-kode i det hele tatt. Viser deltakerkort
|
|
|
|
|
|
(navn, "Eier"-merke -- bevisst INGEN "Deg"-merke, siden
|
|
|
|
|
|
`PublicRoundParticipantOut` ikke eksponerer `user_id` og det uansett
|
|
|
|
|
|
ikke gir mening i tilskuer-rammen), Utslag/HCP/Tildelte slag (allerede
|
|
|
|
|
|
offentlig eksponert data), "Så langt"-progresjon (fra
|
|
|
|
|
|
`/public/rounds/{id}/leaderboard`), og flight-gruppering.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Nytt offentlig endepunkt `GET /public/rounds/{id}/
|
|
|
|
|
|
flight-group`.** Speiler den eksisterende autentiserte `/rounds/{id}/
|
|
|
|
|
|
flight-group`, men med en ny `_viewable_flight_rows`-variant av
|
|
|
|
|
|
`_accessible_flight_rows` som bruker `_can_view_round` (eier/medspiller/
|
|
|
|
|
|
`public`/riktig-kategorisert `friends`-venn) per SØSKEN-flight uavhengig
|
|
|
|
|
|
-- en flight-gruppe kan ha søsken med ulik `visibility_mode`, og en
|
|
|
|
|
|
tilskuer skal kun se de søsknene de faktisk har innsyn i, akkurat som
|
|
|
|
|
|
selve anker-runden. Avklart eksplisitt med bruker (AskUserQuestion) at
|
|
|
|
|
|
flight-gruppering skulle bygges nå, ikke utsettes.
|
|
|
|
|
|
|
|
|
|
|
|
**Lukket, relatert gap:** den gamle `watch-round.tsx` sin lokale
|
|
|
|
|
|
`TWO_SIDED_FORMATS`-liste dekket kun 8 av 17 formater (manglet
|
|
|
|
|
|
`copenhagen/bbb/flag/shamble/money_ball/scramble_solo/scramble_solo_
|
|
|
|
|
|
match`, som falt til "Ingen live-visning tilgjengelig ennå"). Løst
|
|
|
|
|
|
automatisk av Beslutning A (samme `RoundLeaderboard`-komponent som
|
|
|
|
|
|
eiersiden, som allerede dekker alle formater) -- ikke en egen,
|
|
|
|
|
|
ekstra oppgave.
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisert grundig, isolert scratch-miljø** (DB/rolle/MinIO/API-/
|
|
|
|
|
|
frontend-container, pluss en minimal scratch-Caddy for ekte WS-testing,
|
|
|
|
|
|
samme mønster som ADR-044): 6/6 håndregnede sjekker for det nye
|
|
|
|
|
|
flight-group-endepunktet (anonym ser kun offentlige søsken-flighter,
|
|
|
|
|
|
en venn med riktig kategori ser i tillegg `friends`-synlige, eieren ser
|
|
|
|
|
|
alle, en fremmed uten vennskap ser kun offentlige, en privat anker-runde
|
|
|
|
|
|
avvises helt for anonym). `tsc --noEmit` rent på hele frontend-prosjektet
|
|
|
|
|
|
etter refaktoren. Ekte nettleser: **regresjon FØRST** -- eiersidens
|
|
|
|
|
|
`/my-rounds/{id}/scorecard` og `/leaderboard` bekreftet uendret (samme
|
|
|
|
|
|
tall, samme layout) FØR noe nytt ble testet. Deretter tilskuer-sidene:
|
|
|
|
|
|
alle tre faner, "Spillere og runde" uten eier-knapper, ETT allerede-
|
|
|
|
|
|
dekket format (`stroke`) OG ETT tidligere udekket format (`flag`,
|
|
|
|
|
|
Flaggturnering) begge bekreftet å rendre korrekt gjennom
|
|
|
|
|
|
`/watch/{id}/leaderboard` -- beviser formatparitet-lukkingen direkte,
|
|
|
|
|
|
ikke bare i teorien. En privat rundes tilskuer-sider (alle tre) bekreftet
|
|
|
|
|
|
korrekt avvist for en anonym leser (egen isolert nettleser-kontekst,
|
|
|
|
|
|
ikke bare et API-kall) med riktig feilmelding og "Tilbake til
|
|
|
|
|
|
dashbordet"-lenke. Scratch-miljøet ryddet opp fullstendig etter bruk.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** enhver endring i selve eiersidens
|
|
|
|
|
|
`PlayerList`/`ManageRoundDialog`-logikk (den nevnte "Fullfør runde"-
|
|
|
|
|
|
gating-kvaliteten er en egen, urelatert observasjon).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-046: Emoji-reaksjoner + trådede kommentarer på innlegg
|
|
|
|
|
|
|
|
|
|
|
|
Reist av brukeren 2026-08-06, som del av samme melding som "nyeste først,
|
|
|
|
|
|
over alt": "man skal kunne like (eller bruke andre emojier) og kommentere
|
|
|
|
|
|
på innlegg." Avklart eksplisitt med bruker (AskUserQuestion, tre
|
|
|
|
|
|
spørsmål): (1) "innlegg" = BÅDE de eksisterende meldingene (rundekommentar/
|
|
|
|
|
|
lag-chat-melding/oppslagstavle-post) OG dashbord-feedens oppføringer, (2)
|
|
|
|
|
|
kommentarer skal være TRÅDET (svar på svar, flere nivåer), (3) reaksjoner
|
|
|
|
|
|
følger samme omfang som kommentarer.
|
|
|
|
|
|
|
|
|
|
|
|
**Nøkkelfunn fra research:** dashbord-feeden (`/my-feed`, ADR-044
|
|
|
|
|
|
Beslutning E) er IKKE en egen entitetstype ("runde fullført"-kort) — hver
|
|
|
|
|
|
feed-oppføring ER ganske enkelt én `round_message`-rad, aggregert på tvers
|
|
|
|
|
|
av runder. Å bygge reaksjoner/kommentarer på `round_message` dekker derfor
|
|
|
|
|
|
AUTOMATISK både rundens egen kommentarseksjon og feeden — ingen egen
|
|
|
|
|
|
tredje entitetstype trengtes. De reelle "innleggs"-typene var dermed kun
|
|
|
|
|
|
to underliggende tabeller: `round_message` (ADR-044, INGEN
|
|
|
|
|
|
`organization_id`/RLS) og `message` (ADR-025, `organization_id` NOT NULL
|
|
|
|
|
|
+ RLS, diskriminert på `scope` for lag-chat/oppslagstavle).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — To parallelle tabellpar, IKKE én polymorf tabell.**
|
|
|
|
|
|
Migrasjon 059: `round_message_reaction`/`round_message_comment` (ingen
|
|
|
|
|
|
RLS, speiler `round_message` selv) og `message_reaction`/`message_comment`
|
|
|
|
|
|
(RLS'et via `app_current_org()`, speiler `message`). Samme presedens som
|
|
|
|
|
|
ADR-044 Beslutning C — en delt tabell på tvers av RLS/ikke-RLS-grensen
|
|
|
|
|
|
ville krevd enten en kunstig organisasjon eller et RLS-unntak, og
|
|
|
|
|
|
CLAUDE.md sin invariant ("organization_id på alle domenetabeller,
|
|
|
|
|
|
håndhevet av RLS") krever egen RLS-håndtering for org-siden uansett.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Reaksjoner: fast kuratert sett, én per bruker per
|
|
|
|
|
|
innlegg.** Seks emojier (👍 ❤️ 😂 😮 😢 🙏), validert i app-laget (400 ved
|
|
|
|
|
|
ukjent emoji) — IKKE et fritt emoji-utvalg. Bytte av emoji ERSTATTER
|
|
|
|
|
|
(UPSERT via `ON CONFLICT (post_id, user_id) DO UPDATE`), stables ikke —
|
|
|
|
|
|
matcher "like (eller bruke andre emojier)"-fraseringen (velg ÉN).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Kommentarer: flat lagring, kun tekst, kaskade-sletting.**
|
|
|
|
|
|
`parent_comment_id` selv-referanse, lagret FLATT — frontend bygger
|
|
|
|
|
|
tre-strukturen fra en kronologisk (eldst-først) liste (motsatt av resten
|
|
|
|
|
|
av appens "nyeste først"-strømmer, bevisst: en samtaletråd leses
|
|
|
|
|
|
top-til-bunn, ulikt en oppdateringsstrøm). Ingen bilde i selve
|
|
|
|
|
|
kommentaren (innlegget har allerede sitt eget). Sletting av en kommentar
|
|
|
|
|
|
med svar under KASKADE-SLETTER hele undertråden (FK `ON DELETE CASCADE`)
|
|
|
|
|
|
— ikke myk-slett-med-plassholder, samme "kun slett, aldri rediger"-
|
|
|
|
|
|
filosofi som `round_message`/`message` selv. Frontend viser en eksplisitt
|
|
|
|
|
|
advarsel ("Sletter du denne, forsvinner også svarene under.") før
|
|
|
|
|
|
bekreftet sletting av en kommentar med svar.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Autorisasjon speiler eksisterende regler per
|
|
|
|
|
|
innleggstype, ingen ny modell.** Hvem kan reagere/kommentere = hvem kan
|
|
|
|
|
|
POSTE selve innlegget (rundekommentar: `_can_view_round`/"kan se = kan
|
|
|
|
|
|
bidra"; lag-chat: rostret på laget; oppslagstavle: `_may_post_to_feed`).
|
|
|
|
|
|
Hvem kan slette en kommentar = samme regel som innleggstypen allerede
|
|
|
|
|
|
bruker for å slette selve innlegget (rundekommentar: forfatter ELLER
|
|
|
|
|
|
rundeeier; lag-chat: KUN forfatter, ingen unntak; oppslagstavle: forfatter
|
|
|
|
|
|
ELLER org-admin via `is_org_admin`). Egen reaksjon fjernes alltid fritt av
|
|
|
|
|
|
den som satte den.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — Ingen ny WebSocket-payload-kanal.** Samme minimalt-
|
|
|
|
|
|
fotavtrykk-valg som ADR-044 Beslutning D: klienten refetcher reaksjons-/
|
|
|
|
|
|
kommentaroppsummeringen kun etter EGEN handling, ingen live-push til andre
|
|
|
|
|
|
samtidige seere i v1. **Viktig rettelse funnet under nettleserverifisering:**
|
|
|
|
|
|
de nye kommentar-endepunktene i `round_messages.py` kalte først
|
|
|
|
|
|
`broadcast_round_update()` (kopiert inn ved en inkonsekvens mot egen
|
|
|
|
|
|
plan) — dette trigget rundens EKSISTERENDE "noe endret seg"-WS-tick, som
|
|
|
|
|
|
`RoundMessages` reagerer på med en full refetch av HELE meldingslisten,
|
|
|
|
|
|
og med det kollapset enhver allerede-utvidet kommentartråd ANDRE STEDER
|
|
|
|
|
|
på siden ved hver eneste kommentarhandling. Fjernet fra de nye
|
|
|
|
|
|
kommentar-endepunktene (reaksjons-endepunktene hadde aldri kallet) —
|
|
|
|
|
|
oppdaget og rettet FØR utrulling, takket være ekte nettleserverifisering
|
|
|
|
|
|
i steget rett etterpå (ikke bare et API-testskript).
|
|
|
|
|
|
|
|
|
|
|
|
**Backend:** `app/routers/round_messages.py` fikk
|
|
|
|
|
|
`PUT/DELETE /rounds/{id}/messages/{id}/reaction` og
|
|
|
|
|
|
`GET/POST /rounds/{id}/messages/{id}/comments` +
|
|
|
|
|
|
`DELETE .../comments/{id}`, pluss `reactions`/`comment_count`-felt lagt
|
|
|
|
|
|
til på `RoundMessageOut` OG `FeedEntryOut` (grupperte batch-spørringer,
|
|
|
|
|
|
ingen N+1). `app/routers/messaging.py` fikk samme sett ×2 (lag-chat +
|
|
|
|
|
|
oppslagstavle), importerer `ALLOWED_REACTION_EMOJIS`/`ReactionSummary`/
|
|
|
|
|
|
`ReactionIn`/`CommentIn` fra `round_messages.py` for å unngå duplisert
|
|
|
|
|
|
typedefinisjon (bevisst IKKE en delt SQL-hjelpemodul på tvers av filene,
|
|
|
|
|
|
siden `plain_connection()` vs. `org_connection()` uansett gjør
|
|
|
|
|
|
spørringene ulike nok at abstraksjon over filgrensen ville vært kunstig).
|
|
|
|
|
|
|
|
|
|
|
|
**Frontend:** ny delt komponent `post-engagement.tsx` — reaksjonsrad
|
|
|
|
|
|
(kuratert sett, egen valgt fremhevet grønt) + trådet kommentarfelt (lazy-
|
|
|
|
|
|
lastet ved utvidelse, "Svar"-knapp per kommentar med innrykk per nivå,
|
|
|
|
|
|
kaskade-advarsel ved sletting), bygget via ÉN Claude-skrevet V0-prompt
|
|
|
|
|
|
(standard arbeidsfordeling) og integrert hånd-kodet i FIRE eksisterende
|
|
|
|
|
|
steder: `round-messages.tsx`, `feed.tsx` (`FeedCard`), `team-chat.tsx`,
|
|
|
|
|
|
`public-tournament.tsx` (`TournamentFeed`). To bevisste
|
|
|
|
|
|
integrasjonsjusteringer utover ren copy-inn: (1) byttet V0-eksportens
|
|
|
|
|
|
egendefinerte inline SVG-ikoner til `lucide-react` (`MessageSquare`/
|
|
|
|
|
|
`Reply`/`Trash2`) for å følge DESIGN_SYSTEM.md sin "ingen annen ikonpakke
|
|
|
|
|
|
blandet inn"-regel, (2) `feed.tsx` sin `FeedCard` var opprinnelig ÉN stor
|
|
|
|
|
|
`<Link>` som pakket inn HELE kortet — `PostEngagement` måtte flyttes UT av
|
|
|
|
|
|
selve lenken (til en søskenposisjon under) for å unngå ugyldig nøstet
|
|
|
|
|
|
interaktivt innhold og utilsiktet navigering ved klikk på en
|
|
|
|
|
|
reaksjonsknapp.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst forenklet, ikke en bug:** `public-tournament.tsx` sin
|
|
|
|
|
|
`TournamentFeed` setter `canModerate={false}` alltid — den ekte "forfatter
|
|
|
|
|
|
ELLER org-admin"-regelen håndheves korrekt av BACKEND uansett (bekreftet i
|
|
|
|
|
|
autorisasjonsmatrisen under), men frontend mangler foreløpig et signal for
|
|
|
|
|
|
"er denne innloggede brukeren org-admin i akkurat denne organisasjonen" å
|
|
|
|
|
|
style et ekstra slett-ikon med for andres kommentarer. En org-admin som
|
|
|
|
|
|
vil moderere andres kommentar kan det (API-et tillater det), men ser ikke
|
|
|
|
|
|
en slett-knapp for det i UI-et ennå — en ren UI-fullstendighets-luke, ikke
|
|
|
|
|
|
et sikkerhetshull. Kan lukkes senere ved å tilføye ett felt (f.eks. i
|
|
|
|
|
|
tournament-info-oppslaget) uten skjemaendring.
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert grundig** (isolert scratch-DB/rolle fra
|
|
|
|
|
|
`teecup_db`-mal, egen API-container): full autorisasjonsmatrise for alle
|
|
|
|
|
|
tre innleggstyper (reager/bytt-reaksjon-som-upsert/fjern-reaksjon,
|
|
|
|
|
|
kommentér på toppnivå, svar på en kommentar, slett en kommentar med svar
|
|
|
|
|
|
under og bekreft kaskade, riktig avvisning per type sin faktiske
|
|
|
|
|
|
post-/slette-regel — lag-chat kun forfatter, oppslagstavle forfatter-
|
|
|
|
|
|
eller-org-admin via ekte org-medlemskap, rundekommentar forfatter-eller-
|
|
|
|
|
|
rundeeier). RLS-isolasjon for `message_reaction` bekreftet eksplisitt
|
|
|
|
|
|
(annen orgs `app.current_org` ser 0 rader). `/feed`-aggregeringen bekreftet
|
|
|
|
|
|
å vise samme reaksjons-/kommentardata som selve rundesiden (samme
|
|
|
|
|
|
`round_message.id` gjenbrukt). `test_isolation.sql` 12/12 uendret.
|
|
|
|
|
|
Ekte typesjekket produksjonsbuild av frontend kjørt og bekreftet (fanget
|
|
|
|
|
|
INGEN nye feil, ren build). **Ekte nettleserverifisering** (egen scratch-
|
|
|
|
|
|
Caddy for sesjon/WS): reagert, byttet reaksjon (upsert bekreftet visuelt),
|
|
|
|
|
|
postet trådet samtale (rot + svar, riktig innrykk), slettet en kommentar
|
|
|
|
|
|
med svar og bekreftet kaskaden fjernet begge fra UI-et, bekreftet
|
|
|
|
|
|
`feed.tsx` sin `PostEngagement` IKKE trigger navigering ved reaksjonsklikk
|
|
|
|
|
|
(Link-nøsting-fiksen virker), bekreftet `team-chat.tsx` og
|
|
|
|
|
|
`public-tournament.tsx` sin Oppslagstavle begge rendrer komponenten
|
|
|
|
|
|
korrekt uten konsollfeil. Fant og rettet WS-tick-regresjonen (Beslutning
|
|
|
|
|
|
E) i dette steget. Scratch-miljøet (DB/rolle/API-/frontend-/Caddy-
|
|
|
|
|
|
containere/images) ryddet opp fullstendig etter bruk.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** push-varsler ved ny kommentar/reaksjon,
|
|
|
|
|
|
sanntids-oppdatering hos ANDRE samtidige seere, fritt emoji-utvalg,
|
|
|
|
|
|
redigering av kommentarer, myk-slett-med-plassholder.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-047: Leaderboard — lukk gapet mot en konkurrentapp-referanse, uten å bygge om fra bunnen
|
|
|
|
|
|
|
|
|
|
|
|
Brukeren viste en video av GolfGameBook sitt leaderboard (trykk-for-å-
|
|
|
|
|
|
utvide rad → fullt hull-for-hull-scorekort inline + sosiale handlinger
|
|
|
|
|
|
per spiller) og ba om noe minst like bra i TeeCup — eksplisitt **IKKE**
|
|
|
|
|
|
en kopi av selve utseendet.
|
|
|
|
|
|
|
|
|
|
|
|
**Nøkkelfunn fra research, som endret omfanget vesentlig fra "bygg dette
|
|
|
|
|
|
fra bunnen":** det meste av kjernemekanikken referansen viser var
|
|
|
|
|
|
ALLEREDE bygget og live i `round-leaderboard.tsx`, fra en tidligere runde
|
|
|
|
|
|
(FEATURE_BACKLOG.md "Leaderboard for runder og turneringer", 2026-07-26)
|
|
|
|
|
|
som selv var inspirert av samme konkurrentapp — og allerede DA bevisst
|
|
|
|
|
|
designet med egen form+farge-språk (sirkel/firkant, grønn/oransje) i
|
|
|
|
|
|
stedet for å kopiere referansens fargebruk. `IndividualBoard` (dekker
|
|
|
|
|
|
`stroke/stableford/skins/flag/bbb/copenhagen`) hadde allerede utvidbare
|
|
|
|
|
|
rader (`IndividualRow`) med et fullt hull-for-hull-rutenett
|
|
|
|
|
|
(`HoleByHole`). Det som faktisk manglet: (1) ingen sosial/kommentar-
|
|
|
|
|
|
tilgang noe sted i leaderboardet, (2) ingen kompakt oppsummeringslinje
|
|
|
|
|
|
under rutenettet, (3) `TwoSidedBoard`/`HighLowBoard` (to-sidede formater)
|
|
|
|
|
|
hadde INGEN utvidelse i det hele tatt — kun en statisk hull-strip, (4)
|
|
|
|
|
|
`TeamFlightBoard` hadde en flat, ikke-utvidbar råscore-liste.
|
|
|
|
|
|
|
|
|
|
|
|
**Avklart med bruker (AskUserQuestion, to spørsmål) FØR bygging:**
|
|
|
|
|
|
1. **Sosial-kobling:** `post-engagement.tsx` (ADR-046) reagerer/
|
|
|
|
|
|
kommenterer på ETT spesifikt `round_message`-innlegg, ikke runden som
|
|
|
|
|
|
helhet — det finnes intet "innlegg per deltaker" i skjemaet (og
|
|
|
|
|
|
GolfGameBooks egen "Kommentar til spillefeeden"-knapp åpner uansett
|
|
|
|
|
|
samme delte feed uansett hvilken rad man trykker på). **Svar: delt
|
|
|
|
|
|
tråd, lenket fra hver rad** — ingen ny per-deltaker-entitet, ingen ny
|
|
|
|
|
|
migrasjon, ingen backend-endring i det hele tatt denne runden.
|
|
|
|
|
|
2. **To-sidede formater:** siden disse kun har 2 "sider" (ikke en
|
|
|
|
|
|
spillerliste), gir "én rad per spiller"-mønsteret ikke mening.
|
|
|
|
|
|
**Svar: ÉN utvidbar detaljseksjon for hele kampen**, ikke et
|
|
|
|
|
|
per-rad-mønster.
|
|
|
|
|
|
|
|
|
|
|
|
**Bygget, alt i `round-leaderboard.tsx` (+ to eksisterende filer fikk en
|
|
|
|
|
|
anker-id):**
|
|
|
|
|
|
- **`HoleByHole`** (IndividualBoard) fikk en kompakt oppsummeringslinje
|
|
|
|
|
|
(par-sum/totalscore/plassering, egen ordlyd — IKKE referansens) + en
|
|
|
|
|
|
"💬 Kommentarer"-tekstlenke.
|
|
|
|
|
|
- **`TeamFlightBoard`** sin flate råscore-liste konvertert til samme
|
|
|
|
|
|
utvidbare mønster via en ny `TeamMemberRow` (gjenbruker `HoleByHole`
|
|
|
|
|
|
uendret, uten rangeringskolonnen — et lags medlemmer rangeres ikke seg
|
|
|
|
|
|
imellom der).
|
|
|
|
|
|
- **`TwoSidedBoard`/`HighLowBoard`** fikk en ny, delt
|
|
|
|
|
|
`MatchDetailSection` — ÉN "Vis hull-for-hull med score"-knapp som,
|
|
|
|
|
|
lazy, monterer den nå eksporterte `MatchScorecardGrid`
|
|
|
|
|
|
(`round-scorecard.tsx`, samme ADR-045-presedens: eksporter en
|
|
|
|
|
|
allerede-godkjent, modul-privat komponent uendret, ren
|
|
|
|
|
|
synlighetsendring) + samme kommentar-lenke.
|
|
|
|
|
|
- **Type-utvidelse, ikke ny data:** `ApiRoundHeader`/`ApiRoundParticipant`
|
|
|
|
|
|
i round-leaderboard.tsx fikk flere felt lagt til (`tee_name_snapshot`,
|
|
|
|
|
|
`guest_name`, `display_name`, `is_owner`, `playing_handicap`) — alle
|
|
|
|
|
|
allerede returnert av samme `${apiPrefix}/{roundId}`-endepunkt
|
|
|
|
|
|
(bekreftet mot `RoundOut`/`PublicRoundOut` i `app/routers/rounds.py`),
|
|
|
|
|
|
bare ikke tidligere typet der. `guest_name` er valgfritt (fraværende i
|
|
|
|
|
|
`publicMode`, siden `PublicRoundParticipantOut` ikke har feltet) — en
|
|
|
|
|
|
ny `toGridRound()`-adapter defaulter det til `null` før
|
|
|
|
|
|
`MatchScorecardGrid` mates, trygt siden komponenten aldri leser feltet.
|
|
|
|
|
|
- **Anker-scroll:** `round-detail.tsx`/`watch-round.tsx` sin
|
|
|
|
|
|
kommentarseksjon fikk `id="kommentarer"`, PLUSS en liten
|
|
|
|
|
|
`useEffect(() => { if (round && hash === "#kommentarer")
|
|
|
|
|
|
scrollIntoView() }, [round])` i begge — nettleserens EGEN anker-scroll
|
|
|
|
|
|
(som skjer FØR async data er lastet) rakk ikke frem til et element som
|
|
|
|
|
|
ennå ikke fantes i DOM-en; funnet og rettet under nettleserverifisering.
|
|
|
|
|
|
|
|
|
|
|
|
**Ingen ny V0-runde, ingen backend-endring.** Alt er gjenbruk/eksport av
|
|
|
|
|
|
allerede-eksisterende, allerede godkjente komponenter/mønstre til nye
|
|
|
|
|
|
steder — "liten justering av eksisterende komponenter med allerede
|
|
|
|
|
|
etablerte mønstre"-unntaket fra den stående V0-arbeidsfordelingen.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst forenklet, ikke en bug:** ingen kommentar-ANTALL-merke på selve
|
|
|
|
|
|
leaderboard-lenken (unngår et ekstra API-kall for kun et tall på en
|
|
|
|
|
|
lenke).
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert** (isolert DB fra `teecup_db`-mal, egen API-/
|
|
|
|
|
|
frontend-/Caddy-container): `tsc --noEmit` rent, ekte produksjonsbuild
|
|
|
|
|
|
kjørt og bekreftet ren. Ekte nettleser: **regresjon FØRST** —
|
|
|
|
|
|
`IndividualBoard`s eksisterende utvidelse bekreftet identisk for
|
|
|
|
|
|
slagspill-formatet (samme tall, samme rad-innhold) FØR noe nytt ble
|
|
|
|
|
|
testet. Deretter: ny oppsummeringslinje + kommentar-lenke bekreftet på
|
|
|
|
|
|
slagspill (inkl. anker-scroll faktisk fungerende, ikke bare landet på
|
|
|
|
|
|
riktig side), ny `MatchDetailSection`/`MatchScorecardGrid`-utvidelse
|
|
|
|
|
|
bekreftet på et fourball-format med ekte 4-spiller-data (riktig
|
|
|
|
|
|
hull-for-hull-score, riktig løpende matchstatus "1 UP"/"AS"/"Dormie 1"),
|
|
|
|
|
|
samme fourball-format bekreftet identisk i `publicMode` (anonym
|
|
|
|
|
|
tilskuer-kontekst, egen isolert nettleser-kontekst — kommentar-lenken
|
|
|
|
|
|
riktig pekende til `/watch/{id}#kommentarer` i stedet for
|
|
|
|
|
|
`/my-rounds/...`). Ingen konsollfeil utover forventet 401 på
|
|
|
|
|
|
`/auth/me` for den anonyme leseren (allerede tolerert i koden).
|
|
|
|
|
|
|
|
|
|
|
|
**Ikke direkte nettleser-testet: `TeamFlightBoard` (shamble/money_ball)
|
|
|
|
|
|
og `HighLowBoard` (high_low_high)** — ingen eksisterende testdata for
|
|
|
|
|
|
disse formatene i scratch-DB-malen, og å håndkonstruere gyldige
|
|
|
|
|
|
runde+deltaker+hull-fixturer for dem fra bunnen ble vurdert som for
|
|
|
|
|
|
tidkrevende/feilutsatt for denne verifiseringsrunden. Vurdert lav risiko
|
|
|
|
|
|
likevel: `TeamFlightBoard`s nye `TeamMemberRow` gjenbruker `HoleByHole`
|
|
|
|
|
|
**uendret** (samme funksjon som allerede ble browser-testet via
|
|
|
|
|
|
`IndividualBoard`), og `HighLowBoard`s nye seksjon bruker **nøyaktig
|
|
|
|
|
|
samme** `MatchDetailSection`/`toGridRound`/`MatchScorecardGrid`-kobling
|
|
|
|
|
|
som allerede ble browser-testet via `TwoSidedBoard` — risikoflaten
|
|
|
|
|
|
(prop-tredding, type-adapter, komponent-montering) er identisk, ikke en
|
|
|
|
|
|
egen, utestet kodevei. Anbefales likevel spot-sjekket i ekte nettleser
|
|
|
|
|
|
neste gang en runde med disse formatene finnes eller opprettes.
|
|
|
|
|
|
|
|
|
|
|
|
**Merk, funnet under research, IKKE rettet i denne runden:** en
|
|
|
|
|
|
skrivefeil i eksisterende kode — `useLeaderboardData`s returnerte
|
|
|
|
|
|
variabel heter `viewerParticipantId` men inneholder faktisk den
|
|
|
|
|
|
INNLOGGEDE BRUKERENS `user_id` (fra `/auth/me`), ikke en
|
|
|
|
|
|
deltaker-id. Kun en forvirrende variabelnavngiving, ikke en funksjonell
|
|
|
|
|
|
feil (verdien er korrekt for det den faktisk brukes til alle steder,
|
|
|
|
|
|
inkludert det nye `MatchScorecardGrid`-kallet, som nettopp forventer
|
|
|
|
|
|
bruker-id under navnet `viewerId`) — la stå urørt, utenfor omfanget av
|
|
|
|
|
|
denne runden.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** `RoundLeaderboardMini` (topp-3-widgeten,
|
|
|
|
|
|
skal forbli et raskt overblikk), `ScrambleSoloResultView` (eget format,
|
|
|
|
|
|
lite utbredt), skudd-nivå-detalj (fairway/putts/GIR) utover det
|
|
|
|
|
|
`HoleByHole` allerede viser (den fulle scorekort-siden dekker det),
|
|
|
|
|
|
kommentar-antall-merke på leaderboard-lenken, et separat ett-trykks
|
|
|
|
|
|
"lik hele runden", "privat melding" (ingen DM-funksjon finnes).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-048: Slag-for-slag GPS-avstandsmåling
|
|
|
|
|
|
|
|
|
|
|
|
Reist av brukeren 2026-08-06: "Måle lenge på slag ... Fra der man er
|
|
|
|
|
|
ELLER fra et valgt punkt på et satellittfoto, til der man står ved siden
|
|
|
|
|
|
av ballen. Man skal også kunne si hvilken kølle man slo med. Dette kan
|
|
|
|
|
|
deles i feeden, eller beholdes i eget grensesnitt." Avklart eksplisitt med
|
|
|
|
|
|
bruker (AskUserQuestion, tre spørsmål, 2026-08-07): (1) v1 skal dekke
|
|
|
|
|
|
BÅDE individuelle runder OG lagformater (fourball/scramble/foursome m.fl.,
|
|
|
|
|
|
der hull henger av `round_side_id`), ikke bare individuell, (2)
|
|
|
|
|
|
delingsteksten skal være auto-generert MEN redigerbar, pluss et utsnitt
|
|
|
|
|
|
av satellittfotoet med slaget tegnet inn, (3) måling skal være tilgjengelig
|
|
|
|
|
|
både som en knapp i scoreførings-veiviseren OG som en alltid-synlig
|
|
|
|
|
|
merkelapp på hull-kortet (retroaktiv måling).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Kartleverandør: Mapbox GL JS, ikke Leaflet+Esri.** Esris
|
|
|
|
|
|
gratis satellittlag (den vanlige gratis Leaflet-kilden) forbyr eksplisitt
|
|
|
|
|
|
kommersiell bruk uten en separat, betalt ArcGIS-lisens — diskvalifiserende
|
|
|
|
|
|
gitt TeeCups kommersielle retning. Mapbox: 50 000 gratis kartlastninger/
|
|
|
|
|
|
mnd, ingen kortbinding for å starte, ~$5/1000 utover, eksplisitt tillatt
|
|
|
|
|
|
for kommersiell bruk.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Kostnadskontroll, tre tiltak.** (1) ETT kart-instans per
|
|
|
|
|
|
måle-økt (montert når "velg punkt på kart" åpnes, aldri remontert per
|
|
|
|
|
|
tap/pan — Mapbox fakturerer per initialisering, ikke per interaksjon).
|
|
|
|
|
|
(2) Kartet lastes KUN ved eksplisitt "velg punkt på kart" — velger
|
|
|
|
|
|
spilleren "min posisjon nå" for BEGGE punkt, initialiseres Mapbox GL JS
|
|
|
|
|
|
aldri (må verifiseres eksplisitt via nettverksfane under scratch-testing,
|
|
|
|
|
|
ikke bare visuelt, se verifiseringsseksjonen for ADR-048 i CHANGELOG.md
|
|
|
|
|
|
når den skrives). (3) Selve avstandsberegningen er ren klient-side
|
|
|
|
|
|
Haversine (`frontend/lib/geo.ts`), aldri et Mapbox-API-kall.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Ny `round_shot`-tabell (migrasjon 060), IKKE en
|
|
|
|
|
|
gjenåpning av ADR-033s avvisning av fri slag-for-slag-logging som
|
|
|
|
|
|
hull-statistikk-DATAMODELL.** Dette er en separat, valgfri måle-funksjon;
|
|
|
|
|
|
`round_hole` sine faste felt (`club_off_tee`, `putts` osv.) er uberørt.
|
|
|
|
|
|
`round_shot` kjenner kun `round_hole_id` — IKKE `round_participant_id`/
|
|
|
|
|
|
`round_side_id` direkte — og arver dermed eierskap (individuell ELLER
|
|
|
|
|
|
lagformat) TRANSITIVT via `round_hole` sin eksisterende
|
|
|
|
|
|
`round_participant_id`/`round_side_id`-XOR (migrasjon 031). Dette dekker
|
|
|
|
|
|
begge hull-typer fra v1 uten egen XOR-logikk i den nye tabellen, via
|
|
|
|
|
|
speilede API-endepunkter
|
|
|
|
|
|
(`/participants/{id}/holes/{n}/shots` og `/sides/{id}/holes/{n}/shots`,
|
|
|
|
|
|
samme dobbelte mønster som `RoundHoleOut`/`RoundSideHoleOut` allerede
|
|
|
|
|
|
bruker for selve hull-scoringen). Samme non-RLS `plain_connection()`-
|
|
|
|
|
|
mønster som resten av det frittstående-runde-subsystemet (ADR-033
|
|
|
|
|
|
Beslutning A). `shot_number` er eksplisitt og hull-tolerant (ikke `ORDER
|
|
|
|
|
|
BY captured_at`) — unngår at GPS-tidsstempel-drift stille endrer
|
|
|
|
|
|
rekkefølgen; sletting midt i sekvensen etterlater et gap, ingen
|
|
|
|
|
|
omnummerering. Kun `UPDATE (shared_round_message_id)` er grantet (ett
|
|
|
|
|
|
smalt, eksplisitt unntak fra "slett og opprett på nytt, aldri rediger"-
|
|
|
|
|
|
prinsippet, se Beslutning E) — ellers ingen UPDATE, samme filosofi som
|
|
|
|
|
|
`round_message`.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — Koordinater: rene `double precision` lat/lng-par, ikke
|
|
|
|
|
|
PostGIS.** Første funksjon i prosjektet som lagrer koordinater i det hele
|
|
|
|
|
|
tatt (60 migrasjoner uten presedens frem til nå). Volumet (maks noen
|
|
|
|
|
|
titalls rader per runde) og operasjonssettet (kun punkt-til-punkt
|
|
|
|
|
|
storsirkel-avstand, regnet klient-side) rettferdiggjør ikke en ny, tung
|
|
|
|
|
|
geo-avhengighet.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — Deling gjenbruker `round_message` (ADR-044) uendret, via
|
|
|
|
|
|
en lenke.** Et nytt `POST /rounds/{id}/shots/{id}/share`-endepunkt
|
|
|
|
|
|
genererer satellitt-utsnittet SERVER-SIDE (Mapbox Static Images API, egen
|
|
|
|
|
|
HEMMELIG `TEECUP_MAPBOX_SECRET_TOKEN` — atskilt fra frontendens
|
|
|
|
|
|
OFFENTLIGE, URL-restriktere `NEXT_PUBLIC_MAPBOX_TOKEN` som Mapbox GL JS
|
|
|
|
|
|
bruker i nettleseren), laster bildet opp via eksisterende
|
|
|
|
|
|
`storage.upload_image()`, og setter INN en vanlig `round_message`-rad —
|
|
|
|
|
|
ingen ny feed-mekanisme. `shared_round_message_id` er en LENKE (ikke et
|
|
|
|
|
|
bool-flagg) slik at "delt" alltid reflekterer om meldingen faktisk
|
|
|
|
|
|
finnes (`ON DELETE SET NULL` hvis meldingen slettes separat i feeden).
|
|
|
|
|
|
Rekkefølgen er bevisst: slaget opprettes ALLTID først (uten deling);
|
|
|
|
|
|
deling er et frivillig, påfølgende steg — unngår at en feilet
|
|
|
|
|
|
slag-opprettelse kan etterlate en foreldreløs feed-melding. Mapbox-kallet
|
|
|
|
|
|
degraderer grasiøst til ren tekst uten satellittbilde ved feil/manglende
|
|
|
|
|
|
token (samme mønster som SMTP/push i `config.py`), blokkerer ikke selve
|
|
|
|
|
|
delingen.
|
|
|
|
|
|
|
2026-08-08 21:38:10 +02:00
|
|
|
|
**Tillegg 2026-08-08 — sluttpunktet (ballen) kan nå OGSÅ velges på kart,
|
|
|
|
|
|
ikke bare GPS.** Brukeren, etter å ha brukt funksjonen i praksis: "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 den opprinnelige "sluttpunkt: alltid GPS, aldri
|
|
|
|
|
|
kart"-delen av flyt-beskrivelsen over (resten av ADR-048 står uendret) —
|
|
|
|
|
|
historikken beholdes over, ikke slettet, per CLAUDE.md-regelen om at en
|
|
|
|
|
|
endret beslutning får et tillegg, ikke en retusjert original.
|
|
|
|
|
|
|
|
|
|
|
|
- **end-steget** tilbyr nå samme valg som start-steget: "Jeg er ved ballen
|
|
|
|
|
|
nå" (GPS, uendret oppførsel) vs. "Vis kart mens jeg går" (nytt,
|
|
|
|
|
|
`MapPointPicker` med et `referencePoint`). Migrasjon `061` la til
|
|
|
|
|
|
`round_shot.end_method` (samme `gps`/`map_tap`-CHECK-mønster som
|
|
|
|
|
|
`start_method`, `DEFAULT 'gps'` siden ALLE slag før dette tillegget
|
|
|
|
|
|
faktisk ble målt med GPS for sluttpunktet).
|
|
|
|
|
|
- **`MapPointPicker` generalisert** til å ta et valgfritt `referencePoint`
|
|
|
|
|
|
(kun brukt på ball-steget): når satt, vises utslagspunktet som en fast,
|
|
|
|
|
|
ikke-flyttbar oransje markør ("Utslag"-merkelapp), og selve
|
|
|
|
|
|
punkt-markøren som plasseres ved tap er grønn i stedet for oransje —
|
|
|
|
|
|
samme fargekonvensjon som allerede fantes i delings-bildet
|
|
|
|
|
|
(`pin-s-a+ff5a1f` / `pin-s-b+2f7a3f`), nå ført konsekvent gjennom
|
|
|
|
|
|
UI-et også. Løser "slagpunktet må være en del av det jeg ser" konkret:
|
|
|
|
|
|
kartet henter brukerens live GPS-posisjon OG kjenner utslagspunktet, og
|
|
|
|
|
|
kjører `map.fitBounds([nåværende posisjon, utslagspunkt])` slik at begge
|
|
|
|
|
|
garantert er i bildet med det samme (ikke bare sentrert på det ene) —
|
|
|
|
|
|
faller tilbake til å sentrere på utslagspunktet alene (zoom 17) hvis GPS
|
|
|
|
|
|
feiler. Dekker samtidig "jeg vil se kartet mens jeg går fram til
|
|
|
|
|
|
ballen": brukeren kan la kartet stå åpent (satellittbildet, med
|
|
|
|
|
|
utslagsmarkøren som fast referansepunkt) mens de fysisk beveger seg, og
|
|
|
|
|
|
trykke der ballen faktisk ligger når de er fremme — i stedet for å måtte
|
|
|
|
|
|
vente på et GPS-fix. Fortsatt kun ETT kart-instans (Beslutning B
|
|
|
|
|
|
uendret): samme "mountes når steget åpnes, aldri på nytt per tap/pan"-
|
|
|
|
|
|
prinsipp gjelder nå BEGGE punkt-steg, ikke bare start.
|
|
|
|
|
|
- Scratch-verifisert (2026-08-08): migrasjon + CHECK-constraint
|
|
|
|
|
|
(ugyldig `end_method` avvist), API-nivå (`end_method` lagres/returneres
|
|
|
|
|
|
korrekt for begge verdier, default `gps` når feltet utelates), og full
|
|
|
|
|
|
nettleser-gjennomgang via Chrome DevTools MCP (GPS-fabrikkert
|
|
|
|
|
|
utslagspunkt → "Vis kart mens jeg går" → referansemarkør + fitBounds
|
|
|
|
|
|
bekreftet visuelt → tap plasserte grønn ballmarkør → lagret slag hadde
|
|
|
|
|
|
`start_method=gps, end_method=map_tap` i databasen, verifisert med
|
|
|
|
|
|
direkte SQL). Se CHANGELOG.md for full detalj.
|
|
|
|
|
|
|
2026-08-10 07:00:11 +02:00
|
|
|
|
**Tillegg 2026-08-08 (del 2) — ball-steget viser nå ALLTID kartet, med
|
|
|
|
|
|
løpende (sanntids) posisjon og avstand, og allerede målte slag viser nå
|
|
|
|
|
|
satellittfoto i listen.** Umiddelbart etter tillegget over, bruker: "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." Dette endrer Beslutning B
|
|
|
|
|
|
sitt "kart lastes kun ved eksplisitt 'velg punkt på kart'"-prinsipp
|
|
|
|
|
|
BEVISST for ball-steget spesifikt (fortsatt uendret for start-steget):
|
|
|
|
|
|
|
|
|
|
|
|
- **Ball-steget slo sammen "GPS vs. kart"-valget til ÉN alltid-synlig
|
|
|
|
|
|
kart-visning.** Den forrige separate GPS-only-idle-skjermen (bare en
|
|
|
|
|
|
"Jeg er ved ballen nå"-knapp, ingen kart) og det egne `end_map`-steget
|
|
|
|
|
|
er fjernet -- `MapPointPicker` rendres nå direkte når "end"-steget nås,
|
|
|
|
|
|
alltid med `referencePoint=startPoint`. `MapPointPicker` viste
|
|
|
|
|
|
opprinnelig BEGGE bekreftelsesknappene når `referencePoint` var satt:
|
|
|
|
|
|
"Bekreft ballens posisjon" (trykket punkt, `end_method: "map_tap"`) og
|
|
|
|
|
|
"Jeg er ved ballen nå" (siste sporede posisjon, `end_method: "gps"`).
|
|
|
|
|
|
**Korrigert 2026-08-09** (samme dag, etter faktisk bruk): bruker,
|
|
|
|
|
|
"bekreft ballens posisjon er unødvendig" -- trykk-for-å-plassere-ballen
|
|
|
|
|
|
fjernet igjen. Ball-steget bekrefter nå UTELUKKENDE via sporet
|
|
|
|
|
|
GPS-posisjon; kartet der er rent informativt (referansepunkt +
|
|
|
|
|
|
sanntidsposisjon/-avstand), ikke lenger klikkbart for plassering.
|
|
|
|
|
|
Backendens `end_method`-kolonne/CHECK aksepterer fortsatt begge verdier
|
|
|
|
|
|
uendret (historiske `map_tap`-rader fra den korte perioden dette var
|
|
|
|
|
|
aktivt skal fortsatt leses korrekt) -- kun frontend-UI-et endret, ingen
|
|
|
|
|
|
migrasjon. Start-steget er UPÅVIRKET: trykk-for-å-plassere fungerer der
|
|
|
|
|
|
fortsatt akkurat som før.
|
|
|
|
|
|
- **Løpende posisjonssporing (`watchPosition`), kun på ball-steget.**
|
|
|
|
|
|
Dette er et bevisst, avgrenset unntak fra "ingen løpende watchPosition"
|
|
|
|
|
|
-- den opprinnelige begrunnelsen for det forbudet var KART-
|
|
|
|
|
|
lastnings-kostnad (Mapbox fakturerer initialisering), og `watchPosition`
|
|
|
|
|
|
i seg selv utløser ALDRI en ny kartinitialisering eller et nytt
|
|
|
|
|
|
Mapbox-API-kall -- det er ren nettleser-GPS som kun oppdaterer en
|
|
|
|
|
|
eksisterende markørs posisjon på det ALLEREDE lastede kartet. Samme
|
|
|
|
|
|
ETT-kart-instans-prinsipp står dermed fortsatt fast; kun selve
|
|
|
|
|
|
markør-/avstandsdataene oppdateres kontinuerlig. `watchPosition` ryddes
|
|
|
|
|
|
opp (`clearWatch`) når komponenten avmonteres.
|
|
|
|
|
|
- **Sanntids-avstand** vises som et stort tall nederst på kartet,
|
|
|
|
|
|
beregnet med `haversineMeters(referencePoint, livePosition)` (samme
|
|
|
|
|
|
rene funksjon som allerede fantes i `frontend/lib/geo.ts`, tidligere
|
|
|
|
|
|
brukt kun for forhåndsvisningen -- `shot-measurement-sheet.tsx` sin
|
|
|
|
|
|
egen duplikate Haversine-implementasjon fjernet til fordel for denne,
|
|
|
|
|
|
ren opprydding uten atferdsendring).
|
|
|
|
|
|
- **Kostnadskonsekvens, eksplisitt notert:** ball-steget laster nå
|
|
|
|
|
|
Mapbox-kartet for HVERT målt slag, uansett om brukeren til slutt
|
|
|
|
|
|
bekrefter via GPS eller kart-trykk (siden kartet uansett vises for at
|
|
|
|
|
|
brukeren skal se referansepunktet + sanntids-avstand mens de går).
|
|
|
|
|
|
Start-steget er UENDRET (kart lastes fortsatt kun ved eksplisitt "velg
|
|
|
|
|
|
punkt på kart") -- kun halvparten av et målt slags to punkter utløser nå
|
|
|
|
|
|
alltid en kartlastning. Vurdert og akseptert: 50 000 gratis
|
|
|
|
|
|
kartlastninger/mnd (Beslutning A) gir god margin for forventet volum.
|
|
|
|
|
|
- **Allerede målte slag i listen** (`round-detail.tsx`, `ShotList`) viser
|
|
|
|
|
|
nå et lite satellitt-thumbnail (160×160, samme offentlige, URL-
|
|
|
|
|
|
restrikterte token og samme `pin-s-a`/`pin-s-b`-fargekonvensjon som
|
|
|
|
|
|
forhåndsvisningen) bygget direkte fra `ShotRecord`s allerede-returnerte
|
|
|
|
|
|
`start_lat`/`start_lng`/`end_lat`/`end_lng` (utvidet fra kun
|
|
|
|
|
|
`id`/`club`/`distance_meters`/`shared_round_message_id`) -- løser "jeg
|
|
|
|
|
|
burde jo se slaget selv, selv om det ikke er delt" sitt neste lag: ikke
|
|
|
|
|
|
bare klubbe/avstand som tekst, men faktisk HVOR slaget ble slått. Ingen
|
|
|
|
|
|
backend-endring nødvendig -- `ShotOut` returnerte allerede alle fire
|
|
|
|
|
|
koordinatene, kun frontend-typen/rendringen manglet dem.
|
|
|
|
|
|
|
2026-08-07 22:02:56 +02:00
|
|
|
|
**Backend:** `app/routers/rounds.py` fikk `ShotIn`/`ShotOut`/`ShotShareIn`
|
|
|
|
|
|
og seks nye endepunkter (deltaker-GET/POST, side-GET/POST, DELETE, share),
|
|
|
|
|
|
alle bygget på den eksisterende `_get_accessible_round_or_404` (samme
|
|
|
|
|
|
"eier ELLER lenket medspiller kan føre for hele flighten"-regel som
|
|
|
|
|
|
`update_hole`/side-PATCH allerede bruker). `_resolve_round_message_author_name`
|
|
|
|
|
|
flyttet fra `round_messages.py` til `rounds.py` (som allerede var den
|
|
|
|
|
|
importerte, ikke-importerende parten av de to filene) for å unngå en
|
|
|
|
|
|
sirkulær import da `share_shot` trengte å gjenbruke den uendret.
|
|
|
|
|
|
|
|
|
|
|
|
**Frontend:** `frontend/lib/geo.ts` (ren Haversine-funksjon, ingen
|
|
|
|
|
|
avhengigheter). Eksisterende kølle-plukker-mønster i `ScoringWizard`
|
|
|
|
|
|
(round-detail.tsx) trukket ut til en delt `ClubPicker`-komponent (ren
|
|
|
|
|
|
refaktor, bekreftet typesjekk uendret) slik at den nye måle-flyten kan
|
|
|
|
|
|
gjenbruke nøyaktig samme pill-knapp-interaksjon. `mapbox-gl` lagt til
|
|
|
|
|
|
(pakken har egne typedefinisjoner -- den separate `@types/mapbox-gl`-
|
|
|
|
|
|
pakken er en utdatert stub, bevisst IKKE installert), `NEXT_PUBLIC_MAPBOX_TOKEN` tredd gjennom
|
|
|
|
|
|
`frontend/Dockerfile` (build-tid, IKKE runtime — `NEXT_PUBLIC_`-variabler
|
|
|
|
|
|
bakes inn i klient-bundlen ved `next build`, samme fallgruve-mønster som
|
|
|
|
|
|
`TEECUP_API_ORIGIN` allerede dokumenterer i Dockerfile-kommentarene, men
|
|
|
|
|
|
speilet KUN i builder-steget her siden bruken er ren klient-side) og
|
|
|
|
|
|
`docker-compose.yml` sitt `teecup_frontend.build.args`. Selve
|
|
|
|
|
|
`ShotMeasurementSheet`-komponenten (alle steg + kart-plassholder) går via
|
|
|
|
|
|
én Claude-skrevet V0-prompt (standard arbeidsfordeling) — ekte Mapbox GL
|
|
|
|
|
|
JS-integrasjon i kart-steget kobles på hånd-kodet ETTER V0-eksporten, ikke
|
|
|
|
|
|
av V0 selv (V0-prompten ber eksplisitt om en visuell plassholder, ikke et
|
|
|
|
|
|
ekte kartbibliotek, for å unngå at V0 genererer en selvstendig
|
|
|
|
|
|
kart-komponent som remonterer per interaksjon og dermed bryter
|
|
|
|
|
|
Beslutning B).
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** redigering av et allerede logget slag (slett
|
|
|
|
|
|
og mål på nytt i stedet), en historisk "vis alle slag på kart etter
|
|
|
|
|
|
runden"-visualisering.
|
2026-07-16 07:18:01 +02:00
|
|
|
|
|
2026-08-10 07:00:11 +02:00
|
|
|
|
## ADR-049: `/velkommen` — landingsside for innlogget-men-ikke-fullført-profil
|
|
|
|
|
|
|
|
|
|
|
|
Reist av brukeren 2026-08-09: "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. Under dette bør TeeCup presenteres." Bekreftet: ny, avgrenset
|
|
|
|
|
|
side (ikke en full erstatning av `/dashboard`, se drøftingen i chatten)
|
|
|
|
|
|
— dashbordet for FULLFØRT profil er uendret.
|
|
|
|
|
|
|
|
|
|
|
|
**Problemet:** en innlogget bruker med ufullstendig profil ble tidligere
|
|
|
|
|
|
sendt RETT til `/account` sitt påtvungne skjema ("Fullfør profilen din"),
|
|
|
|
|
|
uten noen kontekst om hva de nettopp logget seg inn på. Brukeren
|
|
|
|
|
|
observerte dette som en reell feil i praksis (antas: forvirring/frafall),
|
|
|
|
|
|
ikke bare en teoretisk innvending.
|
|
|
|
|
|
|
|
|
|
|
|
**Løsning: ny side `frontend/app/velkommen/page.tsx` +
|
|
|
|
|
|
`frontend/components/teecup/velkommen.tsx`,** satt inn i redirect-kjeden
|
|
|
|
|
|
mellom innlogging og `/account`:
|
|
|
|
|
|
- `frontend/app/page.tsx` og `frontend/app/logg-inn/page.tsx` sine
|
|
|
|
|
|
server-side redirects, OG `dashboard.tsx` sin klient-side
|
|
|
|
|
|
`profile_complete`-vakt (`loadMe()`), peker nå til `/velkommen` i
|
|
|
|
|
|
stedet for `/account` når profilen er ufullstendig. `/velkommen` selv
|
|
|
|
|
|
redirecter videre til `/dashboard` (komplett profil) eller `/logg-inn`
|
|
|
|
|
|
(ikke innlogget) — kan altså ikke nås "feil" via direkte URL.
|
|
|
|
|
|
- Siden viser, i rekkefølge: TeeCup-ordmerke + "Logg ut", en
|
|
|
|
|
|
personlig hilsen (`me.first_name ?? me.display_name`, samme
|
|
|
|
|
|
navneformat-regel som dashbordets hilsen, CLAUDE.md), den
|
|
|
|
|
|
eksisterende `InstallPrompt`-komponenten UENDRET (samme PWA-
|
|
|
|
|
|
oppfordring som dashbordet allerede bruker), en primær "Fullfør
|
|
|
|
|
|
profilen din"-CTA, TRE synlige men visuelt 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), og en presentasjon av TeeCup
|
|
|
|
|
|
(innhold hentet fra `teecup-beskrivelse.md`, skrevet om til kort
|
|
|
|
|
|
UI-tekst — ikke limt inn rått som markdown).
|
|
|
|
|
|
- Trykk på en låst handling viser en delt påminnelse
|
|
|
|
|
|
(`role="alert"`, skjermleser-varslet automatisk) med lenke videre til
|
|
|
|
|
|
`/account`, i stedet for å navigere — samme "forklar, ikke bare
|
|
|
|
|
|
blokkér"-prinsipp brukeren ba om.
|
|
|
|
|
|
- Bygget med samme "clubhouse"-palett-tokens (`--tee-strong`,
|
|
|
|
|
|
`--clubhouse-*`) og samme `TeeCupWordmark`/`InstallPrompt`-komponenter
|
|
|
|
|
|
som `/logg-inn` og det allerede reskinnede dashbordet (2026-08-08,
|
|
|
|
|
|
se CHANGELOG.md) — ikke funnet opp på nytt, gjenbruker den etablerte
|
|
|
|
|
|
retningen for akkurat denne delen av appen.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst avgrenset omfang:** ingen ny server-side håndheving av
|
|
|
|
|
|
`profile_complete` er lagt til på andre sider (`/my-rounds`,
|
|
|
|
|
|
`/my-friends` osv. sjekker i dag kun innlogging, ikke profil-status —
|
|
|
|
|
|
en pre-eksisterende, ikke relatert inkonsistens, ikke rørt her). Kun de
|
|
|
|
|
|
tre inngangspunktene som faktisk styrte hvor en ufullstendig-profil-
|
|
|
|
|
|
bruker havnet (root, `/logg-inn`, dashbordets egen klient-vakt) er
|
|
|
|
|
|
endret.
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-050: Sikkerhetsgjennomgang 2026-08-09 — rate limiting, sikkerhetshoder, inputvalidering
|
|
|
|
|
|
|
|
|
|
|
|
Brukeren ba om en full sikkerhetsgjennomgang ("sjekk alle felter som kan
|
|
|
|
|
|
fylles ut, og sjekk alle URL-er... lar siden/appen seg hacke?"). En
|
|
|
|
|
|
dedikert agent gjennomgikk autentisering, HELE autorisasjonslaget på
|
|
|
|
|
|
tvers av 16 routere, input-validering, filopplasting, CORS/nettverk og
|
|
|
|
|
|
hemmeligheter i git-historikken. Konklusjon: autorisasjons-/IDOR-laget er
|
|
|
|
|
|
uvanlig grundig og konsekvent (ingen bekreftet IDOR i noe skrive-
|
|
|
|
|
|
endepunkt) — de reelle funnene var manglende rate limiting (HØY),
|
|
|
|
|
|
manglende sikkerhetshoder (LAV-MIDDELS), inkonsekvent input-validering
|
|
|
|
|
|
(LAV), og én liten informasjonslekkasje i ett BBB-endepunkt
|
|
|
|
|
|
(informativt). Bruker: "tett sikkerhetshullene først."
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Rate limiting, i minnet, ikke Redis (ennå).** Ny
|
|
|
|
|
|
`app/rate_limit.py`: en enkel fast-vindu-teller (`RateLimiter`) brukt
|
|
|
|
|
|
enten som FastAPI-dependency (IP-basert, via `client_ip()` som stoler på
|
|
|
|
|
|
`X-Forwarded-For` — trygt KUN fordi appen ikke er nåbar unntatt gjennom
|
|
|
|
|
|
Caddy, samme tillitsmodell som `should_use_secure_cookies()`) eller kalt
|
|
|
|
|
|
direkte med en egendefinert nøkkel. Lagt til på fem `app/routers/auth.py`
|
|
|
|
|
|
-endepunkter: `request-link`/`login-password`/`2fa/email/request`
|
|
|
|
|
|
(IP-basert, 5-10 forsøk/10 min — ressursen er ikke entydig knyttet til én
|
|
|
|
|
|
konto), og `2fa/verify`/`2fa/setup/confirm` (nøkkel = pending-/innlogget
|
|
|
|
|
|
bruker-ID, 8 forsøk/5 min — her ER ressursen én bestemt kontos kode, en
|
|
|
|
|
|
angriper med en gyldig pending-sesjon kunne ellers omgått IP-basert
|
|
|
|
|
|
begrensning ved å bytte IP). Trygt i minnet KUN fordi `teecup_api` kjører
|
|
|
|
|
|
som ÉN uvicorn-prosess (ingen `--workers`-flagg) — samme kjente
|
|
|
|
|
|
begrensning som andre in-memory-cacher i appen (se punkt 2 i listen
|
|
|
|
|
|
under). Ved fremtidig skalering til flere workers/containere MÅ dette
|
|
|
|
|
|
flyttes til Redis.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Sikkerhetshoder i Caddy, ikke Next.js-middleware.**
|
|
|
|
|
|
Lagt til i `/opt/teeoff/deploy/Caddyfile` sin `teecup.golf`-blokk (delt
|
|
|
|
|
|
fil med `teeoff.no`, KUN teecup.golf-blokken endret): `X-Frame-Options:
|
|
|
|
|
|
DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy`,
|
|
|
|
|
|
`Strict-Transport-Security`, og en `Content-Security-Policy`. CSP-en er
|
|
|
|
|
|
bevisst IKKE en streng nonce-basert policy — appen bruker inline
|
|
|
|
|
|
`style`-attributter mange steder (React `style={{...}}`, hele
|
|
|
|
|
|
"clubhouse"-paletten) og Next.js sin hydrering kan trenge inline script,
|
|
|
|
|
|
så `'unsafe-inline'` er beholdt for script-src/style-src for å unngå å
|
|
|
|
|
|
knekke appen blindt. Verdien ligger i det som ER strammet inn:
|
|
|
|
|
|
`object-src`/`frame-ancestors`/`base-uri` blokkerer hele angrepsklasser,
|
|
|
|
|
|
og `connect-src`/`img-src` er begrenset til KUN Mapbox (slagmåling,
|
|
|
|
|
|
ADR-048) — en fremtidig XSS-bug kan ikke enkelt eksfiltrere data til en
|
|
|
|
|
|
vilkårlig tredjeparts-vert. Bekreftet i ekte nettleser mot produksjon:
|
|
|
|
|
|
ingen CSP-brudd i konsollen, et ekte Mapbox-kall (200 OK) fungerer
|
|
|
|
|
|
uendret.
|
|
|
|
|
|
|
|
|
|
|
|
**Reell driftsfallgruve funnet under utrulling, verdt å dokumentere:**
|
|
|
|
|
|
`docker exec teeoff_caddy caddy reload` (OG en direkte admin-API-`/load`
|
|
|
|
|
|
-PUT) rapporterte begge suksess uten feil, men endringen slo likevel
|
|
|
|
|
|
ALDRI igjennom — `md5sum` av filen inne i containeren avvek fra filen på
|
|
|
|
|
|
verten. Rotårsak: Docker sin bind-mount av EN ENKELT FIL (`./deploy/
|
|
|
|
|
|
Caddyfile:/etc/caddy/Caddyfile:ro`) er bundet til INODE-en filen hadde
|
|
|
|
|
|
ved containerens oppstart. Et skriveverktøy som lagrer atomisk (skriv til
|
|
|
|
|
|
midlertidig fil + `rename()`, vanlig og trygt mønster generelt) bytter ut
|
|
|
|
|
|
inode-en på verten — den kjørende containerens mount fortsetter da å
|
|
|
|
|
|
referere den GAMLE, nå frikoblede inode-en, usynlig for `reload`/admin-
|
|
|
|
|
|
API (som begge leser filen på nytt, men containerens FILSYSTEM-syn av
|
|
|
|
|
|
stien er allerede feil). Løsning: `docker restart teeoff_caddy` (ikke
|
|
|
|
|
|
bare `reload`) — dette er en STÅENDE fallgruve for enhver fremtidig
|
|
|
|
|
|
Caddyfile-endring, ikke unikt for denne runden. Kort, ufarlig avbrudd for
|
|
|
|
|
|
BEGGE sidene (teeoff.no og teecup.golf) ved restart, siden de deler
|
|
|
|
|
|
samme Caddy-container.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — Inputvalidering, konsistens fremfor nye regler.** Ingen
|
|
|
|
|
|
nye valideringsprinsipper — kun manglende `max_length`/`ge`/`le` lagt til
|
|
|
|
|
|
der de manglet, etter EKSAKT samme mønster som allerede fantes andre
|
|
|
|
|
|
steder i samme fil (f.eks. `PlayerCreate.handicap_index` fikk samme
|
|
|
|
|
|
`ge=-10, le=54` som `auth.py` sin `ProfileUpdate.handicap_index` allerede
|
|
|
|
|
|
hadde). Én reell feil unngått underveis: `PersonalCourseTeeRatingIn.par`
|
|
|
|
|
|
ble først satt til `ge=3, le=6` (feilaktig antatt å være per-hull-par, som
|
|
|
|
|
|
`PersonalCourseHoleIn.par`) — DB-skjemaet (`020_personal_rounds.sql`,
|
|
|
|
|
|
ingen CHECK-constraint på den kolonnen) avslørte at det faktisk er
|
|
|
|
|
|
BANENS TOTALE par brukt i WHS-beregningen, rettet til `ge=54, le=90` før
|
|
|
|
|
|
utrulling. Endret: `rounds.py` (`PersonalCourseTeeRatingIn`/
|
|
|
|
|
|
`PersonalCourseTeeIn.name`/`PersonalCourseCreate.name`), `players.py`
|
|
|
|
|
|
(`PlayerCreate`/`PlayerUpdate`, begge — `handicap_index`/`mobile`/
|
|
|
|
|
|
`nickname`/`country`/`club`/`club_member_number`), `round_messages.py`
|
|
|
|
|
|
(`CommentIn.body`, delt/importert av `messaging.py` sine to bruksseder —
|
|
|
|
|
|
IKKE duplisert), samt tre `Form(default=None)`-felt (`round_messages.py`
|
|
|
|
|
|
+ to i `messaging.py`) som fikk `max_length=2000` (tekst) eller
|
|
|
|
|
|
`max_length=20` (invitasjonskode, som er 6 tegn generert, se
|
|
|
|
|
|
`tournaments.py._generate_join_code`).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — BBB-endepunkt: autorisasjon flyttet FØR formatsjekk.**
|
|
|
|
|
|
`update_bbb_hole` i `rounds.py` kalte `_get_accessible_round_or_404`
|
|
|
|
|
|
ETTER å ha lest og validert `play_format`/`holes_planned` — en bruker
|
|
|
|
|
|
UTEN tilgang til en fremmed runde kunne dermed skille "runden finnes
|
|
|
|
|
|
ikke" (404) fra "runden finnes, feil format" (400) via feilmeldingen,
|
|
|
|
|
|
uten å oppnå noen skrivetilgang. Byttet rekkefølge: autorisasjon leses
|
|
|
|
|
|
FØRST, deretter format/hull-omfang. `round_row` kan ikke lenger være
|
|
|
|
|
|
`None` etter en vellykket autorisasjonssjekk, så den separate
|
|
|
|
|
|
null-sjekken er fjernet (var uansett aldri nåbar etter omorganiseringen).
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert** (tjuesjuende scratch-miljø denne økten): rate
|
|
|
|
|
|
limiting bekreftet å faktisk utløse 429 ved riktig terskel (10./11. forsøk
|
|
|
|
|
|
på login-password, 6. forsøk på request-link), gyldig/ugyldig
|
|
|
|
|
|
input-validering bekreftet begge veier (HCP over 54 avvist, gyldig HCP
|
|
|
|
|
|
akseptert, for lang mobil avvist), BBB-endepunktet bekreftet å returnere
|
|
|
|
|
|
403 NOT_AUTHORIZED (ikke lenger 400 format-lekkasje) for en bruker uten
|
|
|
|
|
|
tilgang, OG fortsatt 200 OK for den faktiske eieren (regresjonssjekk).
|
|
|
|
|
|
`teecup_db`s ACL bekreftet uendret før/etter, scratch-ressurser ryddet
|
|
|
|
|
|
opp fullstendig. CSP verifisert i ekte nettleser mot produksjon (se
|
|
|
|
|
|
Beslutning B).
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang denne runden** (nevnt av agenten, ikke fulgt
|
|
|
|
|
|
opp): full linje-for-linje-gjennomgang av `individual_tournaments.py`/
|
|
|
|
|
|
`tournaments.py`/`order_of_merit.py`/`courses.py` (kun stikkprøvd),
|
|
|
|
|
|
Web Push-abonnement-kapring-teori (praktisk risiko vurdert svært lav,
|
|
|
|
|
|
push-endepunkt-URL-er er ugjettbare), noen faktisk penetrasjonstest (kun
|
|
|
|
|
|
statisk kodegjennomgang).
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-051: 13-årsgrense for kontoregistrering + presentasjonstekst-korrigeringer
|
|
|
|
|
|
|
|
|
|
|
|
Brukeren, 2026-08-10, etter å ha lest PDF-vedlegget "Barns personvern i
|
|
|
|
|
|
golfapp" (en Gemini-samtale om GDPR/personvernkrav for mindreårige i en
|
|
|
|
|
|
GPS+UGC-app): "Vi må gjøre noe med de som er barn. For det første:
|
|
|
|
|
|
Tillatt aldri de som er under 13 år å registrere seg i appen." Samtidig,
|
|
|
|
|
|
to relaterte presentasjonskorrigeringer: et annet PDF-vedlegg ("Installere
|
|
|
|
|
|
PWA fra Chrome på iPhone") viste at installasjonsteksten feilaktig
|
|
|
|
|
|
hevdet at PWA-installasjon KREVER Safari på iOS -- siden iOS 16.4 gjelder
|
|
|
|
|
|
ikke det lenger. Og: "Jeg tror ikke vi skal nevne golfklubber i
|
|
|
|
|
|
beskrivelsen av TeeCup," pluss et ønske om å fremheve at
|
|
|
|
|
|
turneringsadministrasjon/-presentasjon fungerer like godt på PC.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — 13-årsgrense håndheves i `PATCH /auth/profile`, ikke i
|
|
|
|
|
|
et eget registreringssteg.** Siden `profile_complete` (fødselsdato er ett
|
|
|
|
|
|
av de obligatoriske feltene) allerede er den ENESTE veien forbi
|
|
|
|
|
|
`/velkommen`-sperren (ADR-049) og inn i resten av appen, er dette det
|
|
|
|
|
|
naturlige, allerede-eksisterende knutepunktet -- en under-13-åring kan
|
|
|
|
|
|
rett og slett aldri fullføre profilen sin, og kommer dermed aldri forbi
|
|
|
|
|
|
`/velkommen`s låste hurtighandlinger. Ingen ny tabell/kolonne, ingen nytt
|
|
|
|
|
|
flagg. Håndhevet SERVER-SIDE i `app/routers/auth.py` sin `update_profile`
|
|
|
|
|
|
(alder beregnet fra `body.birth_date`, avvist med `400 UNDER_MINIMUM_AGE`
|
|
|
|
|
|
hvis under 13 -- eksakt dagsgrense, ikke bare årstall), PLUSS
|
|
|
|
|
|
klientside i BEGGE skjemaer som setter fødselsdato
|
|
|
|
|
|
(`ProfileOnboarding` og `ProfileSection` i `account-settings.tsx`, delt
|
|
|
|
|
|
`computeAge()`-hjelpefunksjon) for umiddelbar tilbakemelding før
|
|
|
|
|
|
serveren i det hele tatt kontaktes.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst IKKE utvidet til `player`-tabellen** (organisasjonens
|
|
|
|
|
|
spillerpool, `players.py`/`registration.py`). Dette er en annen
|
|
|
|
|
|
datamodell: en `player`-rad representerer en ROSTER-oppføring en
|
|
|
|
|
|
ARRANGØR/klubb legger inn om en person, IKKE nødvendigvis en person med
|
|
|
|
|
|
egen app-innlogging (`player.user_id` er nullable, akkurat denne
|
|
|
|
|
|
frikoblingen er selve poenget). Brukerens instruks var "de som er under
|
|
|
|
|
|
13 år å registrere seg" -- altså SELV opprette en app-konto, ikke at en
|
|
|
|
|
|
voksen arrangør registrerer et barn som deltaker i en turnering (det
|
|
|
|
|
|
siste er faktisk PDF-vedleggets EGEN anbefalte, tryggere løsning for
|
|
|
|
|
|
yngre spillere -- "sub-accounts" ført av en voksen, se PDF-ens punkt 2
|
|
|
|
|
|
under "Anbefalte løsninger"). Å blokkere `player`-tabellen ville brutt
|
|
|
|
|
|
akkurat den mekanismen.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — PWA-installasjonstekst skiller nå Safari/Chrome på
|
|
|
|
|
|
iOS.** `install-prompt.tsx` sin `isIOS()`-sjekk fanger ALLE iOS-
|
|
|
|
|
|
nettlesere (deler WebKit-motor), men Del-ikonets PLASSERING er ulik
|
|
|
|
|
|
(Safari: nederst på skjermen. Chrome: oppe til høyre i adressefeltet) --
|
|
|
|
|
|
generisk "i Safari"-tekst var dermed direkte feil for en Chrome-bruker,
|
|
|
|
|
|
ikke bare unøyaktig. Ny `iosBrowser()`-funksjon skiller på `CriOS`
|
|
|
|
|
|
(Chrome) i user agent-strengen (vanlig "Chrome"-sniffing ville feilaktig
|
|
|
|
|
|
også truffet Safari, som deler samme motor). Ukjente/andre iOS-
|
|
|
|
|
|
nettlesere får en nøytral "i nettleseren din"-tekst i stedet for å gjette.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — presentasjonstekst korrigert i to filer.**
|
|
|
|
|
|
`teecup-beskrivelse.md` OG `velkommen.tsx` sin "Hva er TeeCup?"-seksjon
|
|
|
|
|
|
(sistnevnte var bevisst skrevet om fra førstnevnte, ikke limt inn rått,
|
|
|
|
|
|
se ADR-049) -- begge fikk "golfklubber" fjernet fra målgruppe-
|
|
|
|
|
|
beskrivelsen (både "hva det er i dag" og "hva det skal bli"-seksjonene
|
|
|
|
|
|
i beskrivelses-dokumentet), og et nytt punkt lagt til om at
|
|
|
|
|
|
turneringsadministrasjon/-presentasjon fungerer minst like godt på PC
|
|
|
|
|
|
som på telefon. "Klubbhus-stemning" (personlighet/tone-beskrivelsen,
|
|
|
|
|
|
navnet på selve designretningen) er UENDRET -- det er ikke en
|
|
|
|
|
|
målgruppe-påstand, kun en stemningsbeskrivelse.
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert** (tjueåttende scratch-miljø denne økten): eksakt
|
|
|
|
|
|
dagsgrense bekreftet med tre testtilfeller mot ekte API -- 10-åring
|
|
|
|
|
|
avvist (400 UNDER_MINIMUM_AGE), nøyaktig 13 år i dag akseptert (200,
|
|
|
|
|
|
`profile_complete: true`), én dag under 13 år avvist (400) -- samme
|
|
|
|
|
|
grensesnitt-endepunkt, kun fødselsdatoen endret mellom kallene. Bekreftet
|
|
|
|
|
|
i ekte nettleser: klientside-feilmeldingen vises umiddelbart med rød
|
|
|
|
|
|
kant ved en 2018-fødselsdato, "Fortsett"-knappen forblir deaktivert.
|
|
|
|
|
|
Presentasjonsteksten på `/velkommen` bekreftet uten "golfklubber" og med
|
|
|
|
|
|
det nye PC-punktet. `tsc --noEmit`/`py_compile` rene, ingen konsollfeil.
|
|
|
|
|
|
`teecup_db`s ACL bekreftet uendret, scratch-ressurser ryddet opp
|
|
|
|
|
|
fullstendig.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang denne runden** (drøftet med bruker, avtalt som
|
|
|
|
|
|
egen fremtidig runde): rapportering/blokkering/moderering av
|
|
|
|
|
|
brukergenerert innhold (bilder/kommentarer) og "privat som standard for
|
|
|
|
|
|
mindreårige"-personvern for GPS-/rundedeling -- begge reelle,
|
|
|
|
|
|
substansielle krav fra PDF-vedlegget for at appen skal godkjennes i App
|
|
|
|
|
|
Store/Google Play med UGC+GPS, men et eget, avgrenset prosjekt (admin-
|
|
|
|
|
|
panel, databasefiltrering, 24-timers responstid), ikke noe som hører
|
|
|
|
|
|
hjemme i samme runde som en enkel aldersgrense.
|
|
|
|
|
|
|
2026-08-10 09:07:33 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-052: Full clubhouse-overtagelse (designrunden) — 2026-08-10
|
|
|
|
|
|
|
|
|
|
|
|
Brukeren 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 egen `nr-*`-
|
|
|
|
|
|
palett, se ADR-048) skulle over på "clubhouse"-paletten
|
|
|
|
|
|
(ADR-047-oppfølgingen, hittil kun 7 filer: innlogging, dashbord,
|
|
|
|
|
|
bunnnav, velkommen) i én samlet runde, fremfor den tidligere planen om
|
|
|
|
|
|
gradvis retemaing skjerm for skjerm når de likevel røres.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — CSS-variabel-remap i `globals.css`, IKKE per-fil
|
|
|
|
|
|
className-omskriving.** Utforsking (Explore-agent) bekreftet to
|
|
|
|
|
|
avgjørende fakta: (1) `components/ui/*` (button/card/badge/input osv.)
|
|
|
|
|
|
og samtlige ~90 gjenværende "Forest Green"-filer er RENE
|
|
|
|
|
|
token-konsumenter -- et grep etter hardkodede `bg-white`/`text-gray-*`/
|
|
|
|
|
|
`slate-*` utenfor de 7 clubhouse-filene ga null treff. (2) Clubhouse-
|
|
|
|
|
|
paletten var allerede definert som egne CSS-variabler
|
|
|
|
|
|
(`--tee-strong`/`--clubhouse-*`) additivt i samme fil. Konsekvens:
|
|
|
|
|
|
`:root`s shadcn-tokens (`--background`, `--card`, `--primary` osv.)
|
|
|
|
|
|
ble redefinert til å KOPIERE clubhouse sine verdier direkte, i stedet
|
|
|
|
|
|
for å endre className i 90 filer. `bg-primary`/`bg-card`/
|
|
|
|
|
|
`text-muted-foreground` osv. beholder navnene sine over hele appen --
|
|
|
|
|
|
det som endret seg er hvilken farge de peker på. Ingen av de ~90
|
|
|
|
|
|
filene ble rørt.
|
|
|
|
|
|
|
|
|
|
|
|
`--destructive`, `--brand-orange(-foreground)`, `--info(-foreground)`,
|
|
|
|
|
|
`--gold(-foreground)`, `--chart-1..6` er BEVISST uendret -- de er
|
|
|
|
|
|
distinkte semantiske signalfarger (feil/fare, sekundær-CTA, nøytral
|
|
|
|
|
|
info, personlig rekord, statistikk-skala), ikke del av
|
|
|
|
|
|
"nøytral/primær"-identiteten paletten styrer. `--brand-orange`
|
|
|
|
|
|
(opprinnelig `#ff5722`) er dessuten allerede nesten identisk med
|
|
|
|
|
|
clubhouse sin `--cup` (`#ff5427`) -- samme merkevarefamilie (ADR-009/
|
|
|
|
|
|
016), ingen synlig dissonans.
|
|
|
|
|
|
|
|
|
|
|
|
**Kontrast-korreksjon funnet FØR utrulling, ikke etterpå:** dagens
|
|
|
|
|
|
`--primary-foreground` var nesten-svart (riktig for den gamle LYSE
|
|
|
|
|
|
grønne primærfargen). Clubhouse sin `--tee-strong` (`#2f6b1e`) er en
|
|
|
|
|
|
MØRK, mettet grønn, brukt i eksisterende kode med `text-white`
|
|
|
|
|
|
(`velkommen.tsx`). `--primary-foreground` ble derfor satt til hvit ved
|
|
|
|
|
|
remap -- uten denne rettelsen ville teksten på hver primærknapp i hele
|
|
|
|
|
|
appen blitt ulesbar mørk-på-mørk umiddelbart ved deploy.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — mørk variant designet av V0 på bestilling, ikke
|
|
|
|
|
|
oppfunnet av Claude.** Clubhouse hadde ingen mørk-modus-variant (kun
|
|
|
|
|
|
"fast lys-modus" på de 7 opprinnelige sidene). Bruker fikk valget
|
|
|
|
|
|
(`AskUserQuestion`) mellom "fjern mørk modus, hele appen blir fast
|
|
|
|
|
|
lys" og "lag et prompt til V0 om å designe en mørk variant" -- valgte
|
|
|
|
|
|
sistnevnte. Claude-forfattet V0-prompt ga V0 de ti låste lyse
|
|
|
|
|
|
clubhouse-verdiene som fasit-referanse og ba om et rent fargesvar
|
|
|
|
|
|
(ingen kode/komponenter) for et mørk-modus-motstykke, med eksplisitt
|
|
|
|
|
|
WCAG AA-kontrastkrav. V0 svarte (limt inn av bruker som en full
|
|
|
|
|
|
prosjekt-zip, `globals.css`-diffen inneholdt kun den nye `.dark`-
|
|
|
|
|
|
blokken) med: `--tee: #a3d165`, `--tee-strong: #3c8226`,
|
|
|
|
|
|
`--cup: #ff6a3d`, `--cup-strong: #ff8355`, `--clubhouse-bg: #131a0f`,
|
|
|
|
|
|
`--clubhouse-card: #1d2616`, `--clubhouse-ink: #eef3e6`,
|
|
|
|
|
|
`--clubhouse-muted: #a7b598`, `--clubhouse-border: #34402a`,
|
|
|
|
|
|
`--clubhouse-field: #26301d`. Claude verifiserte V0s egne oppgitte
|
|
|
|
|
|
kontrasttall med en uavhengig WCAG-beregning (ikke bare tillit til V0s
|
|
|
|
|
|
kommentar) -- alle stemte eksakt: ink/bg 15.7:1, muted/bg 8.2:1,
|
|
|
|
|
|
muted/card 7.2:1, hvit/tee-strong 4.76:1 (så vidt over AA-grensen på
|
|
|
|
|
|
4.5:1, verifisert med vilje siden det var det trangeste tallet).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — ingen className-endring trengtes i de 7 opprinnelige
|
|
|
|
|
|
clubhouse-filene for mørk-modus-støtte.** Disse bruker eksplisitte
|
|
|
|
|
|
`bg-clubhouse-bg`/`text-tee-strong`-klasser (ikke de generiske
|
|
|
|
|
|
`bg-background`/`text-primary`-navnene). Opprinnelig plan (se
|
|
|
|
|
|
plan-fil) antok disse måtte skrives om for å følge mørk modus. Viste
|
|
|
|
|
|
seg unødvendig: siden Tailwinds `@theme inline` genererer disse
|
|
|
|
|
|
klassene som ren `var(--tee-strong)`-referanse, og en `.dark`-blokk
|
|
|
|
|
|
(+ tilsvarende `@media (prefers-color-scheme: dark)`-blokk) for de RÅ
|
|
|
|
|
|
`--tee`/`--cup`/`--clubhouse-*`-variablene ble lagt til samtidig (av
|
|
|
|
|
|
symmetri-/fullstendighetshensyn, se Beslutning A), arver disse 7
|
|
|
|
|
|
filene mørk modus automatisk via vanlig CSS-variabel-cascade -- helt
|
|
|
|
|
|
uavhengig av om de bruker `bg-clubhouse-bg` eller `bg-background` som
|
|
|
|
|
|
klassenavn. Ingen kode i disse 7 filene endret.
|
|
|
|
|
|
|
|
|
|
|
|
App-en har INGEN manuell mørk/lys-bryter (`app/layout.tsx` har ingen
|
|
|
|
|
|
`ThemeProvider`/`next-themes`, kun `@media (prefers-color-scheme:
|
|
|
|
|
|
dark)`) -- mørk modus er og forblir rent OS-styrt.
|
|
|
|
|
|
|
|
|
|
|
|
**Liten tilleggsrettelse i samme runde:** `app/layout.tsx`s
|
|
|
|
|
|
`themeColor`-metadata (nettleser-/statuslinje-tint, brukt av PWA-
|
|
|
|
|
|
installasjon) pekte fortsatt på de gamle fargene (`#8BC24A`/
|
|
|
|
|
|
`#1c261d`) -- oppdatert til de nye bakgrunnsfargene (`#f3f6ec`/
|
|
|
|
|
|
`#131a0f`), samme "rett opportunistisk opp når den likevel røres"-
|
|
|
|
|
|
prinsipp som tilgjengelighetsregelen i CLAUDE.md.
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert** (full stack: scratch-DB med alle 61
|
|
|
|
|
|
migrasjoner, `teecup_app_dr1`-rolle, hyphenert scratch-MinIO, scratch
|
|
|
|
|
|
API- og frontend-container, ekte seedet data via de faktiske API-
|
|
|
|
|
|
endepunktene -- personlig bane, runde, fem hullscore, IKKE rå SQL mot
|
|
|
|
|
|
domenetabellene). `tsc --noEmit` rent, ekte produksjonsbuild av
|
|
|
|
|
|
frontend rent (samme build som ville kjørt i `docker compose build`).
|
|
|
|
|
|
Browserverifisert i ekte nettleser (Chrome DevTools MCP,
|
|
|
|
|
|
`emulate colorScheme`) i BÅDE lys og mørk modus på: `/logg-inn` (alle
|
|
|
|
|
|
tilstander, uendret utseende), `/velkommen` (låst-profil-siden),
|
|
|
|
|
|
`/dashboard` (tom + med ekte seedet rundekort), `/my-rounds/[id]`
|
|
|
|
|
|
(scoreførings-hovedskjermen, appens største fil, 6726 linjer --
|
|
|
|
|
|
tidligere Forest Green, nå automatisk clubhouse via remap),
|
|
|
|
|
|
scorekort-tabellen (tett datagrid), `/account` (skjematung side).
|
|
|
|
|
|
Alle konsistente, god kontrast, ingen konsollfeil utover en harmløs
|
|
|
|
|
|
PWA-installasjons-infomelding. `/my-rounds/new` (ny-runde-veiviseren)
|
|
|
|
|
|
bekreftet visuelt UENDRET -- ingen smitte fra remappen inn i
|
|
|
|
|
|
`nr-*`-paletten. `BottomNav`s bevisste palett-nøytrale "flytende hvit
|
|
|
|
|
|
flate" (se CHANGELOG punkt 40) forblir hvit i mørk modus også -- dette
|
|
|
|
|
|
er et eksisterende, bevisst designvalg fra V0, ikke en regresjon fra
|
|
|
|
|
|
denne runden. `teecup_db`s ACL bekreftet uendret før/etter, alle
|
|
|
|
|
|
scratch-ressurser (DB, rolle, MinIO-volum, container-images) ryddet
|
|
|
|
|
|
opp fullstendig.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** dette er en FARGE-/identitetsovertagelse,
|
|
|
|
|
|
ikke en layout-/komponent-redesign av de ~90 filene -- de beholder sin
|
|
|
|
|
|
eksisterende struktur, kun ny fargeidentitet arvet automatisk. Egen,
|
|
|
|
|
|
fremtidig runde om noen av disse skjermene også trenger et
|
|
|
|
|
|
layoutløft, ikke besluttet eller påbegynt her.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-053: Ny-runde-veiviseren retemaet til clubhouse — 2026-08-10
|
|
|
|
|
|
|
|
|
|
|
|
Rett etter ADR-052 ba brukeren om at ny-runde-veiviseren (`nr-*`-
|
|
|
|
|
|
paletten, bevisst holdt utenfor ADR-052 som en tredje, uavhengig V0-
|
|
|
|
|
|
utforsking) også skulle få "det nye designet".
|
|
|
|
|
|
|
|
|
|
|
|
**Samme remap-prinsipp som ADR-052, samme fil.** De 16 `--nr-*`-
|
|
|
|
|
|
variablene i `globals.css` er den ENESTE kilden komponentene i
|
|
|
|
|
|
`components/ny-runde/*` leser (via rå `var(--nr-x)`-arbitrary-syntaks,
|
|
|
|
|
|
ikke `@theme inline`/Tailwind-klassenavn som resten av appen) -- et
|
|
|
|
|
|
grep bekreftet null hardkodet hex i alle 11 filer. Kun disse 16
|
|
|
|
|
|
variablenes VERDIER ble endret; ingen av de 11 filene rørt.
|
|
|
|
|
|
|
|
|
|
|
|
**Hvilke av de 16 som ble byttet til clubhouse, og hvilke som beholdt
|
|
|
|
|
|
sin opprinnelige, distinkte hue:** `--nr-bg/-surface/-surface-2/-ink/
|
|
|
|
|
|
-muted/-border/-accent/-accent-ink` byttet til de tilsvarende
|
|
|
|
|
|
clubhouse-verdiene (samme kilde som ADR-052, lys OG mørk). `--nr-danger
|
|
|
|
|
|
(-soft)`/`--nr-ok(-soft)` beholdt sin opprinnelige røde/grønne hue
|
|
|
|
|
|
UENDRET i lys modus -- samme prinsipp som `--destructive` i ADR-052 (en
|
|
|
|
|
|
distinkt semantisk signalfarge, ikke del av identiteten paletten
|
|
|
|
|
|
styrer). Disse fire fikk likevel NYE mørke varianter (`--nr-danger:
|
|
|
|
|
|
#ca655d`, `--nr-danger-soft: #382516`, `--nr-ok: #6da985`, `--nr-ok-
|
|
|
|
|
|
soft: #1d371f`) siden `.nr` aldri hadde noen mørk-modus-støtte i det
|
|
|
|
|
|
hele tatt før nå -- de opprinnelige lyse "soft"-bakgrunnene (pastellrosa/
|
|
|
|
|
|
-grønn ment for en hvit flate) ville sett ødelagt ut mot en mørk
|
|
|
|
|
|
overflate, så disse ble tilpasset mørk bakgrunn med samme
|
|
|
|
|
|
alpha-blande-teknikk som under.
|
|
|
|
|
|
|
|
|
|
|
|
**Fire variabler uten direkte clubhouse-kilde, avledet (ikke
|
|
|
|
|
|
oppfunnet fritt):** `--nr-faint`, `--nr-border-strong`, `--nr-accent-
|
|
|
|
|
|
soft`, `--nr-accent-ring` fantes ikke i clubhouse sitt sett på ti. Disse
|
|
|
|
|
|
ble alfa-blandet fra clubhouse sine allerede godkjente farger (f.eks.
|
|
|
|
|
|
`--nr-accent-soft` = clubhouse-bg blandet 14% med `tee`, samme visuelle
|
|
|
|
|
|
rolle som `bg-tee/10`-mønsteret allerede brukt andre steder i
|
|
|
|
|
|
clubhouse-filene). `--nr-faint` sin kontrast ble eksplisitt kalibrert
|
|
|
|
|
|
til å ALDRI havne under originalens egen kontrast (2.61:1 lys / mot
|
|
|
|
|
|
`--nr-bg`) -- endte på 3.28:1 lys / 4.64:1 mørk, komfortabelt over
|
|
|
|
|
|
originalen, ikke bare en tilfeldig fargeblanding.
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert** (eget scratch-miljø, samme fulle oppsett som
|
|
|
|
|
|
ADR-052 -- ny scratch-DB/rolle/MinIO/API/frontend-container).
|
|
|
|
|
|
`tsc --noEmit` rent. Browserverifisert i ekte nettleser i BÅDE lys og
|
|
|
|
|
|
mørk modus: bane-valg-steget, "Egen bane"-søket (øver `--nr-faint` i en
|
|
|
|
|
|
tom-tilstand-boks), og det tette hull-for-hull par/stroke-indeks-
|
|
|
|
|
|
skjemaet (18-raders tabell med dropdowns -- samme datatett-grid-test
|
|
|
|
|
|
som scorekortet i ADR-052). Alle konsistente, god kontrast. Ingen nye
|
|
|
|
|
|
konsollfeil (de tre "issue"-varslene som vises der -- manglende
|
|
|
|
|
|
`autocomplete`/`id`-attributter -- er uendret fra FØR denne runden,
|
|
|
|
|
|
altså forhåndseksisterende i selve V0-eksportens markup, ikke noe denne
|
|
|
|
|
|
CSS-only-endringen rørte eller kan ha forårsaket). Scratch-ressurser
|
|
|
|
|
|
ryddet opp fullstendig, `teecup_db`s ACL bekreftet uendret.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** samme som ADR-052 -- ren fargeovertagelse,
|
|
|
|
|
|
ingen layout-/strukturendring i veiviserens 11 filer.
|
|
|
|
|
|
|
2026-08-10 18:34:31 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-054: Kartretning ved slagmåling — bearing fra eget forrige slag, ikke green-koordinater — 2026-08-10
|
|
|
|
|
|
|
|
|
|
|
|
Brukeren spurte: ved lengdemåling av slag, kan kartet vises slik at "man
|
|
|
|
|
|
går oppover" (opp på skjermen = fremover på hullet) UTEN å registrere
|
|
|
|
|
|
green-koordinater (som verken TeeCup eller teeoff har noe sted, bekreftet
|
|
|
|
|
|
tidligere denne sesjonen)? Svaret var ja, og brukeren ba om at det bygges.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — bearing regnet fra spillerens EGET forrige slag på
|
|
|
|
|
|
hullet, ikke fra en lagret hull-linje.** `round_shot` (ADR-048) lagrer
|
|
|
|
|
|
allerede start-/sluttkoordinater per slag, sortert på `shot_number`.
|
|
|
|
|
|
Ny `bearingDegrees(a, b)` i `frontend/lib/geo.ts` (standard forward-
|
|
|
|
|
|
azimuth-formel, ren funksjon, ingen Mapbox-avhengighet -- samme mønster
|
|
|
|
|
|
som `haversineMeters`) regner ut retningsvinkelen til FORRIGE slag på
|
|
|
|
|
|
hullet (siste element i den allerede hentede `shots`-listen i
|
|
|
|
|
|
`round-detail.tsx` sin `ShotMeasurementEntry`, som GET-endepunktet
|
|
|
|
|
|
allerede returnerer `ORDER BY shot_number`). Denne vinkelen sendes ned
|
|
|
|
|
|
som `initialBearing`-prop gjennom `ShotMeasurementSheet` til BEGGE
|
|
|
|
|
|
`MapPointPicker`-instansene (start-steget OG ball-steget), og settes som
|
|
|
|
|
|
`bearing` i selve Mapbox-konstruktøren.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — statisk, ikke sanntids-rotasjon.** Bearingen settes ÉN
|
|
|
|
|
|
gang ved kart-initialisering, aldri oppdatert løpende mens brukeren går.
|
|
|
|
|
|
En kontinuerlig rotasjon etter live GPS-heading (`coords.heading` fra
|
|
|
|
|
|
`watchPosition`, som allerede kjører på ball-steget for sanntids-avstand)
|
|
|
|
|
|
ble vurdert og avvist -- GPS-heading er notorisk ustøyende ved lav
|
|
|
|
|
|
gangfart, og en urolig/hakkete kartrotasjon mens brukeren går ville vært
|
|
|
|
|
|
verre enn ingen rotasjon. Samme begrunnelse som ADR-048s eksisterende
|
|
|
|
|
|
"ett kart-instans, aldri re-initialisert"-prinsipp.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C -- fallback-rekkefølge for det FØRSTE slaget på et hull**
|
|
|
|
|
|
(ingen forrige slag å regne bearing fra ennå): (1) ett engangs-forsøk på
|
|
|
|
|
|
`coords.heading` fra det samme posisjonsoppslaget kartet uansett gjør for
|
|
|
|
|
|
sentrering (kun populert når enheten beveger seg, ofte `null` for et
|
|
|
|
|
|
enkeltstående oppslag -- akseptert, ikke en feiltilstand), (2) nord (0)
|
|
|
|
|
|
som endelig fallback. Ingen ny geolocation-spørring lagt til utover det
|
|
|
|
|
|
som allerede fantes.
|
|
|
|
|
|
|
|
|
|
|
|
Ingen migrasjon, ingen backend-endring -- rent klientside, tre filer
|
|
|
|
|
|
(`lib/geo.ts`, `components/shot/map-point-picker.tsx`,
|
|
|
|
|
|
`components/shot/shot-measurement-sheet.tsx`) pluss beregningen i
|
|
|
|
|
|
`components/round-detail.tsx` sin `ShotMeasurementEntry` (som allerede
|
|
|
|
|
|
har `shots`-listen fra sin eksisterende `useEffect`-henting).
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisert i to lag:** (1) `bearingDegrees()` unit-verifisert
|
|
|
|
|
|
frittstående (Node, fire kjente himmelretninger fra ett origo-punkt --
|
|
|
|
|
|
nord/øst/sør/vest ga 0.0/90.0/180.0/270.0 eksakt). (2) Ende-til-ende i
|
|
|
|
|
|
ekte nettleser (eget scratch-miljø, samme fulle DB/rolle/MinIO/API/
|
|
|
|
|
|
frontend-oppsett som tidligere runder denne økten): seedet ett slag rett
|
|
|
|
|
|
ØST (bearing 90°) på hull 1 via de ekte API-endepunktene, midlertidig
|
|
|
|
|
|
konsollogg i `initMap()` bekreftet kartet fikk `bearing≈89.996`
|
|
|
|
|
|
(avrundingsdifferanse fra at 0.01°-lengdegrad-delta ikke er eksakt øst
|
|
|
|
|
|
på en kule -- korrekt) da måle-arket ble åpnet på hull 1. Hull 2 (ingen
|
|
|
|
|
|
tidligere slag) ga `bearing=0` (nord-fallback), som forventet. Debug-
|
|
|
|
|
|
loggen fjernet igjen etter verifisering, `tsc --noEmit` rent. Scratch-
|
|
|
|
|
|
ressurser ryddet opp fullstendig, `teecup_db`s ACL bekreftet uendret.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** sanntids-rotasjon under gange (se Beslutning
|
|
|
|
|
|
B), og en egen "nullstill til nord"-knapp i kart-UI-et (ikke bedt om,
|
|
|
|
|
|
lav verdi når bearingen uansett er statisk og sjelden feil).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-055: Nytt app-ikon (ball/pokal/tee), full-bleed, korrigert etter reell tegnefeil — 2026-08-10
|
|
|
|
|
|
|
|
|
|
|
|
Brukeren: dagens app-ikon var "fremdeles et gammelt utkast" til tross for
|
|
|
|
|
|
at et tidligere punkt (CHANGELOG, 2026-08-06) hevdet ikonene var "beskåret
|
|
|
|
|
|
fra den ekte TeeCup-logoen". Visuell inspeksjon av `icons/icon-512.png`,
|
|
|
|
|
|
`icon-maskable-512.png` og `apple-icon.png` (åpnet direkte i nettleser)
|
|
|
|
|
|
bekreftet brukerens vurdering: riktig motiv (golfball/pokal/tee), men en
|
|
|
|
|
|
grov beskjæring med enorm hvit padding rundt en liten sentrert grafikk --
|
|
|
|
|
|
nesten ulesbart ved favikon-størrelse (32×32), og for lite innhold i
|
|
|
|
|
|
sentrum til å tåle Androids sirkel-/dråpe-maskering.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A -- ny komposisjon via V0, samme motiv.** Claude skrev en
|
|
|
|
|
|
V0-prompt som IKKE ba om et nytt motiv, kun en ny KOMPOSISJON av det
|
|
|
|
|
|
eksisterende (bruker la ved den ekte kilde-SVG-en, `TeeCup-logo.svg`,
|
|
|
|
|
|
i samme V0-melding): full-bleed bakgrunn kant til kant (ingen
|
|
|
|
|
|
gjennomsiktig/hvit "luft"), motivet skalert til å fylle et sentrert
|
|
|
|
|
|
"safe zone"-område (~70-75%) som tåler både iOS- og Android-maskering,
|
|
|
|
|
|
ingen egen avrunding tegnet inn (plattformen masker selv), lesbart ned
|
|
|
|
|
|
til 32×32. V0 leverte ett master-SVG (1024×1024) med en mørkegrønn
|
|
|
|
|
|
full-bleed bakgrunn (`#14351c`) og motivet nestet i et sentrert
|
|
|
|
|
|
undervindu.
|
|
|
|
|
|
|
|
|
|
|
|
**Reelt funn under verifisering, IKKE en V0-feiltolkning.** Første
|
|
|
|
|
|
V0-svar hadde to synlige "hakk" skåret inn i pokal-kroppen ved
|
|
|
|
|
|
skulder-/hank-overgangen. Claude sammenlignet først mot feil kildefil
|
|
|
|
|
|
(en annen, rasterbasert `TeeCup-logo.svg` funnet i `Temp-uploads/` fra
|
|
|
|
|
|
tidligere i sesjonen) og konkluderte feilaktig at V0 måtte ha "tegnet på
|
|
|
|
|
|
nytt etter øyemål". Bruker delte deretter den FAKTISKE kilde-SVG-en
|
|
|
|
|
|
(`TeeCup-logo-kun.svg`, rene Inkscape-vektorbaner) direkte i chatten.
|
|
|
|
|
|
Rendret stort på hvit bakgrunn viste denne at hakkene faktisk stammer fra
|
|
|
|
|
|
en ekte, pre-eksisterende unøyaktighet i selve kildekunsten: to
|
|
|
|
|
|
overlappende oransje former (`path20`, fyll `#fd5524`, og `path21`, fyll
|
|
|
|
|
|
`#fe5d2c`) dekker ikke hverandre helt i to punkter. Usynlig på hvit
|
|
|
|
|
|
bakgrunn (de to oransjetonene ligner hverandre), men synlig som et hakk
|
|
|
|
|
|
på en sterkt kontrasterende mørk bakgrunn, siden bakgrunnsfargen lyser
|
|
|
|
|
|
gjennom gapet. V0s opprinnelige "tatt verbatim, ikke tegnet på nytt"
|
|
|
|
|
|
-påstand var altså korrekt -- Claudes første mistanke var feil.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B -- fikset ved å utvide, ikke omtegne.** Claude ga V0 en
|
|
|
|
|
|
presis, kildehenvisende rettemelding (identifiserte de to konkrete
|
|
|
|
|
|
path-fyllfargene og hvorfor gapet oppstår). V0s fiks: la til en synlig
|
|
|
|
|
|
`stroke` i samme farge som fyllet (`stroke="#fd5524" stroke-width="0.6"`)
|
|
|
|
|
|
på `path20`, som tetter gapet ved å utvide den underliggende formens
|
|
|
|
|
|
synlige kant, i stedet for å redigere selve bezier-kurvene. To små
|
|
|
|
|
|
håndtak-innvendige "glans"-former (opprinnelig nær-hvite, ville vist som
|
|
|
|
|
|
lyse flekker i håndtak-hullene mot mørk bakgrunn) fikk samtidig fyll
|
|
|
|
|
|
byttet til bakgrunnsfargen (`#14351c`) -- korrekt, siden de fungerer som
|
|
|
|
|
|
utsparinger, ikke skyggelegging, i denne fargekonteksten.
|
|
|
|
|
|
|
|
|
|
|
|
**Claude verifiserte fiksen visuelt** (stor rendring, samme
|
|
|
|
|
|
sammenligningsteknikk som avdekket problemet) før integrering --
|
|
|
|
|
|
hakkene bekreftet borte, ingen nye artefakter.
|
|
|
|
|
|
|
|
|
|
|
|
**Integrering:** ett 1024×1024 master-SVG er eneste kilde. Claude
|
|
|
|
|
|
rasterte selv alle nødvendige størrelser via ekte nettleser-rendring
|
|
|
|
|
|
(Chrome DevTools MCP, `devicePixelRatio=1` for eksakte piksler, bekreftet
|
|
|
|
|
|
med `file`-kommandoen etterpå) -- IKKE V0-genererte PNG-er, for å unngå
|
|
|
|
|
|
enhver eksport-unøyaktighet: `icons/icon-192.png`, `icons/icon-512.png`,
|
|
|
|
|
|
`icons/icon-maskable-512.png` (samme kilde som icon-512 -- V0s
|
|
|
|
|
|
komposisjon var allerede safe-zone-riktig, ingen egen beskjæring
|
|
|
|
|
|
trengtes), `apple-icon.png` (180×180), `icon-light-32x32.png` og
|
|
|
|
|
|
`icon-dark-32x32.png` (samme bilde for begge -- det nye ikonet har sin
|
|
|
|
|
|
egen faste mørkegrønne bakgrunn og trenger ikke lenger to
|
|
|
|
|
|
tema-varianter, ulikt en eventuell fremtidig gjennomsiktig/hvit
|
|
|
|
|
|
favikon-variant). `public/icon.svg` (SVG-favikon-fallback) erstattet med
|
|
|
|
|
|
master-SVG-et selv.
|
|
|
|
|
|
|
|
|
|
|
|
`app/manifest.ts` sin `theme_color`/`background_color` sto fortsatt på
|
|
|
|
|
|
de gamle Forest Green-fargene (`#8BC24A`/`#ffffff`) -- oppdatert til
|
|
|
|
|
|
clubhouse (ADR-052): `theme_color: "#2f6b1e"` (primær merkevarefarge,
|
|
|
|
|
|
Android UI-tinting), `background_color: "#f3f6ec"` (lys bakgrunn,
|
|
|
|
|
|
PWA-splashskjerm).
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert:** `tsc --noEmit` rent. Egen frontend-only
|
|
|
|
|
|
scratch-container (ingen DB/API-endring i denne runden) -- ekte
|
|
|
|
|
|
produksjonsbuild, `GET /manifest.webmanifest` bekreftet nye farger,
|
|
|
|
|
|
`icon.svg`/`icons/icon-192.png`/`icon-light-32x32.png` bekreftet 200 OG
|
|
|
|
|
|
visuelt korrekte i ekte nettleser. Scratch-ressurser ryddet opp
|
|
|
|
|
|
fullstendig.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** `teecup-wordmark.svg` (det fulle
|
|
|
|
|
|
ikon+tekst-ordmerket, brukt andre steder) er IKKE endret -- kun de rene
|
|
|
|
|
|
app-ikon-filene.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-056: Reell WHS-svakhet + reell rundevisnings-bug — funnet via ekstern kodegjennomgang, 2026-08-10
|
|
|
|
|
|
|
|
|
|
|
|
Brukeren delte en ekstern vurdering av appen (parafrasert): to "harde
|
|
|
|
|
|
kjerner" -- WHS-beregningen og live-scoring/samtidighet -- er der en
|
|
|
|
|
|
vibe-kodet app typisk svikter, og fortjener ekte enhetstester mot kjente
|
|
|
|
|
|
fasitcaser, ikke selvbekreftende tester. To parallelle Explore-agenter
|
|
|
|
|
|
gransket hver sin kjerne. WHS-agentens funn ga direkte opphav til
|
|
|
|
|
|
Beslutning A under. Samtidighets-agentens funn (last-write-wins er et
|
|
|
|
|
|
BEVISST, allerede dokumentert valg i ADR-028 -- ikke en overraskelse)
|
|
|
|
|
|
ga IKKE grunnlag for umiddelbar koding, kun en åpen designbeslutning
|
|
|
|
|
|
brukeren må ta først (se "Åpent, ikke besluttet" nederst).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A -- WHS Rule 3.1b sitt >54/4+-slag-unntak implementert og
|
|
|
|
|
|
fasit-testet.** Agentens revisjon fant at `handicap_engine.py` sin NDB-
|
|
|
|
|
|
cap (`max_hole_score_for_handicap`) manglet et reelt, publisert WHS-
|
|
|
|
|
|
unntak: ved banehandicap over 54 OG 4 eller flere mottatte slag på ett
|
|
|
|
|
|
hull, er maks hullscore par+5 (IKKE den vanlige par+2+mottatte slag).
|
|
|
|
|
|
Claude verifiserte dette FØR koding direkte mot kildePDF-en (`WHS_Rules_
|
|
|
|
|
|
of_Handicapping_2024.pdf`, side 37, `pdftotext`-søk, ikke agentens
|
|
|
|
|
|
gjenfortelling): "Where a Course Handicap is calculated at more than 54
|
|
|
|
|
|
and a player receives 4 or more strokes on a hole, the maximum hole
|
|
|
|
|
|
score is par + 5 for handicap purposes."
|
|
|
|
|
|
|
|
|
|
|
|
Lagt til som et nytt, valgfritt `course_handicap`-parameter (standard
|
|
|
|
|
|
`None`) på `max_hole_score_for_handicap` og `adjusted_gross_score` --
|
|
|
|
|
|
bakoverkompatibelt (uendret oppførsel for ethvert kall som ikke sender
|
|
|
|
|
|
det). To produksjonskallsteder i `rounds.py` (`update_hole` sin
|
|
|
|
|
|
"plukket opp"-gren, og rundefullførings-differensial-beregningen)
|
|
|
|
|
|
oppdatert til å sende med den allerede tilgjengelige
|
|
|
|
|
|
`course_handicap_snapshot`.
|
|
|
|
|
|
|
|
|
|
|
|
Fire nye, presist begrunnede tester lagt til i `test_handicap_engine.py`
|
|
|
|
|
|
(grensetilfellene eksplisitt: eksakt 54 -- IKKE "mer enn 54", eksakt 3
|
|
|
|
|
|
slag -- IKKE "4 eller flere", samt end-til-ende via
|
|
|
|
|
|
`adjusted_gross_score`). 117/117 tester grønne (opp fra 115). Scratch-
|
|
|
|
|
|
verifisert også via ekte API-kall (deltaker med banehandicap 63, hull
|
|
|
|
|
|
med 4 mottatte slag, "plukket opp"-endepunktet returnerte 9 som
|
|
|
|
|
|
forventet, ikke det gamle 10).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B -- `round.tee_name_snapshot` synkes nå med eierens eget
|
|
|
|
|
|
utslagsbytte.** Egen, urelatert bug rapportert av bruker samme økt (se
|
|
|
|
|
|
skjermdump): rundens toppheader (delt på tvers av Score/Scorekort/
|
|
|
|
|
|
Leaderboard) viste et gammelt utslag ("50") selv etter at brukeren
|
|
|
|
|
|
hadde endret utslaget til "55" -- bekreftet i `round_participant` (begge
|
|
|
|
|
|
rader riktig "55") vs. `round.tee_name_snapshot` (fortsatt "50"). Root
|
|
|
|
|
|
cause: `PATCH /rounds/{round_id}/participants/{participant_id}`
|
|
|
|
|
|
(`update_participant`, brukt ved enkelt-deltaker-utslagsbytte) skrev
|
|
|
|
|
|
KUN til `round_participant`, aldri til `round` -- ulikt den separate
|
|
|
|
|
|
hele-bane-bytte-grenen i `update_round`, som alltid har oppdatert begge
|
|
|
|
|
|
sammen. De to skrive-stiene var ikke holdt i synk.
|
|
|
|
|
|
|
|
|
|
|
|
Fiks: når denne PATCH-en endrer `tee_name` for EIERENS EGEN
|
|
|
|
|
|
deltaker-rad (`current["user_id"] == current["round_owner_user_id"]`,
|
|
|
|
|
|
ikke en medspillers/gjests -- ulike deltakere kan bevisst spille ulike
|
|
|
|
|
|
utslag, se `ParticipantCreate.tee_name`), oppdateres
|
|
|
|
|
|
`round.tee_name_snapshot` til samme verdi i samme kall -- speiler
|
|
|
|
|
|
nøyaktig hvordan `create_round`/`_create_participant` allerede holder
|
|
|
|
|
|
disse to i synk ved selve opprettelsen.
|
|
|
|
|
|
|
|
|
|
|
|
Scratch-verifisert: eierens eget utslagsbytte oppdaterte
|
|
|
|
|
|
`round.tee_name_snapshot` korrekt; et påfølgende utslagsbytte for en
|
|
|
|
|
|
GJEST lot `round.tee_name_snapshot` stå urørt (bekrefter at kun
|
|
|
|
|
|
eierens egen rad trigger synken, som tiltenkt).
|
|
|
|
|
|
|
|
|
|
|
|
**Ikke rettet i denne runden (venter på egen brukerbekreftelse, se
|
|
|
|
|
|
CHANGELOG):** den konkrete, allerede-live runden brukeren viste
|
|
|
|
|
|
skjermdump av (`round.tee_name_snapshot` fortsatt "50" i ekte
|
|
|
|
|
|
`teecup_db` for akkurat den runden) -- kodefiksen over hindrer nye
|
|
|
|
|
|
tilfeller, men retter ikke historisk feil data. Krever en engangs
|
|
|
|
|
|
`UPDATE`-setning mot ekte `teecup_db`, vist og bekreftet separat per
|
|
|
|
|
|
CLAUDE.md.
|
|
|
|
|
|
|
|
|
|
|
|
**Besluttet samme økt:** samtidighets-/konflikt-halvparten av den
|
|
|
|
|
|
eksterne vurderingen krevde en designbeslutning før noe kunne bygges --
|
|
|
|
|
|
bruker fikk valget mellom "enkel versjonssjekk + tydelig feilmelding",
|
|
|
|
|
|
"feltvis i stedet for radvis overskriving", og "ikke nå" via
|
|
|
|
|
|
`AskUserQuestion`, valgte det anbefalte første alternativet. Bygget
|
|
|
|
|
|
samme økt, se ADR-057.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-057: Optimistisk versjonssjekk for samtidig hull-redigering — 2026-08-10
|
|
|
|
|
|
|
|
|
|
|
|
Direkte oppfølging av ADR-056: bruker valgte "enkel versjonssjekk +
|
|
|
|
|
|
tydelig feilmelding" for `update_hole`/`update_side_hole` sin
|
|
|
|
|
|
last-write-wins-oppførsel (ADR-028, bevisst den gangen, men brukeren
|
|
|
|
|
|
ønsket det endret nå).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A -- én ny `version`-kolonne på `round_hole` (migrasjon
|
|
|
|
|
|
062), dekker begge eierskapstyper.** `round_hole` er allerede delt
|
|
|
|
|
|
mellom deltaker- og side-eide hull (XOR-constraint) -- én kolonne holder
|
|
|
|
|
|
for begge `update_hole`/`update_side_hole`. `DEFAULT 1`, økes med 1 for
|
|
|
|
|
|
hver skrivning. Klienten sender `expected_version` tilbake (valgfritt --
|
|
|
|
|
|
`None`/utelatt hopper over sjekken, samme bakoverkompatible mønster som
|
|
|
|
|
|
resten av appens valgfrie felt).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B -- atomisk sjekk i selve UPDATE-en, ikke en separat
|
|
|
|
|
|
"les så skriv".** `WHERE ... AND ($N::int IS NULL OR version = $N::int)`
|
|
|
|
|
|
i samme setning som selve skrivningen -- ingen TOCTOU-vindu mellom sjekk
|
|
|
|
|
|
og skrivning. Et `None`-resultat skilles fra "hullet finnes ikke" via
|
|
|
|
|
|
enten en allerede-utført eksistenssjekk lenger opp i funksjonen
|
|
|
|
|
|
(`update_hole`, som uansett må slå opp par/stroke-index før en
|
|
|
|
|
|
"plukket opp"-beregning) eller en egen fallback-eksistenssjekk
|
|
|
|
|
|
(`update_side_hole`, som ikke hadde noen forhåndssjekk fra før) -- aldri
|
|
|
|
|
|
tvetydig hvilken feil som returneres.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C -- offline-køen kjeder versjonen fremover innad i én
|
|
|
|
|
|
flush-runde.** Reelt funn under design (før noe ble bygget, ikke en bug
|
|
|
|
|
|
funnet i ettertid): uten dette ville en spillers EGNE påfølgende
|
|
|
|
|
|
offline-redigeringer av samme hull avvist HVERANDRE som falske
|
|
|
|
|
|
konflikter, siden lokal state (og dermed enqueue-tidspunktets
|
|
|
|
|
|
`expected_version`) aldri oppdateres mellom to sekvensielle
|
|
|
|
|
|
avspillinger i samme batch. `flushQueue` (`offline-queue.ts`) holder nå
|
|
|
|
|
|
et lite `Map<url, siste kjente versjon>` gjennom hele flush-runden og
|
|
|
|
|
|
overstyrer `expected_version` på påfølgende oppføringer til samme URL
|
|
|
|
|
|
med den nyeste kjente verdien.
|
|
|
|
|
|
|
|
|
|
|
|
**Reelt UX-hull funnet UNDER scratch-verifisering, rettet før
|
|
|
|
|
|
utrulling.** Første implementasjon gjenbrukte komponentens
|
|
|
|
|
|
eksisterende `error`-tilstand for konfliktmeldingen. Viste seg (kun ved
|
|
|
|
|
|
ekte to-enhets-test i nettleser, ikke synlig fra kode-lesing alene) at
|
|
|
|
|
|
`error` er en FATAL, hele-siden-erstattende tilstand (brukt for f.eks.
|
|
|
|
|
|
"runden finnes ikke") -- en forbigående hull-konflikt tok dermed over
|
|
|
|
|
|
HELE skjermen og tvang brukeren til å navigere bort, i stedet for en
|
|
|
|
|
|
liten varsel. Rettet med en egen, dismissbar `conflictNotice`-tilstand
|
|
|
|
|
|
(banner øverst i siden, "Skjul"-knapp, resten av UI-et forblir fullt
|
|
|
|
|
|
brukbart) -- gjenbrukt også for den eksisterende (fra før denne runden)
|
|
|
|
|
|
kø-synk-feilmeldingen, som hadde nøyaktig samme fullskjerm-problem.
|
|
|
|
|
|
|
|
|
|
|
|
**Scratch-verifisert i tre lag, alle i ekte nettleser/API, ingen
|
|
|
|
|
|
enhetstester på TS-siden (ingen testinfrastruktur for det i
|
|
|
|
|
|
prosjektet ennå):**
|
|
|
|
|
|
1. Backend, `update_hole` OG `update_side_hole` hver for seg (ekte
|
|
|
|
|
|
API-kall): enhet A skriver først (lykkes, versjon 1→2), enhet B med
|
|
|
|
|
|
samme (nå utdaterte) `expected_version` avvist med 409 og eksakt
|
|
|
|
|
|
forventet feiltekst, A sin verdi bekreftet fortsatt lagret (ikke B
|
|
|
|
|
|
sin avviste), B henter ny versjon og lykkes på nytt forsøk. Bekreftet
|
|
|
|
|
|
at PATCH uten `expected_version` fortsatt fungerer (bakoverkompatibelt).
|
|
|
|
|
|
`update_side_hole` i tillegg bekreftet at et ikke-eksisterende hull
|
|
|
|
|
|
fortsatt gir 404, ikke feilaktig 409.
|
|
|
|
|
|
2. To ISOLERTE nettleser-kontekster (`isolatedContext`, egne
|
|
|
|
|
|
informasjonskapsel-rom -- ekte to-enheter-simulering, ikke bare to
|
|
|
|
|
|
faner som deler økt) som samme flight-medlem: enhet B med en allerede
|
|
|
|
|
|
åpen, utdatert veiviser fikk 409 ved innsending, veiviseren viste
|
|
|
|
|
|
automatisk den ferske (A sin) verdien i stedet for B sitt avviste
|
|
|
|
|
|
forsøk, banneret vist og dismissbart, RESTEN AV SIDEN forble
|
|
|
|
|
|
fullt brukbar (dette avdekket UX-hullet over).
|
|
|
|
|
|
3. Offline-kø-kjeding: ekte nettverks-emulering (DevTools "Offline"),
|
|
|
|
|
|
to påfølgende redigeringer av SAMME hull mens frakoblet, tilbake på
|
|
|
|
|
|
nett -- begge synkroniserte korrekt (endte på siste verdi), INGEN
|
|
|
|
|
|
falsk konflikt seg imellom, "venter på synk"-indikatoren forsvant helt.
|
|
|
|
|
|
|
|
|
|
|
|
`tsc --noEmit` og `py_compile` rene gjennom hele runden. Scratch-
|
|
|
|
|
|
ressurser ryddet opp fullstendig hver gang, `teecup_db`s ACL bekreftet
|
|
|
|
|
|
uendret.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** ORG-TURNERINGENES match-scoring
|
|
|
|
|
|
(`session-scorecard.tsx`, `/orgs/{id}/matches/{id}/hole-scores`) er et
|
|
|
|
|
|
HELT separat system (annen tabell, annen router) med trolig samme
|
|
|
|
|
|
last-write-wins-egenskap -- IKKE undersøkt eller endret denne runden,
|
|
|
|
|
|
siden verken den opprinnelige eksterne vurderingen eller brukerens
|
|
|
|
|
|
oppfølging nevnte det spesifikt. Egen, fremtidig vurdering om det
|
|
|
|
|
|
trengs der også.
|
|
|
|
|
|
|
2026-08-10 22:25:02 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-058: Automatisert backend-testinfrastruktur — RLS, versjonssjekk, aldersgrense — 2026-08-10
|
|
|
|
|
|
|
|
|
|
|
|
Direkte oppfølging av investor-statusrapporten (kodebasert revisjon,
|
|
|
|
|
|
samme dag): "nesten ingen automatisert testdekning utenfor HCP-motoren"
|
|
|
|
|
|
pekt ut som det klart største enkeltfunnet. Bruker ba om å ta tak i
|
|
|
|
|
|
akkurat dette, med to avklarte avgrensninger: (1) usikkert om Forgejo
|
|
|
|
|
|
Actions faktisk har en registrert runner for dette repoet på
|
|
|
|
|
|
forgejo.jegvil.no — bekreftet KUN at instansen har `has_actions: true`
|
|
|
|
|
|
for repoet (anonymt API-kall), ikke at en runner faktisk plukker opp
|
|
|
|
|
|
jobber. CI-workflow-fil er derfor BEVISST utenfor denne runden — egen,
|
|
|
|
|
|
senere runde, verifisert empirisk ved faktisk push. (2) Frontend-
|
|
|
|
|
|
testoppsett (Vitest/Playwright) valgt bort denne runden — kun backend.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — ekte integrasjonstester mot en automatisert scratch-
|
|
|
|
|
|
database, ikke mot mocks.** Samme prinsipp som all manuell scratch-
|
|
|
|
|
|
verifisering i prosjektet til nå (se CLAUDE.md), men automatisert i
|
|
|
|
|
|
`scripts/run_backend_tests.sh`: oppretter en midlertidig database +
|
|
|
|
|
|
en midlertidig rolle i den SAMME `teeoff_db`-Postgres-containeren,
|
|
|
|
|
|
kjører ALLE 62 migrasjonene i rekkefølge, kjører testene, dropper
|
|
|
|
|
|
database + rolle igjen. Rører aldri ekte `teecup_db` — bekreftet
|
|
|
|
|
|
eksplisitt etter hver kjøring (ACL uendret, ingen gjenglemte
|
|
|
|
|
|
scratch-ressurser).
|
|
|
|
|
|
|
|
|
|
|
|
Migrasjon 002 hardkoder BÅDE rollenavnet `teecup_app` (cluster-globalt
|
|
|
|
|
|
navn, delt med den ekte rollen) OG databasenavnet `teecup_db` i én
|
|
|
|
|
|
GRANT-setning. Begge patches med `sed` (ordgrense-presist, unngår å
|
|
|
|
|
|
treffe `teecup_app_password`/`teecup_app_exists`-variabelnavnene i
|
|
|
|
|
|
samme fil) til scratch-spesifikke navn før migrasjonen kjøres — samme
|
|
|
|
|
|
sed-patch-mønster som er brukt manuelt i alle tidligere scratch-runder
|
|
|
|
|
|
i prosjektet, nå skriptet.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — testene kaller de FAKTISKE router-/auth-funksjonene
|
|
|
|
|
|
direkte (`app.routers.rounds.update_hole`, `app.auth.get_authorized_org`,
|
|
|
|
|
|
`app.routers.auth.update_profile`), ikke en gjenimplementering av
|
|
|
|
|
|
logikken i SQL.** FastAPI sin `Depends()`-injeksjon trengs ikke når
|
|
|
|
|
|
funksjonen kalles direkte i Python — de resolverte verdiene (en
|
|
|
|
|
|
`CurrentUser`, en allerede-verifisert `organization_id`) sendes rett
|
|
|
|
|
|
inn som vanlige argumenter. Dette betyr testene kjører gjennom SAMME
|
|
|
|
|
|
kodesti som produksjon (samme SQL, samme feilhåndtering, samme
|
|
|
|
|
|
`app_error`) — IKKE en parallell test-bare implementasjon som kunne
|
|
|
|
|
|
drevet fra virkeligheten uten å bli fanget opp.
|
|
|
|
|
|
|
|
|
|
|
|
Testene kjører i en dedikert `Dockerfile.test`-container (pytest +
|
|
|
|
|
|
pytest-asyncio, IKKE del av prod-imaget) tilkoblet samme
|
|
|
|
|
|
`teeoff_default`-Docker-nettverk som `teecup_api` selv bruker i
|
|
|
|
|
|
produksjon — nødvendig fordi Postgres-containeren ikke har noen
|
|
|
|
|
|
host-publisert port i dette miljøet, kun cluster-intern DNS
|
|
|
|
|
|
(`teeoff_db:5432`, nøyaktig slik den ekte appen kobler til).
|
|
|
|
|
|
`lifespan`/MinIO-oppstart (`app.main`) er BEVISST unngått — testene
|
|
|
|
|
|
importerer routerne direkte i stedet for å boote hele ASGI-appen, så
|
|
|
|
|
|
ingen ekte MinIO-bucket berøres av denne runden.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — tre dekningsområder, valgt etter risiko, ikke
|
|
|
|
|
|
fullstendighet.** 17 tester totalt:
|
|
|
|
|
|
- `tests/test_rls_isolation.py` (4 tester) — automatiserer
|
|
|
|
|
|
`test_isolation.sql`s manuelle sjekker: cross-org SELECT lekker
|
|
|
|
|
|
aldri rader (selv uten eksplisitt WHERE i spørringen), cross-org
|
|
|
|
|
|
UPDATE påvirker 0 rader, INGEN org-kontekst satt gir TOMT resultat
|
|
|
|
|
|
(ikke "alt" — regresjonsvern for migrasjon 005s NULL-guard-fiks),
|
|
|
|
|
|
og `get_authorized_org` avviser et faktisk ikke-medlem FØR noen
|
|
|
|
|
|
org-scopet spørring i det hele tatt kjøres.
|
|
|
|
|
|
- `tests/test_concurrency_version_check.py` (6 tester) — ADR-057s
|
|
|
|
|
|
versjonssjekk, nå automatisert: vellykket skrivning inkrementerer
|
|
|
|
|
|
versjon, en utdatert `expected_version` gir 409 STALE_VERSION,
|
|
|
|
|
|
`expected_version=None` hopper bevisst over sjekken
|
|
|
|
|
|
(bakoverkompatibilitet), et ikke-eksisterende hull gir 404 (ikke
|
|
|
|
|
|
409) — alt dekket for BÅDE `update_hole` (deltaker-eide hull) og
|
|
|
|
|
|
`update_side_hole` (side-eide hull, egen 404-vs-409-logikk lagt til
|
|
|
|
|
|
i samme runde som versjonskolonnen).
|
|
|
|
|
|
- `tests/test_auth_age_gate.py` (5 tester) — 13-årsgrensen på selve
|
|
|
|
|
|
dagsgrensen (eksakt 13 år i dag tillatt, én dag for tidlig avvist),
|
|
|
|
|
|
godt under grensen avvist, andre feltoppdateringer uten fødselsdato
|
|
|
|
|
|
utløser ikke sjekken, og en avvist oppdatering skriver INGENTING
|
|
|
|
|
|
til databasen (ingen delvis skrivning før feilen kastes).
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisert:** alle 17 nye tester + de eksisterende 117 HCP-testene
|
|
|
|
|
|
grønne. Ekte `teecup_db` sitt rolleoppsett (`teecup_app`:
|
|
|
|
|
|
`NOSUPERUSER`/`NOBYPASSRLS`) og radantall i sentrale tabeller bekreftet
|
|
|
|
|
|
uendret etter kjøring. Ingen deploy denne runden — dette er ren
|
|
|
|
|
|
testinfrastruktur, ingen produksjonskode endret.
|
|
|
|
|
|
|
2026-08-10 22:41:48 +02:00
|
|
|
|
**Bevisst utenfor omfang, egne fremtidige runder:** frontend-tester, og
|
|
|
|
|
|
dekning av flere router-funksjoner enn de tre høyest-risiko-områdene
|
|
|
|
|
|
over (f.eks. øvrige `rounds.py`-endepunkter,
|
|
|
|
|
|
`individual_tournaments.py`, org-turnering-match-scoring sin
|
|
|
|
|
|
`scoring.py` — sistnevnte spesielt interessant siden ADR-057
|
|
|
|
|
|
dokumenterte AT den mangler samtidighetsvern, men det er foreløpig
|
|
|
|
|
|
udekket av en test som beviser det empirisk). CI-workflow-filen ble
|
|
|
|
|
|
løst SAMME dag — se ADR-059.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-059: Selvhostet Forgejo Actions-runner — 2026-08-10
|
|
|
|
|
|
|
|
|
|
|
|
Direkte oppfølging av ADR-058: `.forgejo/workflows/backend-tests.yml`
|
|
|
|
|
|
ble lagt til og pushet som en EMPIRISK test av om
|
|
|
|
|
|
forgejo.jegvil.no faktisk hadde en aktiv runner for dette repoet
|
|
|
|
|
|
(instansen svarte `has_actions: true` på et anonymt API-kall, men det
|
|
|
|
|
|
beviste ikke at noe faktisk plukker opp jobber). Resultat: jobben la
|
|
|
|
|
|
seg i kø som "Waiting" og ble stående uendret i over et minutt — ingen
|
|
|
|
|
|
runner fantes. Bruker ba om at det rettes.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — selvhostet `act_runner` PÅ DENNE serveren, ikke en
|
|
|
|
|
|
ekstern/skyhostet runner.** `run_backend_tests.sh` er avhengig av
|
|
|
|
|
|
`docker exec teeoff_db` og `teeoff_default`-nettverket, som kun
|
|
|
|
|
|
eksisterer på denne verten. En runner et annet sted ville uansett
|
|
|
|
|
|
ikke kunnet kjøre testene slik de er skrevet i dag.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — Docker-utenfor-Docker (DooD) via
|
|
|
|
|
|
`docker_host: automount` i runner-konfigen, ikke Docker-i-Docker.**
|
|
|
|
|
|
Jobb-containeren får HOST-ens `docker.sock` bind-montert inn (samme
|
|
|
|
|
|
mønster som runneren selv bruker for å starte jobb-containere i
|
|
|
|
|
|
utgangspunktet), slik at `scripts/run_backend_tests.sh` sine egne
|
|
|
|
|
|
`docker build`/`docker run`/`docker exec`-kall oppretter ekte
|
|
|
|
|
|
søsken-containere på host-nivå — ikke en nøstet, isolert
|
|
|
|
|
|
docker-daemon som ikke ville sett `teeoff_db` i det hele tatt.
|
|
|
|
|
|
Jobb-image: `catthehacker/ubuntu:act-latest` (de facto standardimage
|
|
|
|
|
|
for act/act_runner — har docker-CLI, git, bash forhåndsinstallert,
|
|
|
|
|
|
unngår `apt-get install` i hvert eneste kjør).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — registrerings-tokenet ble ALDRI limt inn i chatten.**
|
|
|
|
|
|
Bruker hentet det selv fra Forgejo sitt UI (repo → Settings → Actions
|
|
|
|
|
|
→ Runners) og la det i en midlertidig fil med `chmod 600` på serveren;
|
|
|
|
|
|
Claude leste filen direkte via shell, registrerte runneren
|
|
|
|
|
|
(`forgejo-runner register --no-interactive`), og makulerte
|
|
|
|
|
|
tokenfilen umiddelbart etterpå (`shred -u`). Selve
|
|
|
|
|
|
runner-legitimasjonen som oppsto (`.forgejo-runner/.runner` — en ekte,
|
|
|
|
|
|
langlevd hemmelighet, ikke det korte engangs-registreringstokenet) er
|
|
|
|
|
|
gitignored, `chmod 600`, aldri committet — samme disiplin som
|
|
|
|
|
|
`.env`.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — kjøres via `docker-compose.yml`, ikke en løs
|
|
|
|
|
|
`docker run`.** Startet først manuelt for å bevise at det fungerte
|
|
|
|
|
|
(ekte push → "Waiting" → runner registrert → jobb plukket opp →
|
|
|
|
|
|
"Success", verifisert i Forgejo sitt UI), deretter flyttet inn som en
|
|
|
|
|
|
egen `teecup-forgejo-runner`-tjeneste i `docker-compose.yml` for
|
|
|
|
|
|
samme drift-disiplin som resten av stacken (`restart: unless-stopped`,
|
|
|
|
|
|
dokumentert, ikke en engangs-kommando ingen husker senere).
|
|
|
|
|
|
`group_add: "112"` (docker-gruppens GID på DENNE verten) er
|
|
|
|
|
|
nødvendig fordi runner-imagets prosess kjører som ikke-root — uten
|
|
|
|
|
|
den kan den ikke lese den mountede sokkelen selv om den er tilgjengelig.
|
|
|
|
|
|
Vertsspesifikk verdi, sjekk på nytt om dette noensinne flyttes.
|
|
|
|
|
|
|
|
|
|
|
|
**Sikkerhetsnotat, bevisst akseptert:** tilgang til `docker.sock`
|
|
|
|
|
|
tilsvarer root-ekvivalent kontroll over VERTEN (enhver som kan
|
|
|
|
|
|
opprette en container kan mounte hva som helst fra filsystemet).
|
|
|
|
|
|
Dette er en reell heving av angrepsflaten sammenlignet med resten av
|
|
|
|
|
|
stacken, som ingen andre containere her har. Akseptert fordi (a) det
|
|
|
|
|
|
er en direkte konsekvens av arkitekturen `run_backend_tests.sh` allerede
|
|
|
|
|
|
hadde (samme prinsipp, bare automatisert fremfor kjørt manuelt fra en
|
|
|
|
|
|
already-priviligert shell), og (b) runneren kjører KUN denne ene
|
|
|
|
|
|
repoens CI, på en enkeltpersons egen server — ikke en delt/flerbruker-
|
|
|
|
|
|
instans.
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisert:** ekte push → `.forgejo/workflows/backend-tests.yml`
|
|
|
|
|
|
kjørte, viste "Success" i Forgejo sitt UI (grønn hake, samme run som
|
|
|
|
|
|
sto "Waiting" rett før runneren startet). `teecup_db`s rolleoppsett og
|
|
|
|
|
|
fravær av gjenglemte scratch-databaser bekreftet uendret etter kjøring.
|
2026-08-10 22:25:02 +02:00
|
|
|
|
|
2026-08-10 23:19:29 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-060: Optimistisk versjonssjekk for org-turneringers match-scoring — 2026-08-10
|
|
|
|
|
|
|
|
|
|
|
|
Fortsettelse av robusthetslinjen fra ADR-057: bruker ba eksplisitt om å
|
|
|
|
|
|
tette det siste kjente, dokumenterte hullet — org-turneringenes
|
|
|
|
|
|
match-scoring (`hole_score`/`match_hole_result`,
|
|
|
|
|
|
`app/routers/scoring.py`, `session-scorecard.tsx`) hadde INGEN
|
|
|
|
|
|
samtidighetsvern, ren `INSERT ... ON CONFLICT DO UPDATE` siste-skriving-
|
|
|
|
|
|
vinner, dokumentert som bevisst utenfor omfang i både ADR-057 og
|
|
|
|
|
|
ADR-058.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — samme `version`-kolonne-mønster, men UPSERT i stedet
|
|
|
|
|
|
for UPDATE.** Migrasjon 063 legger `version integer NOT NULL DEFAULT 1`
|
|
|
|
|
|
til BÅDE `hole_score` og `match_hole_result` (to separate tabeller, to
|
|
|
|
|
|
separate scoring-modi — `stroke` vs. `hole_result` — se
|
|
|
|
|
|
`session.scoring_mode`). Ulikt round_hole (som alltid har en
|
|
|
|
|
|
forhåndseksisterende rad å oppdatere) skriver disse to endepunktene
|
|
|
|
|
|
via `INSERT ... ON CONFLICT DO UPDATE`, siden FØRSTE registrering av et
|
|
|
|
|
|
hull ikke har noen eksisterende rad. Postgres sin
|
|
|
|
|
|
`DO UPDATE SET ... WHERE <betingelse>` løser dette elegant: betingelsen
|
|
|
|
|
|
sjekkes KUN på selve UPDATE-grenen (konflikt-tilfellet) og ignoreres
|
|
|
|
|
|
fullstendig på ren INSERT — en helt ny rad tar alltid INSERT-veien og
|
|
|
|
|
|
bryr seg aldri om `expected_version`, uansett hva klienten sendte.
|
|
|
|
|
|
Konsekvens: `row is None` etter denne UPSERT-en betyr ALLTID en ekte
|
|
|
|
|
|
versjonskonflikt, ALDRI "finnes ikke" — INSERT-grenen dekker
|
|
|
|
|
|
"finnes ikke"-tilfellet transparent. Enklere enn round_hole/
|
|
|
|
|
|
`update_side_hole`, som trengte en egen eksistenssjekk for å skille de
|
|
|
|
|
|
to.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — offline-kø-kjedingen (`offline-queue.ts`) måtte
|
|
|
|
|
|
generaliseres, ikke bare gjenbrukes.** Reelt funn UNDER design, før
|
|
|
|
|
|
noe ble bygget: ADR-057s versjonskjeding nøklet på `url` alene, riktig
|
|
|
|
|
|
for round_hole (én URL per hull). Men `/hole-scores`/`/hole-results`
|
|
|
|
|
|
bruker SAMME URL for ALLE hull i en match — uten en mer presis nøkkel
|
|
|
|
|
|
ville versjonen returnert fra hull 3 sin skriving blitt brukt som
|
|
|
|
|
|
"forventet versjon" for en påfølgende, urelatert skriving til hull 7 i
|
|
|
|
|
|
samme flush-runde, verre enn ingen kjeding i det hele tatt. Løst med et
|
|
|
|
|
|
nytt, valgfritt `resourceKey`-felt på `QueueEntry`
|
|
|
|
|
|
(default = `url`, så round_hole sin eksisterende bruk er 100 %
|
|
|
|
|
|
bakoverkompatibel, uendret oppførsel) — `session-scorecard.tsx` sender
|
|
|
|
|
|
nå en egen nøkkel per hull+side+enhet (`strokeKey`/`resultKey`).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — autorisasjon i testene, ikke i produksjonskoden.**
|
|
|
|
|
|
`submit_hole_score`/`submit_hole_result` krever at brukeren enten ER en
|
|
|
|
|
|
faktisk match-deltaker (via `player`/`team_roster`/`match_participant`-
|
|
|
|
|
|
kjeden) ELLER org-eier/admin (`is_org_admin`-fallback,
|
|
|
|
|
|
`team_authz.py`). Testene bruker bevisst org-eier-fallbacken for de
|
|
|
|
|
|
fleste tilfellene (langt billigere fixture-oppsett), men bygger den
|
|
|
|
|
|
FULLE deltaker-kjeden for individuell-ball-testen spesifikt, siden
|
|
|
|
|
|
`match_participant_id`-oppslaget i `submit_hole_score` er uavhengig av
|
|
|
|
|
|
org-admin-status og krever en ekte rad.
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisert:** 6 nye pytest-integrasjonstester
|
|
|
|
|
|
(`tests/test_scoring_concurrency.py`) — delt-ball OG individuell-ball-
|
|
|
|
|
|
grenen i `submit_hole_score` hver for seg, `submit_hole_result`, alle
|
|
|
|
|
|
med vellykket skrivning/versjonsøkning, 409 STALE_VERSION ved konflikt,
|
|
|
|
|
|
og bekreftet at en avvist skrivning faktisk IKKE ble lagret. Alle 23
|
|
|
|
|
|
backend-tester (17 fra før + 6 nye) grønne i samme kjøring via CI-
|
2026-08-10 23:44:30 +02:00
|
|
|
|
runneren fra ADR-059. `tsc --noEmit` rent.
|
2026-08-10 23:19:29 +02:00
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** ingen browser-basert to-enhets-verifisering
|
|
|
|
|
|
denne runden (ulikt ADR-057) — pytest-dekningen ble vurdert som
|
|
|
|
|
|
tilstrekkelig gitt at testene kaller de faktiske router-funksjonene
|
|
|
|
|
|
direkte, og frontend-siden (`session-scorecard.tsx`) følger nøyaktig
|
|
|
|
|
|
samme, allerede browser-verifiserte mønster som `round-detail.tsx` fikk
|
|
|
|
|
|
i forrige runde.
|
|
|
|
|
|
|
2026-08-10 23:44:30 +02:00
|
|
|
|
**Rullet ut 2026-08-10**, bruker bekreftet eksplisitt — migrasjon 063
|
|
|
|
|
|
kjørt mot ekte `teecup_db`, `teecup_api`/`teecup_frontend` bygget/
|
|
|
|
|
|
restartet rent, ingen konsollfeil. CI plukket opp pushen automatisk og
|
|
|
|
|
|
kjørte grønt (samme runner som ADR-059).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-061: Frontend-testinfrastruktur (Vitest) — 2026-08-10
|
|
|
|
|
|
|
|
|
|
|
|
Direkte fortsettelse av ADR-058/060, som begge eksplisitt navnga
|
|
|
|
|
|
frontend-tester som bevisst utenfor omfang. Bruker ba om å ta tak i det
|
|
|
|
|
|
som neste steg i robusthetslinjen, nå som backend-testriggen og CI-
|
|
|
|
|
|
runneren stod klare.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — Vitest, ren logikk-testing (`lib/**`), ingen
|
|
|
|
|
|
komponenttester ennå.** Node-miljø, ingen jsdom/@testing-library i
|
|
|
|
|
|
denne runden — det finnes ingen komponenttester å kjøre ennå, og
|
|
|
|
|
|
`node`-miljø er raskere/enklere når det ikke trengs. Tre moduler valgt
|
|
|
|
|
|
etter samme risikobaserte prinsipp som backend-rundene:
|
|
|
|
|
|
- `lib/geo.ts` (`haversineMeters`/`bearingDegrees`) — ekte GPS-
|
|
|
|
|
|
avstandsmåling (ADR-048), fasit-forankret mot kjente geodetiske
|
|
|
|
|
|
verdier (Oslo–Bergen luftlinje, 1 breddegrad ved ekvator ≈ 111,2 km),
|
|
|
|
|
|
ikke bare selv-konsistente tall.
|
|
|
|
|
|
- `lib/offline-queue.ts` — regresjonsvern for `resourceKey`-fiksen fra
|
|
|
|
|
|
ADR-060 selv: egen test beviser eksplisitt at to ulike ressurser som
|
|
|
|
|
|
deler samme URL IKKE lenger blander sammen versjonsnummer, og at
|
|
|
|
|
|
round_hole sin eksisterende bruk (ingen `resourceKey` satt) fortsatt
|
|
|
|
|
|
faller tilbake til url-basert nøkling uendret. `fake-indexeddb` gir en
|
|
|
|
|
|
ekte IndexedDB-implementasjon i test-miljøet — ikke en mocket
|
|
|
|
|
|
tilnærming til køen.
|
|
|
|
|
|
- `lib/ny-runde/formats.ts` — fullstendighets-/konsistenssjekker på
|
|
|
|
|
|
tvers av format-listene (`FORMAT_TO_API`, `FORMAT_MAP`, `API_TO_FORMAT`,
|
|
|
|
|
|
`TWO_SIDED` m.fl.), direkte motivert av et tidligere kjent
|
|
|
|
|
|
ømtåelig-punkt (separate `play_format`-gatingsett i flere
|
|
|
|
|
|
frontend-filer, lett å glemme å oppdatere ett sted når et nytt
|
|
|
|
|
|
format legges til). Testene fanger spesifikt opp det TypeScript IKKE
|
|
|
|
|
|
gjør automatisk: `FORMAT_MAP` bygges med en `as`-cast som omgår
|
|
|
|
|
|
fullstendighetssjekk, og en vanlig array som `FORMATS` har ingen
|
|
|
|
|
|
kompilator-garanti for å dekke alle `FormatId`-verdier.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — pnpm, ikke npm.** Prosjektet bruker pnpm
|
|
|
|
|
|
(`pnpm-lock.yaml`, Dockerfilens `corepack enable && pnpm install`) —
|
|
|
|
|
|
et første forsøk med `npm install` feilet med en intern npm/arborist-
|
|
|
|
|
|
krasj nettopp FORDI `node_modules` allerede var strukturert som et
|
|
|
|
|
|
pnpm-lagret (`.pnpm/`-innhold), ikke fordi noe var korrupt. Ingen skade
|
|
|
|
|
|
skjedd (bekreftet med `pnpm install --frozen-lockfile`, som validerte
|
|
|
|
|
|
hele treet på nytt), men en påminnelse om å sjekke lockfile-typen FØR
|
|
|
|
|
|
man antar npm er riktig verktøy.
|
|
|
|
|
|
|
|
|
|
|
|
**Reelt funn, rettet i samme runde:** `frontend/pnpm-workspace.yaml` var
|
|
|
|
|
|
ignorert i `frontend/.gitignore` (arvet fra den opprinnelige V0-sandbox-
|
|
|
|
|
|
malen, der fila kun inneholdt V0-interne sandbox-detaljer). Filen fikk
|
|
|
|
|
|
nå reelt, deploy-relevant innhold (`allowBuilds: sharp: true`, fra
|
|
|
|
|
|
`pnpm approve-builds` — uten den feiler `pnpm install` i et rent
|
|
|
|
|
|
miljø/CI med `[ERR_PNPM_IGNORED_BUILDS]`). Ignore-regelen fjernet,
|
|
|
|
|
|
fila sporet i git — ellers ville CI feilet på akkurat denne
|
|
|
|
|
|
installasjonen hver eneste gang, usynlig lokalt siden node_modules
|
|
|
|
|
|
allerede var godkjent på denne serveren fra før.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — egen, lettvekts CI-workflow, ikke gjenbruk av backend-
|
|
|
|
|
|
sin.** `.forgejo/workflows/frontend-tests.yml` trigger kun på
|
|
|
|
|
|
`frontend/lib/**`/`package.json`/lockfile-endringer, bruker
|
|
|
|
|
|
`actions/setup-node@v4` (Node 22, matcher Dockerfilen) + `corepack
|
|
|
|
|
|
enable` + `pnpm install --frozen-lockfile` + `pnpm test` — INGEN
|
|
|
|
|
|
Docker-utenfor-Docker, ingen `docker.sock`-tilgang nødvendig, siden
|
|
|
|
|
|
denne suiten ikke rører database eller containere i det hele tatt.
|
|
|
|
|
|
Enklere og tryggere blast-radius enn backend-workflowen.
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisert:** 30 tester, 3 filer, alle grønne lokalt
|
|
|
|
|
|
(`./node_modules/.bin/vitest run`, og via `pnpm test` etter
|
|
|
|
|
|
`allowBuilds`-fiksen). Ingen produksjonskode endret -- ren
|
|
|
|
|
|
testinfrastruktur.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** komponenttester (jsdom/@testing-library),
|
|
|
|
|
|
og en tilsvarende fullstendighetssjekk PÅ TVERS AV `round-detail.tsx`/
|
|
|
|
|
|
`scorecard.tsx`/`leaderboard.tsx` sine egne, separate
|
|
|
|
|
|
`play_format`-gatingsett (kjent ømtålelig punkt, men disse er store
|
|
|
|
|
|
komponentfiler som ville krevd en helt annen testtilnærming enn ren
|
|
|
|
|
|
lib-import — egen, fremtidig vurdering).
|
|
|
|
|
|
|
2026-08-11 08:01:42 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-062: Åtte brukerrapporterte UX-funn — "Ny runde" og score-registrering — 2026-08-11
|
|
|
|
|
|
|
|
|
|
|
|
Bruker sendte åtte konkrete, skjermbilde-dokumenterte problemer fra
|
|
|
|
|
|
faktisk bruk (dashbord, ny-runde-veiviseren, score-registrering, feed).
|
|
|
|
|
|
Delt i to spor etter samme prinsipp som tidligere runder: reelle
|
|
|
|
|
|
logikkfeil rettet direkte, reelt interaksjonsdesign sendt via V0.
|
|
|
|
|
|
|
|
|
|
|
|
**Direkte rettet, ingen V0 (4 av 8):**
|
|
|
|
|
|
1. **`round-card.tsx`: "Hull"-cellen viste alltid PLANLAGT antall hull
|
|
|
|
|
|
(`holes_planned`), aldri antall FAKTISK spilt** — en runde markert
|
|
|
|
|
|
"Fullført" etter f.eks. 9 av 18 hull viste fortsatt "18". Data fantes
|
|
|
|
|
|
allerede (`my_holes_played` sendes for alle runder, uansett status);
|
|
|
|
|
|
selve "X/Y hull spilt"-indikatoren var bare feilaktig gatet til
|
|
|
|
|
|
`status === "active"`. Rettet til `played < round.holes` (viser
|
|
|
|
|
|
avviket uansett status, skjuler seg selv når spilt == planlagt).
|
|
|
|
|
|
2. **Putt-avstand-knappenes etiketter** (`round-detail.tsx`) —
|
|
|
|
|
|
fem av seks brukte "<Xm"-mønster, den sjette ("8m+") brøt mønsteret
|
|
|
|
|
|
med et "+"-suffiks og ble dermed lett oversett. Etikettene endret til
|
|
|
|
|
|
konsekvent "X-Ym"-format (kun `label`, ikke `value`/`PuttBucket`-
|
|
|
|
|
|
kontrakten — ingen backend-endring). Bekreftet via git-historikk at
|
|
|
|
|
|
dette var Claude-forfattet, ikke V0 (brukeren spurte eksplisitt).
|
|
|
|
|
|
3. **"Se feed" på dashbordet** — for anonymt/uspesifikt. Endret til "Se
|
|
|
|
|
|
venneaktivitet", knyttet direkte til seksjonens egen kontekst
|
|
|
|
|
|
("Venner på banen").
|
|
|
|
|
|
4. **@-tagging av medspillere i feeden** — vurdert, IKKE bygget denne
|
|
|
|
|
|
runden (egen, betydelig funksjon: datamodell + søk + rendering).
|
|
|
|
|
|
Anbefalt regel hvis/når den bygges: treff begrenset til faktiske
|
|
|
|
|
|
relasjoner (medspillere/venner, ikke hele brukerbasen — personvern),
|
|
|
|
|
|
og en ekte søkbar nedtrekksliste ved "@" fremfor fritekst-tolkning
|
|
|
|
|
|
(upålitelig med flere like navn).
|
|
|
|
|
|
|
|
|
|
|
|
**Sendt via V0, mottatt som "tee-cup (9).zip", diffet mot live-treet FØR
|
|
|
|
|
|
noe ble tatt inn (samme rutine som alltid — kun `delivery/`-mappen var
|
|
|
|
|
|
den faktiske leveransen, resten var V0s egen sandbox-scaffolding, ikke
|
|
|
|
|
|
tatt inn):**
|
|
|
|
|
|
|
|
|
|
|
|
5. **Ny-runde steg 2: "Antall hull" og "Avanserte handicap-
|
|
|
|
|
|
innstillinger" flyttet FØR det store 18-knappers formatrutenettet**
|
|
|
|
|
|
(var sist, lett oversett bak noe man må skrolle forbi). Ren
|
|
|
|
|
|
rekkefølge-endring, ingen ny funksjonalitet.
|
|
|
|
|
|
6. **Score-registreringens "detaljer"-steg delt i to synlige seksjoner**
|
|
|
|
|
|
("Retning" / "Detaljer per hull") med overskrift+divider, og en ny
|
|
|
|
|
|
`ScrollFade`-hjelpekomponent (bunn-fade + nedoverpil, ResizeObserver-
|
|
|
|
|
|
drevet, skjuler seg selv ved bunn) som løser at brukere ikke visste
|
|
|
|
|
|
de måtte skrolle for å finne Chip/Bunker/Straffeslag/Anywayslag.
|
|
|
|
|
|
7. **Anywayslag endret fra tall-rutenett til +/−-stepper** — samme
|
|
|
|
|
|
inndatamønster som Chip/Bunker/Straffeslag nå (alle fire er
|
|
|
|
|
|
konseptuelt samme type data, brøt tidligere mønster uten grunn).
|
|
|
|
|
|
8. **Netto par-markør på Slag-knappene**: en liten oransje prikk (med
|
|
|
|
|
|
kontrastring, synlig i BÅDE valgt og uvalgt tilstand) på knappen som
|
|
|
|
|
|
tilsvarer spillerens personlige netto par (rå par + `strokesReceived`,
|
|
|
|
|
|
allerede tilgjengelig i komponenten), pluss en tekstlig
|
|
|
|
|
|
"Ditt netto par (N)"-forklaring over selve rutenettet (tilgjengelighet
|
|
|
|
|
|
— ikke avhengig av fargesyn alene). `aria-label` utvidet tilsvarende.
|
|
|
|
|
|
9. **Putter-velgeren fikk egen visuell identitet** (oransje venstre-
|
|
|
|
|
|
aksentkant + flagg-ikon på tittelraden) for å skille den fra Slag-
|
|
|
|
|
|
velgeren, som ellers er nøyaktig samme rutenett-komponent. Selve
|
|
|
|
|
|
tallknappenes etablerte grønne "valgt"-stil er UENDRET.
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisering:** `tsc --noEmit` rent på alle endringer. Full scratch-
|
|
|
|
|
|
stack bygget (scratch-DB med alle 63 migrasjoner, egen scratch-MinIO,
|
|
|
|
|
|
scratch-API- og scratch-frontend-container, ekte produksjonsbuild av
|
|
|
|
|
|
frontend — ikke bare `tsc`). Ekte bruker opprettet og logget inn via
|
|
|
|
|
|
magic-link-bypass (`DEV_LOG_MAGIC_LINKS`), egen bane opprettet via ekte
|
|
|
|
|
|
API-kall (`POST /personal-courses`) med HCP 24 satt på spilleren for å
|
|
|
|
|
|
garantere `strokes_received > 0` på hull 1 — nødvendig for faktisk å se
|
|
|
|
|
|
netto-par-markøren i bruk, ikke bare anta at koden er riktig.
|
|
|
|
|
|
Browserverifisert (Chrome DevTools MCP, mobil viewport 390×844) i BÅDE
|
|
|
|
|
|
lys og mørk modus: ny-runde steg 2 sin nye rekkefølge, seksjonsdeling og
|
|
|
|
|
|
scroll-hint (bekreftet `opacity-100`→`opacity-0` ved faktisk scroll til
|
|
|
|
|
|
bunn, ikke bare visuell antagelse), netto-par-prikk (både alene og
|
|
|
|
|
|
samtidig med "valgt"-tilstand), Putter-aksent, de nye "X-Ym"-putt-
|
|
|
|
|
|
avstand-etikettene. Ingen konsollfeil. Scratch-miljøet ryddet opp
|
|
|
|
|
|
fullstendig (containere, images, database, rolle), `teecup_db`s
|
|
|
|
|
|
rolleoppsett bekreftet uendret.
|
|
|
|
|
|
|
2026-08-11 09:10:17 +02:00
|
|
|
|
**Oppfølging 2026-08-11 — punkt 5 (ChoiceRow) IKKE løst, brukeren
|
|
|
|
|
|
rapporterte tilbake:** "Avstand første putt" sine seks alternativer
|
|
|
|
|
|
viste seg fortsatt som 5 knapper på én rad + 1 alene, ikke ryddig 3+3
|
|
|
|
|
|
som antatt. Root cause: `ChoiceRow` (den delte komponenten, IKKE
|
|
|
|
|
|
`NumberPicker`) brukte `flex flex-wrap` + `flex-1` per knapp, som pakker
|
|
|
|
|
|
så mange knapper som teksten tillater per rad i stedet for et fast
|
|
|
|
|
|
antall — tilfeldigvis akkurat 5+1 for disse seks korte etikettene.
|
|
|
|
|
|
Rettet til `grid grid-cols-3` (samme mønster som `NumberPicker` allerede
|
|
|
|
|
|
brukte). Alle FIRE andre `ChoiceRow`-bruk i filen (Kjønn,
|
|
|
|
|
|
Statistikk-nivå × 2) har nøyaktig 3 valg fra før — visuelt uendret for
|
|
|
|
|
|
dem, kun 6-alternativs-tilfellet endrer seg. Browserverifisert lys+mørk.
|
2026-08-11 14:42:29 +02:00
|
|
|
|
Committet (`17e4793`), deployet 2026-08-11 sammen med tagging-runden
|
|
|
|
|
|
(se ADR-063).
|
|
|
|
|
|
|
|
|
|
|
|
**Oppfølging 2026-08-11 — punkt 6 (ScrollFade) hadde TO reelle feil,
|
|
|
|
|
|
brukeren rapporterte via skjermbilde av Retning-steget:** "Den
|
|
|
|
|
|
sprettende ned-pilen fungerer ikke. Den ligger OPPÅ en annen nedpil."
|
|
|
|
|
|
|
|
|
|
|
|
1. `ResizeObserver` observerte scroll-BEHOLDEREN (`el`), ikke
|
|
|
|
|
|
innholdet. Beholderens boks-størrelse er fast (`flex-1`, bundet av
|
|
|
|
|
|
veiviserens layout) og endrer seg ALDRI når man bytter steg —
|
|
|
|
|
|
observeren fyrte derfor aldri når et steg med reelt overflow ble
|
|
|
|
|
|
vist, og `hasMore` ble stående på sin opprinnelige (ofte `false`)
|
|
|
|
|
|
verdi. Hintet virket rett og slett ikke der det trengtes, nøyaktig
|
|
|
|
|
|
det brukeren rapporterte. Rettet ved å observere en egen
|
|
|
|
|
|
`contentRef`-div rundt `children` i stedet for beholderen selv.
|
|
|
|
|
|
2. Fade-/piloverlayet er absolutt posisjonert over de siste 64px av
|
|
|
|
|
|
scroll-området uten noen garanti om at ekte innhold ikke havner
|
|
|
|
|
|
der. På Retning-steget (med fullt kølle-utvalg satt på testbrukeren
|
|
|
|
|
|
for å reprodusere brukerens eksakte rutenett) landet nettopp
|
|
|
|
|
|
"Kort"-knappens eget `ArrowDown`-ikon i den sonen — to piler oppå
|
|
|
|
|
|
hverandre, akkurat som beskrevet. Rettet med en usynlig 64px-buffer
|
|
|
|
|
|
(`SCROLL_FADE_HEIGHT_PX`) etter innholdet; `hasMore`-utregningen
|
|
|
|
|
|
trekker fra samme høyde så bufferen selv aldri gir et falskt
|
|
|
|
|
|
positivt hint på kort innhold.
|
|
|
|
|
|
|
|
|
|
|
|
Browserverifisert (samme reproduksjon: full kølle-liste for å tvinge
|
|
|
|
|
|
frem overflow): hintet vises korrekt (`opacity: 1`) når steget faktisk
|
|
|
|
|
|
overflower, forsvinner korrekt (`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. Committet
|
|
|
|
|
|
(`ee91d03`).
|
2026-08-11 09:10:17 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-063: @-tagging av medspillere/venner i runde-feeden — 2026-08-11
|
|
|
|
|
|
|
|
|
|
|
|
Reist av bruker som en av åtte UX-funn (ADR-062, punkt 7), bevisst
|
|
|
|
|
|
utsatt der og bakt inn i FEATURE_BACKLOG.md som egen sak. Bruker ba om å
|
|
|
|
|
|
sette den i gang som egen runde.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — kun FAKTISKE relasjoner er taggbare, aldri fritekstsøk
|
|
|
|
|
|
i hele brukerbasen** (personvernbegrunnelsen fra FEATURE_BACKLOG-notatet
|
|
|
|
|
|
fulgt direkte). `_taggable_candidates()` (ny hjelpefunksjon,
|
|
|
|
|
|
`round_messages.py`) returnerer UNION av rundens lenkede deltakere og
|
|
|
|
|
|
avsenderens egne venner (`friendship`, status='accepted', begge
|
|
|
|
|
|
retninger) — ALDRI et generelt personsøk. Håndhevet BÅDE i det nye
|
|
|
|
|
|
søkeendepunktet (`GET /rounds/{id}/taggable-people`) OG server-side ved
|
|
|
|
|
|
innsending (klientens forslag stoles aldri blindt på).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — offset-basert tag-span, ikke innebygd tekst-syntaks.**
|
|
|
|
|
|
`round_message_tag`/`round_message_comment_tag` (migrasjon 064) lagrer
|
|
|
|
|
|
`start_index`/`end_index` inn i `body`-teksten SLIK DEN BLE SKREVET, ikke
|
|
|
|
|
|
en `@[navn](id)`-markdown-lignende syntaks i selve teksten. Trygt fordi
|
|
|
|
|
|
verken `round_message` eller `round_message_comment` NOENSINNE kan
|
|
|
|
|
|
redigeres i etterkant (kun slettes, se 058/059) — offsettene kan derfor
|
|
|
|
|
|
aldri gå ut av synk med teksten. Den viste teksten forblir alltid
|
|
|
|
|
|
NØYAKTIG det avsenderen skrev; kun lenkemålet (bruker-ID) løses opp mot
|
|
|
|
|
|
NÅVÆRENDE navn ved lesing, ikke frosset — en tag skal peke på personen,
|
|
|
|
|
|
ikke et navn-øyeblikksbilde (bevisst forskjell fra `author_display_name`,
|
|
|
|
|
|
som ER frosset).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — ugyldige tags forkastes stille, hele innlegget avvises
|
|
|
|
|
|
aldri.** En tag mot noen som ikke lenger er taggbar (f.eks. sluttet å
|
|
|
|
|
|
være medspiller mellom autocomplete og innsending), eller med en offset
|
|
|
|
|
|
utenfor tekstens lengde, filtreres bare bort (`_validate_and_prepare_tags`)
|
|
|
|
|
|
— resten av innlegget/kommentaren postes uendret. Unngår at en
|
|
|
|
|
|
kapp-løpstilstand på klientsiden blokkerer en ellers gyldig melding.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — varsling gjenbruker eksisterende `type="round"`,
|
|
|
|
|
|
ingen ny notification-kategori.** `notification.type` er begrenset til
|
|
|
|
|
|
fire faste verdier (`friend`/`tournament`/`round`/`result`, migrasjon
|
|
|
|
|
|
026/033, koblet til brukerens egne e-post-preferanser per kategori) — en
|
|
|
|
|
|
ny femte kategori ville krevd UI-endring i varslingsinnstillingene også,
|
|
|
|
|
|
utenfor denne rundens omfang. `"{fullt navn} tagget deg i et innlegg."`
|
|
|
|
|
|
sendes til hver gyldig tagget person (aldri til seg selv), samme
|
|
|
|
|
|
`create_notification()`-vei (in-app + push + evt. e-post) som alt annet
|
|
|
|
|
|
rundevarsel.
|
|
|
|
|
|
|
|
|
|
|
|
**To parallelle tabeller, ikke én delt polymorf** (`round_message_tag`/
|
|
|
|
|
|
`round_message_comment_tag`) — samme konvensjon som
|
|
|
|
|
|
`round_message_reaction` vs. `message_reaction` (migrasjon 059).
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisert:** 8 nye pytest-tester (`tests/test_message_tags.py`) —
|
|
|
|
|
|
kandidatlisten inkluderer venn og rundedeltaker, ekskluderer fremmede og
|
|
|
|
|
|
seg selv, søkefiltrering, gyldig tag på både innlegg og kommentar (med
|
|
|
|
|
|
varsel bekreftet skrevet til `notification`-tabellen), ugyldig tag
|
|
|
|
|
|
(fremmed / utenfor tekstlengde / seg selv) forkastes stille UTEN å
|
|
|
|
|
|
avvise selve meldingen. Alle 31 backend-tester (23 fra før + 8 nye)
|
|
|
|
|
|
grønne i samme kjøring. `teecup_db` bekreftet uendret.
|
|
|
|
|
|
|
2026-08-11 10:16:08 +02:00
|
|
|
|
**Frontend — oppdatering samme dag:** opprinnelig sendt som eget
|
|
|
|
|
|
V0-prompt (se over), men bruker gikk tom for V0-credits og ba Claude
|
|
|
|
|
|
implementere promptet selv i stedet. `lib/mentions.ts` (ren offset-/
|
|
|
|
|
|
diff-logikk, 15 enhetstester) + `components/mention-input.tsx`
|
|
|
|
|
|
(`MentionTextarea`-autocomplete + `TaggedText`-rendring), koblet inn i
|
|
|
|
|
|
`round-messages.tsx` (innlegg), `post-engagement.tsx` (kommentarer —
|
|
|
|
|
|
delt av flere meldingssystemer, `roundId`/`tags` gjort valgfrie for å
|
|
|
|
|
|
ikke bryte de org-scopede callerne) og `feed.tsx` (aggregert
|
|
|
|
|
|
`/my-feed`-visning — der backendens `GET /feed` manglet `tags` ble
|
|
|
|
|
|
også lagt til her, oppdaget under dette arbeidet). Scratch-verifisert
|
|
|
|
|
|
i nettleser (autocomplete, tagging i innlegg og kommentar, rendring som
|
|
|
|
|
|
lenke både i rundevisning og i feed, varsel bekreftet, personvern-
|
|
|
|
|
|
scoping bekreftet i UI også, lys+mørk). `tsc --noEmit` rent, 45/45
|
|
|
|
|
|
vitest grønt. Se CHANGELOG.md.
|
|
|
|
|
|
|
|
|
|
|
|
Org-turneringenes tilsvarende "Banter Board" (`message`/
|
|
|
|
|
|
`message_comment`) fikk IKKE samme funksjon — helt separat system,
|
|
|
|
|
|
ikke nevnt i den opprinnelige forespørselen, fortsatt bevisst utenfor
|
|
|
|
|
|
omfang.
|
2026-08-11 09:10:17 +02:00
|
|
|
|
|
2026-08-12 21:39:13 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ADR-064: GolfAPI.io som tredje banekilde — avstand til mål (rangefinder) — 2026-08-12
|
|
|
|
|
|
|
|
|
|
|
|
Brukeren fikk et GolfAPI.io-token (20 API-kall) og ba om avstandsmåling
|
|
|
|
|
|
til grønn (front/midt/bak) og hindringer — notert som en kjent, ikke-
|
|
|
|
|
|
designet ambisjon siden 2026-07-22 (se FEATURE_BACKLOG.md), atskilt fra
|
|
|
|
|
|
den allerede byggede slag-for-slag GPS-avstandsmålingen (ADR-048, som
|
|
|
|
|
|
måler AD HOC-punkter brukeren selv velger, ikke faste banepunkter).
|
|
|
|
|
|
Startbane: **Tjøme Golfklubb** — bekreftet av bruker at den IKKE finnes
|
|
|
|
|
|
i TeeOff, som gjør dette til det generelle "bane utenfor TeeOffs
|
|
|
|
|
|
dekning"-tilfellet FEATURE_BACKLOG.md (tillegg 2026-08-03) allerede
|
|
|
|
|
|
pekte ut som naturlig neste steg, ikke et spesialtilfelle.
|
|
|
|
|
|
|
|
|
|
|
|
**Live validert FØR noe ble bygget** (ekte GolfAPI-kall, ikke antatt fra
|
|
|
|
|
|
research alene): søk (`GET /clubs?name=Tjøme`), fullt scorekort
|
|
|
|
|
|
(`GET /courses/{id}` — par/SI per hull, 4 tees med per-hull-lengde i
|
|
|
|
|
|
meter, course rating/slope inkl. front9/back9), og koordinater
|
|
|
|
|
|
(`GET /coordinates/{id}` — 167 punkter for Tjøme: grønn front/midt/bak
|
|
|
|
|
|
per hull, bunkere, vann, avstandsmarkører, tee-punkter). Kostnad
|
|
|
|
|
|
bekreftet: søk 0,1 kall, full henting (course + coordinates) 2 kall —
|
|
|
|
|
|
matcher brukerens egen "20 kall ≈ 10 baner"-anslag.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning A — ett delt, globalt cache-lag (`golfapi_course`/
|
|
|
|
|
|
`_hole`/`_tee`/`_coordinate`, migrasjon 065), ALDRI live oppslag.**
|
|
|
|
|
|
Motsatt av teeoffs policy (fritt, offentlig API, live oppslag er
|
|
|
|
|
|
kostnadsfritt) — GolfAPI-kall koster ekte, sterkt begrenset budsjett.
|
|
|
|
|
|
GolfAPIs avtale tillater eksplisitt permanent caching ("no need to call
|
|
|
|
|
|
the API to fetch the same course multiple times"), så cachen hentes KUN
|
|
|
|
|
|
én gang NOENSINNE per fysisk bane, uansett hvor mange organisasjoner/
|
|
|
|
|
|
brukere som senere kobler til den samme banen — håndhevet av én sentral
|
|
|
|
|
|
chokepoint-funksjon, `golfapi_cache.get_or_fetch_golfapi_course()`, som
|
|
|
|
|
|
ALL import-kode må gå via (aldri et direkte `golfapi_client`-kall fra et
|
|
|
|
|
|
endepunkt). Speiler ADR-019s "import ved eksplisitt valg, ikke live
|
|
|
|
|
|
oppslag"-filosofi, men med en enda strengere begrunnelse (penger, ikke
|
|
|
|
|
|
bare transaksjonssikkerhet/oppetid).
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning B — kopier inn i BEGGE eksisterende banemodeller, ikke en
|
|
|
|
|
|
tredje.** Bekreftet med bruker (AskUserQuestion): GolfAPI-importerte
|
|
|
|
|
|
baner skal kunne brukes i BÅDE org-turneringer OG frittstående runder.
|
|
|
|
|
|
- `course` (org-scopet): `course_source`-enum utvidet med
|
|
|
|
|
|
`'international'`; `external_course_ref` lagrer GolfAPIs `courseID`
|
|
|
|
|
|
direkte (samme kolonne teeoff allerede bruker til
|
|
|
|
|
|
`facility_slug:course_id`). Nye endepunkt `international-search`/
|
|
|
|
|
|
`international-import` i `courses.py`, speiler `official-search`/
|
|
|
|
|
|
`official-import` (ADR-019) strukturelt og validerings-messig
|
|
|
|
|
|
("fail loudly" på ufullstendige data, aldri en delvis import).
|
|
|
|
|
|
- `personal_course` (global, ADR-042): ny nullable
|
|
|
|
|
|
`external_golfapi_course_id`-kolonne + delvis unik indeks (samme
|
|
|
|
|
|
mønster som `course`s migrasjon 010). Speilende endepunkter i
|
|
|
|
|
|
`rounds.py`. `round.course_source` trenger INGEN endring — en
|
|
|
|
|
|
GolfAPI-importert bane blir en helt vanlig `personal_course`-rad
|
|
|
|
|
|
(`course_source='custom'`), kun med proveniens-kolonnen satt.
|
|
|
|
|
|
- Begge kopier-inn-stegene leser FRA det samme delte cache-laget —
|
|
|
|
|
|
importeres Tjøme via en turnering OG senere via en frittstående runde,
|
|
|
|
|
|
koster det GolfAPI 0 ekstra kall andre gang, uansett rekkefølge.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning C — koordinatene kopieres ALDRI inn per import.** Ulikt
|
|
|
|
|
|
par/rating (som MÅ fryses per import for reproduserbare HCP-resultater,
|
|
|
|
|
|
ADR-007/ADR-019s prinsipp), er koordinatene ikke en del av HCP-
|
|
|
|
|
|
beregningen i det hele tatt. Rangefinder-oppslag (nytt endepunkt
|
|
|
|
|
|
`GET /rounds/{id}/holes/{n}/target-points`) leser derfor alltid DIREKTE
|
|
|
|
|
|
fra den delte `golfapi_course_coordinate`-cachen via banens lagrede
|
|
|
|
|
|
GolfAPI-courseID — én fysisk bane har ett koordinatsett, uansett hvor
|
|
|
|
|
|
mange ganger banen er importert til ulike organisasjoner/brukere.
|
|
|
|
|
|
Endepunktet returnerer RÅ punkter (lat/lng), ALDRI en ferdigregnet
|
|
|
|
|
|
avstand — en forhåndsberegnet avstand ville vært utdatert i det
|
|
|
|
|
|
øyeblikket spilleren beveger seg. Klienten Haversine-regner selv
|
|
|
|
|
|
(`frontend/lib/geo.ts`, samme rene funksjon som ADR-048 allerede bygget)
|
|
|
|
|
|
mot spillerens EGEN, ferske GPS-posisjon.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning D — v1-visning: ren tall-/tekstvisning, intet kart.**
|
|
|
|
|
|
Bekreftet med bruker (AskUserQuestion) — "142 m front / 151 m midt /
|
|
|
|
|
|
163 m bak" pluss evt. nærmeste hindring, ingen Mapbox-kartlasting i det
|
|
|
|
|
|
hele tatt. Null ekstra driftskostnad utover selve GolfAPI-importen,
|
|
|
|
|
|
matcher Claude sin egen tidligere loggførte anbefaling
|
|
|
|
|
|
(FEATURE_BACKLOG.md, 2026-08-06) og ADR-048s etablerte kostnadskontroll-
|
|
|
|
|
|
filosofi (kart lastes kun når det er strengt nødvendig). Brukeren
|
|
|
|
|
|
foreslo selv en fremtidig **freemium-idé** (gratis = tall, betalt =
|
|
|
|
|
|
interaktivt satellittkart/"Plays Like"-vind-/høydejustering/flyover,
|
|
|
|
|
|
etter mønster fra etablerte konkurrenter som Hole19/18Birdies) —
|
|
|
|
|
|
BEVISST NOTERT, IKKE BYGGET: appen har ingen betalingsinfrastruktur i
|
|
|
|
|
|
dag, og v1-beslutningen (tall, ikke kart) er uansett riktig uavhengig av
|
|
|
|
|
|
om et fremtidig betalt lag legges til senere.
|
|
|
|
|
|
|
|
|
|
|
|
**Beslutning E — datamodell for koordinatene speiler GolfAPIs egen POI-
|
|
|
|
|
|
struktur, men normalisert til lesbar tekst.** `poi_type` (grønn/bunker/
|
|
|
|
|
|
vann/trær/avstandsmarkør/dogleg/vei/tee-punkt), `location`
|
|
|
|
|
|
(front/midt/bak — IKKE navngitt "green_location": Tjømes bunkere hadde
|
|
|
|
|
|
også front/bak-punkter, feltet er generisk for ethvert POI, ikke kun
|
|
|
|
|
|
grønn) og `side_fairway` (venstre/senter/høyre) er tekst+CHECK, ikke
|
|
|
|
|
|
egne enum-typer — samme "kan utvides uten `ALTER TYPE`"-begrunnelse som
|
|
|
|
|
|
`session.format` allerede bruker (001).
|
|
|
|
|
|
|
|
|
|
|
|
**Budsjettdisiplin:** import er ALLTID en eksplisitt, synlig
|
|
|
|
|
|
brukerhandling (et "Hent bane fra GolfAPI"-søk + valg), ALDRI
|
|
|
|
|
|
automatisk/implisitt — med kun ~15-16 kall igjen etter denne rundens
|
|
|
|
|
|
utvikling+verifisering (2,3 til research/validering + 2 til det siste
|
|
|
|
|
|
bevisste, ekte E2E-kallet), er dette en reell begrensning appen må
|
|
|
|
|
|
respektere fremover, ikke en teoretisk bekymring.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor denne runden:** rangefinder-VISNINGEN i selve
|
|
|
|
|
|
scorekortet (`round-detail.tsx` sin hull-header har allerede reservert
|
|
|
|
|
|
plass ved GIR-merket siden 2026-07-25) er ikke koblet inn ennå — venter
|
|
|
|
|
|
på en Claude-skrevet V0-prompt (samme arbeidsdeling som ADR-048s
|
|
|
|
|
|
`ShotMeasurementSheet`), ikke bygget håndkodet. Org-siden fikk full
|
|
|
|
|
|
import-støtte (`international-search`/`-import`), men INGEN tilsvarende
|
|
|
|
|
|
rangefinder-visning i turneringens scoreførings-UI ennå — ingen
|
|
|
|
|
|
reservert plass finnes der i dag, egen, senere vurdering når/hvis
|
|
|
|
|
|
etterspurt. GolfAPI som generell fallback for ALLE norske søk (ikke
|
|
|
|
|
|
bare når TeeOff mangler banen) er en bevisst IKKE-endring — Tjøme er
|
|
|
|
|
|
eksempelet som utløste dette, ikke en politikkendring for Norge
|
|
|
|
|
|
generelt.
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisert:** migrasjon 065 kjørt mot en automatisert scratch-database
|
|
|
|
|
|
(`scripts/run_backend_tests.sh`-mønsteret) — alle 31 eksisterende
|
|
|
|
|
|
backend-tester fortsatt grønne (ingen regresjon). Egen scratch-runde
|
|
|
|
|
|
(fixture-basert, monkeypatchet `golfapi_client` med de FAKTISKE Tjøme-
|
|
|
|
|
|
svarene fra research-fasen, for å unngå å bruke flere av de knappe
|
|
|
|
|
|
API-kallene under iterasjon) bekreftet: cache-chokepoktet henter GolfAPI
|
|
|
|
|
|
nøyaktig én gang og ALDRI på nytt ved gjentatte kall (idempotens), 18
|
|
|
|
|
|
hull + 4 tees + 167 koordinater cachet korrekt, org-import oppretter en
|
|
|
|
|
|
`course`-rad med `source='international'` + riktig `hole`/`tee_rating`-
|
|
|
|
|
|
antall (18 hull, 8 tee_rating-rader — 4 tees × 2 kjønn), duplikat-import
|
|
|
|
|
|
avvist av unik indeks i BEGGE modeller (`course_org_external_ref_unique`
|
|
|
|
|
|
fra migrasjon 010, ny `personal_course_golfapi_ref_unique`), og
|
|
|
|
|
|
target-points-oppslaget returnerer korrekte front/midt/bak-punkter for
|
|
|
|
|
|
et gitt hull. **Ett bevisst, siste ekte kall** mot GolfAPI (course +
|
|
|
|
|
|
coordinates, 2 kall) bekreftet at hele kjeden — `golfapi_client.py`s
|
|
|
|
|
|
faktiske HTTP-kall inkludert, ikke bare cache-logikken — fungerer mot
|
|
|
|
|
|
den virkelige tjenesten, ikke bare mot fixtures. `teecup_db` bekreftet
|
|
|
|
|
|
uendret gjennom hele verifiseringen (`\l`-sjekk før/etter, scratch-
|
|
|
|
|
|
database+rolle droppet). Frontend: `tsc --noEmit` rent, 45/45 vitest.
|
|
|
|
|
|
**Tillegg samme dag — rangefinder-visningen koblet inn og rullet ut.**
|
|
|
|
|
|
Brukeren kjørte V0-prompten (zip 10), resultatet
|
|
|
|
|
|
(`components/target-distance.tsx`) matchet spesifikasjonen nøyaktig.
|
|
|
|
|
|
Claude koblet den inn via en ny `components/hole-target-distance.tsx`
|
|
|
|
|
|
(GPS/fetch/Haversine-logikken, samme `watchPosition`-mønster som
|
|
|
|
|
|
ADR-048), montert i `ScoringWizard` sin hull-header
|
|
|
|
|
|
(`round-detail.tsx`). Fant og fikset en reell driftsfeil under
|
|
|
|
|
|
utrullingen: `TEECUP_GOLFAPI_TOKEN` var satt i `.env`, men
|
|
|
|
|
|
`docker-compose.yml` sin `teecup_api`-tjeneste videreførte den aldri
|
|
|
|
|
|
til containeren. Full-stack scratch-verifisert (ekte innlogging, ekte
|
|
|
|
|
|
import via det virkelige endepunktet -- cache-treff, 0 nye API-kall --
|
|
|
|
|
|
ekte runde, nettleserverifisert lys+mørk med geolocation-emulering:
|
|
|
|
|
|
front/midt/bak-avstander og nærmeste hindring rendret korrekt, ingen
|
|
|
|
|
|
konsollfeil). **Rullet ut 2026-08-12**, bruker bekreftet. Migrasjon 065
|
|
|
|
|
|
kjørt mot ekte `teecup_db`, `docker compose build && up -d` kjørt (to
|
|
|
|
|
|
ganger denne dagen -- brukeren stanset bevisst mellom første og andre
|
|
|
|
|
|
gang for å laste opp V0-eksporten). Se CHANGELOG.md punkt 77 for full
|
|
|
|
|
|
verifiseringsdetalj.
|
|
|
|
|
|
|
2026-08-13 07:39:29 +02:00
|
|
|
|
**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, funksjonen kunne sees — det viste seg at
|
|
|
|
|
|
2026-08-12-rundens søk-UI kun var koblet inn i "endre bane"-skjemaet
|
|
|
|
|
|
(`round-detail.tsx`), ikke i den faktiske `/my-rounds/new`-veiviseren
|
|
|
|
|
|
(`components/ny-runde/`, en helt separat komponenttre). Lagt til der.
|
|
|
|
|
|
Under verifisering av DENNE tilkoblingen (ikke forrige runde, som kun
|
|
|
|
|
|
reelt HTTP-testet POST-import, aldri GET-søket) ble en ekte 500-feil
|
|
|
|
|
|
funnet: `GET /personal-courses/{personal_course_id}` (eksisterende,
|
|
|
|
|
|
generisk) var registrert FØR den nye, mer spesifikke
|
|
|
|
|
|
`GET /personal-courses/international-search` — FastAPI matcher ruter i
|
|
|
|
|
|
registreringsrekkefølge, så den generiske ruten fanget grådig opp
|
|
|
|
|
|
"international-search" som en ugyldig UUID. Lærdom: en ny literal-path-
|
|
|
|
|
|
rute som deler prefiks med en eksisterende parameterisert rute MÅ
|
|
|
|
|
|
registreres FØR den, ikke etter — nå kommentert direkte i koden
|
|
|
|
|
|
(`rounds.py`) for å unngå gjentakelse. Org-siden (`courses.py`) hadde
|
|
|
|
|
|
aldri dette problemet (ingen bar `/courses/{course_id}`-rute der).
|
|
|
|
|
|
Full scratch-verifisert på nytt (ekte klikk-gjennom i selve "Ny
|
|
|
|
|
|
runde"-veiviseren, ingen konsollfeil, søket bekreftet returnere reelle
|
|
|
|
|
|
treff). **Rullet ut 2026-08-13.** Se CHANGELOG.md punkt 77 (tillegg)
|
|
|
|
|
|
for full detalj.
|
|
|
|
|
|
|
2026-08-13 21:24:25 +02:00
|
|
|
|
**Tillegg 2026-08-13 — to nye baner (Larvik/Seasidebanen, Nesbyen/
|
|
|
|
|
|
Nesfjellet) + en reell GolfAPI-datakvalitetsbug funnet under importen.**
|
|
|
|
|
|
Nesbyen har `hasGPS=0` hos GolfAPI (ingen koordinater), så koordinatene
|
|
|
|
|
|
ble registrert MANUELT av bruker (124 punkter, feltbefart) og satt inn
|
|
|
|
|
|
direkte i den samme delte `golfapi_course_coordinate`-cachen som
|
|
|
|
|
|
Beslutning C beskriver — skjemaet er kilde-agnostisk, rangefinderen
|
|
|
|
|
|
fungerer identisk uansett om et punkt kom fra API-et eller ble hånd-
|
|
|
|
|
|
registrert. To nye `poi_type`-verdier lagt til (migrasjon 067): `rock`
|
|
|
|
|
|
(fjellknaus — regnes som hinder) og `layup` (til fairway — kun
|
|
|
|
|
|
informativt, ikke et hinder).
|
|
|
|
|
|
|
|
|
|
|
|
Under selve importen krasjet koden to ganger på et reelt GolfAPI-funn:
|
|
|
|
|
|
tjenesten returnerer noen ganger en TOM STRENG (`""`) i stedet for
|
|
|
|
|
|
`null`/fraværende felt for `courseRatingMen`/`slopeMen`/
|
|
|
|
|
|
`courseRatingWomen`/`slopeWomen` — ikke bare for kvinner, som ADR-019s
|
|
|
|
|
|
tilsvarende teeoff-antakelse forutsatte, Nesbyen manglet `slopeMen`
|
|
|
|
|
|
også. Fikset på tre nivåer: (1) `golfapi_cache.py` normaliserer nå
|
|
|
|
|
|
`""` → `None` for alle fire ratingfelt FØR insert, (2)
|
|
|
|
|
|
`golfapi_course_tee.course_rating_men`/`slope_men` gjort nullable
|
|
|
|
|
|
(migrasjon 068 — var feilaktig `NOT NULL`), (3) begge import-
|
|
|
|
|
|
endepunktene (`courses.py`s `international-import`, `rounds.py`s
|
|
|
|
|
|
personal-course-ekvivalent) bygger nå rating per utslag defensivt —
|
|
|
|
|
|
hopper over et utslag HELT (ingen `tee`/`personal_course_tee`-rad) hvis
|
|
|
|
|
|
verken herre- eller dame-rating er brukbar, og avviser HELE importen
|
|
|
|
|
|
med `EXTERNAL_DATA_INCOMPLETE` FØR transaksjonen starter hvis
|
|
|
|
|
|
bokstavelig talt intet utslag har noen brukbar rating (fail-fast,
|
|
|
|
|
|
samme ADR-019 Beslutning C-filosofi). Nesbyens 6 utslag hadde faktisk
|
|
|
|
|
|
INGEN rating i det hele tatt hos GolfAPI (ikke bare tomme strenger på
|
|
|
|
|
|
enkeltfelt) — brukeren oppga alle 6 utslags course rating/slope
|
|
|
|
|
|
(herre+dame, ett utslag uten damerating) manuelt fra klubbens eget
|
|
|
|
|
|
scorekort, satt inn direkte i `golfapi_course_tee`-cachen før import.
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisert:** `python3 -m py_compile` rent på alle tre endrede
|
|
|
|
|
|
Python-filer, full `./scripts/run_backend_tests.sh` (31/31, migrasjon
|
|
|
|
|
|
068 bekreftet anvendbar). Selve importen kjørt direkte mot ekte
|
|
|
|
|
|
`teecup_db` via et engangsskript i `teecup_api`-containeren (speiler
|
|
|
|
|
|
`import_international_personal_course`s kopier-inn-logikk nøyaktig) —
|
|
|
|
|
|
etterpå bekreftet med en 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** — migrasjon 067+068 kjørt mot ekte `teecup_db`,
|
|
|
|
|
|
`teecup_api` bygget/omstartet, ren oppstartslogg.
|
|
|
|
|
|
|
2026-07-16 07:18:01 +02:00
|
|
|
|
Disse må avklares før eller under de relevante fasene:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Sesjons-secret:** TeeOff lar `PUBLIC_SESSION_SECRET` falle tilbake på
|
|
|
|
|
|
`JWT_SECRET`. TeeCup bør bruke separate, uavhengige secrets. *(Sikkerhet)*
|
|
|
|
|
|
2. **Cache over flere prosesser:** In-memory `dict`-cache på `app.state` deles ikke
|
|
|
|
|
|
mellom flere workers/containere. Ved skalering trengs Redis. *(Skalering)*
|
|
|
|
|
|
3. **Prising:** Per organisasjon (abonnement) eller per turnering? Påvirker ikke
|
|
|
|
|
|
isolasjonsmodellen, men påvirker fakturerings-/kvotemodell.
|
2026-07-29 22:20:50 +02:00
|
|
|
|
4. **Scramble-grensesnitt — ✅ HELT FERDIG 2026-07-29.** Arkitekturen skal
|
|
|
|
|
|
ta høyde for formatet; eksakt UI-løsning spesifiseres senere.
|
|
|
|
|
|
**Utvidet 2026-07-25, DELVIS AVKLART OG BYGGET 2026-07-28:** brukeren
|
|
|
|
|
|
ba om statistikk over hvor mange utslag hver spiller har hatt (dvs.
|
|
|
|
|
|
hvor mange ganger spillerens drive ble valgt). Bygget for FRITTSTÅENDE
|
|
|
|
|
|
RUNDER (ADR-039 sitt delt-ball-format, `round_hole.selected_
|
|
|
|
|
|
participant_id`, migrasjon 036) — se FEATURE_BACKLOG.md ("Scramble:
|
|
|
|
|
|
statistikk over utslag brukt per spiller") for full detalj.
|
|
|
|
|
|
**Fullført 2026-07-29:** samme funksjon bygget for org-scopede
|
|
|
|
|
|
turnering-scramble/-greensome også (`hole_score.selected_
|
2026-08-03 06:24:57 +02:00
|
|
|
|
participant_id`, migrasjon 039) — samme mønster, se CHANGELOG.md
|
2026-07-29 22:20:50 +02:00
|
|
|
|
2026-07-29 for full detalj (validering, delt `submitStroke`-utvidelse,
|
|
|
|
|
|
ny `SelectedDriverSummary` i `session-scorecard.tsx`). Begge domener
|
|
|
|
|
|
dekket, ingen kjent gjenstående forskjell mellom scramble og greensome.
|
2026-07-16 07:18:01 +02:00
|
|
|
|
5. **Individuell-vs-delt-ball i `hole_score`:** Håndheves i app-laget, ikke av
|
|
|
|
|
|
databasen (CHECK når ikke opp til `session.format`). Motoren/API-et må passe
|
|
|
|
|
|
på at f.eks. et foursome ikke får per-spiller-scorer.
|
2026-07-26 07:54:00 +02:00
|
|
|
|
6. **Individuelle turneringer, flerrunde-turneringer og Order of Merit**
|
|
|
|
|
|
(reist 2026-07-26). **Grunnstruktur for de to første AVKLART samme
|
|
|
|
|
|
dag, se ADR-037:** ny, parallell org-scopet datamodell (IKKE en
|
|
|
|
|
|
utvidelse av ADR-033s `round`-tabeller), samme `tournament`-tabell
|
|
|
|
|
|
med ny `format_type`-diskriminator, flerrunde via ny
|
|
|
|
|
|
`tournament_round`-tabell, rå slag lagres + poeng caches per format
|
|
|
|
|
|
(samme mønster som match-play). **Fortsatt åpent:** de fem konkrete
|
|
|
|
|
|
formatenes egne poengregler (Københavner m.fl.), og Order of Merit
|
|
|
|
|
|
(sesong-sammenlagt på tvers av flere turneringer) — bekreftet som et
|
|
|
|
|
|
beslektet, men SISTE steg, ikke designet ennå. Ingen migrasjon
|
|
|
|
|
|
skrevet — ADR-037 er ren struktur-beslutning.
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
7. **Frittstående runder: ekte spillformer** (slagspill/match/skins/
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
|
par-lag-konkurranse, reist 2026-07-28) — **AVKLART, BYGGET (backend +
|
|
|
|
|
|
frontend) OG LIVE SAMME DAG, se ADR-039.** `round.play_format` styrer
|
|
|
|
|
|
nå faktisk både HCP-beregning (via en portert versjon av turnering-
|
|
|
|
|
|
motoren) og et `format-result`-lese-endepunkt for scorekort-
|
|
|
|
|
|
presentasjon, med tilhørende UI for sideoppsett/skins-konfig/
|
|
|
|
|
|
matchstatus-visning — se FEATURE_BACKLOG.md/CLAUDE.md for full detalj
|
|
|
|
|
|
og etterfølgende samme-dags oppfølgingsrunder. "Flere flighter i én
|
|
|
|
|
|
frittstående runde" (egen seksjon i FEATURE_BACKLOG.md) forblir et
|
|
|
|
|
|
adskilt, ikke-relatert spørsmål (grupperer RUNDER, ikke deltakere
|
|
|
|
|
|
innad i én runde).
|
2026-07-16 07:18:01 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-08-13 21:24:25 +02:00
|
|
|
|
## ADR-065: Per-deltaker-fullføring av frittstående runder + scorekort på e-post — 2026-08-13
|
|
|
|
|
|
|
|
|
|
|
|
Brukerens konkrete scenario: fire spillere sammen i én runde, to går 18
|
|
|
|
|
|
hull, én går 9, én må avslutte etter 14. Med dagens modell
|
|
|
|
|
|
(`round.completed_at`, rundeomfattende) kan runden ikke fullføres for
|
|
|
|
|
|
de ferdige spillerne uten samtidig å låse scoreføring for de som
|
|
|
|
|
|
fortsatt spiller (`readOnly = completed` var alt-eller-ingenting) — og
|
|
|
|
|
|
ingen kunne få scorekortet sitt på e-post før HELE runden var ferdig.
|
|
|
|
|
|
|
|
|
|
|
|
Bekreftet med bruker (AskUserQuestion) og ett tillegg i etterkant:
|
|
|
|
|
|
1. **Rundens egen fullføring forblir MANUELL** — eieren trykker
|
|
|
|
|
|
fortsatt "Fullfør runde" til slutt (HCP-beregning/varsler skjer
|
|
|
|
|
|
fortsatt der, uendret). Per-deltaker-fullføring er KUN en nagging-
|
|
|
|
|
|
brems + trigger for e-post, IKKE en auto-cascade til hele rundens
|
|
|
|
|
|
fullføring.
|
|
|
|
|
|
2. **Scorekort på e-post sendes UMIDDELBART** når en spiller selv blir
|
|
|
|
|
|
ferdig, ikke samlet ved rundefullføring — mest nyttig nettopp for
|
|
|
|
|
|
spilleren som går av tidlig.
|
|
|
|
|
|
3. **Tillegg:** må også kunne sende scorekort til en enkelt spiller i
|
|
|
|
|
|
ETTERTID, lenge etter at runden allerede er fullført (glemt/tapt
|
|
|
|
|
|
e-post, ønske om å sende på nytt) — et eget on-demand-endepunkt,
|
|
|
|
|
|
ikke bundet til selve fullførings-transisjonen.
|
|
|
|
|
|
|
|
|
|
|
|
**Bevisst utenfor omfang:** delt-ball-formater (foursome/scramble/
|
|
|
|
|
|
greensome, som fullfører per SIDE via `round_side`, ikke per deltaker)
|
|
|
|
|
|
— brukerens eksempel er individuell slagspill. Strukturelt symmetrisk
|
|
|
|
|
|
follow-up når/hvis etterspurt.
|
|
|
|
|
|
|
|
|
|
|
|
**Datamodell (migrasjon 066):** `round_participant.completed_at
|
|
|
|
|
|
timestamptz`, ingen CHECK mot `round.completed_at` — applikasjonslaget
|
|
|
|
|
|
garanterer rekkefølgen (deltaker-fullføring skjer alltid FØR/uavhengig
|
|
|
|
|
|
av rundens egen, aldri etter at runden er låst).
|
|
|
|
|
|
|
|
|
|
|
|
**Tilgang — `completed` er et FLIGHT-STYRT felt, ikke eier-/selv-
|
|
|
|
|
|
only.** Speiler `update_hole`s eksisterende begrunnelse ("en lenket
|
|
|
|
|
|
medspiller kan registrere score for HELE flighten") — enhver med
|
|
|
|
|
|
tilgang til runden (eier ELLER lenket medspiller) kan avslutte/
|
|
|
|
|
|
gjenåpne EN HVILKEN SOM HELST deltaker, akkurat som de allerede kan
|
|
|
|
|
|
føre score for en hvilken som helst deltaker. Ny
|
|
|
|
|
|
`_FLIGHT_MANAGED_PARTICIPANT_FIELDS = {"completed"}`-konstant i
|
|
|
|
|
|
`rounds.py` hopper eksplisitt over både eier-only-sjekken og
|
|
|
|
|
|
selv-only-sjekken KUN for dette feltet — uendret for alle andre felt i
|
|
|
|
|
|
samme kall. Avvist med `ALREADY_COMPLETED` (409) hvis `round.
|
|
|
|
|
|
completed_at` allerede er satt — ingen åpning/lukking av
|
|
|
|
|
|
enkeltdeltakere etter at hele runden er låst.
|
|
|
|
|
|
|
|
|
|
|
|
**E-post — én delt funksjon, to mottakertyper.**
|
|
|
|
|
|
`_send_participant_round_summary(conn, round_id, participant_id,
|
|
|
|
|
|
round_label) -> bool` erstatter den gamle gjeste-only
|
|
|
|
|
|
`_send_guest_round_summaries` — slår nå opp BÅDE gjest (`guest_email`)
|
|
|
|
|
|
OG lenket konto (`app_user.email` via LEFT JOIN), returnerer om en
|
|
|
|
|
|
adresse fantes. Kun den FØRSTE `null → true`-transisjonen av
|
|
|
|
|
|
`completed_at` trigger e-post (idempotent — re-sending `completed:
|
|
|
|
|
|
true` når allerede sann, eller `completed: false`, sender aldri på
|
|
|
|
|
|
nytt automatisk). `complete_round` har en fallback-løkke: enhver
|
|
|
|
|
|
deltaker som fortsatt er `completed_at IS NULL` når hele runden
|
|
|
|
|
|
fullføres, får det satt nå OG e-post sendt der også — dekker det
|
|
|
|
|
|
vanlige tilfellet (ingen trykket "avslutt" underveis) uten
|
|
|
|
|
|
dobbel-sending for de som allerede var individuelt avsluttet.
|
|
|
|
|
|
`app/email.py`s `send_round_summary_email` fikk `is_linked_account`/
|
|
|
|
|
|
`round_id`-parametre: lenkede mottakere får en direkte
|
|
|
|
|
|
`/my-rounds/{round_id}`-lenke (ingen magic-link, de har allerede
|
|
|
|
|
|
konto) og hilsen med `app_user.first_name` (navneformat-regelen i
|
|
|
|
|
|
CLAUDE.md), gjester beholder den opprinnelige magic-link-CTA-en.
|
|
|
|
|
|
|
|
|
|
|
|
**Nytt on-demand-endepunkt** (tillegg 3 over): `POST /rounds/{round_
|
|
|
|
|
|
id}/participants/{participant_id}/send-scorecard` — fungerer UANSETT
|
|
|
|
|
|
fullføringsstatus, også lenge etter at runden er fullført. Samme
|
|
|
|
|
|
flight-styrte tilgang som `completed`-feltet, men bevisst INGEN
|
|
|
|
|
|
`ALREADY_COMPLETED`-sperre (leser og sender kun, muterer aldri
|
|
|
|
|
|
databasetilstand). 204 ved suksess, `NO_EMAIL_ADDRESS` (400) hvis
|
|
|
|
|
|
deltakeren ikke har noen registrert adresse.
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisert:** `python3 -m py_compile` + full
|
|
|
|
|
|
`./scripts/run_backend_tests.sh`. Egen scratch-database: 4-spiller
|
|
|
|
|
|
frittstående slagspill-runde (blanding lenket bruker + gjest med
|
|
|
|
|
|
e-post), ulikt antall hull spilt, avsluttet to deltakere manuelt via
|
|
|
|
|
|
PATCH — bekreftet e-post trigges umiddelbart med riktig mottaker-
|
|
|
|
|
|
variant (gjest vs. lenket, ulik lenke/hilsen), `ALREADY_COMPLETED` ved
|
|
|
|
|
|
forsøk etter rundefullføring, `complete_round` sender KUN til
|
|
|
|
|
|
ikke-allerede-avsluttede deltakere, en lenket medspiller (ikke eier)
|
|
|
|
|
|
kan avslutte en ANNEN deltaker (flight-styrt tilgang bekreftet), angre
|
|
|
|
|
|
(`completed: false`) fungerer før rundens egen fullføring, og det nye
|
|
|
|
|
|
on-demand-endepunktet fungerer også lenge etter rundefullføring.
|
|
|
|
|
|
Deployet til ekte `teecup_db`/`teecup_api` sammen med migrasjon 067
|
|
|
|
|
|
(GolfAPI-tillegg, se ADR-064-tillegg over), 2026-08-13.
|
|
|
|
|
|
|
2026-08-13 21:36:57 +02:00
|
|
|
|
**Tillegg samme dag — frontend-UI bygget og rullet ut.** `Player.
|
|
|
|
|
|
completed` lagt til (avledet fra `ApiParticipant.completed`), delt
|
|
|
|
|
|
`ParticipantCompletionActions`-komponent gjenbrukt i BÅDE
|
|
|
|
|
|
`PlayerHoleCards` (Score-fanen) og `PlayerList` ("Spillere og
|
|
|
|
|
|
runde"-fanen) -- "Avslutt for [navn]" (skjult når runden selv er
|
|
|
|
|
|
fullført, siden backend uansett avviser da), "Ferdig ✓ (N hull)" +
|
|
|
|
|
|
"Angre" (kun når ikke rundefullført), "Send scorekort" (alltid synlig,
|
|
|
|
|
|
uansett fullføringsstatus -- dekker "send til enkeltspiller i etterkant,
|
|
|
|
|
|
lenge etter fullføring"-tillegget). `allHolesEnteredForEveryone` og
|
|
|
|
|
|
hurtig-hopp-stripens "registrert"-sjekk teller nå en `completed`-spiller
|
|
|
|
|
|
som ferdig uavhengig av faktisk hull-dekning. `advanceWizardPlayer`
|
|
|
|
|
|
hopper over fullførte spillere i samlebånds-kjeden. Et ikke-spilt hull
|
|
|
|
|
|
for en fullført spiller viser nå "Ferdig" i stedet for "Registrer" +
|
|
|
|
|
|
den stiplede pluss-boksen.
|
|
|
|
|
|
|
|
|
|
|
|
**Verifisert:** `tsc --noEmit` rent, 45/45 vitest. Ekte nettleser-
|
|
|
|
|
|
verifisering (ekte innlogging inkl. reell TOTP-2FA-kode generert fra
|
|
|
|
|
|
kontoens lagrede secret, ekte runde opprettet på Tjøme -- cachet bane,
|
|
|
|
|
|
0 nye GolfAPI-kall -- med to deltakere): "Avslutt for X"/"Send
|
|
|
|
|
|
scorekort" synlig på begge kort i begge faner, avslutning av gjesten
|
|
|
|
|
|
trigget e-post umiddelbart (ingen feil i `teecup_api`-loggen, SMTP
|
|
|
|
|
|
konfigurert), UI oppdaterte seg til "Ferdig ✓"-merke + "Angre"-lenke +
|
|
|
|
|
|
"Ferdig"-tekst i score-kolonnen, "Send scorekort" for eieren (lenket
|
|
|
|
|
|
konto, andre kodesti enn gjesten) ga "Sendt ✓"-tilbakemelding, "Angre"
|
|
|
|
|
|
tilbakestilte korrekt og var persistent etter fane-bytte. Bekreftet
|
|
|
|
|
|
visuelt lys+mørk. Ingen konsollfeil gjennom hele forløpet. Testrunden
|
|
|
|
|
|
slettet etter verifisering (`DELETE /rounds/{id}`, bekreftet fjernet fra
|
|
|
|
|
|
`teecup_db`). **Rullet ut 2026-08-13** -- ren frontend-endring (ingen ny
|
|
|
|
|
|
migrasjon), `docker compose build teecup_frontend && up -d
|
|
|
|
|
|
teecup_frontend`.
|
2026-08-13 21:24:25 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-16 07:18:01 +02:00
|
|
|
|
## Utviklingsplan (rekkefølge)
|
|
|
|
|
|
|
|
|
|
|
|
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)
|
|
|
|
|
|
2. ✅ Denne beslutningsloggen (dette dokumentet)
|
|
|
|
|
|
3. ✅ Handicap-motor som frittstående, testet bibliotek (ADR-005) — 24 tester, R&A-verifisert
|
|
|
|
|
|
4. ✅ Databaseskjema (`001_initial_schema.sql`) — RLS, miksede formater, pool
|
|
|
|
|
|
5. ⏩ Backend-API + regelmotor-integrasjon (neste)
|
|
|
|
|
|
6. ⬜ Frontend (admin + score-registrering)
|
|
|
|
|
|
7. ⬜ PWA & offline-synk
|