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
|
2026-07-16 14:03:50 +02:00
|
|
|
|
**Sist oppdatert:** 2026-07-16
|
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.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-16 07:18:01 +02:00
|
|
|
|
## Åpne spørsmål (ikke besluttet ennå)
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
4. **Scramble-grensesnitt:** Arkitekturen skal ta høyde for formatet; eksakt
|
|
|
|
|
|
UI-løsning spesifiseres senere.
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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
|