teecup/FEATURE_BACKLOG.md

568 lines
37 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TeeCup — Funksjons-backlog
> Formål: fange ALT som er ønsket for TeeCup på ett sted, slik at intet krav går
> tapt mellom økter, modeller eller samtaler. Kilder: de to opprinnelige
> Gemini-samtalene (funksjonalitet + Docker) og arbeidet gjort med Claude.
>
> Status-koder: ✅ ferdig · 🔨 pågår · 📋 planlagt/fanget · ❓ trenger beslutning
> · 🔀 endret fra opprinnelig råd · 💤 utsatt (bevisst)
>
> Sist oppdatert: 2026-07-17
---
## Fundament (bygget)
| Funksjon | Status | Notat |
|---|---|---|
| Handicap-/matchmotor (testet) | ✅ | 24 tester, R&A-verifisert. Erstatter Geminis løse JS-funksjon. |
| Formater: singles, fourball, foursome, greensome, scramble | ✅ | I motoren, allowance som konfig. |
| Brutto/netto + prosent-allowance (75 %, 90 %, 3/4) | ✅ | ADR-005. Prosentene bekreftet mot Golf GameBook (ADR-014). |
| 9-hulls (front/back) slagfordeling | ✅ | ADR-008, egen funksjon + test. |
| Miksede formater per turnering (økter) | ✅ | ADR-007. Ryder Cup-strukturen. |
| Spiller-pool m/ reserver, delvis deltakelse | ✅ | ADR-007. |
| Multi-tenant skjema + RLS | ✅ | ADR-001/003. Isolasjon bevist med oppførselstest. |
| Dedikert app-rolle (ingen superuser/BYPASSRLS) | ✅ | migrasjon 002. |
| API: oppsett (spillere/lag/roster/økter/matcher/blind draw) | ✅ | Verifisert for ekte mot scratch-db + engangscontainer. |
| API: scoring (hole_score/match_hole_result, matchstatus, handicap-beregning) | ✅ | ADR-012/014. Verifisert for ekte, inkl. fourball better-ball og race-sikker recompute. |
| Banedata fra teeoff via API | ✅ | ADR-004 → **ADR-019**, LIVE 2026-07-18. Import (kopi) ved eksplisitt organisator-valg, ikke live oppslag ved hver bruk. Se egen seksjon under. |
| Konfigurerbar handicap-pipeline (4 brytere) | ✅ | ADR-014. Bygget i `app/handicap.py`, brukt av scoring-runden. |
| Ekte autentisering (magic-link + JWT-sesjon) | ✅ | ADR-009. `app/routers/auth.py` + migrasjon `004_auth.sql`. `X-Debug-User-Id`-stubben er helt fjernet. |
| RLS-tomstreng-fiks (`app_current_org()`) | ✅ | Migrasjon `005_rls_null_guard.sql`. Se detaljer under. |
| Organisasjon-bootstrap (opprette ny org via API) | ✅ | `POST /orgs`, `app/routers/organizations.py`. Se detaljer under. |
| Ekte SMTP-utsending av magic-link | ✅ | `app/email.py`. Se detaljer under. |
| Containerisert, LIVE på `teecup.teeoff.no` | ✅ | `Dockerfile` + `docker-compose.yml`. Se egen seksjon under — to reelle driftshendelser funnet og rettet. |
| Dato/klokkeslett på turnering/økt/match | ✅ | ADR-015. `session.scheduled_at` + `tee_interval_minutes` → utledet `match.tee_time`. Se egen seksjon under. |
| Maskinlesbar feilkode-kontrakt (`{"code",...}`) | ✅ | ADR-015. Full retrofit, alle 39 tidligere steder. Se egen seksjon under. |
| i18n-forberedelse (nb/en) | ✅ | ADR-015. `preferred_locale` + ekte engelsk e-postmal. Se egen seksjon under. |
| Frontend: innlogging + verifisering, LIVE | ✅ | ADR-016. Next.js på `teecup.teeoff.no`, ekte magic-link-flyt bevist med reell e-post. Se egen seksjon under. |
| Frontend: dashboard (org-bytter/-opprettelse + turneringsliste), LIVE | ✅ | `/dashboard`. Kablet mot `/auth/me`, `/orgs`, `/orgs/{id}/tournaments`. Skrive-flyt bekreftet med ekte data (org "Tjøme Gents" + turnering opprettet av bruker). |
| Frontend: lag/roster-skjerm, LIVE | ✅ | `/tournaments/[id]`. To nye backend-endepunkter bygget samtidig (`PATCH`/`DELETE` roster). Skrive-flyt ikke testet med ekte data ennå. |
| Selvregistrering + utvidet spillerprofil (API) | ✅ | ADR-017. `app/routers/registration.py` (offentlig, uautentisert), utvidet `players.py`, e-post-basert `player.user_id`-kobling ved innlogging. Backend komplett og live. Frontend-påmeldingsskjema/landingssider gjenstår (egen ADR-runde). |
| Roster: endre kaptein / fjern spiller (`PATCH`/`DELETE`) | ✅ | `app/routers/tournaments.py`. Bevisst ingen "kun én kaptein"-håndhevelse ennå — se «Brukerroller»-punktet under. |
| Egendefinerte baner (`course`) via API | ✅ | Nytt `app/routers/courses.py` (`GET`/`POST /orgs/{id}/courses`). Kun `source='custom'` — ADR-004s teeoff-integrasjon (`source='official'`) fortsatt kun vedtatt, ikke bygget. Se egen seksjon under. |
| Frontend: program-skjerm (økter/tidsplan), LIVE | ✅ | `/tournaments/[id]/program`. Se egen seksjon under. |
---
### Frontend: innlogging + verifisering — ✅ BYGGET OG VERIFISERT LIVE 2026-07-17
- Første frontend-skjerm i prosjektet. Designet i V0 (login-skjerm, merkevare
form/farge hentet fra Teeoff-logoen — IKKE navn/logo, se ADR-016s
begrunnelse og tidligere samtale), hentet inn som `frontend/` (Next.js +
Tailwind + shadcn/ui).
- **Kvalitetsrunde på V0-output før bruk:** fargetokens i `globals.css` var
OKLCH-TILNÆRMINGER av de forespurte hex-fargene (`#8bc24a`/`#ff5722`), ikke
eksakte — regnet ut presise OKLCH-ekvivalenter og rettet alle 6
forekomster (lys/mørk/system-mørk). Fjernet `@vercel/analytics` helt
(ga null verdi på egen-hostet infrastruktur, kun en unødvendig
tredjeparts nettverkskall). Fjernet `typescript: { ignoreBuildErrors:
true }` (bygget kjører nå ekte typesjekk). Fjernet dødt
`pnpm.overrides`-felt. `frontend/.gitignore` manglet `.pnpm-store/`
årsaken til at VSCode viste 10 000 "ukjente" filer ved første
commit-forsøk; rettet.
- **Kablet mot ekte API** (`app/routers/auth.py` sine `/auth/request-link`
og `/auth/verify-link`): `frontend/next.config.mjs` sin `rewrites()`
proxyer `/auth/*`/`/orgs/*`/`/health` server-side til API-et — se
ADR-016 for hele begrunnelsen (same-origin, ingen CORS, cookie uendret).
`components/login-form.tsx` sender ekte `POST /auth/request-link`;
ny `app/verify/page.tsx` + `components/verify-form.tsx` mottar
`?token=...` fra e-postlenken (automatisk verifisering) ELLER viser et
manuelt "lim inn koden"-felt (nødvendig fallback, ikke overflødig — se
under).
- **Backend-endring i samme runde:** `app/email.py`/`app/config.py` fikk en
ny `PUBLIC_BASE_URL`-innstilling (default `https://teecup.teeoff.no`,
valgfri) — e-postmalen sender nå en EKTE klikkbar lenke
(`{base}/verify?token=...`) i tillegg til den rå koden som fallback
(samme mal-mekanisme som i18n-runden, ADR-015).
- **Containerisert og rullet ut LIVE** (ny `frontend/Dockerfile`,
multi-stage, Next.js `output: "standalone"`; ny `teecup_frontend`-tjeneste
i `docker-compose.yml`). Caddy (`teecup.teeoff.no`, i det SEPARATE
`teeoff`-repoet) peker nå på `teecup_frontend` i stedet for `teecup_api`
direkte — se ADR-016 for hvorfor, og CLAUDE.md-status for
driftsdetaljene (samme stale-Caddy-inode-hendelse som
containeriseringsrunden, løst likt).
- **Verifisert med FAKTISK e-postlevering, ikke bare curl:** ekte
`POST /auth/request-link` sendt til brukerens egen adresse over
`https://teecup.teeoff.no`, ekte e-post mottatt med en ekte klikkbar
lenke, lenken åpnet i nettleser, landet på en fungerende `/verify`-side,
sesjon opprettet — brukeren bekreftet "jeg er tilsynelatende innlogget."
Første gang en hel bruker-vendt flyt (ikke bare API-et isolert) er bevist
ende-til-ende i produksjon.
---
### Dato/klokkeslett + feilkode-kontrakt + i18n-forberedelse — ✅ BYGGET OG VERIFISERT 2026-07-17
- Reist av brukeren rett før frontend-arbeidet: kan turnering/økt/match ha
dato/klokkeslett, og må appen forberedes for flerspråklighet? Begge er
API-kontraktspørsmål som er langt billigere å løse FØR frontend bygges enn
å ettermontere — se ADR-015 for alle tre beslutningene i sin helhet.
- **Migrasjon `006_scheduling_and_locale.sql`:** alt additivt (nullable/
default) — `tournament.end_date` (+ `CHECK` mot `start_date`),
`session.scheduled_at`/`tee_interval_minutes`/`start_hole`,
`match.tee_time_override`, `app_user.preferred_locale`,
`magic_link_token.locale` (begge sistnevnte `CHECK IN ('nb','en')`). Kjørt
permanent mot den ekte `teecup_db` (se under).
- **Feilkoder:** ny `app_error(status_code, code, message)`-factory i
`app/errors.py`, alle 39 tidligere norsk-hardkodede `HTTPException(...,
detail="...")`-steder på tvers av 6 filer (`errors.py`, `auth.py`,
`routers/auth.py`, `routers/tournaments.py`, `routers/matches.py`,
`routers/scoring.py`) retrofittet til en delt ~15-kode-taksonomi. Endelig
`grep -rn 'detail="' app/` ga NULL treff.
- **Dato/tid i API-et:** `tournament.start_date` (fantes i skjemaet siden
001, men var ALDRI koblet til API-modellene før nå — funnet og fikset som
en naturlig bivirkning av å legge til `end_date`) + `end_date` eksponert;
`session` sine tre nye felt eksponert i `SessionCreate`/`SessionOut`;
`match.tee_time` utledet i Python i både `create_match` og `list_matches`
(ingen ekstra spørring — økten hentes allerede for andre formål der).
- **i18n:** `MagicLinkRequest.locale` (`Literal["nb","en"]`, default `nb`)
sendes av klienten, følger med i token-raden, brukes til å velge
e-postmal OG (kun ved førstegangsopprettelse) `app_user.preferred_locale`.
Ekte engelsk e-postmal lagt inn i `app/email.py` (ikke bare rørlegging) —
konkret bevis på at flerspråklighet fungerer ende-til-ende, ikke bare at
et felt eksisterer.
- **Verifisert grundig mot en fersk `teecup_scratch`** (migrasjoner
001→006, `test_isolation.sql` 12/12 uendret, engangs-API-container):
feilkode-form bekreftet på tvers av 5 filer (`NOT_FOUND`/`LIMIT_REACHED`
fra tournaments.py, `DUPLICATE` fra roster, `NOT_AUTHENTICATED` uten
cookie, `NOT_ROSTERED_ON_TEAM` fra matches.py sin `add_participant`);
`tee_time` bekreftet å øke riktig per `sequence` (10 min intervall →
08:00/08:10/08:20), `tee_time_override` bekreftet å vinne over utledet
verdi, økt uten `scheduled_at` bekreftet å gi `tee_time: null` (ikke
feil); i18n bekreftet full runde — ny bruker med `locale:"en"` fikk
`preferred_locale:"en"`, en ETTERFØLGENDE `request-link` for samme bruker
med `locale:"nb"` skiftet e-postmalen men IKKE den lagrede preferansen
(bekreftet uendret via `/auth/me`).
- **Migrasjon 006 kjørt mot ekte `teecup_db` 2026-07-17**, bruker bekreftet
eksplisitt i samme økt — kjørte rent, `test_isolation.sql` fortsatt 12/12.
- **Mindre driftslærdom, funnet OG rettet samme runde:** et innledende
`grep -v -i 'pass|secret|key'`-filter jeg brukte for å lese ikke-sensitive
`.env`-nøkler fanget IKKE opp `TEECUP_DATABASE_URL`, som bar
`teecup_app`-passordet innebygd i selve URL-en (i tillegg duplisert av det
samme passordet som allerede lå rent i `TEECUP_APP_PASSWORD`) — passordet
endte dermed synlig i et verktøyresultat. Flagget til brukeren umiddelbart
(samme mønster som SMTP-passord-hendelsen under containeriseringsrunden).
**Rettet:** `.env` bygget om til fem separate, rent navngitte felt
(`TEECUP_DB_HOST/PORT/NAME/USER/PASS`), `app/config.py`+`app/db.py` bygger
`asyncpg`-poolen fra disse i stedet for én DSN-streng,
`docker-compose.yml` oppdatert tilsvarende. Se CLAUDE.md-status for full
driftsdetalj, inkl. at dette krevde et fullt
`docker compose up -d --build --force-recreate` (ikke bare
`--force-recreate` — imaget bygger inn `app/` ved build-tid). Verifisert
ende-til-ende mot den live stacken (`Application startup complete`,
`/auth/me` ga rent `401` over ekte https, `teeoff.no` upåvirket).
---
### Containerisering og go-live — ✅ FERDIG 2026-07-16
- `POST /orgs` osv. var siste kodebit; dette var første gang noe rørte EKTE,
PERMANENT infrastruktur (ekte `teecup_db`, ekte langtlevende container, den
DELTE Caddy-instansen som også ruter live `teeoff.no`).
- **Bygget:** ekte `teecup_db` opprettet, migrasjoner 001→005 kjørt permanent
(samme filer, ingen endringer), `test_isolation.sql` bestått (ruller
alltid tilbake, trygt å kjøre mot en database som skal bli stående).
`Dockerfile` (speiler scratch-rundenes bevist-riktige volumoppsett:
`app/` + `handicap_engine.py` på samme relative plassering) +
`docker-compose.yml` (tjeneste `teecup_api`, joiner det eksisterende,
eksterne `teeoff_default`-nettverket). Ny Caddy-blokk for
`teecup.teeoff.no` i `/opt/teeoff/deploy/Caddyfile`.
- **To reelle driftshendelser, begge funnet og rettet i sanntid:**
1. **Stale bind-mount-inode:** `teeoff_caddy` sin `Caddyfile`-mount er en
ENKELTFIL-bind-mount, låst til inoden som fantes da containeren sist
startet (13 dager tidligere). Fil-redigering via atomisk rename laget
en ny inode på samme sti — containeren fortsatte å lese den GAMLE filen
uansett hvor mange ganger `caddy validate`/`reload`/admin-API `/load`
ble kjørt (alle opererte på den uendrede gamle filen, derav ingen
synlig feil). Løsning: full `docker restart teeoff_caddy` (brukeren
bekreftet eksplisitt — avvek fra planens "kun graceful reload"-løfte,
ga noen sekunders nedetid for `teeoff.no`).
2. **Alvorlig nettverksalias-kollisjon** (oppdaget rett etter omstarten, da
EKTE teeoff-trafikk — `/api/facilities?...` med ekte klubb-slugs som
`borregaard-golfklubb` — dukket opp i `teecup_api` sin logg):
`docker-compose.yml` sin service-nøkkel var `api:`, identisk med
teeoffs eget `api`-servicenavn. Docker Compose registrerer
nettverksalias etter service-NAVNET (ikke bare `container_name`) på
delte nettverk, så begge containerne fikk alias `api`
`teeoff_default` — Caddys `reverse_proxy api:8000` i teeoffs egen
config kunne da tilfeldig treffe enten ekte `teeoff_api` eller
`teecup_api`, dvs. ekte brukertrafikk til teeoff.no kunne bli besvart
av TeeCup-koden. **Rettet umiddelbart:** stoppet `teecup_api` først
(hindre videre feilruting), ga service-nøkkelen navnet `teecup_api`,
gjenopprettet, bekreftet med `docker network inspect` at alias `api`
KUN peker på ekte `teeoff_api`.
**Lærdom for fremtidige tjenester på delt nettverk:** ALLTID gi
docker-compose sin service-nøkkel (ikke bare `container_name`) et
prosjekt-unikt navn når flere uavhengige compose-prosjekter deler samme
eksterne nettverk — service-navnet blir også et DNS-alias.
- **Mindre driftslærdom (samlet):** (a) `.env` leses IKKE på nytt av en
allerede kjørende container — `docker compose up -d --force-recreate`
kreves etter enhver `.env`-endring; (b) et `#`-tegn i et upassordet
`.env`-passord kuttes som kommentar av Compose sin parser, anførselstegn
(helst enkle) løser det; (c) jeg eksponerte ved et uhell to secret-verdier
i eget debug-output mens jeg feilsøkte en `.env`-korrupsjon (manglende
linjeskift) — brukeren roterte passordet som forsiktighetsregel.
- **Verifisert ende-til-ende mot den ekte, live stacken:** `teeoff.no`
upåvirket gjennom hele prosessen; `https://teecup.teeoff.no/health` → 200
med automatisk utstedt TLS; full magic-link-innlogging (ekte e-post
mottatt, `verify-link` ga `Secure`-flagget cookie siden vi nå er over ekte
https, `/auth/me` fungerte med sesjonen).
---
### Ekte SMTP-utsending — ✅ BYGGET OG VERIFISERT 2026-07-16
- Brukeren la inn egne, uavhengige SMTP-credentials i `.env`
(`TEECUP_SMTP_SERVER/PORT/USER/PASS`, `TEECUP_FROM_EMAIL` — ADR-009, ikke
delt med teeoff). Kun nøkkelnavn ble lest for å bekrefte de fantes, ALDRI
verdiene (CLAUDE.md sin sikkerhetsregel).
- **Løsning:** ny `app/email.py` (`send_magic_link_email`, `smtplib` kjørt via
`asyncio.to_thread` siden det er et synkront bibliotek — samme mønster som
teeoffs egen fungerende utsending). Håndterer BEGGE vanlige
SMTP-tilkoblingsmåter dynamisk (implisitt TLS på port 465 vs. STARTTLS på
andre porter) siden porten bevisst ikke ble lest under planlegging.
`app/config.py` fikk nye, valgfrie innstillinger (`SMTP_CONFIGURED` avledet
fra at alle fem er satt) — IKKE `_required`, så scratch-/dev-testing
fungerer fortsatt uten SMTP satt opp, via `TEECUP_DEV_LOG_MAGIC_LINKS`.
- **Bevisst designvalg:** en driftsfeil i selve utsendingen (feil passord,
SMTP nede, eller ingen leveringsmåte konfigurert i det hele tatt) logges
kun server-side og endrer ALDRI klientens respons — alt annet ville brutt
anti-enumereringsgarantien i `request-link` (klienten skal ikke kunne
skille "e-posten finnes ikke" fra "e-posten finnes men utsendingen
feilet").
- **Verifisert i to trinn:** (1) dev-log-flyten uendret uten SMTP satt
(regresjonstest av eksisterende scratch-løype), (2) én ekte test-e-post
sendt til en adresse brukeren oppga, med de ekte credentials videreført fra
`.env` til scratch-containeren uten at jeg noensinne leste verdiene selv —
**brukeren bekreftet mottak** av en e-post med innloggingskode. Dette er
første gang noe i dette prosjektet er verifisert ved faktisk levering til
en ekte, ekstern mottaker, ikke bare via curl/scratch-container.
---
### Organisasjon-bootstrap — ✅ BYGGET OG VERIFISERT 2026-07-16
- Gjennomgang av alle routere hadde avdekket at INGEN endepunkt opprettet en
`organization`-rad — i alle testrunder denne økten var organisasjoner satt
inn direkte med superbruker-SQL. En ekte førstegangsbruker hadde ingen vei
til å opprette klubben/bedriften sin og bli owner. Reelt blokkerende, ikke
en utsettbar produktbeslutning.
- **Løsning:** nytt `POST /orgs {"name": ...}`, autorisert med
`get_current_user` (ikke `get_authorized_org` — sirkulært før org-en
finnes). Ingen ny migrasjon nødvendig.
- **Selvrefererende RLS-bootstrap bekreftet å fungere** (kommentaren i 002 om
en "privilegert sti" var ALDRI bygget og viste seg unødvendig): generer
org-ens uuid i Python FØR innsetting, sett `app.current_org` til nøyaktig
den verdien via den eksisterende `org_connection()`, sett så inn
`organization`-raden med samme id. `org_self`-policyens implisitte
`WITH CHECK` (id = `app_current_org()`) blir da trivielt sann.
`teecup_app` (NOSUPERUSER/NOBYPASSRLS) trenger altså INGEN egen privilegert
tilkobling for å bootstrappe sin egen første organisasjonsrad.
- **Verifisert med 5 tester**, inkludert en negativ kontroll som beviser
mekanismen er presis, ikke et RLS-hull: forsøkte å sette inn en
organisasjon med en MISMATCHENDE id (annen enn `app.current_org`) — avvist
med `insufficient_privilege`, som forventet. Også bekreftet: ny org fungerer
normalt med eksisterende endepunkter (GET/POST tournaments), dukker opp
riktig i `/auth/me`, og full kryss-org-isolasjon holder mellom to
uavhengig opprettede organisasjoner.
---
### RLS-tomstreng-bug — ✅ FIKSET 2026-07-16
- Alle RLS-policyer i 001/003 (`org_isolation` på 14 tabeller + `org_self`
`organization`) brukte `current_setting('app.current_org', true)::uuid`.
Denne håndterte NULL trygt (ga ingen rader, som tiltenkt), men IKKE
tomstreng — en gjenbrukt asyncpg-pool-tilkobling der en TIDLIGERE
forespørsel satte GUC-en via `SET LOCAL` kunne lese den tilbake som `''`
etter at transaksjonen var ferdig, og casten kastet da en 500 i stedet for
skjemaets lovede "trygg standard: se ingenting".
- **Fiks:** samlet i én `STABLE` SQL-funksjon `app_current_org()` (migrasjon
`005_rls_null_guard.sql`) som gjør `NULLIF(current_setting(...), '')::uuid`
— konverterer tomstreng til NULL FØR cast. Alle 15 policyer alteret
(`ALTER POLICY`) til å bruke funksjonen i stedet for det rå uttrykket.
Verifisert med 3 nye regresjonstester i `test_isolation.sql` (Test 10-12:
tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS
ikke krasj, org-bootstrap-innsetting fungerer rett etter tomstreng-
tilstand) OG ved å faktisk gjenskape original-buggen mot en ekte container
(pool-størrelse 1, varm opp med `org_connection()`, deretter kall
`/auth/me` på samme gjenbrukte tilkobling — gikk fra 500 til 200).
- **Viktig presisering oppdaget underveis:** den opprinnelige planen antok at
`/auth/me` kunne joine `organization` direkte igjen når tomstreng-buggen
var fikset. Det var FEIL — fiksen gjør bare at tomstreng oppfører seg som
NULL (trygt: se ingenting), den endrer IKKE at `org_self`-policyen krever
en MATCHENDE `app.current_org` for å vise en rad i det hele tatt (riktig
RLS-oppførsel, ikke en bug). En bruker kan tilhøre flere organisasjoner
samtidig, så det finnes ingen ÉN kontekst å sette for en tverr-org-
spørring. `/auth/me` slår derfor opp hvert org-navn ETT OM GANGEN via
`org_connection()` (N+1 spørringer, N = antall org-er brukeren tilhører,
typisk 1-3) — verifisert at dette faktisk returnerer navnet korrekt.
---
## Ønsket, men IKKE fanget før nå (fra Gemini-samtalene)
### Brukerroller (utover org-medlemskap)
- **Status:** ❓ trenger beslutning
- De opprinnelige samtalene beskriver: turneringsadmin, **lagkaptein**, spiller,
**tilskuer** (les-only, følger live uten skriverettigheter).
- Vi har i dag org-roller (owner/admin/member) + `is_captain` på roster.
- **Mangler:** «tilskuer» som begrep. Henger sammen med hvem som ser den
offentlige feeden (se Kommunikasjon). Kaptein-rollen bør kanskje gi spesifikke
rettigheter (sette oppstilling), ikke bare være et flagg.
- **Nytt 2026-07-18:** `PATCH .../roster/{id}` (sette/fjerne kaptein) håndhever
bevisst IKKE «kun én kaptein per lag» — flere spillere kan i dag merkes
kaptein samtidig på samme lag. Bør revurderes samtidig med resten av dette
punktet, ikke løses isolert i roster-endepunktet.
### Scoring-autorisasjon: hvem fører, hvem korrigerer, hvem lukker (rejst 2026-07-16)
- **Status:** ❓ trenger beslutning — direkte oppfølger av «Brukerroller» over.
- **Hvem fører score i dag:** alle med en `team_roster`-rad på laget (samme
minimale grense som deltaker/lås, se ADR-013-relatert kode). Ikke
kaptein-only, ikke begrenset til de(n) som faktisk spiller matchen.
- **Hvem kan korrigere en ført score i dag:** akkurat de samme — skriving er en
upsert (`ON CONFLICT ... DO UPDATE`), så en korrigering er ikke skilt fra en
førstegangs-innføring. Ingen godkjenning fra motstanderen, ingen audit-trail
(forrige verdi overskrives sporløst).
- **Match-lås ved avgjørelse: ✅ FIKSET 2026-07-16.** `submit_hole_score` og
`submit_hole_result` (`app/routers/scoring.py`) avviser nå med 409 («Matchen
er avgjort og kan ikke lenger endres.») FØR upserten kjøres, hvis
`match.points_side_a IS NOT NULL` — dette er allerede et pålitelig,
entydig signal siden kolonnen kun settes når `compute_match_state` sier
matchen er avgjort. Gjelder BÅDE nye hull og korrigering av allerede talte
hull. Verifisert: hull etter avgjørelse avvist, korrigering av et
talt hull avvist, GET scorecard fortsatt leselig, ikke-avgjorte matcher
upåvirket. **Bevisst IKKE bygget:** ingen manuell «avslutt match før den
er avgjort»-handling (det grenser mot walkover under, fortsatt utsatt).
Turnering har et `status`-felt (draft/active/completed/archived) men
INGEN endepunkt endrer det ennå — organisator kan ikke markere en turnering
ferdig via API-et i dag (eget, senere punkt).
- **Manglende WO/konsesjon:** hvis en side aldri stiller nok spillere
(`match_participant`-antallet når aldri det økten krever), beregnes
handicap ALDRI (se `app/handicap.py`), og matchen kan derfor ALDRI få et
hull-resultat — den blir hengende uavgjort for alltid. Det finnes ingen
«gi bort hullet/matchen/turneringen»-mekanisme (walkover/konsesjon) i det
hele tatt ennå — verken datamodell eller endepunkt.
- **Avgjort 2026-07-16:**
- **Match-lås ved avgjørelse: ✅ bygget** — se eget punkt over. Automatisk
(ikke en handling noen utfører), så «hvem får låse» ble aldri et
spørsmål som trengte avklaring.
- **Walkover/konsesjon VENTER** til brukerroller (kaptein/organisator,
punktet over) er avgjort — å bygge den nå på dagens løse
«rostret på laget»-grense betyr sannsynligvis å bygge den om senere.
- **Fortsatt åpent:** (a) skal score-føring begrenses til faktiske
matchdeltakere (ikke bare «noen på laget»)? (b) skal korrigering kreve
motpartens godkjenning, eller er upsert-modellen god nok for v1? (c) skal
turnering-status (draft/active/completed/archived) kunne settes via API?
### Blind draw (skjult lagoppstilling)
- **Status:** ✅ skjema (migrasjon 003, `lineup_lock`) + API bygget og verifisert
(`app/routers/matches.py`: synlighetsfilter i SQL, ikke Python-filter — se ADR-013).
- Kapteinene låser oppstillingen skjult; matchene avsløres samtidig når begge er
ferdige.
- **Gjenstår:** hvem som FÅR låse et lag er i dag bare «rostret på laget», ikke
kaptein-spesifikt — se «Brukerroller» over.
### Forenklet scoreføring (uten slagtall)
- **Status:** ✅ skjema (migrasjon 003) + API bygget og verifisert
(`app/routers/scoring.py`: `hole-scores` for `stroke`-modus,
`hole-results` for `hole_result`-modus, begge mater samme `compute_match_state`).
### Bøtekasse (Kangaroo Court)
- **Status:** 📋 planlagt
- Meld inn overtramp med bøter («kastet kølla på hull 4»).
- Henger sammen med Kommunikasjon: bøter deles i feeden. Sannsynligvis en egen
post-type i meldingsmodellen.
### Flerårig statistikk / historikk / MVP
- **Status:** 📋 planlagt (datamodell støtter det allerede)
- Seiersprosent i singelmatcher, historisk MVP, statistikk over år.
- Datamodellen tillater dette fordi spillere lever på org-nivå og gjenbrukes på
tvers av turneringer, og handicap fryses per turnering (reproduserbart).
### Push-varsler
- **Status:** 📋 planlagt (infrastruktur)
- «BREAKING: X vant matchen». PWA push. Egen infrastruktur-bit.
### Sanntid (WebSockets)
- **Status:** ❓ trenger beslutning
- Live leaderboard og chat som oppdateres uten refresh. Vi har leaderboard-viewet,
men ikke sanntidsleveringen. Valg: WebSockets vs. polling.
### Knockout / cup-turnering (egen turneringstype)
- **Status:** 💤 utsatt (egen fremtidig type, ADR-011)
- Utslagsmatcher: 128 spillere → finale + bronsefinale. Rundene er *avhengige*
(vinner går videre), krever bracket/progresjon, seeding, fripass. IKKE i v1.
- v1 er låst til nøyaktig to lag (Ryder Cup-format). Match-modellen er generell, så
denne typen kan legges til senere uten dataomskriving — den legger bare til et
progresjons-lag.
---
## Kommunikasjon (under design)
| Del | Status | Notat |
|---|---|---|
| Lag-intern chat («det hemmelige rommet») | 🔨 | Bekreftet ønsket. Kanal m/ scope `team`. |
| Offentlig runde-feed («Banter Board») | ❓ | Synlighetsnivå fortsatt ikke besluttet for FEED-en spesifikt, men mekanismen finnes nå: `tournament.visibility` + `get_current_user_optional`/`_is_participant()` (ADR-018) er bygget og live — gjenbruk dette, ikke bygg en ny mekanisme. |
| Bilder i feed/chat | 📋 | v1. Objektlagring (MinIO), presigned opplasting. |
| Video | 💤 | Arkitekt for det, bygg senere (ADR-forslag). |
| 1-til-1 direktemeldinger | 💤 | Gemini frarådet for v1; ikke etterspurt av deg. |
| Moderering (for offentlig innhold) | ❓ | Kreves hvis «alle med lenken». Mønster finnes i teeoff. |
---
## Landingssider (turnering + organisasjon) — ADR-018 ✅ HELT FERDIG 2026-07-18
Reist av brukeren 2026-07-18, rett etter registrerings-ADR-en (ADR-017).
Backend, begge frontend-skjermer, Open Graph-metadata OG MinIO-bilde-
opplasting (med AVIF-konvertering) bygget og live samme dag. Kun selve
dra-og-slipp-opplastingsskjermen i frontend (V0) gjenstår.
| Del | Status | Notat |
|---|---|---|
| Synlighetsnivå: offentlig / kun org-medlemmer / kun turnering-deltakere | ✅ | `tournament.visibility` (default `org`, trygg standard) + `organization.public_profile`. Håndheves eksplisitt i `registration.py` — RLS løser IKKE dette alene (se ADR-018 Beslutning B, reell presisering funnet under bygging). Samme trenivå-modell som «Banter Board» under bør gjenbruke dette. |
| Registrering følger samme synlighetsgrense | ✅ | ADR-018 Beslutning D, bekreftet med bruker FØR bygging. |
| "Deltaker"-tilgang (ikke org-medlem, men rostret/registrert) | ✅ | Ny `get_current_user_optional` + `_is_participant()`. Testet: rostret spiller som logget inn fikk tilgang, tilfeldig fremmed ble avvist. |
| Turnering-landingsside: tekst, program, sponsorer, påmelding (API) | ✅ | `description`-felt, `tournament_sponsor`-tabell (navn+lenke), `GET /public/tournaments/{id}/sessions` (gjenbruker blind draw-lås fra ADR-013). Selve SKJERMEN i frontend gjenstår. |
| Org-landingsside: klubbprofil, liste over turneringer (API) | ✅ | `GET /public/orgs/{slug}` — kun `public`-synlige turneringer. Bekreftet: klubb-profil KAN være offentlig (brukerens valg). |
| Lesbar URL (slug) for organisasjon | ✅ | Fantes faktisk allerede i skjemaet siden migrasjon 001 (oversett, funnet da migrasjon 009 feilet mot scratch — se CLAUDE.md-status). Kun `CHECK`-constraints lagt til i 009. |
| Organisator kan faktisk SETTE disse feltene | ✅ | Implisitt hull fylt under bygging: `PATCH /orgs/{id}/tournaments/{id}` (visibility/description/registrering), `PATCH /orgs/{id}` (slug/public_profile), full sponsor-CRUD. |
| Bilder (hero, sponsorlogoer), backend | ✅ | Ny `teecup-minio`-tjeneste, ekte multipart-opplasting → AVIF-konvertering (Pillow) → lagring, live på `teecup.teeoff.no/teecup-media/*`. Selve opplastingsskjermen i frontend (V0) gjenstår. |
| Del-metadata (Open Graph: og:title/og:description) | ✅ | `generateMetadata()``/t/[id]`+`/clubs/[slug]`, ekte data fra API-et, verifisert mot produksjonsimaget. `og:image` gjenstår (MinIO). |
| Blind draw-skjuling på offentlig side | ✅ | Arves automatisk via delt `_fetch_sessions()`-hjelpefunksjon (ADR-018 Beslutning E) — ikke reimplementert. |
| Antall påmeldte / ledige plasser vist åpent | ✅ | `confirmed_count` i `GET /public/tournaments/{id}`. |
| Frontend: turnering-landingsside + påmeldingsskjema, LIVE | ✅ | `/t/[id]`. Tre bekreftelsestilstander (bekreftet/venteliste/godkjenning venter). Verifisert med ekte `POST`-registrering mot scratch. |
| Frontend: org-/klubb-landingsside, LIVE | ✅ | `/clubs/[slug]`. Gjenbruker `TournamentCard` (nå med valgfri `orgId`) for turneringslisten. Verifisert: viser kun `public`-synlige turneringer. |
---
### Program-skjerm (økter/tidsplan) — ✅ BYGGET OG SCRATCH-VERIFISERT 2026-07-18
Sjette V0-skjerm, første i "bygg i rekkefølgen ting brukes"-serien (etter
lag/roster kommer program, så blind draw, scorekort, leaderboard).
`components/tournament-program.tsx`, ny rute `/tournaments/[id]/program`.
Tidslinje over turneringens økter i rekkefølge + opprett-skjema (format,
hullomfang, scoringsmodus, poeng, klokkeslett+intervall, starthull, en
kollapsbar "avansert"-seksjon med ADR-014s fire handicap-brytere).
**Reelt blokkerende hull funnet FØR integrering, ikke etter:**
`SessionCreate.course_id` er påkrevd, men det fantes INGEN vei til å skaffe
en gyldig én — ingen `course`-endepunkt i API-et i det hele tatt, og ADR-004s
teeoff-integrasjon er fortsatt bare vedtatt (ingen utgående HTTP-klient
finnes noe sted i koden). Spurte bruker eksplisitt (samme mønster som andre
scope-avklaringer) — svar: bygg enkel course-CRUD nå. Lagt til
`app/routers/courses.py` (`GET`/`POST /orgs/{id}/courses`, kun
`source='custom'`, ingen hull-/tee-/rating-detaljer denne runden — motoren
bruker foreløpig kun `course_id` som fremmednøkkel). Program-skjemaet fikk et
type-ahead-felt for bane (samme mønster som spiller-type-ahead i
roster-skjermen: søk blant org-ens eksisterende baner, eller opprett ny
inline).
**Reell korrekthetsfeil funnet og rettet FØR den nådde V0-designet i det hele
tatt hadde blitt integrert:** V0-promptet mitt ba om ett generisk
"Scramble"-valg, men skjemaets `CHECK`-constraint og `handicap_engine.py` sin
`Format`-enum krever `scramble_2`/`scramble_4` som DISTINKTE verdier (antall
spillere per side er del av selve formatet) — ren `"scramble"` avvises med
400. Rettet i frontend-mappingen til to egne segment-knapper.
**`allowance_override`-mapping verifisert eksakt mot motor-kontrakten:**
`app/handicap.py` sin `parse_allowance_config`/`_strategy_from_json` forventer
`{type: "combined"|"per_player", percentage: 0..1}` — IKKE en flat prosent.
Frontend velger riktig `type` ut fra om formatet er side-enhet (foursome/
greensome/scramble_2/scramble_4 → `combined`) eller spiller-enhet (fourball/
singles → `per_player`), og konverterer skjemaets 0100-prosentfelt til
01-brøk før sending. Full JSON-rundtur bekreftet i scratch (se under) —
ikke bare antatt riktig fra å lese motorkoden.
**Verifisert grundig mot fersk scratch-infrastruktur** (ny `teecup_scratch`-
database 001→009 + en ISOLERT `teecup_app_scratch`-rolle som arver
`teecup_app` sine grants via `GRANT teecup_app TO teecup_app_scratch`
bevisst IKKE den ekte `teecup_app`-rollen, siden den nå er
produksjonskritisk og rollen er cluster-global på tvers av `teecup_db`/
`teecup_scratch`; en tidligere plandokument sin "drop teecup_app-rolle"-
opprydning er utdatert etter go-live og ble bevisst IKKE fulgt + isolert
scratch-MinIO-container): courses opprettet+listet, kryss-org-isolasjon
bekreftet (org 2 ser ikke org 1 sin bane), økt opprettet med klokkeslett,
økt opprettet med `scramble_4` + full `allowance_override`-rundtur, gammel
`"scramble"`-verdi korrekt avvist (400), `test_isolation.sql` fortsatt
12/12. Ekte typesjekket PRODUKSJONSBUILD (samme `Dockerfile`/multi-stage som
faktisk deployes, ikke bare en dev-server) kjørt og bekreftet — ny
`/tournaments/[id]/program`-rute listet korrekt i build-output.
**Diffet V0-eksporten mot live-treet før noe ble tatt inn** (samme mønster
som alle tidligere runder): kun tre reelt nye filer
(`tournament-program.tsx`, `program/page.tsx`, `ui/switch.tsx`) — resten var
forventede full-reverts av allerede tilpassede filer, ikke rørt.
**Mindre justeringer utover selve V0-promptet:** fjernet V0s dev-only
"forhåndsvis tom/med økter"-knapperad (ikke noe en ekte organisator skal se);
lagt til en fanerad ("Lag og spillere" / "Program") i BÅDE
`tournament-detail.tsx` og den nye skjermen, siden V0 ikke visste om den
andre skjermen når den ble generert i en egen prompt.
**Rullet ut live 2026-07-18**, bruker bekreftet eksplisitt: begge containere
(`teecup_api`, `teecup_frontend`) bygget og redeployet, live sjekker OK
(`/health`, `/dashboard` → 200), `teeoff.no` upåvirket.
---
### Offisiell banedata fra teeoff — ✅ BYGGET OG LIVE 2026-07-18 (ADR-019)
Bevisst sidesprang fra "bygg i rekkefølgen ting brukes" rett etter
program-skjermen: brukeren påpekte at ADR-004s teeoff-integrasjon fortsatt
bare var vedtatt, ikke bygget. Full design i ARCHITECTURE_DECISIONS.md
ADR-019 (fem delbeslutninger). Kort: organisator søker blant teeoff sine
baner i program-skjemaet, velger én, og teecup KOPIERER bane+hull+tee+
tee_rating inn i `teecup_db` (`source='official'`) — ikke et live oppslag
ved hver bruk. Ny `app/teeoff_client.py` (ren HTTP-klient mot
`http://teeoff_api:8000`, internt Docker-nettverk, ingen auth trengs — begge
containere deler allerede `teeoff_default`). To nye endepunkter i
`app/routers/courses.py`: `GET .../courses/official-search[/{slug}]` og
`POST .../courses/official-import`. Migrasjon `010` (unik
`external_course_ref` per org, hindrer dupliserte importer).
**Bevisste avgrensninger for denne runden:** kun 18-hulls baner kan
importeres (teeoffs skjema har ingen egen 9-hulls-inndeling); kun
`full_18`-rating importeres (teeoff har ingen separat front9/back9-rating,
samme valg som ADR-008 allerede tok for egendefinerte baner); ufullstendige
teeoff-data (manglende par/hcp_index på et hull, eller en tee uten NOEN
rating) avviser hele importen tydelig (`EXTERNAL_DATA_INCOMPLETE`) FØR noe
skrives, ikke en delvis importert bane.
**Verifisert grundig, inkludert mot EKTE `teeoff_api`** (ikke en simulert
respons): søk, anlegg-/banevalg, og import kjørt reelt mot den kjørende
produksjonscontaineren (kun lesing) — importerte Borregaard Golfklubb sin
hovedbane, bekreftet alle 18 hull + 8 tee/tee_rating-rader riktig i
databasen, og opprettet en ekte økt med den importerte banen (beviser hele
veien til handicap-motoren, ikke bare selve importen). Reimport avvist
(409), kryss-org-isolasjon bekreftet, ukjent teeoff-slug ga 404,
`test_isolation.sql` 12/12, ekte typesjekket produksjonsbuild av
frontend-utvidelsen.
**Rullet ut live**, bruker bekreftet eksplisitt: migrasjon 010 mot ekte
`teecup_db`, begge containere redeployet, `teeoff.no` upåvirket.
**Reell bug funnet og fikset samme dag, av en bruker som faktisk testet
funksjonen:** bane-søkeboksen ("Hent bane fra teeoff") var et `<form>`
rendret INNI det ytre økt-opprett-skjemaet — ugyldig, nestet HTML. Å klikke
"Søk" submittet i praksis det ytre skjemaet som en ekte side-navigasjon og
vasket bort `?org=...`-parameteren fra URL-en. Fikset ved å fjerne det
indre `<form>`-elementet (vanlig `<div>` + Enter-tast/knapp-klikk i
stedet). Se CLAUDE.md-status for full root cause.
---
## UX / frontend (senere fase)
- 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp
(banebruk i sollys/med solbriller).
- ✅ Offline-first (ADR-006) — prinsipp besluttet; implementasjon senere.
- 📋 PWA: manifest, service workers, «Legg til på hjemskjerm».
---
## Bevisst endret fra opprinnelige (Gemini-)råd
- 🔀 **Banedata:** API mot teeoff (ADR-004), IKKE direkte delt database. Direkte
DB-kobling ville låst TeeCup til teeoffs skjemaendringer.
- 🔀 **Handicap-motor:** egen testet Python-modul, ikke den innlimte JS-funksjonen
(som bl.a. ikke håndterte 9-hull eller konfigurerbare allowances korrekt).
- 🔀 **Tenant-modell:** organisasjon som tenant med RLS, ikke bare «turnering-ID».
- 🔀 **Sesjons-secret:** egne secrets for TeeCup, ikke fallback til teeoffs
(teeoff selv bruker en slik fallback — bevisst unngått her).